@cimplify/cli 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,7 @@ export function PromoBanner() {
41
41
  </section>
42
42
  );
43
43
  }
44
- ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Apple-style split hero. Left: copy + CTAs. Right: large product image.\n * Stacks to image-on-top on mobile. Strings come from `brand.hero` at the\n * call site \u2014 this component is otherwise design-only.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative overflow-hidden bg-foreground text-background">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">\n <div>\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">\n {eyebrow}\n </p>\n <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">\n {title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-7">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="(min-width: 1024px) 50vw, 100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n const initial = brand.shortName.charAt(0).toUpperCase();\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-2.5 group">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">\n {initial}\n </span>\n <span className="text-[18px] font-bold -tracking-[0.025em]">{brand.shortName}</span>\n <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for retail: spec sheets, multi-image\n * galleries, reviews, comparison rails all need vertical real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
44
+ ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Apple-style split hero. Left: copy + CTAs. Right: large product image.\n * Stacks to image-on-top on mobile. Strings come from `brand.hero` at the\n * call site \u2014 this component is otherwise design-only.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative overflow-hidden bg-foreground text-background">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">\n <div>\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">\n {eyebrow}\n </p>\n <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">\n {title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-7">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="(min-width: 1024px) 50vw, 100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n const initial = brand.shortName.charAt(0).toUpperCase();\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-2.5 group">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">\n {initial}\n </span>\n <span className="text-[18px] font-bold -tracking-[0.025em]">{brand.shortName}</span>\n <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for retail: spec sheets, multi-image\n * galleries, reviews, comparison rails all need vertical real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
45
45
 
46
46
  import { useState } from "react";
47
47
 
@@ -902,7 +902,7 @@ export function PromoBanner() {
902
902
  </section>
903
903
  );
904
904
  }
905
- ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Editorial full-bleed fashion hero. Image fills the viewport; copy sits\n * over a dark gradient. Strings come from `brand.hero` at the call site.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative w-full h-[clamp(560px,80vh,820px)] overflow-hidden bg-foreground text-background">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-t from-foreground via-foreground/40 to-foreground/0 pointer-events-none" />\n <div className="absolute inset-0 flex items-end">\n <div className="w-full max-w-7xl mx-auto px-6 sm:px-10 pb-12 sm:pb-16 lg:pb-20">\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-background/10 border border-background/30 backdrop-blur-md text-[11px] uppercase tracking-[0.2em] font-medium">\n <span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n {eyebrow && (\n <p className="text-[12px] uppercase tracking-[0.3em] text-background/70 mb-3">\n {eyebrow}\n </p>\n )}\n <h1 className="font-display text-[clamp(3.5rem,11vw,9rem)] m-0 mb-4 -tracking-[0.04em] leading-[0.92] uppercase">\n {title}\n </h1>\n <p className="max-w-xl text-base sm:text-lg text-background/85 leading-relaxed mb-8">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-7 py-3.5 bg-background text-foreground font-bold text-sm uppercase tracking-wider hover:bg-primary hover:text-primary-foreground transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-7 py-3.5 border border-background/40 text-background hover:bg-background/10 transition-colors text-sm uppercase tracking-wider font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Big block-letter brand mark, mono-spaced\n * uppercase nav. Active-link styling and live cart count live in client\n * islands behind their own Suspense boundaries.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-10 py-4 border-b border-border bg-background/95 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-3">\n <span className="font-display text-[22px] uppercase -tracking-[0.04em] leading-none">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-7 uppercase tracking-wide text-[12px]">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[12px] font-medium uppercase tracking-wider text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for retail: spec sheets, multi-image\n * galleries, reviews, comparison rails all need vertical real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n\n# Playwright runtime artifacts (commit the snapshots in e2e/__snapshots__/)\ntest-results/\nplaywright-report/\nblob-report/\n.playwright/\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/lookbook/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Image from "next/image";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Lookbook \u2014 ${brand.name}`,\n description: "Editorial photography from each Studio FRX drop.",\n};\n\ninterface LookbookEntry {\n drop: string;\n title: string;\n hero: string;\n tiles: string[];\n byline: string;\n date: string;\n}\n\n// Each entry is a drop; tiles are the editorial shots. Replace the URLs\n// with your own asset bucket / CDN before going live.\nconst ENTRIES: LookbookEntry[] = [\n {\n drop: "Drop 04",\n title: "Built for now.",\n date: "Spring 2026",\n byline: "Photographed by Selasi Adjei in Jamestown, Accra.",\n hero: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1485518882345-15568b007407?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 03",\n title: "Heavyweight, hand-screened.",\n date: "Winter 2025",\n byline: "Shot at the Tema dye works.",\n hero: "https://images.unsplash.com/photo-1467043153537-a4fba2cd39ef?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1542838686-37da4a9fd1b3?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 02",\n title: "From the studio floor.",\n date: "Autumn 2025",\n byline: "Studio days, Osu.",\n hero: "https://images.unsplash.com/photo-1525507119028-ed4c629a60a3?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1512436991641-6745cdb1723f?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1490114538077-0a7f8cb49891?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n];\n\nexport default function LookbookPage() {\n return (\n <article>\n <section className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-3">\n Lookbook\n </p>\n <h1 className="font-display text-[clamp(3rem,9vw,7rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.92]">\n The drops, in full.\n </h1>\n <p className="text-base text-muted-foreground max-w-2xl leading-relaxed">\n Each drop is photographed in the room it\'s built in. Editorials below;\n full collection in the shop.\n </p>\n </div>\n </section>\n\n {ENTRIES.map((entry, i) => (\n <section key={entry.drop} className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-16">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {entry.drop} \xB7 {entry.date}\n </p>\n <h2 className="font-display text-[clamp(2rem,5vw,3.5rem)] uppercase m-0 -tracking-[0.04em] leading-[0.95]">\n {entry.title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2">{entry.byline}</p>\n </div>\n <Link\n href="/shop"\n className="hidden sm:inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-foreground hover:text-primary transition-colors whitespace-nowrap"\n >\n Shop {entry.drop} \u2192\n </Link>\n </div>\n\n <div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden bg-muted mb-3">\n <Image\n src={entry.hero}\n alt={`${entry.drop} hero \u2014 ${entry.title}`}\n fill\n sizes="(min-width: 1280px) 1280px, 100vw"\n className="object-cover"\n priority={i === 0}\n />\n </div>\n\n <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">\n {entry.tiles.map((src, ti) => (\n <div\n key={src}\n className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-muted"\n >\n <Image\n src={src}\n alt={`${entry.drop} editorial ${ti + 1}`}\n fill\n sizes="(min-width: 768px) 33vw, 100vw"\n className="object-cover"\n />\n </div>\n ))}\n </div>\n </div>\n </section>\n ))}\n </article>\n );\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
905
+ ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Editorial full-bleed fashion hero. Image fills the viewport; copy sits\n * over a dark gradient. Strings come from `brand.hero` at the call site.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative w-full h-[clamp(560px,80vh,820px)] overflow-hidden bg-foreground text-background">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-t from-foreground via-foreground/40 to-foreground/0 pointer-events-none" />\n <div className="absolute inset-0 flex items-end">\n <div className="w-full max-w-7xl mx-auto px-6 sm:px-10 pb-12 sm:pb-16 lg:pb-20">\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-background/10 border border-background/30 backdrop-blur-md text-[11px] uppercase tracking-[0.2em] font-medium">\n <span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n {eyebrow && (\n <p className="text-[12px] uppercase tracking-[0.3em] text-background/70 mb-3">\n {eyebrow}\n </p>\n )}\n <h1 className="font-display text-[clamp(3.5rem,11vw,9rem)] m-0 mb-4 -tracking-[0.04em] leading-[0.92] uppercase">\n {title}\n </h1>\n <p className="max-w-xl text-base sm:text-lg text-background/85 leading-relaxed mb-8">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-7 py-3.5 bg-background text-foreground font-bold text-sm uppercase tracking-wider hover:bg-primary hover:text-primary-foreground transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-7 py-3.5 border border-background/40 text-background hover:bg-background/10 transition-colors text-sm uppercase tracking-wider font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Big block-letter brand mark, mono-spaced\n * uppercase nav. Active-link styling and live cart count live in client\n * islands behind their own Suspense boundaries.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-10 py-4 border-b border-border bg-background/95 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-3">\n <span className="font-display text-[22px] uppercase -tracking-[0.04em] leading-none">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-7 uppercase tracking-wide text-[12px]">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[12px] font-medium uppercase tracking-wider text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for retail: spec sheets, multi-image\n * galleries, reviews, comparison rails all need vertical real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n\n# Playwright runtime artifacts (commit the snapshots in e2e/__snapshots__/)\ntest-results/\nplaywright-report/\nblob-report/\n.playwright/\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/lookbook/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Image from "next/image";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Lookbook \u2014 ${brand.name}`,\n description: "Editorial photography from each Studio FRX drop.",\n};\n\ninterface LookbookEntry {\n drop: string;\n title: string;\n hero: string;\n tiles: string[];\n byline: string;\n date: string;\n}\n\n// Each entry is a drop; tiles are the editorial shots. Replace the URLs\n// with your own asset bucket / CDN before going live.\nconst ENTRIES: LookbookEntry[] = [\n {\n drop: "Drop 04",\n title: "Built for now.",\n date: "Spring 2026",\n byline: "Photographed by Selasi Adjei in Jamestown, Accra.",\n hero: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1485518882345-15568b007407?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 03",\n title: "Heavyweight, hand-screened.",\n date: "Winter 2025",\n byline: "Shot at the Tema dye works.",\n hero: "https://images.unsplash.com/photo-1467043153537-a4fba2cd39ef?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1542838686-37da4a9fd1b3?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 02",\n title: "From the studio floor.",\n date: "Autumn 2025",\n byline: "Studio days, Osu.",\n hero: "https://images.unsplash.com/photo-1525507119028-ed4c629a60a3?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1512436991641-6745cdb1723f?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1490114538077-0a7f8cb49891?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n];\n\nexport default function LookbookPage() {\n return (\n <article>\n <section className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-3">\n Lookbook\n </p>\n <h1 className="font-display text-[clamp(3rem,9vw,7rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.92]">\n The drops, in full.\n </h1>\n <p className="text-base text-muted-foreground max-w-2xl leading-relaxed">\n Each drop is photographed in the room it\'s built in. Editorials below;\n full collection in the shop.\n </p>\n </div>\n </section>\n\n {ENTRIES.map((entry, i) => (\n <section key={entry.drop} className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-16">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {entry.drop} \xB7 {entry.date}\n </p>\n <h2 className="font-display text-[clamp(2rem,5vw,3.5rem)] uppercase m-0 -tracking-[0.04em] leading-[0.95]">\n {entry.title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2">{entry.byline}</p>\n </div>\n <Link\n href="/shop"\n className="hidden sm:inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-foreground hover:text-primary transition-colors whitespace-nowrap"\n >\n Shop {entry.drop} \u2192\n </Link>\n </div>\n\n <div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden bg-muted mb-3">\n <Image\n src={entry.hero}\n alt={`${entry.drop} hero \u2014 ${entry.title}`}\n fill\n sizes="(min-width: 1280px) 1280px, 100vw"\n className="object-cover"\n priority={i === 0}\n />\n </div>\n\n <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">\n {entry.tiles.map((src, ti) => (\n <div\n key={src}\n className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-muted"\n >\n <Image\n src={src}\n alt={`${entry.drop} editorial ${ti + 1}`}\n fill\n sizes="(min-width: 768px) 33vw, 100vw"\n className="object-cover"\n />\n </div>\n ))}\n </div>\n </div>\n </section>\n ))}\n </article>\n );\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
906
906
 
907
907
  import { useState } from "react";
908
908
 
@@ -1529,7 +1529,7 @@ export const brand: Brand = {
1529
1529
  businessId: "bus_studio_frx",
1530
1530
  },
1531
1531
  };
1532
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "fashion",\n "name": "Fashion",\n "tagline": "Editorial multi-drop fashion storefront with lookbooks and size guides.",\n "industry": "fashion",\n "tags": ["fashion", "apparel", "lookbook"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "fashion",\n "seedBusinessId": "bus_studio_frx"\n }\n}\n' }, { "path": "playwright.config.ts", "kind": "text", "content": 'import { defineConfig, devices } from "@playwright/test";\n\n/**\n * Visual regression config \u2014 boots `bun dev` (mock + next), runs against\n * a stable Chromium, snapshots key pages.\n *\n * Run:\n * bun run check:visual # compare against baseline\n * bun run check:visual --update-snapshots # accept new baseline\n *\n * Baselines live in `e2e/__snapshots__/`. Commit them.\n */\nexport default defineConfig({\n testDir: "./e2e",\n fullyParallel: false,\n forbidOnly: Boolean(process.env.CI),\n retries: process.env.CI ? 1 : 0,\n workers: 1,\n reporter: process.env.CI ? "github" : "list",\n\n use: {\n baseURL: "http://localhost:3000",\n trace: "retain-on-failure",\n // Viewport snapshots are noisy across OS / browser builds. Lock here.\n viewport: { width: 1280, height: 800 },\n },\n\n projects: [\n {\n name: "chromium",\n use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },\n },\n ],\n\n expect: {\n // Anti-flake: allow tiny rendering differences (font hinting across runners).\n toHaveScreenshot: { maxDiffPixelRatio: 0.01 },\n },\n\n webServer: {\n command: "bun dev",\n url: "http://localhost:3000",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n stdout: "ignore",\n stderr: "pipe",\n },\n});\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_currents_electronics\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (fashion: Studio FRX).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_studio_frx\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-grocery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Grocery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_freshmart` for the bundled seed.\n\n## Aesthetic\n\n- **Inter sans only** (no serif) \u2014 friendly, readable for dense product grids.\n- **Fresh green + cream** \u2014 vibrant green primary, warm cream background.\n- **Generous radius**: `0.875rem` \u2014 softer than retail.\n- **Bold, slightly heavy** headings.\n- Schema.org `@type` is `GroceryStore`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Free delivery over GH\u20B5150"),\n collection strips, category grid, newsletter\n shop/page.tsx Aisle (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. weekly box, deals)\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery + slots), returns/page.tsx (quality guarantee),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, GroceryStore JSON-LD |\n| `app/page.tsx` | `brand.hero` (free-delivery badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (delivery slots, sourcing, subscription boxes) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery slots + free-over threshold) |\n| `app/returns/page.tsx` | `brand.returns` (quality guarantee) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Grocery-specific notes\n\n- **Subscription boxes** are modelled as products with `billing_plans` (renders the SubscriptionCard variant). The basket-box landing lives at `/categories/baskets`.\n- **Weight-based pricing**: SDK supports `quantity_pricing` tiers per product, but most grocery products are sold by unit/kg. Real weight-at-pack pricing happens on the backend pricing rule.\n- Mock seed: `--seed grocery` (FreshMart).\n\n## Known TODOs\n\n- Contact + newsletter fake submits.\n- Hero / category / product imagery is Unsplash placeholder \u2014 replace with merchant photography.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
1532
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "fashion",\n "name": "Fashion",\n "tagline": "Editorial multi-drop fashion storefront with lookbooks and size guides.",\n "industry": "fashion",\n "tags": ["fashion", "apparel", "lookbook"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "fashion",\n "seedBusinessId": "bus_studio_frx"\n }\n}\n' }, { "path": "playwright.config.ts", "kind": "text", "content": 'import { defineConfig, devices } from "@playwright/test";\n\n/**\n * Visual regression config \u2014 boots `bun dev` (mock + next), runs against\n * a stable Chromium, snapshots key pages.\n *\n * Run:\n * bun run check:visual # compare against baseline\n * bun run check:visual --update-snapshots # accept new baseline\n *\n * Baselines live in `e2e/__snapshots__/`. Commit them.\n */\nexport default defineConfig({\n testDir: "./e2e",\n fullyParallel: false,\n forbidOnly: Boolean(process.env.CI),\n retries: process.env.CI ? 1 : 0,\n workers: 1,\n reporter: process.env.CI ? "github" : "list",\n\n use: {\n baseURL: "http://localhost:3000",\n trace: "retain-on-failure",\n // Viewport snapshots are noisy across OS / browser builds. Lock here.\n viewport: { width: 1280, height: 800 },\n },\n\n projects: [\n {\n name: "chromium",\n use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },\n },\n ],\n\n expect: {\n // Anti-flake: allow tiny rendering differences (font hinting across runners).\n toHaveScreenshot: { maxDiffPixelRatio: 0.01 },\n },\n\n webServer: {\n command: "bun dev",\n url: "http://localhost:3000",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n stdout: "ignore",\n stderr: "pipe",\n },\n});\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_currents_electronics\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (fashion: Studio FRX).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_studio_frx\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-grocery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Grocery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_freshmart` for the bundled seed.\n\n## Aesthetic\n\n- **Inter sans only** (no serif) \u2014 friendly, readable for dense product grids.\n- **Fresh green + cream** \u2014 vibrant green primary, warm cream background.\n- **Generous radius**: `0.875rem` \u2014 softer than retail.\n- **Bold, slightly heavy** headings.\n- Schema.org `@type` is `GroceryStore`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Free delivery over GH\u20B5150"),\n collection strips, category grid, newsletter\n shop/page.tsx Aisle (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. weekly box, deals)\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery + slots), returns/page.tsx (quality guarantee),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, GroceryStore JSON-LD |\n| `app/page.tsx` | `brand.hero` (free-delivery badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (delivery slots, sourcing, subscription boxes) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery slots + free-over threshold) |\n| `app/returns/page.tsx` | `brand.returns` (quality guarantee) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Grocery-specific notes\n\n- **Subscription boxes** are modelled as products with `billing_plans` (renders the SubscriptionCard variant). The basket-box landing lives at `/categories/baskets`.\n- **Weight-based pricing**: SDK supports `quantity_pricing` tiers per product, but most grocery products are sold by unit/kg. Real weight-at-pack pricing happens on the backend pricing rule.\n- Mock seed: `--seed grocery` (FreshMart).\n\n## Known TODOs\n\n- Contact + newsletter fake submits.\n- Hero / category / product imagery is Unsplash placeholder \u2014 replace with merchant photography.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
1533
1533
 
1534
1534
  import { useState } from "react";
1535
1535
 
@@ -2099,7 +2099,7 @@ export function PromoBanner() {
2099
2099
  </section>
2100
2100
  );
2101
2101
  }
2102
- ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Apple-style split hero. Left: copy + CTAs. Right: large product image.\n * Stacks to image-on-top on mobile. Strings come from `brand.hero` at the\n * call site \u2014 this component is otherwise design-only.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative overflow-hidden bg-foreground text-background">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">\n <div>\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">\n {eyebrow}\n </p>\n <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">\n {title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-7">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="(min-width: 1024px) 50vw, 100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n const initial = brand.shortName.charAt(0).toUpperCase();\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-2.5 group">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">\n {initial}\n </span>\n <span className="text-[18px] font-bold -tracking-[0.025em]">{brand.shortName}</span>\n <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/service-brief.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Three-card editorial panel ("Mechanic\'s brief") \u2014 the auto answer to\n * the pharmacy `<HealthBrief/>`. Reminders about routine service\n * intervals, partner workshops, and seasonal checks (harmattan dust,\n * rainy-season wipers). Strings come from `brand.serviceBrief`.\n */\nexport function ServiceBrief() {\n const { eyebrow, title, cards } = brand.serviceBrief;\n return (\n <section className="relative bg-muted/60 border-y border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-24">\n <div className="max-w-2xl mb-10 sm:mb-12">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.2vw,2.5rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n </div>\n\n <div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-5">\n {cards.map((c, i) => (\n <article\n key={c.eyebrow + c.title}\n className="group flex flex-col bg-card border border-border rounded-2xl p-6 sm:p-7 hover:border-primary/40 hover:shadow-[0_14px_36px_rgb(0_0_0/0.06)] transition-all"\n >\n <div className="flex items-center justify-between mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-accent text-accent-foreground text-[11px] font-mono uppercase tracking-[0.14em]">\n {c.eyebrow}\n </span>\n <span className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground">\n No. {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <h3 className="text-xl sm:text-[22px] font-semibold m-0 -tracking-[0.02em] leading-snug">\n {c.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-muted-foreground leading-relaxed mt-3 flex-1">\n {c.body}\n </p>\n <Link\n href={c.ctaHref}\n className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-primary group-hover:gap-3 transition-all"\n >\n {c.ctaLabel}\n <ArrowIcon />\n </Link>\n </article>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for pharmacy: dosage info, prescription\n * upload inputs, consent signatures, and pharmacist notes all need vertical\n * real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/fitment-finder.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter, useSearchParams } from "next/navigation";\nimport { useMemo, useState } from "react";\nimport { brand } from "@/lib/brand";\n\n/**\n * Vehicle / fitment finder \u2014 the signature auto-parts widget.\n *\n * UX: customer picks Make \u2192 Model \u2192 Year (each subsequent select is\n * filtered by the previous choice). Submit pushes ?fits=<tag>&y=<year>\n * onto the shop URL; the shop page reads the tag and filters the\n * catalogue. Universal-fit products always appear regardless.\n *\n * Data lives entirely in `brand.fitments` (lib/brand.ts) so a merchant\n * can rebrand the catalogue without touching this component. Each\n * fitment entry carries the tag that maps onto product `tags` like\n * `fits:toyota:corolla`.\n */\nexport function FitmentFinder() {\n const router = useRouter();\n const searchParams = useSearchParams();\n const { eyebrow, title, description, ctaLabel, anyMakeLabel, anyModelLabel, anyYearLabel, makes } =\n brand.fitments;\n\n const [make, setMake] = useState<string>(searchParams?.get("make") ?? "");\n const [model, setModel] = useState<string>(searchParams?.get("model") ?? "");\n const [year, setYear] = useState<string>(searchParams?.get("y") ?? "");\n\n const selectedMake = useMemo(() => makes.find((m) => m.slug === make), [make, makes]);\n const models = selectedMake?.models ?? [];\n const selectedModel = useMemo(() => models.find((m) => m.slug === model), [model, models]);\n const years = selectedModel?.years ?? [];\n\n function onSubmit(e: React.FormEvent) {\n e.preventDefault();\n if (!make) return;\n const params = new URLSearchParams();\n if (model) {\n params.set("fits", `fits:${make}:${model}`);\n params.set("make", make);\n params.set("model", model);\n } else {\n params.set("fits", `fits:${make}`);\n params.set("make", make);\n }\n if (year) params.set("y", year);\n router.push(`/shop?${params.toString()}`);\n }\n\n return (\n <section id="fitment" className="max-w-7xl mx-auto px-6 sm:px-8 -mt-10 sm:-mt-14 relative z-10 scroll-mt-20">\n <div className="bg-card border border-border rounded-2xl p-6 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.06)]">\n <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-6">\n <div className="max-w-xl">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2 leading-relaxed">{description}</p>\n </div>\n <FitmentBadge />\n </div>\n\n <form onSubmit={onSubmit} className="grid grid-cols-1 md:grid-cols-[1.2fr_1.2fr_1fr_auto] gap-3">\n <Select\n label="Make"\n value={make}\n placeholder={anyMakeLabel}\n onChange={(v) => {\n setMake(v);\n setModel("");\n setYear("");\n }}\n options={makes.map((m) => ({ value: m.slug, label: m.name }))}\n />\n <Select\n label="Model"\n value={model}\n placeholder={anyModelLabel}\n disabled={!make}\n onChange={(v) => {\n setModel(v);\n setYear("");\n }}\n options={models.map((m) => ({ value: m.slug, label: m.name }))}\n />\n <Select\n label="Year"\n value={year}\n placeholder={anyYearLabel}\n disabled={!model}\n onChange={setYear}\n options={years.map((y) => ({ value: String(y), label: String(y) }))}\n />\n <button\n type="submit"\n disabled={!make}\n className="self-end inline-flex items-center justify-center gap-2 px-6 h-11 rounded-xl bg-primary text-primary-foreground text-sm font-semibold hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed transition-colors"\n >\n {ctaLabel}\n <ArrowIcon />\n </button>\n </form>\n </div>\n </section>\n );\n}\n\ninterface SelectProps {\n label: string;\n value: string;\n placeholder: string;\n options: { value: string; label: string }[];\n onChange: (v: string) => void;\n disabled?: boolean;\n}\n\nfunction Select({ label, value, placeholder, options, onChange, disabled }: SelectProps) {\n return (\n <label className="block">\n <span className="block text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-1.5">\n {label}\n </span>\n <div className="relative">\n <select\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n className="block w-full h-11 pl-3.5 pr-10 rounded-xl border border-border bg-background text-sm font-medium appearance-none disabled:opacity-60 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary"\n >\n <option value="">{placeholder}</option>\n {options.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <ChevronIcon />\n </div>\n </label>\n );\n}\n\nfunction FitmentBadge() {\n return (\n <div className="hidden lg:flex items-center gap-3 px-4 py-3 rounded-xl bg-accent text-accent-foreground">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-5 h-5" aria-hidden>\n <path d="M5 13l2-5h10l2 5" strokeLinejoin="round" />\n <path d="M3 13h18v5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-1H7v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5z" strokeLinejoin="round" />\n <circle cx="7" cy="16" r="1" />\n <circle cx="17" cy="16" r="1" />\n </svg>\n <span className="text-[12px] font-mono uppercase tracking-[0.14em]">\n Universal parts always shown\n </span>\n </div>\n );\n}\n\nfunction ChevronIcon() {\n return (\n <svg\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none"\n aria-hidden\n >\n <path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/auto-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Auto-specific hero. Carbon-dark with a single sport-red accent line.\n * Right column carries the four info chips that anchor trust for an\n * auto-parts merchant: genuine OEM, same-day fitting partners, warranty,\n * cash-on-delivery. The fitment finder sits directly below.\n */\nexport function AutoHero() {\n return (\n <section className="relative overflow-hidden bg-foreground text-background pb-20 sm:pb-28">\n <div className="absolute inset-0 opacity-[0.05] pointer-events-none [background-image:linear-gradient(transparent_0%,transparent_calc(50%-0.5px),rgba(255,255,255,0.6)_50%,transparent_calc(50%+0.5px),transparent_100%)] [background-size:48px_48px]" />\n <div className="absolute -top-32 -right-24 w-[28rem] h-[28rem] rounded-full bg-primary/25 blur-[120px] pointer-events-none" />\n <div className="absolute top-0 left-0 right-0 h-1 bg-primary" />\n\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 pt-14 sm:pt-20">\n <div className="grid grid-cols-1 lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-14 items-end">\n <div>\n <span className="inline-flex items-center gap-2 mb-6 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-background text-[11px] font-mono uppercase tracking-[0.18em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {brand.hero.badge}\n </span>\n <h1 className="text-[clamp(2.5rem,6.4vw,5.25rem)] font-bold m-0 -tracking-[0.04em] leading-[0.98]">\n {brand.hero.title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl mt-6">\n {brand.hero.subtitle}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-8">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-primary text-primary-foreground text-sm font-semibold hover:bg-primary/90 transition-colors"\n >\n {brand.hero.primaryCtaLabel}\n <ArrowIcon />\n </Link>\n {brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref && (\n <Link\n href={brand.hero.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full border border-background/30 text-background text-sm font-semibold hover:bg-background/10 transition-colors"\n >\n {brand.hero.secondaryCtaLabel}\n </Link>\n )}\n </div>\n </div>\n\n <aside className="grid grid-cols-2 gap-3 sm:gap-4 lg:max-w-md">\n <InfoChip eyebrow="Genuine" value="OEM & OEM-grade" />\n <InfoChip eyebrow="Same-day" value="Fitting partners across Accra" />\n <InfoChip eyebrow="Warranty" value="12 months \xB7 parts & labour" />\n <InfoChip eyebrow="Pay your way" value="MoMo \xB7 card \xB7 cash" />\n </aside>\n </div>\n </div>\n </section>\n );\n}\n\ninterface InfoChipProps {\n eyebrow: string;\n value: string;\n}\n\nfunction InfoChip({ eyebrow, value }: InfoChipProps) {\n return (\n <div className="rounded-2xl border border-background/15 bg-background/5 backdrop-blur-sm p-4">\n <p className="text-[10px] font-mono uppercase tracking-[0.18em] text-background/55 mb-1.5">\n {eyebrow}\n </p>\n <p className="text-sm font-semibold text-background -tracking-[0.01em] leading-snug">\n {value}\n </p>\n </div>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
2102
+ ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Apple-style split hero. Left: copy + CTAs. Right: large product image.\n * Stacks to image-on-top on mobile. Strings come from `brand.hero` at the\n * call site \u2014 this component is otherwise design-only.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative overflow-hidden bg-foreground text-background">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">\n <div>\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">\n {eyebrow}\n </p>\n <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">\n {title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-7">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="(min-width: 1024px) 50vw, 100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n const initial = brand.shortName.charAt(0).toUpperCase();\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-2.5 group">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">\n {initial}\n </span>\n <span className="text-[18px] font-bold -tracking-[0.025em]">{brand.shortName}</span>\n <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/service-brief.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Three-card editorial panel ("Mechanic\'s brief") \u2014 the auto answer to\n * the pharmacy `<HealthBrief/>`. Reminders about routine service\n * intervals, partner workshops, and seasonal checks (harmattan dust,\n * rainy-season wipers). Strings come from `brand.serviceBrief`.\n */\nexport function ServiceBrief() {\n const { eyebrow, title, cards } = brand.serviceBrief;\n return (\n <section className="relative bg-muted/60 border-y border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-24">\n <div className="max-w-2xl mb-10 sm:mb-12">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.2vw,2.5rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n </div>\n\n <div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-5">\n {cards.map((c, i) => (\n <article\n key={c.eyebrow + c.title}\n className="group flex flex-col bg-card border border-border rounded-2xl p-6 sm:p-7 hover:border-primary/40 hover:shadow-[0_14px_36px_rgb(0_0_0/0.06)] transition-all"\n >\n <div className="flex items-center justify-between mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-accent text-accent-foreground text-[11px] font-mono uppercase tracking-[0.14em]">\n {c.eyebrow}\n </span>\n <span className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground">\n No. {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <h3 className="text-xl sm:text-[22px] font-semibold m-0 -tracking-[0.02em] leading-snug">\n {c.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-muted-foreground leading-relaxed mt-3 flex-1">\n {c.body}\n </p>\n <Link\n href={c.ctaHref}\n className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-primary group-hover:gap-3 transition-all"\n >\n {c.ctaLabel}\n <ArrowIcon />\n </Link>\n </article>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for pharmacy: dosage info, prescription\n * upload inputs, consent signatures, and pharmacist notes all need vertical\n * real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/fitment-finder.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter, useSearchParams } from "next/navigation";\nimport { useMemo, useState } from "react";\nimport { brand } from "@/lib/brand";\n\n/**\n * Vehicle / fitment finder \u2014 the signature auto-parts widget.\n *\n * UX: customer picks Make \u2192 Model \u2192 Year (each subsequent select is\n * filtered by the previous choice). Submit pushes ?fits=<tag>&y=<year>\n * onto the shop URL; the shop page reads the tag and filters the\n * catalogue. Universal-fit products always appear regardless.\n *\n * Data lives entirely in `brand.fitments` (lib/brand.ts) so a merchant\n * can rebrand the catalogue without touching this component. Each\n * fitment entry carries the tag that maps onto product `tags` like\n * `fits:toyota:corolla`.\n */\nexport function FitmentFinder() {\n const router = useRouter();\n const searchParams = useSearchParams();\n const { eyebrow, title, description, ctaLabel, anyMakeLabel, anyModelLabel, anyYearLabel, makes } =\n brand.fitments;\n\n const [make, setMake] = useState<string>(searchParams?.get("make") ?? "");\n const [model, setModel] = useState<string>(searchParams?.get("model") ?? "");\n const [year, setYear] = useState<string>(searchParams?.get("y") ?? "");\n\n const selectedMake = useMemo(() => makes.find((m) => m.slug === make), [make, makes]);\n const models = selectedMake?.models ?? [];\n const selectedModel = useMemo(() => models.find((m) => m.slug === model), [model, models]);\n const years = selectedModel?.years ?? [];\n\n function onSubmit(e: React.FormEvent) {\n e.preventDefault();\n if (!make) return;\n const params = new URLSearchParams();\n if (model) {\n params.set("fits", `fits:${make}:${model}`);\n params.set("make", make);\n params.set("model", model);\n } else {\n params.set("fits", `fits:${make}`);\n params.set("make", make);\n }\n if (year) params.set("y", year);\n router.push(`/shop?${params.toString()}`);\n }\n\n return (\n <section id="fitment" className="max-w-7xl mx-auto px-6 sm:px-8 -mt-10 sm:-mt-14 relative z-10 scroll-mt-20">\n <div className="bg-card border border-border rounded-2xl p-6 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.06)]">\n <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-6">\n <div className="max-w-xl">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2 leading-relaxed">{description}</p>\n </div>\n <FitmentBadge />\n </div>\n\n <form onSubmit={onSubmit} className="grid grid-cols-1 md:grid-cols-[1.2fr_1.2fr_1fr_auto] gap-3">\n <Select\n label="Make"\n value={make}\n placeholder={anyMakeLabel}\n onChange={(v) => {\n setMake(v);\n setModel("");\n setYear("");\n }}\n options={makes.map((m) => ({ value: m.slug, label: m.name }))}\n />\n <Select\n label="Model"\n value={model}\n placeholder={anyModelLabel}\n disabled={!make}\n onChange={(v) => {\n setModel(v);\n setYear("");\n }}\n options={models.map((m) => ({ value: m.slug, label: m.name }))}\n />\n <Select\n label="Year"\n value={year}\n placeholder={anyYearLabel}\n disabled={!model}\n onChange={setYear}\n options={years.map((y) => ({ value: String(y), label: String(y) }))}\n />\n <button\n type="submit"\n disabled={!make}\n className="self-end inline-flex items-center justify-center gap-2 px-6 h-11 rounded-xl bg-primary text-primary-foreground text-sm font-semibold hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed transition-colors"\n >\n {ctaLabel}\n <ArrowIcon />\n </button>\n </form>\n </div>\n </section>\n );\n}\n\ninterface SelectProps {\n label: string;\n value: string;\n placeholder: string;\n options: { value: string; label: string }[];\n onChange: (v: string) => void;\n disabled?: boolean;\n}\n\nfunction Select({ label, value, placeholder, options, onChange, disabled }: SelectProps) {\n return (\n <label className="block">\n <span className="block text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-1.5">\n {label}\n </span>\n <div className="relative">\n <select\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n className="block w-full h-11 pl-3.5 pr-10 rounded-xl border border-border bg-background text-sm font-medium appearance-none disabled:opacity-60 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary"\n >\n <option value="">{placeholder}</option>\n {options.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <ChevronIcon />\n </div>\n </label>\n );\n}\n\nfunction FitmentBadge() {\n return (\n <div className="hidden lg:flex items-center gap-3 px-4 py-3 rounded-xl bg-accent text-accent-foreground">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-5 h-5" aria-hidden>\n <path d="M5 13l2-5h10l2 5" strokeLinejoin="round" />\n <path d="M3 13h18v5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-1H7v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5z" strokeLinejoin="round" />\n <circle cx="7" cy="16" r="1" />\n <circle cx="17" cy="16" r="1" />\n </svg>\n <span className="text-[12px] font-mono uppercase tracking-[0.14em]">\n Universal parts always shown\n </span>\n </div>\n );\n}\n\nfunction ChevronIcon() {\n return (\n <svg\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none"\n aria-hidden\n >\n <path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/auto-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Auto-specific hero. Carbon-dark with a single sport-red accent line.\n * Right column carries the four info chips that anchor trust for an\n * auto-parts merchant: genuine OEM, same-day fitting partners, warranty,\n * cash-on-delivery. The fitment finder sits directly below.\n */\nexport function AutoHero() {\n return (\n <section className="relative overflow-hidden bg-foreground text-background pb-20 sm:pb-28">\n <div className="absolute inset-0 opacity-[0.05] pointer-events-none [background-image:linear-gradient(transparent_0%,transparent_calc(50%-0.5px),rgba(255,255,255,0.6)_50%,transparent_calc(50%+0.5px),transparent_100%)] [background-size:48px_48px]" />\n <div className="absolute -top-32 -right-24 w-[28rem] h-[28rem] rounded-full bg-primary/25 blur-[120px] pointer-events-none" />\n <div className="absolute top-0 left-0 right-0 h-1 bg-primary" />\n\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 pt-14 sm:pt-20">\n <div className="grid grid-cols-1 lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-14 items-end">\n <div>\n <span className="inline-flex items-center gap-2 mb-6 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-background text-[11px] font-mono uppercase tracking-[0.18em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {brand.hero.badge}\n </span>\n <h1 className="text-[clamp(2.5rem,6.4vw,5.25rem)] font-bold m-0 -tracking-[0.04em] leading-[0.98]">\n {brand.hero.title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl mt-6">\n {brand.hero.subtitle}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-8">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-primary text-primary-foreground text-sm font-semibold hover:bg-primary/90 transition-colors"\n >\n {brand.hero.primaryCtaLabel}\n <ArrowIcon />\n </Link>\n {brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref && (\n <Link\n href={brand.hero.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full border border-background/30 text-background text-sm font-semibold hover:bg-background/10 transition-colors"\n >\n {brand.hero.secondaryCtaLabel}\n </Link>\n )}\n </div>\n </div>\n\n <aside className="grid grid-cols-2 gap-3 sm:gap-4 lg:max-w-md">\n <InfoChip eyebrow="Genuine" value="OEM & OEM-grade" />\n <InfoChip eyebrow="Same-day" value="Fitting partners across Accra" />\n <InfoChip eyebrow="Warranty" value="12 months \xB7 parts & labour" />\n <InfoChip eyebrow="Pay your way" value="MoMo \xB7 card \xB7 cash" />\n </aside>\n </div>\n </div>\n </section>\n );\n}\n\ninterface InfoChipProps {\n eyebrow: string;\n value: string;\n}\n\nfunction InfoChip({ eyebrow, value }: InfoChipProps) {\n return (\n <div className="rounded-2xl border border-background/15 bg-background/5 backdrop-blur-sm p-4">\n <p className="text-[10px] font-mono uppercase tracking-[0.18em] text-background/55 mb-1.5">\n {eyebrow}\n </p>\n <p className="text-sm font-semibold text-background -tracking-[0.01em] leading-snug">\n {value}\n </p>\n </div>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
2103
2103
 
2104
2104
  import { useState } from "react";
2105
2105
 
@@ -2999,7 +2999,7 @@ export const brand: Brand = {
2999
2999
  businessId: "bus_driveline_auto",
3000
3000
  },
3001
3001
  };
3002
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "auto",\n "name": "Auto Parts",\n "tagline": "Auto-parts storefront with vehicle fitment finder and installation booking.",\n "industry": "automotive",\n "tags": ["automotive", "parts", "fitment"],\n "stability": "stable",\n "schemaType": "AutoPartsStore",\n "mock": {\n "seedName": "auto",\n "seedBusinessId": "bus_driveline_auto"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **auto** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Carbon-black + sport-red palette, Inter + JetBrains Mono typography, workshop-modern aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed auto` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Driveline Auto Parts.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Auto-specific behaviour\n\n- **Fitment finder** (`components/fitment-finder.tsx`): Make \u2192 Model \u2192 Year selector on the home page. Submitting pushes `?fits=fits:<make>:<model>` onto `/shop`; the shop page filters the catalogue by matching `tags` on `MockProduct`. Products tagged `fits:universal` always appear.\n- Vehicle data lives in `brand.fitments` (lib/brand.ts) \u2014 add a make/model/year by editing that array.\n- `Bosch`, `Mobil 1`, `Castrol`, `NGK`, `Brembo`, `Bridgestone` brand strip \u2014 swap to your own authorised brands.\n- `AutoPartsStore` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\n```bash\ncimplify-mock --seed pharmacy # Wellspring Pharmacy\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_driveline_auto\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (auto: Driveline Auto Parts).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_driveline_auto\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-restaurant": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Restaurant storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_mamas_kitchen` for the bundled seed.\n\n## Aesthetic\n\n- **Lora serif + Inter** \u2014 warm, food-friendly headings + clean body.\n- **Forest green + cream** \u2014 deep sage primary, warm cream background.\n- **Mid radius**: `0.625rem`.\n- Schema.org `@type` is `Restaurant`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero with daily-special badge,\n collection strips, category grid\n shop/page.tsx Full menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. brunch, mains)\n categories/[slug]/page.tsx Category landing\n\n reservations/page.tsx \u2B50 Restaurant-specific: reservation widget\n (date + lunch/dinner slot + party size)\n backed by service-type "seating" products\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery), returns/page.tsx (refunds), accessibility/page.tsx\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Restaurant JSON-LD |\n| `app/page.tsx` | `brand.hero` (daily-special badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (reservations, dietary, delivery) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup) |\n| `app/returns/page.tsx` | `brand.returns` (refund + cancellation) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns the UI) |\n| `app/reservations/*` | derives seating from service-type products in the catalogue |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Restaurant-specific notes\n\n- **Reservations are service-type products.** Add seating options like "Two-top", "Four-top", "Long table for 12" to the catalogue with `type: "service"`. The `/reservations` page filters them in.\n- The reservation widget adds the chosen seating to cart with the date, time, party size, and notes as cart-item notes; checkout finalises the booking via Cimplify\'s order pipeline.\n- Schema.org `@type` is `Restaurant`. `servesCuisine` etc. can be added to the JSON-LD object in `app/layout.tsx` if needed.\n- Mock seed: `--seed restaurant` (Mama\'s Kitchen).\n\n## Known TODOs\n\n- `/reservations` widget assumes every slot is bookable. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to actually-free windows.\n- Contact form + newsletter fake their submits.\n- Hero / dish photography is Unsplash \u2014 replace with merchant photos.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3002
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "auto",\n "name": "Auto Parts",\n "tagline": "Auto-parts storefront with vehicle fitment finder and installation booking.",\n "industry": "automotive",\n "tags": ["automotive", "parts", "fitment"],\n "stability": "stable",\n "schemaType": "AutoPartsStore",\n "mock": {\n "seedName": "auto",\n "seedBusinessId": "bus_driveline_auto"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **auto** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Carbon-black + sport-red palette, Inter + JetBrains Mono typography, workshop-modern aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed auto` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Driveline Auto Parts.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Auto-specific behaviour\n\n- **Fitment finder** (`components/fitment-finder.tsx`): Make \u2192 Model \u2192 Year selector on the home page. Submitting pushes `?fits=fits:<make>:<model>` onto `/shop`; the shop page filters the catalogue by matching `tags` on `MockProduct`. Products tagged `fits:universal` always appear.\n- Vehicle data lives in `brand.fitments` (lib/brand.ts) \u2014 add a make/model/year by editing that array.\n- `Bosch`, `Mobil 1`, `Castrol`, `NGK`, `Brembo`, `Bridgestone` brand strip \u2014 swap to your own authorised brands.\n- `AutoPartsStore` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\n```bash\ncimplify-mock --seed pharmacy # Wellspring Pharmacy\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_driveline_auto\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (auto: Driveline Auto Parts).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_driveline_auto\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-restaurant": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Restaurant storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_mamas_kitchen` for the bundled seed.\n\n## Aesthetic\n\n- **Lora serif + Inter** \u2014 warm, food-friendly headings + clean body.\n- **Forest green + cream** \u2014 deep sage primary, warm cream background.\n- **Mid radius**: `0.625rem`.\n- Schema.org `@type` is `Restaurant`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero with daily-special badge,\n collection strips, category grid\n shop/page.tsx Full menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. brunch, mains)\n categories/[slug]/page.tsx Category landing\n\n reservations/page.tsx \u2B50 Restaurant-specific: reservation widget\n (date + lunch/dinner slot + party size)\n backed by service-type "seating" products\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery), returns/page.tsx (refunds), accessibility/page.tsx\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Restaurant JSON-LD |\n| `app/page.tsx` | `brand.hero` (daily-special badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (reservations, dietary, delivery) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup) |\n| `app/returns/page.tsx` | `brand.returns` (refund + cancellation) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns the UI) |\n| `app/reservations/*` | derives seating from service-type products in the catalogue |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Restaurant-specific notes\n\n- **Reservations are service-type products.** Add seating options like "Two-top", "Four-top", "Long table for 12" to the catalogue with `type: "service"`. The `/reservations` page filters them in.\n- The reservation widget adds the chosen seating to cart with the date, time, party size, and notes as cart-item notes; checkout finalises the booking via Cimplify\'s order pipeline.\n- Schema.org `@type` is `Restaurant`. `servesCuisine` etc. can be added to the JSON-LD object in `app/layout.tsx` if needed.\n- Mock seed: `--seed restaurant` (Mama\'s Kitchen).\n\n## Known TODOs\n\n- `/reservations` widget assumes every slot is bookable. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to actually-free windows.\n- Contact form + newsletter fake their submits.\n- Hero / dish photography is Unsplash \u2014 replace with merchant photos.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3003
3003
 
3004
3004
  import { useState } from "react";
3005
3005
 
@@ -3529,7 +3529,7 @@ export const brand: Brand = {
3529
3529
  businessId: "bus_mamas_kitchen",
3530
3530
  },
3531
3531
  };
3532
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "restaurant",\n "name": "Restaurant",\n "tagline": "Menu-driven storefront with reservations and table-service support.",\n "industry": "food",\n "tags": ["food", "restaurant", "reservations"],\n "stability": "stable",\n "schemaType": "Restaurant",\n "mock": {\n "seedName": "restaurant",\n "seedBusinessId": "bus_mamas_kitchen"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_default_akua_bakery\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (restaurant: Mama's Kitchen).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_mamas_kitchen\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-services": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Services / spa / wellness storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_serene_spa` for the bundled seed.\n\n## Aesthetic\n\n- **Fraunces serif + Inter** \u2014 warm, slightly editorial headings.\n- **Sand + sage palette** \u2014 warm cream background, deep sage primary.\n- **Generous radius**: `1rem` \u2014 calm, soft.\n- Lower-contrast than retail / fashion.\n- Schema.org `@type` is `BeautySalon`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Now booking"), service strips,\n category grid, newsletter\n shop/page.tsx Treatment menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing (e.g. memberships, packages)\n\n book/page.tsx \u2B50 Services-specific: full booking widget\n (treatment + 14-day date + 30-min slot grid)\n \u2192 adds to cart with slot as note \u2192 /checkout\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx (find a booking)\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |\n| `app/page.tsx` | `brand.hero` ("Now booking") |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |\n| `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |\n| `app/returns/page.tsx` | `brand.returns` (cancellation policy) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/book/*` | derives bookable services from product list (`type: "service"`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Services-specific notes\n\n- **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.\n- Cart-item notes carry the booked time slot \u2014 checkout sends them through Cimplify\'s order pipeline. The merchant\'s downstream system reads the note to schedule the appointment.\n- Mock seed: `--seed services` (Serene Spa).\n\n## Known TODOs\n\n- `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.\n- Contact + newsletter fake submits.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for service scheduling, booking widgets, and add-on selectors** \u2014 the SDK already gets price math, slot matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add product-page\ncimplify add booking-card\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** (`scheduled_start`, `scheduled_end`, `staff_id`, `customer_inputs`, etc.) unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3532
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "restaurant",\n "name": "Restaurant",\n "tagline": "Menu-driven storefront with reservations and table-service support.",\n "industry": "food",\n "tags": ["food", "restaurant", "reservations"],\n "stability": "stable",\n "schemaType": "Restaurant",\n "mock": {\n "seedName": "restaurant",\n "seedBusinessId": "bus_mamas_kitchen"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_default_akua_bakery\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (restaurant: Mama's Kitchen).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_mamas_kitchen\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-services": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Services / spa / wellness storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_serene_spa` for the bundled seed.\n\n## Aesthetic\n\n- **Fraunces serif + Inter** \u2014 warm, slightly editorial headings.\n- **Sand + sage palette** \u2014 warm cream background, deep sage primary.\n- **Generous radius**: `1rem` \u2014 calm, soft.\n- Lower-contrast than retail / fashion.\n- Schema.org `@type` is `BeautySalon`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Now booking"), service strips,\n category grid, newsletter\n shop/page.tsx Treatment menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing (e.g. memberships, packages)\n\n book/page.tsx \u2B50 Services-specific: full booking widget\n (treatment + 14-day date + 30-min slot grid)\n \u2192 adds to cart with slot as note \u2192 /checkout\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx (find a booking)\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |\n| `app/page.tsx` | `brand.hero` ("Now booking") |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |\n| `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |\n| `app/returns/page.tsx` | `brand.returns` (cancellation policy) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/book/*` | derives bookable services from product list (`type: "service"`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Services-specific notes\n\n- **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.\n- Cart-item notes carry the booked time slot \u2014 checkout sends them through Cimplify\'s order pipeline. The merchant\'s downstream system reads the note to schedule the appointment.\n- Mock seed: `--seed services` (Serene Spa).\n\n## Known TODOs\n\n- `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.\n- Contact + newsletter fake submits.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for service scheduling, booking widgets, and add-on selectors** \u2014 the SDK already gets price math, slot matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add product-page\ncimplify add booking-card\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** (`scheduled_start`, `scheduled_end`, `staff_id`, `customer_inputs`, etc.) unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3533
3533
 
3534
3534
  import { useState } from "react";
3535
3535
 
@@ -4034,7 +4034,7 @@ export const brand: Brand = {
4034
4034
  businessId: "bus_serene_spa",
4035
4035
  },
4036
4036
  };
4037
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "services",\n "name": "Services",\n "tagline": "Bookable-services storefront with calendar slots and staff profiles.",\n "industry": "services",\n "tags": ["services", "bookings", "appointments"],\n "stability": "stable",\n "schemaType": "BeautySalon",\n "mock": {\n "seedName": "services",\n "seedBusinessId": "bus_serene_spa"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_default_akua_bakery\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (services: Serene Spa).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_serene_spa\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-bakery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Bakery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\nTo rebrand this storefront end-to-end as a different merchant:\n\n1. **Edit `lib/brand.ts`.** Every visible string reads from this file.\n2. **Edit `app/globals.css`** \u2014 the `@theme { \u2026 }` block holds the design tokens (palette, radius, font references).\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nThat is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes \u2014 they are design-only and read from `brand`.\n\n## Rules\n\n- \u2705 All content edits \u2192 `lib/brand.ts`\n- \u2705 All design-token edits \u2192 `app/globals.css` `@theme` block\n- \u2705 Server Components stay cached via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`\n- \u2705 Client islands (header active link, cart pill, product modal) live behind `<Suspense>`\n- \u274C Don't hardcode strings. If a string isn't in `brand`, add a new field to the `Brand` interface.\n- \u274C Don't disable `cacheComponents: true` in `next.config.ts`.\n- \u274C Don't use `unstable_cache` \u2014 Next 16 uses the `'use cache'` directive.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero, collection strips, category grid\n shop/page.tsx Full catalogue (SDK <CataloguePage/>)\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation (also target of /track-order)\n\n account/page.tsx <CimplifyAccount /> (iframe \u2014 handles sign-in, dashboard)\n account/orders/page.tsx <CimplifyAccount section=\"orders\" />\n account/addresses/page.tsx <CimplifyAccount section=\"addresses\" />\n account/settings/page.tsx <CimplifyAccount section=\"settings\" />\n login/page.tsx redirect \u2192 /account (iframe owns sign-in UI)\n signup/page.tsx redirect \u2192 /account (iframe owns sign-up UI)\n\n contact/page.tsx Contact form (Server Action wiring TODO; currently fakes submit)\n track-order/page.tsx Guest order lookup \u2192 /orders/[id]\n\n about/page.tsx Brand story\n faq/page.tsx FAQ\n shipping/page.tsx Standalone shipping policy\n returns/page.tsx Standalone returns policy\n accessibility/page.tsx Accessibility statement (WCAG 2.1 AA)\n terms/page.tsx Terms of Service\n privacy/page.tsx Privacy Policy\n\n sitemap-page/page.tsx Human-readable HTML sitemap\n sitemap.ts XML sitemap (search engines)\n robots.ts robots.txt\n llms.txt/route.ts LLM-friendly Markdown index\n opensearch.xml/route.ts Browser address-bar search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Organization JSON-LD), `metadataBase` |\n| `app/page.tsx` | `brand.hero` (Hero badge / title / subtitle) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (eyebrows + titles only \u2014 Cimplify Link iframe owns the UI) |\n| `app/login/page.tsx`, `app/signup/page.tsx` | `brand.account` (metadata only \u2014 both redirect to `/account`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `app/sitemap.ts`, `robots.ts` | derives URLs from product/category data |\n| `app/not-found.tsx` | `brand.name` (page title) |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Bakery-specific notes\n\n- Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.\n- Schema.org `@type` is `Bakery` \u2014 set in `brand.schemaType`.\n- Mock seed: `--seed default` (Akua's Bakery). To preview a different industry, edit `dev:mock` in `package.json` and update `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`.\n\n## Known TODOs (not blocking, but worth knowing)\n\n- `app/contact/contact-form.tsx` fakes a successful submit. In production, wire a Server Action that calls `client.support.sendMessage(...)` from `@cimplify/sdk/server`.\n- `components/newsletter.tsx` (homepage strip) similarly fakes submit. Wire to a real list provider.\n- Hero / lookbook imagery is Unsplash placeholder \u2014 replace with merchant assets.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev # boots mock + Next dev server\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
4037
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "services",\n "name": "Services",\n "tagline": "Bookable-services storefront with calendar slots and staff profiles.",\n "industry": "services",\n "tags": ["services", "bookings", "appointments"],\n "stability": "stable",\n "schemaType": "BeautySalon",\n "mock": {\n "seedName": "services",\n "seedBusinessId": "bus_serene_spa"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nUpdate `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787\n+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_default_akua_bakery\n+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Cimplify API base URL.\n# Dev: leave empty so the SDK uses the current origin (localhost:3000),\n# which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).\n# Production: set to your Cimplify host (e.g. https://api.cimplify.io).\nNEXT_PUBLIC_CIMPLIFY_API_URL=\n\n# Tenant public key. The mock accepts any value.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Business id used by the mock seed (services: Serene Spa).\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_serene_spa\n\n# Canonical public site URL \u2014 used by sitemap.xml, robots.txt, llms.txt,\n# and OpenGraph metadata. Set this on production deploys; `cimplify env push`\n# wires it into your linked project.\nNEXT_PUBLIC_SITE_URL=https://example.com\n\n# Business handle (human-readable slug, e.g. \"akua-bakery\"). Used by the\n# UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /\n# Gemini) can discover this storefront's commerce capabilities. Set this\n# on production deploys; leave empty in dev unless you're testing UCP.\nNEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=\n" }], "storefront-bakery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Bakery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\nTo rebrand this storefront end-to-end as a different merchant:\n\n1. **Edit `lib/brand.ts`.** Every visible string reads from this file.\n2. **Edit `app/globals.css`** \u2014 the `@theme { \u2026 }` block holds the design tokens (palette, radius, font references).\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nThat is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes \u2014 they are design-only and read from `brand`.\n\n## Rules\n\n- \u2705 All content edits \u2192 `lib/brand.ts`\n- \u2705 All design-token edits \u2192 `app/globals.css` `@theme` block\n- \u2705 Server Components stay cached via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`\n- \u2705 Client islands (header active link, cart pill, product modal) live behind `<Suspense>`\n- \u274C Don't hardcode strings. If a string isn't in `brand`, add a new field to the `Brand` interface.\n- \u274C Don't disable `cacheComponents: true` in `next.config.ts`.\n- \u274C Don't use `unstable_cache` \u2014 Next 16 uses the `'use cache'` directive.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero, collection strips, category grid\n shop/page.tsx Full catalogue (SDK <CataloguePage/>)\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation (also target of /track-order)\n\n account/page.tsx <CimplifyAccount /> (iframe \u2014 handles sign-in, dashboard)\n account/orders/page.tsx <CimplifyAccount section=\"orders\" />\n account/addresses/page.tsx <CimplifyAccount section=\"addresses\" />\n account/settings/page.tsx <CimplifyAccount section=\"settings\" />\n login/page.tsx redirect \u2192 /account (iframe owns sign-in UI)\n signup/page.tsx redirect \u2192 /account (iframe owns sign-up UI)\n\n contact/page.tsx Contact form (Server Action wiring TODO; currently fakes submit)\n track-order/page.tsx Guest order lookup \u2192 /orders/[id]\n\n about/page.tsx Brand story\n faq/page.tsx FAQ\n shipping/page.tsx Standalone shipping policy\n returns/page.tsx Standalone returns policy\n accessibility/page.tsx Accessibility statement (WCAG 2.1 AA)\n terms/page.tsx Terms of Service\n privacy/page.tsx Privacy Policy\n\n sitemap-page/page.tsx Human-readable HTML sitemap\n sitemap.ts XML sitemap (search engines)\n robots.ts robots.txt\n llms.txt/route.ts LLM-friendly Markdown index\n opensearch.xml/route.ts Browser address-bar search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Organization JSON-LD), `metadataBase` |\n| `app/page.tsx` | `brand.hero` (Hero badge / title / subtitle) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (eyebrows + titles only \u2014 Cimplify Link iframe owns the UI) |\n| `app/login/page.tsx`, `app/signup/page.tsx` | `brand.account` (metadata only \u2014 both redirect to `/account`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `app/sitemap.ts`, `robots.ts` | derives URLs from product/category data |\n| `app/not-found.tsx` | `brand.name` (page title) |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Bakery-specific notes\n\n- Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.\n- Schema.org `@type` is `Bakery` \u2014 set in `brand.schemaType`.\n- Mock seed: `--seed default` (Akua's Bakery). To preview a different industry, edit `dev:mock` in `package.json` and update `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`.\n\n## Known TODOs (not blocking, but worth knowing)\n\n- `app/contact/contact-form.tsx` fakes a successful submit. In production, wire a Server Action that calls `client.support.sendMessage(...)` from `@cimplify/sdk/server`.\n- `components/newsletter.tsx` (homepage strip) similarly fakes submit. Wire to a real list provider.\n- Hero / lookbook imagery is Unsplash placeholder \u2014 replace with merchant assets.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev # boots mock + Next dev server\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
4038
4038
 
4039
4039
  import { useState } from "react";
4040
4040
 
@@ -4754,7 +4754,7 @@ export function PromoBanner() {
4754
4754
  </section>
4755
4755
  );
4756
4756
  }
4757
- ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/symptom-finder.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\ntype IconKey = (typeof brand.symptomFinder.tiles)[number]["iconKey"];\n\n/**\n * Symptom-led entry grid \u2014 the pharmacy answer to the retail\n * "category tiles" row. Eight tiles ("Pain & fever", "Cold & flu",\n * "Baby care", \u2026) each linked to the appropriate category page, with\n * an inline SVG icon keyed by `iconKey`. Strings + ordering come from\n * `brand.symptomFinder`.\n */\nexport function SymptomFinder() {\n const { eyebrow, title, description, tiles } = brand.symptomFinder;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="max-w-2xl mb-8 sm:mb-10">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.2vw,2.5rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n <p className="text-sm sm:text-base text-muted-foreground mt-3 leading-relaxed">\n {description}\n </p>\n </div>\n\n <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">\n {tiles.map((t) => (\n <Link\n key={t.label + t.iconKey}\n href={t.href}\n className="group relative flex flex-col items-start gap-3 p-5 sm:p-6 rounded-2xl bg-card border border-border hover:border-primary/40 hover:shadow-[0_10px_30px_rgb(0_0_0/0.06)] transition-all"\n >\n <span className="grid place-items-center w-11 h-11 rounded-xl bg-accent text-accent-foreground group-hover:bg-primary group-hover:text-primary-foreground transition-colors">\n {ICONS[t.iconKey]}\n </span>\n <span className="text-[15px] sm:text-base font-semibold tracking-tight">\n {t.label}\n </span>\n <span className="absolute top-5 right-5 text-muted-foreground/40 group-hover:text-primary group-hover:translate-x-0.5 transition-all">\n <ArrowIcon />\n </span>\n </Link>\n ))}\n </div>\n </section>\n );\n}\n\nconst ICONS: Record<IconKey, React.ReactNode> = {\n pain: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M12 3a4 4 0 0 0-4 4v2H6a3 3 0 0 0 0 6h2v2a4 4 0 0 0 8 0v-2h2a3 3 0 0 0 0-6h-2V7a4 4 0 0 0-4-4z" strokeLinejoin="round" />\n </svg>\n ),\n cold: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M12 3v18M3 12h18M5 5l14 14M19 5L5 19" strokeLinecap="round" />\n </svg>\n ),\n allergy: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <circle cx="12" cy="12" r="3" />\n <path d="M12 4v3M12 17v3M4 12h3M17 12h3M6 6l2 2M16 16l2 2M6 18l2-2M16 8l2-2" strokeLinecap="round" />\n </svg>\n ),\n sleep: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M21 13A9 9 0 1 1 11 3a7 7 0 0 0 10 10z" strokeLinejoin="round" />\n </svg>\n ),\n baby: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <circle cx="12" cy="9" r="4" />\n <path d="M9 9c0 .5.5 1 1 1M14 9c0 .5.5 1 1 1" strokeLinecap="round" />\n <path d="M5 20c1-3 4-5 7-5s6 2 7 5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n "first-aid": (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <rect x="3" y="7" width="18" height="13" rx="2" />\n <path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />\n <path d="M12 11v6M9 14h6" strokeLinecap="round" />\n </svg>\n ),\n vitamins: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <rect x="3.5" y="9" width="17" height="6" rx="3" transform="rotate(-30 12 12)" />\n <path d="M9 9.5l5.5 5.5" />\n </svg>\n ),\n diabetes: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M12 3l5 5a7 7 0 1 1-10 0l5-5z" strokeLinejoin="round" />\n <path d="M9 14h6M12 11v6" strokeLinecap="round" />\n </svg>\n ),\n};\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-4 h-4" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/pharmacy-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Pharmacy-specific hero. Editorial split: tagline + key trust line on\n * the left, a stack of "open now / license / delivery" info chips on\n * the right. Designed to sit directly above `<UrgentCtas />` which\n * vertically overlaps the bottom edge \u2014 so this hero stays compact\n * (no big product photo).\n */\nexport function PharmacyHero() {\n return (\n <section className="relative overflow-hidden bg-foreground text-background pb-20 sm:pb-28">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:36px_36px]" />\n <div className="absolute -top-32 -right-24 w-[28rem] h-[28rem] rounded-full bg-primary/20 blur-[120px] pointer-events-none" />\n\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 pt-14 sm:pt-20">\n <div className="grid grid-cols-1 lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-14 items-end">\n <div>\n <span className="inline-flex items-center gap-2 mb-6 px-3 py-1.5 rounded-full bg-background/10 text-background/90 text-[11px] font-mono uppercase tracking-[0.18em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />\n {brand.hero.badge}\n </span>\n <h1 className="text-[clamp(2.5rem,6.4vw,5.25rem)] font-bold m-0 -tracking-[0.04em] leading-[0.98]">\n {brand.hero.title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl mt-6">\n {brand.hero.subtitle}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-8">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-background text-foreground text-sm font-semibold hover:bg-background/90 transition-colors"\n >\n {brand.hero.primaryCtaLabel}\n <ArrowIcon />\n </Link>\n {brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref && (\n <Link\n href={brand.hero.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full border border-background/30 text-background text-sm font-semibold hover:bg-background/10 transition-colors"\n >\n {brand.hero.secondaryCtaLabel}\n </Link>\n )}\n </div>\n </div>\n\n <aside className="grid grid-cols-2 gap-3 sm:gap-4 lg:max-w-md">\n <InfoChip\n eyebrow="Open now"\n value={brand.contact.hours.split("\xB7")[0]?.trim() ?? brand.contact.hours}\n live\n />\n <InfoChip eyebrow="Delivery" value="Free \xB7 same-day \xB7 Accra" />\n <InfoChip eyebrow="Licensed" value="Pharmacy Council of Ghana" />\n <InfoChip eyebrow="Insurance" value="NHIS + private accepted" />\n </aside>\n </div>\n </div>\n </section>\n );\n}\n\ninterface InfoChipProps {\n eyebrow: string;\n value: string;\n live?: boolean;\n}\n\nfunction InfoChip({ eyebrow, value, live }: InfoChipProps) {\n return (\n <div className="rounded-2xl border border-background/15 bg-background/5 backdrop-blur-sm p-4">\n <div className="flex items-center gap-2 mb-1.5">\n <span className="text-[10px] font-mono uppercase tracking-[0.18em] text-background/55">\n {eyebrow}\n </span>\n {live && (\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />\n )}\n </div>\n <p className="text-sm font-semibold text-background -tracking-[0.01em] leading-snug">\n {value}\n </p>\n </div>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/health-brief.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Three-card editorial panel ("the pharmacist\'s brief") that replaces\n * the retail-style "best sellers" rail. Curated by the pharmacist team\n * around the time of year \u2014 harmattan, malaria season, flu shots, etc.\n * Strings come from `brand.healthBrief`.\n */\nexport function HealthBrief() {\n const { eyebrow, title, cards } = brand.healthBrief;\n return (\n <section className="relative bg-muted/60 border-y border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-24">\n <div className="max-w-2xl mb-10 sm:mb-12">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.2vw,2.5rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n </div>\n\n <div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-5">\n {cards.map((c, i) => (\n <article\n key={c.eyebrow + c.title}\n className="group flex flex-col bg-card border border-border rounded-2xl p-6 sm:p-7 hover:border-primary/40 hover:shadow-[0_14px_36px_rgb(0_0_0/0.06)] transition-all"\n >\n <div className="flex items-center justify-between mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-accent text-accent-foreground text-[11px] font-mono uppercase tracking-[0.14em]">\n {c.eyebrow}\n </span>\n <span className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground">\n No. {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <h3 className="text-xl sm:text-[22px] font-semibold m-0 -tracking-[0.02em] leading-snug">\n {c.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-muted-foreground leading-relaxed mt-3 flex-1">\n {c.body}\n </p>\n <Link\n href={c.ctaHref}\n className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-primary group-hover:gap-3 transition-all"\n >\n {c.ctaLabel}\n <ArrowIcon />\n </Link>\n </article>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Apple-style split hero. Left: copy + CTAs. Right: large product image.\n * Stacks to image-on-top on mobile. Strings come from `brand.hero` at the\n * call site \u2014 this component is otherwise design-only.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative overflow-hidden bg-foreground text-background">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">\n <div>\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">\n {eyebrow}\n </p>\n <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">\n {title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-7">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="(min-width: 1024px) 50vw, 100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n const initial = brand.shortName.charAt(0).toUpperCase();\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-2.5 group">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">\n {initial}\n </span>\n <span className="text-[18px] font-bold -tracking-[0.025em]">{brand.shortName}</span>\n <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/urgent-ctas.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Two-column urgent-action panel for the pharmacy home page. Left card is\n * prescription upload; right card is pharmacist consult. Sits directly\n * under the hero \u2014 the two things a pharmacy visitor is most likely to\n * want, surfaced before any browsing.\n */\nexport function UrgentCtas() {\n const { prescription, consult } = brand.urgentCtas;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 -mt-10 sm:-mt-14 relative z-10">\n <div className="grid grid-cols-1 md:grid-cols-2 gap-4">\n <Link\n href={prescription.ctaHref}\n className="group block bg-card border border-border rounded-2xl p-7 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.06)] hover:shadow-[0_20px_50px_rgb(0_0_0/0.10)] hover:border-primary/40 transition-all"\n >\n <div className="flex items-start justify-between gap-3 mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-accent text-accent-foreground text-[11px] font-mono uppercase tracking-[0.14em]">\n {prescription.eyebrow}\n </span>\n <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-muted text-muted-foreground text-[10px] font-mono uppercase tracking-[0.14em]">\n <LockIcon /> {prescription.badge}\n </span>\n </div>\n <UploadIllustration />\n <h3 className="text-[22px] sm:text-2xl font-semibold mt-5 -tracking-[0.02em] leading-snug">\n {prescription.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-muted-foreground leading-relaxed mt-2">\n {prescription.body}\n </p>\n <span className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-primary group-hover:gap-3 transition-all">\n {prescription.ctaLabel}\n <ArrowIcon />\n </span>\n </Link>\n\n <Link\n href={consult.ctaHref}\n className="group block bg-foreground text-background rounded-2xl p-7 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.10)] hover:shadow-[0_20px_50px_rgb(0_0_0/0.18)] transition-all"\n >\n <div className="flex items-start justify-between gap-3 mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-background/10 text-background text-[11px] font-mono uppercase tracking-[0.14em]">\n {consult.eyebrow}\n </span>\n <span className="inline-flex items-center gap-2 px-2 py-0.5 rounded-full bg-background/10 text-background/85 text-[10px] font-mono uppercase tracking-[0.14em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />\n {consult.hoursLabel}\n </span>\n </div>\n\n <div className="flex items-center gap-3 mt-2">\n <div className="grid place-items-center w-11 h-11 rounded-full bg-background/15 text-background text-base font-semibold">\n {consult.pharmacistName\n .split(" ")\n .map((s) => s[0])\n .join("")\n .slice(0, 2)\n .toUpperCase()}\n </div>\n <div>\n <p className="text-sm font-semibold text-background">{consult.pharmacistName}</p>\n <p className="text-[12px] font-mono uppercase tracking-[0.14em] text-background/60">\n {consult.hoursValue}\n </p>\n </div>\n </div>\n\n <h3 className="text-[22px] sm:text-2xl font-semibold mt-5 -tracking-[0.02em] leading-snug">\n {consult.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-background/75 leading-relaxed mt-2">\n {consult.body}\n </p>\n <span className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-background group-hover:gap-3 transition-all">\n {consult.ctaLabel}\n <ArrowIcon />\n </span>\n </Link>\n </div>\n </section>\n );\n}\n\nfunction UploadIllustration() {\n return (\n <div className="relative h-24 sm:h-28 rounded-xl border border-dashed border-primary/40 bg-primary/[0.04] grid place-items-center overflow-hidden">\n <div className="flex flex-col items-center gap-1.5 text-primary/80">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M12 3v12" strokeLinecap="round" />\n <path d="M7 8l5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />\n <path d="M5 17v3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n <span className="text-[11px] font-mono uppercase tracking-[0.16em]">Drag \xB7 JPG \xB7 PDF</span>\n </div>\n </div>\n );\n}\n\nfunction LockIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-3 h-3" aria-hidden>\n <rect x="5" y="11" width="14" height="9" rx="2" />\n <path d="M8 11V8a4 4 0 0 1 8 0v3" />\n </svg>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for pharmacy: dosage info, prescription\n * upload inputs, consent signatures, and pharmacist notes all need vertical\n * real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
4757
+ ` }, { "path": "components/trust-bar.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n delivery: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />\n <circle cx="7" cy="17" r="1.5" />\n <circle cx="18" cy="17" r="1.5" />\n </svg>\n ),\n warranty: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />\n <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n payment: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <rect x="3" y="6" width="18" height="12" rx="2" />\n <line x1="3" y1="10" x2="21" y2="10" />\n </svg>\n ),\n verified: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n support: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <path d="M18 18a8 8 0 1 0-12 0" />\n <path d="M3 18h4v3H3zM17 18h4v3h-4z" strokeLinejoin="round" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport function TrustBar() {\n const items = brand.trustItems;\n if (!items || items.length === 0) return null;\n return (\n <section className="border-y border-border bg-card">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">\n {items.map((it) => (\n <div key={it.label} className="flex items-start gap-3">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">\n {ICONS[it.iconKey] ?? FALLBACK_ICON}\n </div>\n <div className="min-w-0">\n <p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">\n {it.label}\n </p>\n <p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>\n <p className="text-xs text-muted-foreground leading-snug">{it.description}</p>\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">\n <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-3xl mx-auto">\n {badge && (\n <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">\n {badge}\n </span>\n )}\n <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">\n {subtitle}\n </p>\n )}\n </div>\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/symptom-finder.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\ntype IconKey = (typeof brand.symptomFinder.tiles)[number]["iconKey"];\n\n/**\n * Symptom-led entry grid \u2014 the pharmacy answer to the retail\n * "category tiles" row. Eight tiles ("Pain & fever", "Cold & flu",\n * "Baby care", \u2026) each linked to the appropriate category page, with\n * an inline SVG icon keyed by `iconKey`. Strings + ordering come from\n * `brand.symptomFinder`.\n */\nexport function SymptomFinder() {\n const { eyebrow, title, description, tiles } = brand.symptomFinder;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="max-w-2xl mb-8 sm:mb-10">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.2vw,2.5rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n <p className="text-sm sm:text-base text-muted-foreground mt-3 leading-relaxed">\n {description}\n </p>\n </div>\n\n <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">\n {tiles.map((t) => (\n <Link\n key={t.label + t.iconKey}\n href={t.href}\n className="group relative flex flex-col items-start gap-3 p-5 sm:p-6 rounded-2xl bg-card border border-border hover:border-primary/40 hover:shadow-[0_10px_30px_rgb(0_0_0/0.06)] transition-all"\n >\n <span className="grid place-items-center w-11 h-11 rounded-xl bg-accent text-accent-foreground group-hover:bg-primary group-hover:text-primary-foreground transition-colors">\n {ICONS[t.iconKey]}\n </span>\n <span className="text-[15px] sm:text-base font-semibold tracking-tight">\n {t.label}\n </span>\n <span className="absolute top-5 right-5 text-muted-foreground/40 group-hover:text-primary group-hover:translate-x-0.5 transition-all">\n <ArrowIcon />\n </span>\n </Link>\n ))}\n </div>\n </section>\n );\n}\n\nconst ICONS: Record<IconKey, React.ReactNode> = {\n pain: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M12 3a4 4 0 0 0-4 4v2H6a3 3 0 0 0 0 6h2v2a4 4 0 0 0 8 0v-2h2a3 3 0 0 0 0-6h-2V7a4 4 0 0 0-4-4z" strokeLinejoin="round" />\n </svg>\n ),\n cold: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M12 3v18M3 12h18M5 5l14 14M19 5L5 19" strokeLinecap="round" />\n </svg>\n ),\n allergy: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <circle cx="12" cy="12" r="3" />\n <path d="M12 4v3M12 17v3M4 12h3M17 12h3M6 6l2 2M16 16l2 2M6 18l2-2M16 8l2-2" strokeLinecap="round" />\n </svg>\n ),\n sleep: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M21 13A9 9 0 1 1 11 3a7 7 0 0 0 10 10z" strokeLinejoin="round" />\n </svg>\n ),\n baby: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <circle cx="12" cy="9" r="4" />\n <path d="M9 9c0 .5.5 1 1 1M14 9c0 .5.5 1 1 1" strokeLinecap="round" />\n <path d="M5 20c1-3 4-5 7-5s6 2 7 5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n ),\n "first-aid": (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <rect x="3" y="7" width="18" height="13" rx="2" />\n <path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />\n <path d="M12 11v6M9 14h6" strokeLinecap="round" />\n </svg>\n ),\n vitamins: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <rect x="3.5" y="9" width="17" height="6" rx="3" transform="rotate(-30 12 12)" />\n <path d="M9 9.5l5.5 5.5" />\n </svg>\n ),\n diabetes: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-6 h-6" aria-hidden>\n <path d="M12 3l5 5a7 7 0 1 1-10 0l5-5z" strokeLinejoin="round" />\n <path d="M9 14h6M12 11v6" strokeLinecap="round" />\n </svg>\n ),\n};\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-4 h-4" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/brand-marquee.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\n/**\n * Brand authority strip. Wordmark-only (avoids licensing issues with real\n * logos), monospaced for the brand-as-typography aesthetic.\n */\nexport function BrandMarquee() {\n const strip = brand.brandStrip;\n if (!strip || strip.brands.length === 0) return null;\n return (\n <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">\n <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">\n {strip.headline}\n </p>\n <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">\n {strip.brands.map((b) => (\n <span\n key={b}\n className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"\n >\n {b}\n </span>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/pharmacy-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Pharmacy-specific hero. Editorial split: tagline + key trust line on\n * the left, a stack of "open now / license / delivery" info chips on\n * the right. Designed to sit directly above `<UrgentCtas />` which\n * vertically overlaps the bottom edge \u2014 so this hero stays compact\n * (no big product photo).\n */\nexport function PharmacyHero() {\n return (\n <section className="relative overflow-hidden bg-foreground text-background pb-20 sm:pb-28">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:36px_36px]" />\n <div className="absolute -top-32 -right-24 w-[28rem] h-[28rem] rounded-full bg-primary/20 blur-[120px] pointer-events-none" />\n\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 pt-14 sm:pt-20">\n <div className="grid grid-cols-1 lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-14 items-end">\n <div>\n <span className="inline-flex items-center gap-2 mb-6 px-3 py-1.5 rounded-full bg-background/10 text-background/90 text-[11px] font-mono uppercase tracking-[0.18em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />\n {brand.hero.badge}\n </span>\n <h1 className="text-[clamp(2.5rem,6.4vw,5.25rem)] font-bold m-0 -tracking-[0.04em] leading-[0.98]">\n {brand.hero.title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl mt-6">\n {brand.hero.subtitle}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-8">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-background text-foreground text-sm font-semibold hover:bg-background/90 transition-colors"\n >\n {brand.hero.primaryCtaLabel}\n <ArrowIcon />\n </Link>\n {brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref && (\n <Link\n href={brand.hero.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-6 py-3 rounded-full border border-background/30 text-background text-sm font-semibold hover:bg-background/10 transition-colors"\n >\n {brand.hero.secondaryCtaLabel}\n </Link>\n )}\n </div>\n </div>\n\n <aside className="grid grid-cols-2 gap-3 sm:gap-4 lg:max-w-md">\n <InfoChip\n eyebrow="Open now"\n value={brand.contact.hours.split("\xB7")[0]?.trim() ?? brand.contact.hours}\n live\n />\n <InfoChip eyebrow="Delivery" value="Free \xB7 same-day \xB7 Accra" />\n <InfoChip eyebrow="Licensed" value="Pharmacy Council of Ghana" />\n <InfoChip eyebrow="Insurance" value="NHIS + private accepted" />\n </aside>\n </div>\n </div>\n </section>\n );\n}\n\ninterface InfoChipProps {\n eyebrow: string;\n value: string;\n live?: boolean;\n}\n\nfunction InfoChip({ eyebrow, value, live }: InfoChipProps) {\n return (\n <div className="rounded-2xl border border-background/15 bg-background/5 backdrop-blur-sm p-4">\n <div className="flex items-center gap-2 mb-1.5">\n <span className="text-[10px] font-mono uppercase tracking-[0.18em] text-background/55">\n {eyebrow}\n </span>\n {live && (\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />\n )}\n </div>\n <p className="text-sm font-semibold text-background -tracking-[0.01em] leading-snug">\n {value}\n </p>\n </div>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/health-brief.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Three-card editorial panel ("the pharmacist\'s brief") that replaces\n * the retail-style "best sellers" rail. Curated by the pharmacist team\n * around the time of year \u2014 harmattan, malaria season, flu shots, etc.\n * Strings come from `brand.healthBrief`.\n */\nexport function HealthBrief() {\n const { eyebrow, title, cards } = brand.healthBrief;\n return (\n <section className="relative bg-muted/60 border-y border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-24">\n <div className="max-w-2xl mb-10 sm:mb-12">\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.2vw,2.5rem)] font-semibold m-0 -tracking-[0.025em] leading-tight">\n {title}\n </h2>\n </div>\n\n <div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-5">\n {cards.map((c, i) => (\n <article\n key={c.eyebrow + c.title}\n className="group flex flex-col bg-card border border-border rounded-2xl p-6 sm:p-7 hover:border-primary/40 hover:shadow-[0_14px_36px_rgb(0_0_0/0.06)] transition-all"\n >\n <div className="flex items-center justify-between mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-accent text-accent-foreground text-[11px] font-mono uppercase tracking-[0.14em]">\n {c.eyebrow}\n </span>\n <span className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground">\n No. {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <h3 className="text-xl sm:text-[22px] font-semibold m-0 -tracking-[0.02em] leading-snug">\n {c.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-muted-foreground leading-relaxed mt-3 flex-1">\n {c.body}\n </p>\n <Link\n href={c.ctaHref}\n className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-primary group-hover:gap-3 transition-all"\n >\n {c.ctaLabel}\n <ArrowIcon />\n </Link>\n </article>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/feature-hero.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\n\ninterface FeatureHeroProps {\n eyebrow: string;\n title: React.ReactNode;\n description: string;\n primaryCta: { label: string; href: string };\n secondaryCta?: { label: string; href: string };\n imageUrl: string;\n imageAlt: string;\n badge?: string;\n}\n\n/**\n * Apple-style split hero. Left: copy + CTAs. Right: large product image.\n * Stacks to image-on-top on mobile. Strings come from `brand.hero` at the\n * call site \u2014 this component is otherwise design-only.\n */\nexport function FeatureHero({\n eyebrow,\n title,\n description,\n primaryCta,\n secondaryCta,\n imageUrl,\n imageAlt,\n badge,\n}: FeatureHeroProps) {\n return (\n <section className="relative overflow-hidden bg-foreground text-background">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">\n <div>\n {badge && (\n <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />\n {badge}\n </span>\n )}\n <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">\n {eyebrow}\n </p>\n <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">\n {title}\n </h1>\n <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">\n {description}\n </p>\n <div className="flex flex-wrap items-center gap-3 mt-7">\n <Link\n href={primaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {primaryCta.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n {secondaryCta && (\n <Link\n href={secondaryCta.href}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {secondaryCta.label}\n </Link>\n )}\n </div>\n </div>\n <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">\n <Image\n src={imageUrl}\n alt={imageAlt}\n fill\n sizes="(min-width: 1024px) 50vw, 100vw"\n className="object-cover"\n priority\n />\n <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n const initial = brand.shortName.charAt(0).toUpperCase();\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-center gap-2.5 group">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">\n {initial}\n </span>\n <span className="text-[18px] font-bold -tracking-[0.025em]">{brand.shortName}</span>\n <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">\n {brand.microTag}\n </span>\n </Link>\n <nav className="flex items-center gap-5 sm:gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n </nav>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/urgent-ctas.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Two-column urgent-action panel for the pharmacy home page. Left card is\n * prescription upload; right card is pharmacist consult. Sits directly\n * under the hero \u2014 the two things a pharmacy visitor is most likely to\n * want, surfaced before any browsing.\n */\nexport function UrgentCtas() {\n const { prescription, consult } = brand.urgentCtas;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 -mt-10 sm:-mt-14 relative z-10">\n <div className="grid grid-cols-1 md:grid-cols-2 gap-4">\n <Link\n href={prescription.ctaHref}\n className="group block bg-card border border-border rounded-2xl p-7 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.06)] hover:shadow-[0_20px_50px_rgb(0_0_0/0.10)] hover:border-primary/40 transition-all"\n >\n <div className="flex items-start justify-between gap-3 mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-accent text-accent-foreground text-[11px] font-mono uppercase tracking-[0.14em]">\n {prescription.eyebrow}\n </span>\n <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-muted text-muted-foreground text-[10px] font-mono uppercase tracking-[0.14em]">\n <LockIcon /> {prescription.badge}\n </span>\n </div>\n <UploadIllustration />\n <h3 className="text-[22px] sm:text-2xl font-semibold mt-5 -tracking-[0.02em] leading-snug">\n {prescription.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-muted-foreground leading-relaxed mt-2">\n {prescription.body}\n </p>\n <span className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-primary group-hover:gap-3 transition-all">\n {prescription.ctaLabel}\n <ArrowIcon />\n </span>\n </Link>\n\n <Link\n href={consult.ctaHref}\n className="group block bg-foreground text-background rounded-2xl p-7 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.10)] hover:shadow-[0_20px_50px_rgb(0_0_0/0.18)] transition-all"\n >\n <div className="flex items-start justify-between gap-3 mb-4">\n <span className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-background/10 text-background text-[11px] font-mono uppercase tracking-[0.14em]">\n {consult.eyebrow}\n </span>\n <span className="inline-flex items-center gap-2 px-2 py-0.5 rounded-full bg-background/10 text-background/85 text-[10px] font-mono uppercase tracking-[0.14em]">\n <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />\n {consult.hoursLabel}\n </span>\n </div>\n\n <div className="flex items-center gap-3 mt-2">\n <div className="grid place-items-center w-11 h-11 rounded-full bg-background/15 text-background text-base font-semibold">\n {consult.pharmacistName\n .split(" ")\n .map((s) => s[0])\n .join("")\n .slice(0, 2)\n .toUpperCase()}\n </div>\n <div>\n <p className="text-sm font-semibold text-background">{consult.pharmacistName}</p>\n <p className="text-[12px] font-mono uppercase tracking-[0.14em] text-background/60">\n {consult.hoursValue}\n </p>\n </div>\n </div>\n\n <h3 className="text-[22px] sm:text-2xl font-semibold mt-5 -tracking-[0.02em] leading-snug">\n {consult.title}\n </h3>\n <p className="text-sm sm:text-[15px] text-background/75 leading-relaxed mt-2">\n {consult.body}\n </p>\n <span className="inline-flex items-center gap-2 mt-5 text-sm font-semibold text-background group-hover:gap-3 transition-all">\n {consult.ctaLabel}\n <ArrowIcon />\n </span>\n </Link>\n </div>\n </section>\n );\n}\n\nfunction UploadIllustration() {\n return (\n <div className="relative h-24 sm:h-28 rounded-xl border border-dashed border-primary/40 bg-primary/[0.04] grid place-items-center overflow-hidden">\n <div className="flex flex-col items-center gap-1.5 text-primary/80">\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M12 3v12" strokeLinecap="round" />\n <path d="M7 8l5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />\n <path d="M5 17v3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n <span className="text-[11px] font-mono uppercase tracking-[0.16em]">Drag \xB7 JPG \xB7 PDF</span>\n </div>\n </div>\n );\n}\n\nfunction LockIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-3 h-3" aria-hidden>\n <rect x="5" y="11" width="14" height="9" rx="2" />\n <path d="M8 11V8a4 4 0 0 1 8 0v3" />\n </svg>\n );\n}\n\nfunction ArrowIcon() {\n return (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5" aria-hidden>\n <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n );\n}\n' }, { "path": "components/newsletter.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { brand } from "@/lib/brand";\n\nexport function Newsletter() {\n const n = brand.newsletter;\n const [email, setEmail] = useState("");\n const [submitted, setSubmitted] = useState(false);\n\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="rounded-3xl border border-border bg-card p-8 sm:p-12 grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {n.eyebrow}\n </p>\n <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-bold m-0 mb-3 -tracking-[0.025em]">\n {n.title}\n </h2>\n <p className="text-muted-foreground leading-relaxed">{n.body}</p>\n </div>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n setSubmitted(true);\n }}\n className="flex flex-col sm:flex-row gap-2"\n >\n <input\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={n.placeholder}\n disabled={submitted}\n className="flex-1 px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-shadow text-sm disabled:opacity-50"\n />\n <button\n type="submit"\n disabled={submitted}\n className="inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-foreground text-background font-semibold text-sm hover:bg-primary transition-colors disabled:opacity-50"\n >\n {submitted ? n.successLabel : n.submitLabel}\n </button>\n </form>\n </div>\n </section>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n/**\n * Boots the Cimplify SDK client once on the client-side and exposes it via\n * <CimplifyProvider/>.\n *\n * Base-URL resolution:\n * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.\n * 2) Otherwise use the current origin so requests flow through the\n * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).\n * 3) Fall back to 127.0.0.1:8787 only during SSR, when there\'s no window.\n */\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||\n (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">\n <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <div className="flex items-center gap-2.5 mb-4">\n <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">\n {brand.shortName.charAt(0).toUpperCase()}\n </span>\n <span className="text-background text-lg font-bold -tracking-[0.025em]">\n {brand.name}\n </span>\n </div>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-background transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0 text-xs">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link href={link.href} className="hover:text-background transition-colors">\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n\n <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="opacity-70">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that links to the dedicated product page at\n * `/products/<slug>`. Statically pre-renderable (no `useSearchParams`),\n * with `prefetch` enabled so hover \u2192 instant nav. Full product page\n * (vs a modal) is the right pattern for pharmacy: dosage info, prescription\n * upload inputs, consent signatures, and pharmacist notes all need vertical\n * real estate.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `/products/${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/section-heading.tsx", "kind": "text", "content": 'import Link from "next/link";\n\ninterface SectionHeadingProps {\n eyebrow: string;\n title: string;\n description?: string;\n link?: { label: string; href: string };\n}\n\nexport function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {\n return (\n <div className="flex items-end justify-between gap-6 mb-8">\n <div className="max-w-2xl">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">\n {title}\n </h2>\n {description && (\n <p className="mt-2 text-muted-foreground">{description}</p>\n )}\n </div>\n {link && (\n <Link\n href={link.href}\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n {link.label}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n )}\n </div>\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\nconst MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";\n\n/**\n * Same-origin proxy to the Cimplify mock so the browser never makes a\n * cross-origin request in dev (no CORS preflights, no flaky `Origin`\n * mismatches). The SDK\'s base URL stays empty/relative \u2014 requests go to\n * `/api/v1/...` on the Next origin, then this rewrite forwards them to\n * the mock at `127.0.0.1:8787`.\n *\n * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify\n * host and the rewrite continues to work the same way (or remove it and\n * pass the absolute URL through `<CimplifyProvider/>`).\n */\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/signup/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
4758
4758
 
4759
4759
  import { useState } from "react";
4760
4760