@forgecart/cli 2.202610052310.0 → 2.202610071755.0

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 (125) hide show
  1. package/package.json +1 -1
  2. package/templates/storefront-shadcn/.forgecartignore +2 -0
  3. package/templates/storefront-shadcn/Procfile +1 -0
  4. package/templates/storefront-shadcn/README.md +229 -0
  5. package/templates/storefront-shadcn/SEO-MIGRATION.md +708 -0
  6. package/templates/storefront-shadcn/components.json +21 -0
  7. package/templates/storefront-shadcn/next.config.js +105 -0
  8. package/templates/storefront-shadcn/package.json +39 -0
  9. package/templates/storefront-shadcn/postcss.config.js +5 -0
  10. package/templates/storefront-shadcn/src/app/%5F%5Ffc/identify/route.ts +205 -0
  11. package/templates/storefront-shadcn/src/app/%5F%5Ffc/track/route.ts +189 -0
  12. package/templates/storefront-shadcn/src/app/%5F%5Fforge_beacon/route.ts +87 -0
  13. package/templates/storefront-shadcn/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
  14. package/templates/storefront-shadcn/src/app/cart/page.tsx +53 -0
  15. package/templates/storefront-shadcn/src/app/checkout/page.tsx +53 -0
  16. package/templates/storefront-shadcn/src/app/error.tsx +23 -0
  17. package/templates/storefront-shadcn/src/app/global-error.tsx +23 -0
  18. package/templates/storefront-shadcn/src/app/globals.css +156 -0
  19. package/templates/storefront-shadcn/src/app/layout.tsx +185 -0
  20. package/templates/storefront-shadcn/src/app/page.tsx +217 -0
  21. package/templates/storefront-shadcn/src/app/pages/[slug]/not-found.tsx +24 -0
  22. package/templates/storefront-shadcn/src/app/pages/[slug]/page.tsx +115 -0
  23. package/templates/storefront-shadcn/src/app/ping/route.ts +21 -0
  24. package/templates/storefront-shadcn/src/app/products/[slug]/not-found.tsx +18 -0
  25. package/templates/storefront-shadcn/src/app/products/[slug]/page.tsx +317 -0
  26. package/templates/storefront-shadcn/src/app/products/page.tsx +107 -0
  27. package/templates/storefront-shadcn/src/app/register/page.tsx +54 -0
  28. package/templates/storefront-shadcn/src/app/reset-password/page.tsx +60 -0
  29. package/templates/storefront-shadcn/src/app/robots.ts +69 -0
  30. package/templates/storefront-shadcn/src/app/sitemap.ts +106 -0
  31. package/templates/storefront-shadcn/src/app/verify/page.tsx +157 -0
  32. package/templates/storefront-shadcn/src/components/CartView.tsx +333 -0
  33. package/templates/storefront-shadcn/src/components/ForgeErrorBeacon.tsx +102 -0
  34. package/templates/storefront-shadcn/src/components/ForgeTracker.tsx +479 -0
  35. package/templates/storefront-shadcn/src/components/ForgecartDesigner.tsx +43 -0
  36. package/templates/storefront-shadcn/src/components/Header.tsx +73 -0
  37. package/templates/storefront-shadcn/src/components/LanguageSwitcher.tsx +96 -0
  38. package/templates/storefront-shadcn/src/components/LocaleLink.tsx +49 -0
  39. package/templates/storefront-shadcn/src/components/ProductCard.tsx +59 -0
  40. package/templates/storefront-shadcn/src/components/ProductPurchase.tsx +235 -0
  41. package/templates/storefront-shadcn/src/components/account/AccountMessage.tsx +63 -0
  42. package/templates/storefront-shadcn/src/components/account/RegisterForm.tsx +257 -0
  43. package/templates/storefront-shadcn/src/components/account/RequestPasswordResetForm.tsx +93 -0
  44. package/templates/storefront-shadcn/src/components/account/ResetPasswordForm.tsx +163 -0
  45. package/templates/storefront-shadcn/src/components/checkout/AddressStep.tsx +271 -0
  46. package/templates/storefront-shadcn/src/components/checkout/CheckoutFlow.tsx +551 -0
  47. package/templates/storefront-shadcn/src/components/checkout/CheckoutGate.tsx +55 -0
  48. package/templates/storefront-shadcn/src/components/checkout/PaymentElementForm.tsx +140 -0
  49. package/templates/storefront-shadcn/src/components/checkout/PaymentFormEmbed.tsx +89 -0
  50. package/templates/storefront-shadcn/src/components/checkout/RatesStep.tsx +115 -0
  51. package/templates/storefront-shadcn/src/components/ui/alert.tsx +75 -0
  52. package/templates/storefront-shadcn/src/components/ui/badge.tsx +40 -0
  53. package/templates/storefront-shadcn/src/components/ui/button.tsx +64 -0
  54. package/templates/storefront-shadcn/src/components/ui/card.tsx +28 -0
  55. package/templates/storefront-shadcn/src/components/ui/input.tsx +26 -0
  56. package/templates/storefront-shadcn/src/components/ui/label.tsx +22 -0
  57. package/templates/storefront-shadcn/src/components/ui/native-select.tsx +27 -0
  58. package/templates/storefront-shadcn/src/components/ui/skeleton.tsx +21 -0
  59. package/templates/storefront-shadcn/src/components/ui/utils.ts +16 -0
  60. package/templates/storefront-shadcn/src/instrumentation.ts +109 -0
  61. package/templates/storefront-shadcn/src/lib/account/account-link.ts +76 -0
  62. package/templates/storefront-shadcn/src/lib/account/register-state.ts +133 -0
  63. package/templates/storefront-shadcn/src/lib/account/reset-password-state.ts +111 -0
  64. package/templates/storefront-shadcn/src/lib/account/verify-state.ts +56 -0
  65. package/templates/storefront-shadcn/src/lib/account-actions.ts +76 -0
  66. package/templates/storefront-shadcn/src/lib/account-session.ts +47 -0
  67. package/templates/storefront-shadcn/src/lib/action-result.ts +30 -0
  68. package/templates/storefront-shadcn/src/lib/asset-alt.ts +34 -0
  69. package/templates/storefront-shadcn/src/lib/backend-actions.ts +20 -0
  70. package/templates/storefront-shadcn/src/lib/backend-client.ts +47 -0
  71. package/templates/storefront-shadcn/src/lib/cart-context.tsx +236 -0
  72. package/templates/storefront-shadcn/src/lib/checkout-session.ts +185 -0
  73. package/templates/storefront-shadcn/src/lib/content/page-metadata.ts +113 -0
  74. package/templates/storefront-shadcn/src/lib/content/render-fields.tsx +256 -0
  75. package/templates/storefront-shadcn/src/lib/content/resolve-page.ts +143 -0
  76. package/templates/storefront-shadcn/src/lib/error-messages.ts +24 -0
  77. package/templates/storefront-shadcn/src/lib/experiments.ts +333 -0
  78. package/templates/storefront-shadcn/src/lib/forgecart.ts +464 -0
  79. package/templates/storefront-shadcn/src/lib/format.ts +89 -0
  80. package/templates/storefront-shadcn/src/lib/identify-forward.ts +152 -0
  81. package/templates/storefront-shadcn/src/lib/locale/channel-locales-loader.ts +169 -0
  82. package/templates/storefront-shadcn/src/lib/locale/channel-locales-map.ts +46 -0
  83. package/templates/storefront-shadcn/src/lib/locale/channel-locales.ts +191 -0
  84. package/templates/storefront-shadcn/src/lib/locale/grammar.ts +194 -0
  85. package/templates/storefront-shadcn/src/lib/locale/localized-path.ts +55 -0
  86. package/templates/storefront-shadcn/src/lib/locale/middleware-plan.ts +107 -0
  87. package/templates/storefront-shadcn/src/lib/locale/request-binding.ts +80 -0
  88. package/templates/storefront-shadcn/src/lib/locale/request-locale.ts +66 -0
  89. package/templates/storefront-shadcn/src/lib/marketing-params.ts +213 -0
  90. package/templates/storefront-shadcn/src/lib/money.ts +50 -0
  91. package/templates/storefront-shadcn/src/lib/seo/alternates.ts +123 -0
  92. package/templates/storefront-shadcn/src/lib/seo/json-ld.ts +266 -0
  93. package/templates/storefront-shadcn/src/lib/seo/metadata.ts +419 -0
  94. package/templates/storefront-shadcn/src/lib/seo/noindex.ts +218 -0
  95. package/templates/storefront-shadcn/src/lib/seo/public-origin.ts +166 -0
  96. package/templates/storefront-shadcn/src/lib/seo/redirect-plan.ts +86 -0
  97. package/templates/storefront-shadcn/src/lib/seo/resolve-path.ts +107 -0
  98. package/templates/storefront-shadcn/src/lib/seo/scaffolded-routes.ts +83 -0
  99. package/templates/storefront-shadcn/src/lib/seo/sidecar.ts +75 -0
  100. package/templates/storefront-shadcn/src/lib/seo/site-verification.ts +98 -0
  101. package/templates/storefront-shadcn/src/lib/seo/sitemap-cache.ts +114 -0
  102. package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +321 -0
  103. package/templates/storefront-shadcn/src/lib/session-actions.ts +61 -0
  104. package/templates/storefront-shadcn/src/lib/session-cookies.ts +98 -0
  105. package/templates/storefront-shadcn/src/lib/shop-config.ts +51 -0
  106. package/templates/storefront-shadcn/src/lib/shop-session.ts +151 -0
  107. package/templates/storefront-shadcn/src/lib/track-forward.ts +200 -0
  108. package/templates/storefront-shadcn/src/lib/uuid.ts +19 -0
  109. package/templates/storefront-shadcn/src/middleware.ts +379 -0
  110. package/templates/storefront-shadcn/src/seo/redirects.ts +44 -0
  111. package/templates/storefront-shadcn/src/server/app.module.ts +18 -0
  112. package/templates/storefront-shadcn/src/server/backend-api.ts +26 -0
  113. package/templates/storefront-shadcn/src/server/backend-method.decorator.ts +23 -0
  114. package/templates/storefront-shadcn/src/server/bootstrap.ts +122 -0
  115. package/templates/storefront-shadcn/src/server/customer-extras/customer-extras.module.ts +13 -0
  116. package/templates/storefront-shadcn/src/server/customer-extras/service/customer-extras.service.ts +58 -0
  117. package/templates/storefront-shadcn/src/server/customer-extras/type/customer-extras.types.ts +11 -0
  118. package/templates/storefront-shadcn/src/server/forge/live-revision.ts +158 -0
  119. package/templates/storefront-shadcn/src/server/forgecart/forgecart-client.factory.ts +69 -0
  120. package/templates/storefront-shadcn/src/server/forgecart/forgecart.module.ts +9 -0
  121. package/templates/storefront-shadcn/src/server/runner.ts +90 -0
  122. package/templates/storefront-shadcn/src/server/types.ts +36 -0
  123. package/templates/storefront-shadcn/tsconfig.json +25 -0
  124. package/templates/storefront-shadcn-sdk-floor.json +1174 -0
  125. package/templates/template-set.json +10 -0
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { getBackend } from '../../../../server/bootstrap';
4
+
5
+ /**
6
+ * DEV-ONLY registry listing — the agent's verification surface for backend
7
+ * features. After authoring a method, the agent probes this route and
8
+ * confirms the new name is listed; a boot failure (the api-map drift check)
9
+ * surfaces here as a 500 carrying the actionable message.
10
+ *
11
+ * Gated on `NODE_ENV === 'development'`: the literal check is inlined by
12
+ * the bundler, so a production build answers 404 and dead-code-eliminates
13
+ * the body. Method NAMES are the only thing exposed — never inputs,
14
+ * outputs, or configuration.
15
+ */
16
+ export async function GET(): Promise<NextResponse> {
17
+ if (process.env.NODE_ENV !== 'development') {
18
+ return NextResponse.json({ error: 'not found' }, { status: 404 });
19
+ }
20
+ try {
21
+ const { registry } = await getBackend();
22
+ return NextResponse.json({ methods: [...registry.keys()].sort() });
23
+ } catch (error) {
24
+ const reason = error instanceof Error ? error.message : String(error);
25
+ return NextResponse.json({ error: reason }, { status: 500 });
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ import type { Metadata } from 'next';
2
+ import { connection } from 'next/server';
3
+ import { Suspense } from 'react';
4
+
5
+ import { CartView } from '../../components/CartView';
6
+ import { Skeleton } from '../../components/ui/skeleton';
7
+ import { getChannelSellingPlanGroups } from '../../lib/forgecart';
8
+ import { getRequestLocale } from '../../lib/locale/request-binding';
9
+ import { routeRobots } from '../../lib/seo/noindex';
10
+
11
+ /**
12
+ * Never indexable: a cart is per-shopper state, identical in copy for everyone
13
+ * and meaningless to a visitor arriving from a search result.
14
+ */
15
+ export function generateMetadata(): Metadata {
16
+ return { robots: routeRobots('/cart') };
17
+ }
18
+
19
+ /**
20
+ * Cart route. The shopper's cart itself lives on their OWN websocket
21
+ * (`CartView` hydrates it client-side); the server's only read here is the
22
+ * PUBLIC channel subscription plans off the anonymous channel singleton —
23
+ * streamed as a hole while the shell renders.
24
+ */
25
+ export default function CartPage() {
26
+ return (
27
+ <Suspense fallback={<CartRouteSkeleton />}>
28
+ <CartWithPlans />
29
+ </Suspense>
30
+ );
31
+ }
32
+
33
+ async function CartWithPlans() {
34
+ await connection();
35
+ const { binding } = await getRequestLocale();
36
+ // Channel-wide subscription plans are public channel data — the one
37
+ // server-fetched input; the "Subscribe to your whole order" box renders
38
+ // only when there are any.
39
+ const channelGroups = await getChannelSellingPlanGroups();
40
+ return <CartView channelGroups={channelGroups} locale={binding} />;
41
+ }
42
+
43
+ function CartRouteSkeleton() {
44
+ return (
45
+ <div className="space-y-8" aria-busy="true" aria-label="Loading your cart">
46
+ <h1 className="text-3xl font-bold tracking-tight">Your cart</h1>
47
+ <div className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
48
+ <Skeleton className="h-64 w-full" />
49
+ <Skeleton className="h-80 w-full" />
50
+ </div>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,53 @@
1
+ import type { Metadata } from 'next';
2
+ import { connection } from 'next/server';
3
+ import { Suspense } from 'react';
4
+
5
+ import { CheckoutGate } from '../../components/checkout/CheckoutGate';
6
+ import { Skeleton } from '../../components/ui/skeleton';
7
+ import { getAvailableCountries } from '../../lib/forgecart';
8
+ import { getRequestLocale } from '../../lib/locale/request-binding';
9
+ import { routeRobots } from '../../lib/seo/noindex';
10
+
11
+ /**
12
+ * Never indexable: checkout is a funnel step, not a destination, and every
13
+ * nested step inherits the decision through `NOINDEX_PATHS`.
14
+ */
15
+ export function generateMetadata(): Metadata {
16
+ return { robots: routeRobots('/checkout') };
17
+ }
18
+
19
+ /**
20
+ * Checkout entry. The server's only job here is the PUBLIC prefetch — the
21
+ * channel's shipping countries off the anonymous channel singleton, streamed
22
+ * as a hole. The session work (the order itself, every
23
+ * checkout mutation) lives on the shopper's websocket: `CheckoutGate`
24
+ * hydrates the live cart client-side and mounts the flow, or shows the
25
+ * honest empty state.
26
+ */
27
+ export default function CheckoutPage() {
28
+ return (
29
+ <div className="mx-auto max-w-2xl space-y-6">
30
+ <h1 className="text-2xl font-bold tracking-tight">Checkout</h1>
31
+ <Suspense fallback={<CheckoutRouteSkeleton />}>
32
+ <CheckoutWithCountries />
33
+ </Suspense>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ async function CheckoutWithCountries() {
39
+ await connection();
40
+ const { binding } = await getRequestLocale();
41
+ const countries = await getAvailableCountries();
42
+ return <CheckoutGate countries={countries} locale={binding} />;
43
+ }
44
+
45
+ function CheckoutRouteSkeleton() {
46
+ return (
47
+ <div className="space-y-4" aria-busy="true" aria-label="Loading checkout">
48
+ <Skeleton className="h-10 w-full" />
49
+ <Skeleton className="h-40 w-full" />
50
+ <Skeleton className="h-10 w-40" />
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { Button } from '../components/ui/button';
4
+
5
+ /**
6
+ * Route-level backstop boundary. The action layer never throws (typed
7
+ * `ActionResult` envelopes end at inline UI), so landing here means a RENDER
8
+ * failed — e.g. a Server Component read against an unreachable shop API.
9
+ * Recovery is a re-render via `reset()`.
10
+ */
11
+ export default function RouteError({ reset }: { error: Error; reset: () => void }) {
12
+ return (
13
+ <div className="mx-auto max-w-xl space-y-4 py-16 text-center">
14
+ <h1 className="text-2xl font-bold tracking-tight text-foreground">Something went wrong</h1>
15
+ <p className="text-muted-foreground">
16
+ The store could not load this page. Please try again in a moment.
17
+ </p>
18
+ <Button type="button" onClick={() => reset()}>
19
+ Try again
20
+ </Button>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Root backstop: replaces the whole document when the root layout itself
5
+ * fails to render. Must own its own <html>/<body> — the layout that normally
6
+ * provides them is the thing that crashed. Styled inline (no globals.css —
7
+ * that import lives in the crashed layout).
8
+ */
9
+ export default function GlobalError({ reset }: { error: Error; reset: () => void }) {
10
+ return (
11
+ <html lang="en">
12
+ <body
13
+ style={{ fontFamily: 'system-ui, sans-serif', padding: '4rem 1rem', textAlign: 'center' }}
14
+ >
15
+ <h1>Something went wrong</h1>
16
+ <p>The store is temporarily unavailable. Please try again in a moment.</p>
17
+ <button type="button" onClick={() => reset()} style={{ padding: '0.5rem 1.25rem' }}>
18
+ Try again
19
+ </button>
20
+ </body>
21
+ </html>
22
+ );
23
+ }
@@ -0,0 +1,156 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * ═══════════════════════════════════════════════════════════════════════════
5
+ * STOREFRONT THEME — the single brand mutation point (design-system contract)
6
+ * ═══════════════════════════════════════════════════════════════════════════
7
+ *
8
+ * Every component in this template speaks SEMANTIC shadcn/ui tokens (bg-card,
9
+ * text-muted-foreground, border-input, the Button / Badge / Alert variants,
10
+ * …), so the `:root` block below is the ONE place a brand lives. Restyling
11
+ * the storefront = editing those values — nothing else. Never scatter raw
12
+ * palette utilities (bg-gray-900, text-white, #hex) through components; if a
13
+ * component needs a color, it needs a TOKEN: declared once in `:root` and
14
+ * exposed to Tailwind through `@theme inline`, as the status tokens are.
15
+ *
16
+ * The daisyUI storefront and this one are rows of one template set. The table
17
+ * is how every daisyUI theme token reads here — the same brand decision under
18
+ * its shadcn/ui name:
19
+ *
20
+ * daisyUI theme token shadcn/ui token (this file)
21
+ * ─────────────────────────── ──────────────────────────────────────────
22
+ * --color-base-100 --card, --popover
23
+ * --color-base-200 --background
24
+ * --color-base-300 --border, --input
25
+ * --color-base-content --foreground (+ --card-foreground,
26
+ * --popover-foreground)
27
+ * --color-primary(-content) --primary(-foreground)
28
+ * --color-secondary(-content) --secondary(-foreground)
29
+ * --color-accent(-content) --accent(-foreground)
30
+ * --color-neutral(-content) --muted(-foreground)
31
+ * --color-error --destructive
32
+ * --color-success(-content) --success(-foreground) declared by this variant
33
+ * --color-warning(-content) --warning(-foreground) declared by this variant
34
+ * --color-info(-content) --info(-foreground) declared by this variant
35
+ * --radius-selector --radius-sm
36
+ * --radius-field --radius-md
37
+ * --radius-box --radius-lg (= --radius)
38
+ * --font-sans (@theme) --font-sans (@theme inline)
39
+ *
40
+ * shadcn/ui names no success, warning or info color, so this variant declares
41
+ * them beside --destructive, each with the -foreground that stays legible on
42
+ * it (the Alert's status variants fill with them). --ring has no daisyUI
43
+ * counterpart — daisyUI draws a focus outline in the control's own color — so
44
+ * it takes the primary ink.
45
+ *
46
+ * The baseline is the daisyUI storefront's own, deliberately UNBRANDED
47
+ * monochrome (near-black on soft gray), carried value for value through the
48
+ * table — with one deliberate exception, the muted pair. daisyUI's neutral is
49
+ * a high-contrast FILL; shadcn/ui's muted pair is the SUBDUED one every
50
+ * vendored component reads for quiet surfaces and secondary text (bg-muted,
51
+ * text-muted-foreground — an input's placeholder among them). The baseline
52
+ * keeps that role: --muted is a soft gray surface, and --muted-foreground the
53
+ * mid-gray ink the daisyUI template writes as text-base-content/60. Accent
54
+ * keeps daisyUI's dark highlight, which is also what a ghost or outline
55
+ * button turns on hover.
56
+ *
57
+ * Radius: daisyUI's selector and field radii are both 0.375rem and its box
58
+ * radius 0.5rem, so --radius is the box radius and the sm/md steps sit one
59
+ * notch below it. A brand font goes in `@theme inline`: swap the --font-sans
60
+ * stack (and add the font's `@font-face`/import above it).
61
+ */
62
+ :root {
63
+ /* Base — white surfaces over a soft-gray substrate, near-black text. */
64
+ --background: #f9fafb;
65
+ --foreground: #111827;
66
+ --card: #ffffff;
67
+ --card-foreground: #111827;
68
+ --popover: #ffffff;
69
+ --popover-foreground: #111827;
70
+ --border: #e5e7eb;
71
+ --input: #e5e7eb;
72
+
73
+ /* Actions — unbranded monochrome. */
74
+ --primary: #111827;
75
+ --primary-foreground: #ffffff;
76
+ --secondary: #374151;
77
+ --secondary-foreground: #ffffff;
78
+ --accent: #1f2937;
79
+ --accent-foreground: #ffffff;
80
+ --muted: #f3f4f6;
81
+ --muted-foreground: #6b7280;
82
+ --ring: #111827;
83
+
84
+ /* Status — conventional semantics, not brand. */
85
+ --destructive: #dc2626;
86
+ --success: #16a34a;
87
+ --success-foreground: #ffffff;
88
+ --warning: #d97706;
89
+ --warning-foreground: #ffffff;
90
+ --info: #2563eb;
91
+ --info-foreground: #ffffff;
92
+
93
+ /* Shape — the daisyUI storefront's box radius. */
94
+ --radius: 0.5rem;
95
+ }
96
+
97
+ /* The tokens as Tailwind theme values: bg-card, text-muted-foreground, rounded-md, font-sans, … */
98
+ @theme inline {
99
+ --color-background: var(--background);
100
+ --color-foreground: var(--foreground);
101
+ --color-card: var(--card);
102
+ --color-card-foreground: var(--card-foreground);
103
+ --color-popover: var(--popover);
104
+ --color-popover-foreground: var(--popover-foreground);
105
+ --color-primary: var(--primary);
106
+ --color-primary-foreground: var(--primary-foreground);
107
+ --color-secondary: var(--secondary);
108
+ --color-secondary-foreground: var(--secondary-foreground);
109
+ --color-muted: var(--muted);
110
+ --color-muted-foreground: var(--muted-foreground);
111
+ --color-accent: var(--accent);
112
+ --color-accent-foreground: var(--accent-foreground);
113
+ --color-destructive: var(--destructive);
114
+ --color-border: var(--border);
115
+ --color-input: var(--input);
116
+ --color-ring: var(--ring);
117
+ --color-success: var(--success);
118
+ --color-success-foreground: var(--success-foreground);
119
+ --color-warning: var(--warning);
120
+ --color-warning-foreground: var(--warning-foreground);
121
+ --color-info: var(--info);
122
+ --color-info-foreground: var(--info-foreground);
123
+
124
+ --radius-sm: calc(var(--radius) - 2px);
125
+ --radius-md: calc(var(--radius) - 2px);
126
+ --radius-lg: var(--radius);
127
+ --radius-xl: calc(var(--radius) + 4px);
128
+
129
+ /* Brand typography mutation point — swap the stack to give the store a font. */
130
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
131
+ "Segoe UI Symbol", "Noto Color Emoji";
132
+ }
133
+
134
+ /* The shadcn/ui base layer: token-colored borders and focus outlines, the page on --background. */
135
+ @layer base {
136
+ * {
137
+ @apply border-border outline-ring/50;
138
+ }
139
+
140
+ body {
141
+ @apply bg-background text-foreground;
142
+ }
143
+ }
144
+
145
+ /*
146
+ * Base document behavior (structural — NOT part of the theme contract).
147
+ * A horizontal touchpad glide must never trigger the browser's swipe-back
148
+ * navigation: mid-checkout that discards the shopper's in-progress state
149
+ * (the refresh-resume path recovers the payment step, but preventing the
150
+ * accidental navigation is the real fix). The back button and keyboard
151
+ * navigation are untouched — only the overscroll gesture is consumed.
152
+ */
153
+ html,
154
+ body {
155
+ overscroll-behavior-x: none;
156
+ }
@@ -0,0 +1,185 @@
1
+ import type { Metadata } from 'next';
2
+ import { notFound, permanentRedirect } from 'next/navigation';
3
+ import { Suspense, type ReactNode } from 'react';
4
+
5
+ import { ForgeErrorBeacon } from '../components/ForgeErrorBeacon';
6
+ import { ForgeTracker } from '../components/ForgeTracker';
7
+ import { ForgecartDesigner } from '../components/ForgecartDesigner';
8
+ import { Header } from '../components/Header';
9
+ import { LocaleLink } from '../components/LocaleLink';
10
+ import { CartProvider } from '../lib/cart-context';
11
+ import { getRequestLocale } from '../lib/locale/request-binding';
12
+ import { shellMetadata } from '../lib/seo/metadata';
13
+ import { getSiteVerifications } from '../lib/seo/site-verification';
14
+ import { getBrowserShopConfig } from '../lib/shop-config';
15
+ import { FORGE_REVISION_META, resolveLiveRevision } from '../server/forge/live-revision';
16
+
17
+ import './globals.css';
18
+
19
+ /**
20
+ * The deployment-wide metadata floor.
21
+ *
22
+ * Resolved per REQUEST rather than declared as a static `metadata` object,
23
+ * because everything in it arrives in the environment of the running server
24
+ * rather than of the machine that built the bundle: the public origin from
25
+ * `.env` (see `lib/seo/public-origin.ts`) and the shop name from the channel
26
+ * itself. A build-time answer would bake the builder's empty environment into
27
+ * every store shipped from that image.
28
+ *
29
+ * The title is the STORE's name, so a merchant who scaffolds a page and writes
30
+ * no metadata gets their own store in the browser tab instead of the word
31
+ * `Storefront` — which is what every page of every ForgeCart store displayed
32
+ * before this. Route-specific claims live on the routes; this is only the
33
+ * floor, and it deliberately carries no canonical (it does not know which
34
+ * route it is wrapping).
35
+ *
36
+ * It reads the SAME resolution the layout body reads below — React's request
37
+ * cache makes `getRequestLocale()` one channel read per request no matter how
38
+ * many callers there are, so the floor costs nothing extra.
39
+ *
40
+ * The site-verification placements (#1530) are read on the same request cache
41
+ * and handed to the SAME composer, so `lib/seo/metadata.ts` stays the only
42
+ * thing in this template that turns a placement into a `<meta>`: the
43
+ * alternative — a hand-written tag in the markup below — would be a second
44
+ * emitter with its own idea of when a placement is servable. They ride the
45
+ * FLOOR rather than a route, because the verifier chooses which page it fetches
46
+ * and a tag on only some of them is a verification that works until it does not.
47
+ *
48
+ * It also carries the DEV-ONLY revision stamp (#847). The tag has to be written
49
+ * where the page is RENDERED, because that is the only moment that knows which
50
+ * tree the shopper is actually looking at: the browser beacon reads it back at
51
+ * throw time, so a promote landing between serve and throw can no longer make
52
+ * the report blame the new revision for the old one's crash. It rides
53
+ * `generateMetadata` rather than a `<meta>` in the JSX below because arbitrary
54
+ * head tags are what `other` is for, and the shell's own metadata contract then
55
+ * stays untouched — the stamp is spread ON TOP of `shellMetadata`'s answer and
56
+ * adds a key no SEO surface reads.
57
+ *
58
+ * Dev-gated at this call site, like every other file in the beacon path: the
59
+ * literal `NODE_ENV` check is what lets a production build eliminate the
60
+ * resolution entirely, so a deployed storefront emits no tag and leaks no
61
+ * revision. Unresolvable is a NORMAL answer and means no tag — the report then
62
+ * arrives untagged and the receiver keeps its existing behaviour.
63
+ */
64
+ export async function generateMetadata(): Promise<Metadata> {
65
+ const { shopName, channelResolved } = await getRequestLocale();
66
+ const siteVerifications = await getSiteVerifications();
67
+ const shell = shellMetadata({ shopName, channelResolved, siteVerifications });
68
+ if (process.env.NODE_ENV !== 'development') return shell;
69
+ const revision = resolveLiveRevision();
70
+ if (revision === null) return shell;
71
+ // MERGED into the shell's `other`, never written over it: that key carries the
72
+ // site-verification placements (#1530), and a storefront pod serves through
73
+ // `npm run dev`, so an overwrite here would strip the provider's tag from
74
+ // every page its verifier can fetch.
75
+ return { ...shell, other: { ...shell.other, [FORGE_REVISION_META]: revision } };
76
+ }
77
+
78
+ /**
79
+ * The root layout — DYNAMIC on every route, by design (D8).
80
+ *
81
+ * It reads the request locale to write `<html lang>`, and `lang` is an
82
+ * attribute on the document root: structurally outside every Suspense
83
+ * boundary, so it cannot come from a streamed hole. That single requirement
84
+ * decides the rendering model for the whole storefront, and `next.config.js`
85
+ * carries the reasoning — Cache Components is off because the escape hatch it
86
+ * would force (a `<Suspense>` above `<body>`) is exactly what makes the two
87
+ * throws below unable to set a status.
88
+ *
89
+ * Those throws are the locale contract's teeth. Raised HERE, in the shell,
90
+ * they reach the one place Next assigns a status: `permanentRedirect` gives a
91
+ * real 308 with a `Location`, and `notFound` a real 404. Raised from inside
92
+ * any boundary they would resolve as client-rendered content instead, and the
93
+ * response would be a 200 carrying a 404 page — a soft-404, which a crawler
94
+ * indexes rather than drops.
95
+ *
96
+ * One consequence worth knowing: on `notFound()` Next discards the app tree
97
+ * and seeds a bare error document, so `app/not-found.tsx` reaches the shopper
98
+ * on client hydration rather than in the SSR body. The STATUS is correct
99
+ * either way, which is what the crawler consumes.
100
+ *
101
+ * The cart still hydrates CLIENT-side over the shopper's own websocket
102
+ * (`CartProvider` + `shop-session.ts`), armed with the channel coordinates
103
+ * serialized here (public storefront token — the admin secret never takes this
104
+ * path).
105
+ */
106
+ export default async function RootLayout({ children }: { children: ReactNode }) {
107
+ const { decision, binding, languageCodes } = await getRequestLocale();
108
+ if (decision.kind === 'redirect') permanentRedirect(decision.to);
109
+ if (decision.kind === 'notFound') notFound();
110
+
111
+ // The shopper's own socket is opened CLIENT-side, so it cannot read the
112
+ // request headers the server resolved the locale from. It gets the resolved
113
+ // language here, which keeps one source of truth — the URL — for both the
114
+ // server's per-locale clients and the browser's session client. Without it
115
+ // the cart would answer in the channel default on a German page.
116
+ const shopConfig = getBrowserShopConfig(binding.locale);
117
+ return (
118
+ <html lang={binding.locale}>
119
+ <body className="min-h-screen bg-background font-sans text-foreground antialiased">
120
+ <CartProvider shopConfig={shopConfig}>
121
+ <Header locale={binding} languageCodes={languageCodes} />
122
+ <main className="mx-auto max-w-6xl px-4 py-10">{children}</main>
123
+ <footer className="mt-8 border-t bg-card px-6 py-8 text-sm text-muted-foreground sm:px-10">
124
+ <div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
125
+ <aside>
126
+ <p className="font-medium text-foreground">Storefront</p>
127
+ <p>Powered by ForgeCart</p>
128
+ </aside>
129
+ <nav aria-label="Footer">
130
+ <div className="grid grid-flow-col gap-4">
131
+ <LocaleLink
132
+ href="/"
133
+ locale={binding}
134
+ className="underline-offset-4 hover:text-foreground hover:underline"
135
+ >
136
+ Home
137
+ </LocaleLink>
138
+ <LocaleLink
139
+ href="/products"
140
+ locale={binding}
141
+ className="underline-offset-4 hover:text-foreground hover:underline"
142
+ >
143
+ Products
144
+ </LocaleLink>
145
+ <LocaleLink
146
+ href="/cart"
147
+ locale={binding}
148
+ className="underline-offset-4 hover:text-foreground hover:underline"
149
+ >
150
+ Cart
151
+ </LocaleLink>
152
+ {/* The only affordance for `/register` (#1471). It sits in the
153
+ footer rather than the header nav on purpose: the template
154
+ ships no sign-in surface yet, and an account entry in the
155
+ primary nav would advertise an account AREA that does not
156
+ exist. A merchant who adds sign-in moves it up. */}
157
+ <LocaleLink
158
+ href="/register"
159
+ locale={binding}
160
+ className="underline-offset-4 hover:text-foreground hover:underline"
161
+ >
162
+ Create account
163
+ </LocaleLink>
164
+ </div>
165
+ </nav>
166
+ </div>
167
+ </footer>
168
+ </CartProvider>
169
+ {/* Invisible client plumbing. The tracker reads usePathname(), which
170
+ needs a boundary of its own — null fallback, these render nothing.
171
+ This must stay INSIDE <body>: a boundary above it would make the
172
+ shell resumable and cost the statuses above. */}
173
+ <Suspense fallback={null}>
174
+ <ForgecartDesigner />
175
+ <ForgeErrorBeacon />
176
+ <ForgeTracker
177
+ enabled={Boolean(
178
+ process.env.FORGECART_SHOP_API_URL && process.env.FORGECART_CHANNEL_TOKEN,
179
+ )}
180
+ />
181
+ </Suspense>
182
+ </body>
183
+ </html>
184
+ );
185
+ }