@carrierllc/mcp 0.2.19 → 0.2.20

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.
Files changed (41) hide show
  1. package/README.md +22 -1
  2. package/dist/chunk-NHQDO2E7.js +461 -0
  3. package/dist/chunk-NHQDO2E7.js.map +1 -0
  4. package/dist/cli.js +822 -105
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.js +144 -315
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/plugin/carrier/commands/storefront.md +24 -12
  10. package/plugin/carrier/skills/carrier-operations/SKILL.md +4 -1
  11. package/templates/storefront/{next.config.ts → next.config.mjs} +2 -6
  12. package/templates/storefront/public/brand/logo.svg +13 -0
  13. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +12 -12
  14. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +5 -5
  15. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +29 -29
  16. package/templates/storefront/src/app/checkout/success/page.tsx +1 -1
  17. package/templates/storefront/src/app/contact/page.tsx +33 -25
  18. package/templates/storefront/src/app/dashboard/page.tsx +7 -4
  19. package/templates/storefront/src/app/globals.css +72 -82
  20. package/templates/storefront/src/app/help/page.tsx +22 -17
  21. package/templates/storefront/src/app/layout.tsx +15 -4
  22. package/templates/storefront/src/app/page.tsx +16 -7
  23. package/templates/storefront/src/app/shop/ShopClient.tsx +7 -3
  24. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +1 -1
  25. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +16 -16
  26. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +1 -1
  27. package/templates/storefront/src/brand.config.ts +3 -3
  28. package/templates/storefront/src/components/faq/FaqSection.tsx +23 -26
  29. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +37 -22
  30. package/templates/storefront/src/components/landing/FinalCTA.tsx +28 -0
  31. package/templates/storefront/src/components/landing/HeroSection.tsx +57 -24
  32. package/templates/storefront/src/components/landing/HowItWorks.tsx +44 -0
  33. package/templates/storefront/src/components/landing/PlanCard.tsx +58 -28
  34. package/templates/storefront/src/components/nav/StorefrontNav.tsx +59 -24
  35. package/templates/storefront/src/components/theme/BrandStyles.tsx +8 -0
  36. package/templates/storefront/src/components/theme/ThemeToggle.tsx +27 -0
  37. package/templates/storefront/src/components/theme/clerk-appearance.ts +33 -35
  38. package/templates/storefront/src/components/theme/theme-provider.tsx +25 -32
  39. package/templates/storefront/src/components/theme/theme-script.tsx +4 -7
  40. package/templates/storefront/src/lib/complete-email-sign-up.ts +3 -3
  41. package/templates/storefront/src/vendor/ui/brand.ts +6 -6
@@ -1,41 +1,61 @@
1
1
  import Link from "next/link";
2
- import { BRAND } from "@/vendor/ui/brand";
2
+ import { BRAND, buildSupportLink } from "@/vendor/ui/brand";
3
3
 
4
4
  const FOOTER_LINKS = {
5
5
  Product: [
6
6
  { label: "Shop eSIMs", href: "/shop" },
7
+ { label: "How it works", href: "/#how-it-works" },
8
+ { label: "Help", href: "/help" },
7
9
  { label: "Dashboard", href: "/dashboard" },
8
- { label: "Help Center", href: "/help" },
9
10
  ],
10
11
  Company: [
11
12
  { label: "Contact", href: "/contact" },
12
13
  { label: "Privacy", href: "/legal/privacy" },
13
14
  { label: "Terms", href: "/legal/terms" },
14
- { label: "Acceptable Use", href: "/legal/acceptable-use" },
15
+ { label: "Acceptable use", href: "/legal/acceptable-use" },
15
16
  ],
16
17
  };
17
18
 
18
19
  export function StorefrontFooter() {
19
20
  return (
20
- <footer className="border-t border-white/10 bg-[var(--brand-bg)]">
21
- <div className="mx-auto max-w-6xl px-4 py-12">
22
- <div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-10">
21
+ <footer className="bg-sand-950 pb-8 pt-16 text-sand-400">
22
+ <div className="mx-auto max-w-6xl px-4">
23
+ <div className="mb-12 grid grid-cols-2 gap-8 md:grid-cols-4">
23
24
  <div className="col-span-2">
24
- <p className="font-semibold text-white mb-2">{BRAND.name}</p>
25
- <p className="text-sm text-white/50 max-w-xs">{BRAND.tagline}</p>
25
+ <p className="mb-2 text-base font-semibold text-sand-50">{BRAND.name}</p>
26
+ <p className="mb-5 max-w-xs text-sm leading-relaxed">{BRAND.tagline}</p>
27
+ <div className="flex flex-wrap gap-2">
28
+ <Link
29
+ href={buildSupportLink("whatsapp", { message: `Hi ${BRAND.name}!` })}
30
+ target="_blank"
31
+ rel="noopener noreferrer"
32
+ className="inline-flex items-center gap-2 rounded-full bg-leaf-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-leaf-600"
33
+ >
34
+ WhatsApp · 24/7
35
+ </Link>
36
+ <Link
37
+ href={buildSupportLink("sms")}
38
+ className="inline-flex items-center gap-2 rounded-full border border-sand-700 bg-sand-900/60 px-4 py-2 text-sm font-semibold text-sand-200 hover:border-[var(--brand-accent)]/40"
39
+ >
40
+ SMS
41
+ </Link>
42
+ <Link
43
+ href={`mailto:${BRAND.support.email}`}
44
+ className="inline-flex items-center gap-2 rounded-full border border-sand-700 bg-sand-900/60 px-4 py-2 text-sm font-semibold text-sand-200 hover:border-[var(--brand-accent)]/40"
45
+ >
46
+ Email
47
+ </Link>
48
+ </div>
26
49
  </div>
27
50
  {Object.entries(FOOTER_LINKS).map(([section, links]) => (
28
51
  <div key={section}>
29
- <p className="text-xs font-semibold uppercase tracking-wider text-white/40 mb-3">
52
+ <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-sand-500">
30
53
  {section}
31
54
  </p>
32
55
  <ul className="space-y-2">
33
56
  {links.map((link) => (
34
57
  <li key={link.href}>
35
- <Link
36
- href={link.href}
37
- className="text-sm text-white/60 hover:text-white transition-colors"
38
- >
58
+ <Link href={link.href} className="text-sm text-sand-400 transition-colors hover:text-sand-50">
39
59
  {link.label}
40
60
  </Link>
41
61
  </li>
@@ -44,16 +64,11 @@ export function StorefrontFooter() {
44
64
  </div>
45
65
  ))}
46
66
  </div>
47
- <div className="border-t border-white/10 pt-6 flex flex-col md:flex-row items-center justify-between gap-2">
48
- <p className="text-xs text-white/40">
49
- &copy; {new Date().getFullYear()} {BRAND.legalName}. All rights reserved.
67
+ <div className="flex flex-col items-start justify-between gap-2 border-t border-sand-800 pt-6 md:flex-row md:items-center">
68
+ <p className="text-xs text-sand-600">
69
+ © {new Date().getFullYear()} {BRAND.legalName}. All rights reserved.
50
70
  </p>
51
- <a
52
- href={`mailto:${BRAND.support.email}`}
53
- className="text-xs text-white/40 hover:text-white transition-colors"
54
- >
55
- {BRAND.support.email}
56
- </a>
71
+ <p className="text-xs text-sand-600">Real humans. 24/7.</p>
57
72
  </div>
58
73
  </div>
59
74
  </footer>
@@ -0,0 +1,28 @@
1
+ import Link from "next/link";
2
+ import brand from "@/brand.config";
3
+
4
+ export function FinalCTA() {
5
+ return (
6
+ <section className="px-4 py-20">
7
+ <div
8
+ className="mx-auto max-w-6xl overflow-hidden rounded-[2rem] px-8 py-14 text-center md:px-16"
9
+ style={{
10
+ background: `linear-gradient(135deg, color-mix(in srgb, var(--brand-accent) 88%, #1A1812), var(--brand-accent-dark))`,
11
+ }}
12
+ >
13
+ <h2 className="text-3xl font-semibold tracking-tight text-white md:text-4xl">
14
+ Travel light. Land connected.
15
+ </h2>
16
+ <p className="mx-auto mt-3 max-w-lg text-sm text-white/85">
17
+ {brand.tagline} Real humans on WhatsApp, 24/7.
18
+ </p>
19
+ <Link
20
+ href="/shop"
21
+ className="mt-8 inline-flex rounded-full bg-white px-7 py-3 text-sm font-semibold text-sand-900 hover:bg-sand-50"
22
+ >
23
+ Browse plans
24
+ </Link>
25
+ </div>
26
+ </section>
27
+ );
28
+ }
@@ -1,51 +1,84 @@
1
- "use client";
2
-
3
1
  import Link from "next/link";
4
- import { motion } from "framer-motion";
5
2
  import brand from "@/brand.config";
6
3
 
4
+ const STATS = [
5
+ { value: "200+", label: "countries" },
6
+ { value: "<30s", label: "to activate" },
7
+ { value: "24/7", label: "human support" },
8
+ ];
9
+
7
10
  export function HeroSection() {
8
11
  return (
9
- <section className="relative min-h-screen flex items-center justify-center overflow-hidden bg-[var(--brand-bg)]">
10
- {/* Radial glow */}
12
+ <section className="relative overflow-hidden">
11
13
  <div
14
+ aria-hidden
12
15
  className="pointer-events-none absolute inset-0"
13
16
  style={{
14
- background: `radial-gradient(ellipse 80% 60% at 50% 0%, ${brand.colors.accent}22 0%, transparent 70%)`,
17
+ background: `radial-gradient(ellipse 70% 50% at 80% 10%, color-mix(in srgb, var(--brand-accent) 22%, transparent), transparent 70%)`,
15
18
  }}
16
19
  />
17
-
18
- <div className="relative z-10 mx-auto max-w-4xl px-4 text-center">
19
- <motion.div
20
- initial={{ opacity: 0, y: 24 }}
21
- animate={{ opacity: 1, y: 0 }}
22
- transition={{ duration: 0.6, ease: "easeOut" }}
23
- >
24
- <p className="mb-4 inline-flex items-center gap-2 rounded-full border border-[var(--brand-accent)]/30 bg-[var(--brand-accent)]/10 px-3 py-1 text-xs font-medium text-[var(--brand-accent)]">
20
+ <div className="relative mx-auto grid max-w-6xl items-center gap-12 px-4 py-20 md:grid-cols-2 md:py-28">
21
+ <div>
22
+ <p className="mb-5 inline-flex rounded-full border border-sand-200 bg-sand-100/80 px-3 py-1 text-xs font-medium text-sand-700 dark:border-sand-700 dark:bg-sand-900 dark:text-sand-200">
25
23
  Global eSIM connectivity
26
24
  </p>
27
- <h1 className="text-5xl md:text-7xl font-bold tracking-tight text-white mb-6 leading-none">
28
- Stay connected,{" "}
29
- <span style={{ color: brand.colors.accent }}>everywhere.</span>
25
+ <h1 className="font-display text-4xl font-semibold tracking-tight text-sand-900 sm:text-5xl md:text-6xl dark:text-sand-50">
26
+ Stay connected,
27
+ <span className="block" style={{ color: "var(--brand-accent)" }}>
28
+ everywhere.
29
+ </span>
30
30
  </h1>
31
- <p className="text-lg md:text-xl text-white/60 mb-10 max-w-2xl mx-auto">
32
- {brand.tagline} Instant digital SIM activation. No physical cards, no roaming fees.
31
+ <p className="mt-5 max-w-md text-base leading-relaxed text-sand-600 dark:text-sand-400">
32
+ {brand.tagline} Instant QR activation. No physical cards. No roaming surprises.
33
33
  </p>
34
- <div className="flex flex-wrap gap-4 justify-center">
34
+ <div className="mt-8 flex flex-wrap items-center gap-3">
35
35
  <Link
36
36
  href="/shop"
37
- className="rounded-full bg-[var(--brand-accent)] px-8 py-3 font-semibold text-white hover:bg-[var(--brand-accent-dark)] transition-colors"
37
+ className="rounded-full px-7 py-3 text-sm font-semibold text-white transition-opacity hover:opacity-90"
38
+ style={{ background: "var(--brand-accent)" }}
38
39
  >
39
40
  Browse plans
40
41
  </Link>
41
42
  <Link
42
- href="/help"
43
- className="rounded-full border border-white/20 px-8 py-3 font-semibold text-white/80 hover:bg-white/5 transition-colors"
43
+ href="/#how-it-works"
44
+ className="rounded-full border border-sand-300 bg-white px-7 py-3 text-sm font-semibold text-sand-800 transition-colors hover:border-sand-400 dark:border-sand-700 dark:bg-sand-900 dark:text-sand-100"
44
45
  >
45
46
  How it works
46
47
  </Link>
47
48
  </div>
48
- </motion.div>
49
+ <dl className="mt-12 grid max-w-md grid-cols-3 gap-4">
50
+ {STATS.map((stat) => (
51
+ <div key={stat.label}>
52
+ <dt className="text-2xl font-semibold text-sand-900 dark:text-sand-50">{stat.value}</dt>
53
+ <dd className="text-xs text-sand-500">{stat.label}</dd>
54
+ </div>
55
+ ))}
56
+ </dl>
57
+ </div>
58
+ <div className="relative hidden md:block">
59
+ <div className="grid grid-cols-2 gap-3">
60
+ {[
61
+ { src: "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=640&q=70&auto=format&fit=crop", label: "Paris" },
62
+ { src: "https://images.unsplash.com/photo-1492571350019-22de08371fd3?w=640&q=70&auto=format&fit=crop", label: "Tokyo" },
63
+ { src: "https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=640&q=70&auto=format&fit=crop", label: "Dubai" },
64
+ { src: "https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=640&q=70&auto=format&fit=crop", label: "London" },
65
+ ].map((item) => (
66
+ <figure
67
+ key={item.label}
68
+ className="relative overflow-hidden rounded-2xl border border-sand-200 bg-sand-100 shadow-sm dark:border-sand-800"
69
+ >
70
+ <img
71
+ src={item.src}
72
+ alt={item.label}
73
+ className="aspect-[4/3] w-full object-cover"
74
+ />
75
+ <figcaption className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-sand-950/70 to-transparent px-3 py-2 text-xs font-medium text-white">
76
+ {item.label}
77
+ </figcaption>
78
+ </figure>
79
+ ))}
80
+ </div>
81
+ </div>
49
82
  </div>
50
83
  </section>
51
84
  );
@@ -0,0 +1,44 @@
1
+ const STEPS = [
2
+ {
3
+ n: "01",
4
+ title: "Pick a plan",
5
+ body: "Choose coverage for one country, a region, or the whole map. Price is on the card.",
6
+ },
7
+ {
8
+ n: "02",
9
+ title: "Pay once",
10
+ body: "Checkout takes a minute. You get a QR code by email as soon as payment clears.",
11
+ },
12
+ {
13
+ n: "03",
14
+ title: "Scan and land connected",
15
+ body: "Open Settings, add the eSIM, done. Most people are online in under 30 seconds.",
16
+ },
17
+ ];
18
+
19
+ export function HowItWorks() {
20
+ return (
21
+ <section id="how-it-works" className="border-y border-sand-200 bg-sand-100/60 py-20 dark:border-sand-800 dark:bg-sand-900/40">
22
+ <div className="mx-auto max-w-6xl px-4">
23
+ <p className="text-xs font-semibold uppercase tracking-[0.18em] text-sand-500">How it works</p>
24
+ <h2 className="mt-2 text-3xl font-semibold tracking-tight text-sand-900 dark:text-sand-50">
25
+ Connected before you reach the gate.
26
+ </h2>
27
+ <div className="mt-10 grid gap-6 md:grid-cols-3">
28
+ {STEPS.map((step) => (
29
+ <div
30
+ key={step.n}
31
+ className="rounded-2xl border border-sand-200 bg-white p-6 dark:border-sand-800 dark:bg-sand-950"
32
+ >
33
+ <p className="text-sm font-semibold" style={{ color: "var(--brand-accent)" }}>
34
+ {step.n}
35
+ </p>
36
+ <h3 className="mt-3 text-lg font-semibold text-sand-900 dark:text-sand-50">{step.title}</h3>
37
+ <p className="mt-2 text-sm leading-relaxed text-sand-600 dark:text-sand-400">{step.body}</p>
38
+ </div>
39
+ ))}
40
+ </div>
41
+ </div>
42
+ </section>
43
+ );
44
+ }
@@ -1,61 +1,91 @@
1
1
  "use client";
2
2
 
3
+ import Link from "next/link";
3
4
  import { motion } from "framer-motion";
4
5
  import { cn } from "@/vendor/ui/cn";
5
6
  import type { SkuTemplate } from "@/vendor/carrier/types";
6
7
 
7
8
  interface PlanCardProps {
8
9
  plan: SkuTemplate;
10
+ href?: string;
9
11
  onSelect?: (plan: SkuTemplate) => void;
10
12
  featured?: boolean;
11
13
  }
12
14
 
13
- export function PlanCard({ plan, onSelect, featured }: PlanCardProps) {
14
- return (
15
- <motion.div
16
- whileHover={{ y: -4 }}
17
- className={cn(
18
- "relative rounded-2xl border p-6 flex flex-col gap-4 cursor-pointer transition-colors",
19
- featured
20
- ? "border-[var(--brand-accent)] bg-[var(--brand-accent)]/5"
21
- : "border-white/10 bg-white/5 hover:border-white/20",
22
- )}
23
- onClick={() => onSelect?.(plan)}
24
- >
15
+ export function PlanCard({ plan, href, onSelect, featured }: PlanCardProps) {
16
+ const destination = href ?? (onSelect ? undefined : `/checkout/${plan.id}`);
17
+
18
+ function handleSelect() {
19
+ onSelect?.(plan);
20
+ }
21
+
22
+ const inner = (
23
+ <>
25
24
  {featured && (
26
- <span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-[var(--brand-accent)] px-3 py-0.5 text-xs font-semibold text-white">
25
+ <span
26
+ className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rounded-full px-3 py-0.5 text-xs font-semibold text-white"
27
+ style={{ background: "var(--brand-accent)" }}
28
+ >
27
29
  Most popular
28
30
  </span>
29
31
  )}
30
32
  <div>
31
- <p className="font-semibold text-white">{plan.name}</p>
32
- <p className="text-sm text-white/50 mt-0.5">{plan.description}</p>
33
+ <p className="font-semibold text-sand-900 dark:text-sand-50">{plan.name}</p>
34
+ <p className="mt-0.5 text-sm text-sand-500">{plan.description}</p>
33
35
  </div>
34
36
  <div className="flex items-baseline gap-1">
35
- <span className="text-3xl font-bold text-white">
37
+ <span className="text-3xl font-bold text-sand-900 dark:text-sand-50">
36
38
  €{(plan.price_cents / 100).toFixed(2)}
37
39
  </span>
38
- <span className="text-sm text-white/40">one-time</span>
40
+ <span className="text-sm text-sand-500">one-time</span>
39
41
  </div>
40
- <ul className="space-y-1.5 text-sm text-white/70">
41
- {plan.features.map((f, i) => (
42
- <li key={i} className="flex items-center gap-2">
43
- <span className="text-[var(--brand-accent)]">✓</span>
42
+ <ul className="space-y-1.5 text-sm text-sand-700 dark:text-sand-300">
43
+ {plan.features.map((f) => (
44
+ <li key={f} className="flex items-center gap-2">
45
+ <span style={{ color: "var(--brand-accent)" }}>✓</span>
44
46
  {f}
45
47
  </li>
46
48
  ))}
47
49
  </ul>
48
- <button
50
+ <span
49
51
  className={cn(
50
- "mt-auto rounded-full py-2 text-sm font-semibold transition-colors",
52
+ "mt-auto block rounded-full py-2 text-center text-sm font-semibold transition-colors",
51
53
  featured
52
- ? "bg-[var(--brand-accent)] text-white hover:bg-[var(--brand-accent-dark)]"
53
- : "border border-white/20 text-white hover:bg-white/10",
54
+ ? "text-white"
55
+ : "border border-sand-300 text-sand-900 hover:bg-sand-100 dark:border-sand-700 dark:text-sand-50 dark:hover:bg-sand-800",
54
56
  )}
55
- onClick={() => onSelect?.(plan)}
57
+ style={featured ? { background: "var(--brand-accent)" } : undefined}
56
58
  >
57
59
  Select plan
58
- </button>
59
- </motion.div>
60
+ </span>
61
+ </>
62
+ );
63
+
64
+ const cardClass = cn(
65
+ "relative flex flex-col gap-4 rounded-2xl border p-6 pt-8 transition-colors",
66
+ featured
67
+ ? "border-[var(--brand-accent)] bg-[color-mix(in_srgb,var(--brand-accent)_6%,white)] dark:bg-[color-mix(in_srgb,var(--brand-accent)_12%,#0F0E0A)]"
68
+ : "border-sand-200 bg-white hover:border-sand-300 dark:border-sand-800 dark:bg-sand-900 dark:hover:border-sand-700",
69
+ );
70
+
71
+ if (destination && !onSelect) {
72
+ return (
73
+ <motion.div whileHover={{ y: -4 }}>
74
+ <Link href={destination} className={cardClass}>
75
+ {inner}
76
+ </Link>
77
+ </motion.div>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <motion.button
83
+ type="button"
84
+ whileHover={{ y: -4 }}
85
+ className={cn(cardClass, "w-full text-left")}
86
+ onClick={handleSelect}
87
+ >
88
+ {inner}
89
+ </motion.button>
60
90
  );
61
91
  }
@@ -1,69 +1,104 @@
1
1
  "use client";
2
2
 
3
+ import { useState } from "react";
3
4
  import Link from "next/link";
4
5
  import { usePathname } from "next/navigation";
5
6
  import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
6
7
  import { cn } from "@/vendor/ui/cn";
7
8
  import brand from "@/brand.config";
9
+ import { ThemeToggle } from "@/components/theme/ThemeToggle";
8
10
 
9
11
  const NAV_LINKS = [
10
- { label: "Shop", href: "/shop" },
12
+ { label: "Plans", href: "/shop" },
13
+ { label: "How it works", href: "/#how-it-works" },
11
14
  { label: "Help", href: "/help" },
12
15
  ];
13
16
 
14
17
  export function StorefrontNav() {
15
18
  const pathname = usePathname();
19
+ const [open, setOpen] = useState(false);
16
20
 
17
21
  return (
18
- <header className="fixed top-0 inset-x-0 z-50 h-16 border-b border-white/10 bg-[var(--brand-bg)]/80 backdrop-blur-md">
19
- <div className="mx-auto max-w-6xl h-full px-4 flex items-center justify-between">
20
- <Link href="/" className="flex items-center gap-2 font-semibold text-white">
22
+ <header className="fixed inset-x-0 top-0 z-50 border-b border-sand-200/80 bg-sand-50/80 backdrop-blur-md dark:border-sand-800 dark:bg-sand-950/80">
23
+ <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
24
+ <Link href="/" className="flex items-center gap-2 text-base font-semibold tracking-tight text-sand-900 dark:text-sand-50">
25
+ <img src="/brand/logo.svg" alt="" width={32} height={32} className="size-8 rounded-lg" />
21
26
  {brand.name}
22
27
  </Link>
23
28
 
24
- <nav className="hidden md:flex items-center gap-6">
25
- {NAV_LINKS.map((link) => (
26
- <Link
27
- key={link.href}
28
- href={link.href}
29
- className={cn(
30
- "text-sm transition-colors",
31
- pathname?.startsWith(link.href)
32
- ? "text-[var(--brand-accent)]"
33
- : "text-white/70 hover:text-white",
34
- )}
35
- >
36
- {link.label}
37
- </Link>
38
- ))}
29
+ <nav className="hidden items-center gap-7 md:flex">
30
+ {NAV_LINKS.map((link) => {
31
+ const active = link.href !== "/#how-it-works" && pathname?.startsWith(link.href);
32
+ return (
33
+ <Link
34
+ key={link.href}
35
+ href={link.href}
36
+ className={cn(
37
+ "text-sm transition-colors",
38
+ active
39
+ ? "font-medium text-[var(--brand-accent)]"
40
+ : "text-sand-600 hover:text-sand-900 dark:text-sand-400 dark:hover:text-sand-50",
41
+ )}
42
+ >
43
+ {link.label}
44
+ </Link>
45
+ );
46
+ })}
39
47
  </nav>
40
48
 
41
- <div className="flex items-center gap-3">
49
+ <div className="flex items-center gap-2 sm:gap-3">
50
+ <ThemeToggle />
42
51
  <SignedOut>
43
52
  <Link
44
53
  href="/sign-in"
45
- className="text-sm text-white/70 hover:text-white transition-colors"
54
+ className="hidden text-sm text-sand-600 hover:text-sand-900 dark:text-sand-400 dark:hover:text-sand-50 sm:inline"
46
55
  >
47
56
  Sign in
48
57
  </Link>
49
58
  <Link
50
59
  href="/shop"
51
- className="rounded-full bg-[var(--brand-accent)] px-4 py-1.5 text-sm font-medium text-white hover:bg-[var(--brand-accent-dark)] transition-colors"
60
+ className="rounded-full px-4 py-1.5 text-sm font-semibold text-white transition-opacity hover:opacity-90"
61
+ style={{ background: "var(--brand-accent)" }}
52
62
  >
53
- Get eSIM
63
+ Get connected
54
64
  </Link>
55
65
  </SignedOut>
56
66
  <SignedIn>
57
67
  <Link
58
68
  href="/dashboard"
59
- className="text-sm text-white/70 hover:text-white transition-colors"
69
+ className="hidden text-sm text-sand-600 hover:text-sand-900 dark:text-sand-400 dark:hover:text-sand-50 sm:inline"
60
70
  >
61
71
  Dashboard
62
72
  </Link>
63
73
  <UserButton afterSignOutUrl="/" />
64
74
  </SignedIn>
75
+ <button
76
+ type="button"
77
+ className="inline-flex size-9 items-center justify-center rounded-full border border-sand-200 text-sand-800 md:hidden dark:border-sand-700 dark:text-sand-100"
78
+ aria-label={open ? "Close menu" : "Open menu"}
79
+ onClick={() => setOpen((v) => !v)}
80
+ >
81
+ <span className="text-lg leading-none">{open ? "×" : "☰"}</span>
82
+ </button>
65
83
  </div>
66
84
  </div>
85
+
86
+ {open && (
87
+ <div className="border-t border-sand-200 bg-sand-50 px-4 py-4 md:hidden dark:border-sand-800 dark:bg-sand-950">
88
+ <nav className="flex flex-col gap-3">
89
+ {NAV_LINKS.map((link) => (
90
+ <Link
91
+ key={link.href}
92
+ href={link.href}
93
+ onClick={() => setOpen(false)}
94
+ className="text-sm font-medium text-sand-800 dark:text-sand-100"
95
+ >
96
+ {link.label}
97
+ </Link>
98
+ ))}
99
+ </nav>
100
+ </div>
101
+ )}
67
102
  </header>
68
103
  );
69
104
  }
@@ -0,0 +1,8 @@
1
+ import brand from "@/brand.config";
2
+
3
+ /** Inject operator accent at runtime so editing brand.config.ts actually re-skins the shop. */
4
+ export function BrandStyles() {
5
+ const { accent, accentDark, accentLight } = brand.colors;
6
+ const css = `:root{--brand-accent:${accent};--brand-accent-dark:${accentDark};--brand-accent-light:${accentLight};}`;
7
+ return <style dangerouslySetInnerHTML={{ __html: css }} />;
8
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { useTheme } from "@/components/theme/theme-provider";
4
+
5
+ export function ThemeToggle() {
6
+ const { theme, toggle } = useTheme();
7
+ const dark = theme === "dark";
8
+ return (
9
+ <button
10
+ type="button"
11
+ onClick={toggle}
12
+ aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}
13
+ className="inline-flex size-9 items-center justify-center rounded-full border border-sand-200 text-sand-700 transition-colors hover:border-sand-400 hover:text-sand-900 dark:border-sand-700 dark:text-sand-200 dark:hover:border-sand-500 dark:hover:text-sand-50"
14
+ >
15
+ {dark ? (
16
+ <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
17
+ <circle cx="12" cy="12" r="4" />
18
+ <path d="M12 3v2M12 19v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M3 12h2M19 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
19
+ </svg>
20
+ ) : (
21
+ <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
22
+ <path d="M21 14.5A8.5 8.5 0 1 1 9.5 3 7 7 0 0 0 21 14.5z" />
23
+ </svg>
24
+ )}
25
+ </button>
26
+ );
27
+ }
@@ -1,51 +1,49 @@
1
- // Inline brand colors — no external package dependency
2
- const colors = {
3
- bg: "#080C16",
4
- accent: "#FF6B35",
5
- accentDark: "#D9461C",
6
- text: "#F5F1EA",
7
- textSecondary: "#C9CCD6",
8
- surface: "#111827",
9
- border: "#1F2937",
1
+ import brand from "@/brand.config";
2
+
3
+ const accent = brand.colors.accent;
4
+ const sand = {
5
+ paper: "#FAFAF7",
6
+ card: "#FFFFFF",
7
+ ink: "#1A1812",
8
+ muted: "#5C5644",
9
+ border: "#E8E4D8",
10
10
  };
11
11
 
12
- // Using a plain object; Clerk accepts this shape via the `appearance` prop.
13
- // Typed as `object` to avoid importing Clerk's internal Appearance type.
14
12
  export const clerkAppearance: object = {
15
13
  variables: {
16
- colorPrimary: colors.accent,
17
- colorBackground: colors.surface,
18
- colorInputBackground: colors.bg,
19
- colorText: colors.text,
20
- colorTextSecondary: colors.textSecondary,
21
- colorInputText: colors.text,
22
- colorNeutral: colors.textSecondary,
23
- borderRadius: "0.5rem",
14
+ colorPrimary: accent,
15
+ colorBackground: sand.card,
16
+ colorInputBackground: sand.paper,
17
+ colorText: sand.ink,
18
+ colorTextSecondary: sand.muted,
19
+ colorInputText: sand.ink,
20
+ colorNeutral: sand.muted,
21
+ borderRadius: "0.75rem",
24
22
  fontFamily: "var(--font-geist-sans, system-ui, sans-serif)",
25
23
  },
26
24
  elements: {
27
25
  card: {
28
- backgroundColor: colors.surface,
29
- border: `1px solid ${colors.border}`,
26
+ backgroundColor: sand.card,
27
+ border: `1px solid ${sand.border}`,
30
28
  boxShadow: "none",
31
29
  },
32
- headerTitle: { color: colors.text },
33
- headerSubtitle: { color: colors.textSecondary },
30
+ headerTitle: { color: sand.ink },
31
+ headerSubtitle: { color: sand.muted },
34
32
  socialButtonsBlockButton: {
35
- backgroundColor: colors.bg,
36
- border: `1px solid ${colors.border}`,
37
- color: colors.text,
33
+ backgroundColor: sand.paper,
34
+ border: `1px solid ${sand.border}`,
35
+ color: sand.ink,
38
36
  },
39
- socialButtonsBlockButtonText: { color: colors.text },
40
- dividerLine: { backgroundColor: colors.border },
41
- dividerText: { color: colors.textSecondary },
42
- formFieldLabel: { color: colors.textSecondary },
37
+ socialButtonsBlockButtonText: { color: sand.ink },
38
+ dividerLine: { backgroundColor: sand.border },
39
+ dividerText: { color: sand.muted },
40
+ formFieldLabel: { color: sand.muted },
43
41
  formFieldInput: {
44
- backgroundColor: colors.bg,
45
- border: `1px solid ${colors.border}`,
46
- color: colors.text,
42
+ backgroundColor: sand.paper,
43
+ border: `1px solid ${sand.border}`,
44
+ color: sand.ink,
47
45
  },
48
- footerActionLink: { color: colors.accent },
49
- identityPreviewEditButton: { color: colors.accent },
46
+ footerActionLink: { color: accent },
47
+ identityPreviewEditButton: { color: accent },
50
48
  },
51
49
  };