@forgecart/cli 2.202610052143.0 → 2.202610060357.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,140 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ import { Button } from '../ui/button';
6
+
7
+ /**
8
+ * In-page Stripe Payment Element — the direct-render adapter for
9
+ * Stripe-model providers (operator decision 2026-08-01: our own payment
10
+ * chrome is never iframed; the srcdoc embed provided no security boundary —
11
+ * it is same-origin, and PCI isolation comes from Stripe's OWN nested
12
+ * input iframes, which apply identically here). The page owns the button
13
+ * and all visible chrome, so the payment step inherits the design system;
14
+ * only the card inputs are Stripe's.
15
+ *
16
+ * Everything this component needs arrives as data from the shop API
17
+ * (`getSessionVariables` + the created session's secret) — no keys and no
18
+ * provider configuration live in the template source. Providers without an
19
+ * in-page adapter keep the hosted-template embed (`PaymentFormEmbed`).
20
+ */
21
+
22
+ /** Minimal structural surface of Stripe.js v3 — the template ships no SDK dep. */
23
+ interface StripeElementsHandle {
24
+ create: (kind: 'payment') => { mount: (node: HTMLElement) => void; unmount: () => void };
25
+ }
26
+ interface StripeHandle {
27
+ elements: (options: { clientSecret: string }) => StripeElementsHandle;
28
+ confirmPayment: (options: {
29
+ elements: StripeElementsHandle;
30
+ redirect: 'if_required';
31
+ }) => Promise<{
32
+ error?: { message?: string };
33
+ paymentIntent?: { id: string; status: string };
34
+ }>;
35
+ }
36
+ declare global {
37
+ interface Window {
38
+ Stripe?: (publishableKey: string) => StripeHandle;
39
+ }
40
+ }
41
+
42
+ /** Load the provider script once; concurrent callers share the same promise. */
43
+ let scriptPromise: Promise<void> | null = null;
44
+ function loadProviderScript(scriptUrl: string): Promise<void> {
45
+ if (window.Stripe) return Promise.resolve();
46
+ scriptPromise ??= new Promise<void>((resolve, reject) => {
47
+ const script = document.createElement('script');
48
+ script.src = scriptUrl;
49
+ script.async = true;
50
+ script.onload = () => resolve();
51
+ script.onerror = () => {
52
+ scriptPromise = null;
53
+ reject(new Error('Payment provider script failed to load'));
54
+ };
55
+ document.head.appendChild(script);
56
+ });
57
+ return scriptPromise;
58
+ }
59
+
60
+ export function PaymentElementForm({
61
+ publishableKey,
62
+ scriptUrl,
63
+ clientSecret,
64
+ payLabel,
65
+ onSuccess,
66
+ onError,
67
+ }: {
68
+ publishableKey: string;
69
+ scriptUrl: string;
70
+ clientSecret: string;
71
+ /** Themed button label, e.g. `Pay €42.00` — the PAGE owns the chrome. */
72
+ payLabel: string;
73
+ onSuccess: (status: string, paymentIntentId: string) => void;
74
+ onError: (message: string) => void;
75
+ }) {
76
+ const mountRef = useRef<HTMLDivElement | null>(null);
77
+ const stripeRef = useRef<StripeHandle | null>(null);
78
+ const elementsRef = useRef<StripeElementsHandle | null>(null);
79
+ const [mounted, setMounted] = useState(false);
80
+ const [paying, setPaying] = useState(false);
81
+ // Callbacks ride a ref so the mount effect binds once per secret.
82
+ const handlersRef = useRef({ onError });
83
+ handlersRef.current = { onError };
84
+
85
+ useEffect(() => {
86
+ let cancelled = false;
87
+ let element: { unmount: () => void } | null = null;
88
+ void loadProviderScript(scriptUrl)
89
+ .then(() => {
90
+ if (cancelled || !window.Stripe || !mountRef.current) return;
91
+ const stripe = window.Stripe(publishableKey);
92
+ const elements = stripe.elements({ clientSecret });
93
+ const paymentElement = elements.create('payment');
94
+ paymentElement.mount(mountRef.current);
95
+ stripeRef.current = stripe;
96
+ elementsRef.current = elements;
97
+ element = paymentElement;
98
+ setMounted(true);
99
+ })
100
+ .catch((error: Error) => {
101
+ if (!cancelled) handlersRef.current.onError(error.message);
102
+ });
103
+ return () => {
104
+ cancelled = true;
105
+ element?.unmount();
106
+ };
107
+ }, [scriptUrl, publishableKey, clientSecret]);
108
+
109
+ async function pay(): Promise<void> {
110
+ const stripe = stripeRef.current;
111
+ const elements = elementsRef.current;
112
+ if (!stripe || !elements || paying) return;
113
+ setPaying(true);
114
+ const result = await stripe.confirmPayment({ elements, redirect: 'if_required' });
115
+ setPaying(false);
116
+ if (result.error) {
117
+ onError(result.error.message ?? 'Payment failed');
118
+ return;
119
+ }
120
+ if (result.paymentIntent) {
121
+ onSuccess(result.paymentIntent.status, result.paymentIntent.id);
122
+ }
123
+ }
124
+
125
+ return (
126
+ <div className="space-y-4">
127
+ {!mounted && <p className="text-sm text-muted-foreground">Loading payment form…</p>}
128
+ <div ref={mountRef} />
129
+ <Button
130
+ type="button"
131
+ onClick={() => void pay()}
132
+ disabled={!mounted || paying}
133
+ data-fc-track="checkout-pay"
134
+ className="w-full"
135
+ >
136
+ {paying ? 'Processing…' : payLabel}
137
+ </Button>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ /**
6
+ * Provider-agnostic host for the shop API's hosted payment form
7
+ * (`PaymentFormTemplate.html` — Stripe, Mollie, … the template carries all
8
+ * provider code; this component contains none).
9
+ *
10
+ * The handshake is the #865 contract, extended with in-band secret delivery:
11
+ * the hosted form posts `iframe-ready`, `payment-success` and
12
+ * `payment-error` to its embedder, and the embedder answers `iframe-ready`
13
+ * by posting `client-secret` back INTO the frame — the session's secret
14
+ * never rides the rendered HTML (which may be logged or cached
15
+ * server-side); the provider template mounts its payment element only once
16
+ * the secret arrives. Every message in both directions is pinned to the
17
+ * page's own origin: the srcdoc iframe is same-origin with this page (no
18
+ * sandbox), so legitimate traffic carries `window.location.origin` — any
19
+ * other origin (another iframe, an extension, a foreign embed) is ignored.
20
+ */
21
+ export interface PaymentFormMessage {
22
+ type: 'iframe-ready' | 'payment-success' | 'payment-error';
23
+ error?: string;
24
+ paymentIntentId?: string;
25
+ status?: string;
26
+ }
27
+
28
+ export function PaymentFormEmbed({
29
+ html,
30
+ clientSecret,
31
+ onSuccess,
32
+ onError,
33
+ }: {
34
+ html: string;
35
+ /** The payment session's secret, delivered to the frame post-`iframe-ready`. */
36
+ clientSecret?: string;
37
+ onSuccess: (message: PaymentFormMessage) => void;
38
+ onError: (message: string) => void;
39
+ }) {
40
+ const [ready, setReady] = useState(false);
41
+ const frameRef = useRef<HTMLIFrameElement | null>(null);
42
+ // Callbacks ride a ref so the message listener binds once per html.
43
+ const handlersRef = useRef({ onSuccess, onError });
44
+ handlersRef.current = { onSuccess, onError };
45
+
46
+ useEffect(() => {
47
+ function onMessage(event: MessageEvent<PaymentFormMessage>): void {
48
+ if (event.origin !== window.location.origin) return;
49
+ const data = event.data;
50
+ if (!data || typeof data !== 'object') return;
51
+ if (data.type === 'iframe-ready') {
52
+ setReady(true);
53
+ return;
54
+ }
55
+ if (data.type === 'payment-success') {
56
+ handlersRef.current.onSuccess(data);
57
+ return;
58
+ }
59
+ if (data.type === 'payment-error') {
60
+ handlersRef.current.onError(data.error ?? 'Payment failed');
61
+ }
62
+ }
63
+ window.addEventListener('message', onMessage);
64
+ return () => window.removeEventListener('message', onMessage);
65
+ }, [html]);
66
+
67
+ // Deliver the secret once the frame is listening. Embedded-confirm
68
+ // templates (Stripe's Payment Element) mount on receipt; templates with no
69
+ // client-side secret (redirect models) simply never consume the message.
70
+ useEffect(() => {
71
+ if (!ready || !clientSecret) return;
72
+ frameRef.current?.contentWindow?.postMessage(
73
+ { type: 'client-secret', clientSecret },
74
+ window.location.origin,
75
+ );
76
+ }, [ready, clientSecret]);
77
+
78
+ return (
79
+ <div className="space-y-2">
80
+ {!ready && <p className="text-sm text-muted-foreground">Loading payment form…</p>}
81
+ <iframe
82
+ ref={frameRef}
83
+ title="Payment form"
84
+ srcDoc={html}
85
+ className="min-h-96 w-full rounded-md border bg-card"
86
+ />
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import type { ExtractedError } from '@forgecart/sdk/shop';
4
+
5
+ import type { ShippingRateGroup } from '../../lib/forgecart';
6
+ import { formatPrice } from '../../lib/format';
7
+ import { Alert, AlertDescription } from '../ui/alert';
8
+ import { Button } from '../ui/button';
9
+ import { Card } from '../ui/card';
10
+ import { cn } from '../ui/utils';
11
+
12
+ /**
13
+ * Shipping-rate selection: one card group per shipment (rates arrive grouped
14
+ * by stock location), radio-cards per rate, per-group `lastRateErrors`
15
+ * alerts (a refusing carrier never hides its siblings' quotes). The CUSTOMER
16
+ * picks — nothing auto-selects; the ArrangingPayment transition requires a
17
+ * selection in every group (#863's `hasSelectedRates` gate).
18
+ */
19
+ export function RatesStep({
20
+ groups,
21
+ onSelect,
22
+ onContinue,
23
+ pending,
24
+ refreshing,
25
+ error,
26
+ }: {
27
+ groups: ShippingRateGroup[];
28
+ onSelect: (rateGroupId: string, rateId: string) => void;
29
+ onContinue: () => void;
30
+ pending: boolean;
31
+ refreshing: boolean;
32
+ error: ExtractedError | null;
33
+ }) {
34
+ if (refreshing) {
35
+ return <p className="text-sm text-muted-foreground">Fetching shipping rates…</p>;
36
+ }
37
+
38
+ const allSelected =
39
+ groups.length > 0 && groups.every((group) => group.rates.some((rate) => rate.selected));
40
+
41
+ return (
42
+ <div className="space-y-4">
43
+ {groups.length === 0 && (
44
+ <Alert role="alert" variant="warning">
45
+ <AlertDescription>No shipping rates are available for this address yet.</AlertDescription>
46
+ </Alert>
47
+ )}
48
+
49
+ {groups.map((group, index) => (
50
+ <Card key={group.id} className="gap-2 p-4">
51
+ <h3 className="text-sm font-medium text-foreground/80">
52
+ {groups.length > 1 ? `Shipment ${index + 1}` : 'Shipping method'}
53
+ {group.stockLocationName ? ` — ships from ${group.stockLocationName}` : ''}
54
+ </h3>
55
+
56
+ {group.lastRateErrors.length > 0 && (
57
+ <Alert role="alert" variant="warning">
58
+ <AlertDescription>
59
+ {group.lastRateErrors
60
+ .map((failure) => `${failure.providerName}: quotes unavailable`)
61
+ .join(' · ')}
62
+ </AlertDescription>
63
+ </Alert>
64
+ )}
65
+
66
+ <div className="flex flex-col gap-2">
67
+ {group.rates.map((rate) => (
68
+ <button
69
+ key={rate.id}
70
+ type="button"
71
+ onClick={() => onSelect(group.id, rate.id)}
72
+ disabled={pending}
73
+ aria-pressed={rate.selected}
74
+ data-fc-track="checkout-select-rate"
75
+ className={cn(
76
+ 'flex items-center justify-between gap-3 rounded-md border px-3 py-2 text-left text-sm transition disabled:opacity-50',
77
+ rate.selected ? 'border-primary ring-1 ring-primary' : 'hover:border-primary',
78
+ )}
79
+ >
80
+ <span className="min-w-0">
81
+ <span className="block font-medium text-foreground">
82
+ {rate.carrier} {rate.service}
83
+ </span>
84
+ {rate.estimatedDays !== null && rate.estimatedDays !== undefined && (
85
+ <span className="block text-xs text-muted-foreground">
86
+ ~{rate.estimatedDays} day{rate.estimatedDays === 1 ? '' : 's'}
87
+ </span>
88
+ )}
89
+ </span>
90
+ <span className="shrink-0 font-medium text-foreground">
91
+ {formatPrice(rate.amount, rate.currency)}
92
+ </span>
93
+ </button>
94
+ ))}
95
+ </div>
96
+ </Card>
97
+ ))}
98
+
99
+ {error && (
100
+ <Alert role="alert" variant="destructive">
101
+ <AlertDescription>{error.message ?? error.code}</AlertDescription>
102
+ </Alert>
103
+ )}
104
+
105
+ <Button
106
+ type="button"
107
+ onClick={onContinue}
108
+ disabled={pending || !allSelected}
109
+ data-fc-track="checkout-to-payment"
110
+ >
111
+ {pending ? 'Saving…' : 'Continue to payment'}
112
+ </Button>
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,75 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import type { ComponentProps } from 'react';
3
+
4
+ import { cn } from './utils';
5
+
6
+ /**
7
+ * The shadcn/ui alert, with the status variants this variant declares.
8
+ *
9
+ * shadcn/ui ships `default` and `destructive`; the storefront also speaks
10
+ * success, warning and info, so those three are variants here, each filled
11
+ * with its own `--success` / `--warning` / `--info` token and the
12
+ * `-foreground` that stays legible on it (see the mapping table in
13
+ * `src/app/globals.css`). An alert is `role="alert"` unless the caller says
14
+ * otherwise — a polite outcome passes `role="status"`.
15
+ */
16
+ const alertVariants = cva(
17
+ 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: 'bg-card text-card-foreground',
22
+ destructive:
23
+ 'bg-card text-destructive [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
24
+ success:
25
+ 'border-success bg-success text-success-foreground *:data-[slot=alert-description]:text-success-foreground/90',
26
+ warning:
27
+ 'border-warning bg-warning text-warning-foreground *:data-[slot=alert-description]:text-warning-foreground/90',
28
+ info: 'border-info bg-info text-info-foreground *:data-[slot=alert-description]:text-info-foreground/90',
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: 'default',
33
+ },
34
+ },
35
+ );
36
+
37
+ function Alert({
38
+ className,
39
+ variant,
40
+ ...props
41
+ }: ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
42
+ return (
43
+ <div
44
+ data-slot="alert"
45
+ role="alert"
46
+ className={cn(alertVariants({ variant }), className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function AlertTitle({ className, ...props }: ComponentProps<'div'>) {
53
+ return (
54
+ <div
55
+ data-slot="alert-title"
56
+ className={cn('col-start-2 min-h-4 font-medium tracking-tight', className)}
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ function AlertDescription({ className, ...props }: ComponentProps<'div'>) {
63
+ return (
64
+ <div
65
+ data-slot="alert-description"
66
+ className={cn(
67
+ 'col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed',
68
+ className,
69
+ )}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export { Alert, AlertDescription, AlertTitle };
@@ -0,0 +1,40 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import type { ComponentProps } from 'react';
4
+
5
+ import { cn } from './utils';
6
+
7
+ /**
8
+ * The shadcn/ui badge — the cart count, a subscription line's label, an
9
+ * applied coupon. As with the button, the variant shadcn/ui paints with a raw
10
+ * `text-white` is not vendored: every colour here is a token.
11
+ */
12
+ const badgeVariants = cva(
13
+ 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&>svg]:pointer-events-none [&>svg]:size-3',
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
18
+ secondary:
19
+ 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
20
+ outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: 'default',
25
+ },
26
+ },
27
+ );
28
+
29
+ function Badge({
30
+ className,
31
+ variant,
32
+ asChild = false,
33
+ ...props
34
+ }: ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : 'span';
36
+
37
+ return <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />;
38
+ }
39
+
40
+ export { Badge, badgeVariants };
@@ -0,0 +1,64 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import type { ComponentProps } from 'react';
4
+
5
+ import { cn } from './utils';
6
+
7
+ /**
8
+ * The shadcn/ui button. `buttonVariants` is exported on its own so a link
9
+ * styled as a button keeps its own element — `<LocaleLink className={
10
+ * buttonVariants()}>` stays one crawlable `<a>` — and `asChild` hands the
11
+ * classes to whatever single child it wraps instead.
12
+ *
13
+ * Every colour is a token of the `:root` block in `src/app/globals.css`; the
14
+ * template has no destructive action, so the variant shadcn/ui paints with a
15
+ * raw `text-white` is not vendored.
16
+ */
17
+ const buttonVariants = cva(
18
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
19
+ {
20
+ variants: {
21
+ variant: {
22
+ default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
23
+ secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
24
+ outline:
25
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
26
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
27
+ link: 'text-primary underline-offset-4 hover:underline',
28
+ },
29
+ size: {
30
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
31
+ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
32
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
33
+ icon: 'size-9',
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: 'default',
38
+ size: 'default',
39
+ },
40
+ },
41
+ );
42
+
43
+ function Button({
44
+ className,
45
+ variant,
46
+ size,
47
+ asChild = false,
48
+ ...props
49
+ }: ComponentProps<'button'> &
50
+ VariantProps<typeof buttonVariants> & {
51
+ asChild?: boolean;
52
+ }) {
53
+ const Comp = asChild ? Slot : 'button';
54
+
55
+ return (
56
+ <Comp
57
+ data-slot="button"
58
+ className={cn(buttonVariants({ variant, size, className }))}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ export { Button, buttonVariants };
@@ -0,0 +1,28 @@
1
+ import type { ComponentProps } from 'react';
2
+
3
+ import { cn } from './utils';
4
+
5
+ /**
6
+ * The shadcn/ui card: a raised `bg-card` surface with its border and radius.
7
+ * The template's cards carry their own headings — a product tile's `<h3>`, a
8
+ * cart panel's `<h2>` — so the heading level stays the page's decision, and
9
+ * `CardContent` pads whatever sits inside.
10
+ */
11
+ function Card({ className, ...props }: ComponentProps<'div'>) {
12
+ return (
13
+ <div
14
+ data-slot="card"
15
+ className={cn(
16
+ 'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm',
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function CardContent({ className, ...props }: ComponentProps<'div'>) {
25
+ return <div data-slot="card-content" className={cn('px-6', className)} {...props} />;
26
+ }
27
+
28
+ export { Card, CardContent };
@@ -0,0 +1,26 @@
1
+ import type { ComponentProps } from 'react';
2
+
3
+ import { cn } from './utils';
4
+
5
+ /**
6
+ * The shadcn/ui text input. An invalid field is marked by `aria-invalid` —
7
+ * the attribute a screen reader announces is the one that paints the
8
+ * destructive border, so the two can never disagree.
9
+ */
10
+ function Input({ className, type, ...props }: ComponentProps<'input'>) {
11
+ return (
12
+ <input
13
+ type={type}
14
+ data-slot="input"
15
+ className={cn(
16
+ 'flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
17
+ 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
18
+ 'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ export { Input };
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ import * as LabelPrimitive from '@radix-ui/react-label';
4
+ import type { ComponentProps } from 'react';
5
+
6
+ import { cn } from './utils';
7
+
8
+ /** The shadcn/ui form label, on Radix's label primitive. */
9
+ function Label({ className, ...props }: ComponentProps<typeof LabelPrimitive.Root>) {
10
+ return (
11
+ <LabelPrimitive.Root
12
+ data-slot="label"
13
+ className={cn(
14
+ 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ export { Label };
@@ -0,0 +1,27 @@
1
+ import type { ComponentProps } from 'react';
2
+
3
+ import { cn } from './utils';
4
+
5
+ /**
6
+ * A native `<select>` dressed as a shadcn/ui field. The checkout's country
7
+ * select stays the browser's own control — it takes part in the form's
8
+ * `required` validation and in the platform's address autofill, which a
9
+ * custom listbox would have to rebuild — and only borrows the input's
10
+ * border, focus ring and `aria-invalid` treatment.
11
+ */
12
+ function NativeSelect({ className, ...props }: ComponentProps<'select'>) {
13
+ return (
14
+ <select
15
+ data-slot="native-select"
16
+ className={cn(
17
+ 'flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
18
+ 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
19
+ 'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ export { NativeSelect };
@@ -0,0 +1,21 @@
1
+ import type { ComponentProps } from 'react';
2
+
3
+ import { cn } from './utils';
4
+
5
+ /**
6
+ * The shadcn/ui loading placeholder. It pulses on `bg-muted`, the quiet
7
+ * surface, rather than on `bg-accent`: this variant's accent carries the
8
+ * daisyUI baseline's strong highlight, which would turn every loading block
9
+ * near-black.
10
+ */
11
+ function Skeleton({ className, ...props }: ComponentProps<'div'>) {
12
+ return (
13
+ <div
14
+ data-slot="skeleton"
15
+ className={cn('animate-pulse rounded-md bg-muted', className)}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Skeleton };
@@ -0,0 +1,16 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ /**
5
+ * shadcn/ui's class helper: joins conditional classes (`clsx`) and lets a
6
+ * later utility win over an earlier one of the same group (`tailwind-merge`),
7
+ * so a component's own classes can be overridden from its `className`.
8
+ *
9
+ * It lives beside the vendored components rather than under `src/lib/`: the
10
+ * library holds the storefront's logic, which every row of the template set
11
+ * shares byte for byte, and this helper belongs to the shadcn/ui components
12
+ * alone. `components.json` points the shadcn/ui CLI's `utils` alias here.
13
+ */
14
+ export function cn(...inputs: ClassValue[]): string {
15
+ return twMerge(clsx(inputs));
16
+ }