@vitrine-kit/vitrine 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/index.js +313 -135
  2. package/kit/registry/README.md +9 -0
  3. package/kit/registry/_index.json +74 -0
  4. package/kit/registry/cart/docs/cart.md +13 -0
  5. package/kit/registry/cart/feature.json +51 -0
  6. package/kit/registry/cart/files/app/(frontend)/cart/page.tsx +21 -0
  7. package/kit/registry/cart/files/app/api/cart/route.ts +41 -0
  8. package/kit/registry/cart/files/components/cart/AddToCart.tsx +40 -0
  9. package/kit/registry/cart/files/components/cart/CartIndicator.tsx +11 -0
  10. package/kit/registry/cart/files/components/cart/CartLineItem.tsx +27 -0
  11. package/kit/registry/cart/files/components/cart/CartSummary.tsx +31 -0
  12. package/kit/registry/cart/files/components/cart/CartView.tsx +31 -0
  13. package/kit/registry/cart/files/lib/cart/data.ts +12 -0
  14. package/kit/registry/cart/files/lib/cart/register.ts +10 -0
  15. package/kit/registry/catalog/docs/catalog.md +18 -0
  16. package/kit/registry/catalog/feature.json +43 -0
  17. package/kit/registry/catalog/files/components/catalog/CategoryNav.tsx +26 -0
  18. package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +42 -0
  19. package/kit/registry/catalog/files/components/catalog/ProductGrid.tsx +25 -0
  20. package/kit/registry/catalog/files/lib/catalog/data.ts +24 -0
  21. package/kit/registry/catalog/files/lib/catalog/register.ts +8 -0
  22. package/kit/registry/checkout/docs/checkout.md +17 -0
  23. package/kit/registry/checkout/feature.json +47 -0
  24. package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -0
  25. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +46 -0
  26. package/kit/registry/checkout/files/lib/checkout/fulfill.ts +72 -0
  27. package/kit/registry/checkout/files/lib/checkout/register.ts +8 -0
  28. package/kit/registry/checkout-paddle/docs/checkout-paddle.md +25 -0
  29. package/kit/registry/checkout-paddle/feature.json +62 -0
  30. package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +22 -0
  31. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +80 -0
  32. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/register.ts +7 -0
  33. package/kit/registry/checkout-stripe/docs/checkout-stripe.md +19 -0
  34. package/kit/registry/checkout-stripe/feature.json +54 -0
  35. package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +23 -0
  36. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +80 -0
  37. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/register.ts +7 -0
  38. package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +23 -0
  39. package/kit/registry/checkout-yookassa/feature.json +52 -0
  40. package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +22 -0
  41. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +92 -0
  42. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/register.ts +7 -0
  43. package/kit/registry/product-page/docs/product-page.md +16 -0
  44. package/kit/registry/product-page/feature.json +39 -0
  45. package/kit/registry/product-page/files/components/product/ProductGallery.tsx +37 -0
  46. package/kit/registry/product-page/files/components/product/ProductView.tsx +42 -0
  47. package/kit/registry/product-page/files/lib/product/data.ts +10 -0
  48. package/kit/registry/seo/docs/seo.md +14 -0
  49. package/kit/registry/seo/feature.json +35 -0
  50. package/kit/registry/seo/files/components/seo/JsonLd.tsx +15 -0
  51. package/kit/registry/seo/files/lib/seo/jsonld.ts +29 -0
  52. package/kit/registry/seo/files/lib/seo/metadata.ts +35 -0
  53. package/kit/registry/stubs/route-glue.d.ts +45 -0
  54. package/kit/registry/tsconfig.json +21 -0
  55. package/kit/registry/tsconfig.routes.json +9 -0
  56. package/kit/templates/README.md +27 -0
  57. package/kit/templates/backend-payload/files/.dockerignore +8 -0
  58. package/kit/templates/backend-payload/files/Dockerfile +29 -0
  59. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/not-found.tsx +18 -0
  60. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/page.tsx +18 -0
  61. package/kit/templates/backend-payload/files/app/(payload)/admin/importMap.js +3 -0
  62. package/kit/templates/backend-payload/files/app/(payload)/api/[...slug]/route.ts +18 -0
  63. package/kit/templates/backend-payload/files/app/(payload)/api/graphql/route.ts +6 -0
  64. package/kit/templates/backend-payload/files/app/(payload)/api/graphql-playground/route.ts +5 -0
  65. package/kit/templates/backend-payload/files/app/(payload)/custom.scss +1 -0
  66. package/kit/templates/backend-payload/files/app/(payload)/layout.tsx +23 -0
  67. package/kit/templates/backend-payload/files/docker-compose.yml +35 -0
  68. package/kit/templates/backend-payload/files/lib/adapter/db-decision.ts +43 -0
  69. package/kit/templates/backend-payload/files/lib/adapter/db-ping.ts +29 -0
  70. package/kit/templates/backend-payload/files/lib/adapter/db.ts +27 -0
  71. package/kit/templates/backend-payload/files/lib/adapter/index.ts +31 -0
  72. package/kit/templates/backend-payload/files/lib/adapter/map.ts +112 -0
  73. package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +79 -0
  74. package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +154 -0
  75. package/kit/templates/backend-payload/files/lib/adapter/payload-types.ts +50 -0
  76. package/kit/templates/backend-payload/files/lib/seed/admin.ts +23 -0
  77. package/kit/templates/backend-payload/files/lib/seed/demo.ts +79 -0
  78. package/kit/templates/backend-payload/files/lib/seed/guards.ts +5 -0
  79. package/kit/templates/backend-payload/files/lib/seed/richtext.ts +26 -0
  80. package/kit/templates/backend-payload/files/lib/seed/run.ts +51 -0
  81. package/kit/templates/backend-payload/files/next.config.mjs +10 -0
  82. package/kit/templates/backend-payload/files/payload.config.ts +32 -0
  83. package/kit/templates/backend-payload/files/seed-assets/placeholder-1.svg +5 -0
  84. package/kit/templates/backend-payload/files/seed-assets/placeholder-2.svg +5 -0
  85. package/kit/templates/backend-payload/files/seed-assets/placeholder-3.svg +5 -0
  86. package/kit/templates/backend-payload/files/seed-assets/placeholder-4.svg +5 -0
  87. package/kit/templates/backend-payload/files/seed-assets/placeholder-5.svg +5 -0
  88. package/kit/templates/backend-vendure/files/.dockerignore +8 -0
  89. package/kit/templates/backend-vendure/files/Dockerfile +24 -0
  90. package/kit/templates/backend-vendure/files/docker-compose.yml +50 -0
  91. package/kit/templates/backend-vendure/files/lib/adapter/graphql.ts +27 -0
  92. package/kit/templates/backend-vendure/files/lib/adapter/index.ts +20 -0
  93. package/kit/templates/backend-vendure/files/lib/adapter/map.ts +107 -0
  94. package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +62 -0
  95. package/kit/templates/backend-vendure/files/lib/adapter/vendure-commerce.ts +76 -0
  96. package/kit/templates/backend-vendure/files/lib/adapter/vendure-types.ts +61 -0
  97. package/kit/templates/backend-vendure/files/lib/seed/populate.ts +22 -0
  98. package/kit/templates/backend-vendure/files/src/index.ts +15 -0
  99. package/kit/templates/backend-vendure/files/vendure-config.ts +56 -0
  100. package/kit/templates/base/files/.claude/commands/add-feature.md +18 -0
  101. package/kit/templates/base/files/.claude/commands/design.md +14 -0
  102. package/kit/templates/base/files/.claude/commands/doctor.md +15 -0
  103. package/kit/templates/base/files/.claude/commands/setup.md +15 -0
  104. package/kit/templates/base/files/.claude/commands/update.md +16 -0
  105. package/kit/templates/base/files/.gitattributes +12 -0
  106. package/kit/templates/base/files/AGENTS.md +20 -0
  107. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +28 -0
  108. package/kit/templates/base/files/app/(frontend)/layout.tsx +32 -0
  109. package/kit/templates/base/files/app/(frontend)/page.tsx +27 -0
  110. package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +43 -0
  111. package/kit/templates/base/files/app/globals.css +6 -0
  112. package/kit/templates/base/files/components/layout/SiteFooter.tsx +15 -0
  113. package/kit/templates/base/files/components/layout/SiteHeader.tsx +23 -0
  114. package/kit/templates/base/files/design/README.md +9 -0
  115. package/kit/templates/base/files/lib/site.ts +10 -0
  116. package/kit/templates/base/files/next.config.mjs +7 -0
  117. package/kit/templates/base/files/postcss.config.mjs +7 -0
  118. package/kit/templates/base/files/public/.gitkeep +0 -0
  119. package/kit/templates/base/files/tailwind.config.ts +13 -0
  120. package/kit/templates/base/files/tsconfig.json +25 -0
  121. package/kit/templates/tsconfig.json +18 -0
  122. package/package.json +9 -5
@@ -0,0 +1,9 @@
1
+ # registry/ — copy-in feature registry
2
+
3
+ The source for `vitrine init` / `vitrine add`. Each feature = a folder with `feature.json` (manifest) + `files/` (sources). On install the files are **copied** into the client repository (the client owns the code).
4
+
5
+ `_index.json` is the registry manifest: the list of all features + the kit version.
6
+
7
+ **Rule (spec §4, §13):** the registry holds only what varies per client (UI, sections, wiring). No critical logic — that lives in `@vitrine-kit/core`. A new feature must depend **only on the contracts**.
8
+
9
+ **Typecheck in the monorepo:** `pnpm typecheck:registry` (components + lib) and `pnpm typecheck:registry-routes` (Next API route glue via `stubs/route-glue.d.ts`). Full Next/Payload pages are checked when the client is instantiated.
@@ -0,0 +1,74 @@
1
+ {
2
+ "$schema": "../schemas/registry-index.schema.json",
3
+ "kitVersion": "0.4.1",
4
+ "contracts": "1.0.0",
5
+ "features": {
6
+ "catalog": {
7
+ "title": "Product catalog",
8
+ "kitVersion": "0.4.1",
9
+ "tier": [
10
+ "catalog",
11
+ "simple-store",
12
+ "full-store"
13
+ ]
14
+ },
15
+ "product-page": {
16
+ "title": "Product page",
17
+ "kitVersion": "0.4.1",
18
+ "tier": [
19
+ "catalog",
20
+ "simple-store",
21
+ "full-store"
22
+ ]
23
+ },
24
+ "seo": {
25
+ "title": "SEO — metadata and JSON-LD",
26
+ "kitVersion": "0.4.1",
27
+ "tier": [
28
+ "catalog",
29
+ "simple-store",
30
+ "full-store"
31
+ ]
32
+ },
33
+ "cart": {
34
+ "title": "Cart",
35
+ "kitVersion": "0.4.1",
36
+ "tier": [
37
+ "simple-store",
38
+ "full-store"
39
+ ]
40
+ },
41
+ "checkout": {
42
+ "title": "Checkout",
43
+ "kitVersion": "0.4.1",
44
+ "tier": [
45
+ "simple-store",
46
+ "full-store"
47
+ ]
48
+ },
49
+ "checkout-stripe": {
50
+ "title": "Payment — Stripe",
51
+ "kitVersion": "0.4.1",
52
+ "tier": [
53
+ "simple-store",
54
+ "full-store"
55
+ ]
56
+ },
57
+ "checkout-paddle": {
58
+ "title": "Payment — Paddle",
59
+ "kitVersion": "0.4.1",
60
+ "tier": [
61
+ "simple-store",
62
+ "full-store"
63
+ ]
64
+ },
65
+ "checkout-yookassa": {
66
+ "title": "Payment — YooKassa",
67
+ "kitVersion": "0.4.1",
68
+ "tier": [
69
+ "simple-store",
70
+ "full-store"
71
+ ]
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,13 @@
1
+ # Feature: cart
2
+
3
+ The cart for the `simple-store` / `full-store` tiers. Depends only on the contracts:
4
+ data goes through `CommerceBackend`, the arithmetic lives in `@vitrine-kit/core` (critical logic).
5
+
6
+ - **Components:** `CartView`, `CartLineItem`, `CartSummary` (hosts the `cart.summary` slot),
7
+ `AddToCart` (client component, `product.purchase` slot), `CartIndicator` (`global.header-actions` slot).
8
+ - **Routes/API (Next glue):** `/cart` (page), `POST/PATCH/DELETE /api/cart`
9
+ (cart id in an httpOnly cookie, mutations via `CommerceBackend`).
10
+ - **Slots:** `product.purchase` → `AddToCart`; `global.header-actions` → `CartIndicator`;
11
+ hosts `cart.summary` (where checkout-stripe places the checkout button).
12
+
13
+ Styled with tokens; the AI design step doesn't touch behavior.
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "../../schemas/feature.schema.json",
3
+ "name": "cart",
4
+ "title": "Cart",
5
+ "kitVersion": "0.4.1",
6
+ "requiresContracts": ">=1.0.0 <2.0.0",
7
+ "tier": [
8
+ "simple-store",
9
+ "full-store"
10
+ ],
11
+ "registryDependencies": [],
12
+ "corePackages": {
13
+ "@vitrine-kit/core": ">=0.1.0"
14
+ },
15
+ "npm": [],
16
+ "files": [
17
+ {
18
+ "from": "files/components/cart/",
19
+ "to": "components/cart/"
20
+ },
21
+ {
22
+ "from": "files/lib/cart/",
23
+ "to": "lib/cart/"
24
+ },
25
+ {
26
+ "from": "files/app/",
27
+ "to": "app/"
28
+ }
29
+ ],
30
+ "config": {
31
+ "set": {
32
+ "features.cart": true
33
+ }
34
+ },
35
+ "slots": [
36
+ {
37
+ "slot": "product.purchase",
38
+ "component": "AddToCart",
39
+ "order": 10
40
+ },
41
+ {
42
+ "slot": "global.header-actions",
43
+ "component": "CartIndicator",
44
+ "order": 20
45
+ }
46
+ ],
47
+ "env": [],
48
+ "claudeDoc": "docs/cart.md",
49
+ "conflicts": [],
50
+ "removable": true
51
+ }
@@ -0,0 +1,21 @@
1
+ // Cart page. The cart id is stored in a cookie; data comes via the CommerceBackend
2
+ // contract (the backend adapter in lib/adapter). Not typechecked in the monorepo
3
+ // (Next glue), verified when the client is instantiated.
4
+ import { cookies } from 'next/headers';
5
+ import { emptyCart } from '@vitrine-kit/core';
6
+ import { getCommerceBackend } from '@/lib/adapter';
7
+ import { CartView } from '@/components/cart/CartView';
8
+ import { siteConfig } from '@/site.config';
9
+
10
+ export default async function CartPage() {
11
+ const cartId = (await cookies()).get('vitrine_cart')?.value;
12
+ const commerce = await getCommerceBackend();
13
+ const cart = cartId ? await commerce.getCart(cartId) : null;
14
+
15
+ return (
16
+ <section className="flex flex-col gap-section">
17
+ <h1 className="font-heading text-fg">Cart</h1>
18
+ <CartView cart={cart ?? emptyCart('', siteConfig.i18n.currency)} />
19
+ </section>
20
+ );
21
+ }
@@ -0,0 +1,41 @@
1
+ // Cart API: POST add, PATCH change quantity, DELETE remove a line.
2
+ // Cart id in an httpOnly cookie. Mutations are delegated to CommerceBackend (lib/adapter),
3
+ // the arithmetic lives in @vitrine-kit/core. Next glue, not typechecked in the monorepo.
4
+ import { cookies } from 'next/headers';
5
+ import { NextResponse } from 'next/server';
6
+ import type { CommerceBackend } from '@vitrine-kit/contracts';
7
+ import { getCommerceBackend } from '@/lib/adapter';
8
+
9
+ const COOKIE = 'vitrine_cart';
10
+
11
+ async function ensureCartId(commerce: CommerceBackend): Promise<string> {
12
+ const jar = await cookies();
13
+ const existing = jar.get(COOKIE)?.value;
14
+ if (existing) return existing;
15
+ const cart = await commerce.createCart();
16
+ jar.set(COOKIE, cart.id, { httpOnly: true, sameSite: 'lax', path: '/' });
17
+ return cart.id;
18
+ }
19
+
20
+ export async function POST(req: Request) {
21
+ const { variantId, quantity = 1 } = (await req.json()) as { variantId: string; quantity?: number };
22
+ const commerce = await getCommerceBackend();
23
+ const id = await ensureCartId(commerce);
24
+ return NextResponse.json(await commerce.addItem(id, variantId, quantity));
25
+ }
26
+
27
+ export async function PATCH(req: Request) {
28
+ const { lineId, quantity } = (await req.json()) as { lineId: string; quantity: number };
29
+ const id = (await cookies()).get(COOKIE)?.value;
30
+ if (!id) return NextResponse.json({ error: 'cart not found' }, { status: 400 });
31
+ const commerce = await getCommerceBackend();
32
+ return NextResponse.json(await commerce.updateItem(id, lineId, quantity));
33
+ }
34
+
35
+ export async function DELETE(req: Request) {
36
+ const { lineId } = (await req.json()) as { lineId: string };
37
+ const id = (await cookies()).get(COOKIE)?.value;
38
+ if (!id) return NextResponse.json({ error: 'cart not found' }, { status: 400 });
39
+ const commerce = await getCommerceBackend();
40
+ return NextResponse.json(await commerce.removeItem(id, lineId));
41
+ }
@@ -0,0 +1,40 @@
1
+ // "Add to cart" button — client component. Mounted into the product.purchase slot
2
+ // (ProductView passes product). Mutation goes through POST /api/cart (the feature's route).
3
+ 'use client';
4
+ import { useState } from 'react';
5
+ import type { Product } from '@vitrine-kit/contracts';
6
+
7
+ export interface AddToCartProps {
8
+ product: Product;
9
+ }
10
+
11
+ export function AddToCart({ product }: AddToCartProps) {
12
+ const [pending, setPending] = useState(false);
13
+ const variantId = product.variants[0]?.id;
14
+
15
+ async function add(): Promise<void> {
16
+ if (!variantId) return;
17
+ setPending(true);
18
+ try {
19
+ await fetch('/api/cart', {
20
+ method: 'POST',
21
+ headers: { 'content-type': 'application/json' },
22
+ body: JSON.stringify({ variantId, quantity: 1 }),
23
+ });
24
+ location.assign('/cart');
25
+ } finally {
26
+ setPending(false);
27
+ }
28
+ }
29
+
30
+ return (
31
+ <button
32
+ type="button"
33
+ onClick={add}
34
+ disabled={pending || !variantId}
35
+ className="vt-add-to-cart rounded-md bg-primary px-gutter py-unit text-primary-fg transition hover:opacity-90 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 ring-ring"
36
+ >
37
+ {pending ? 'Adding…' : 'Add to cart'}
38
+ </button>
39
+ );
40
+ }
@@ -0,0 +1,11 @@
1
+ // Header cart link (global.header-actions slot). Presentational.
2
+ export function CartIndicator() {
3
+ return (
4
+ <a
5
+ href="/cart"
6
+ className="vt-cart-indicator text-fg transition hover:text-primary focus-visible:outline-none focus-visible:ring-2 ring-ring"
7
+ >
8
+ Cart
9
+ </a>
10
+ );
11
+ }
@@ -0,0 +1,27 @@
1
+ // Cart line item — presentational.
2
+ import type { CartLine } from '@vitrine-kit/contracts';
3
+ import { formatMoney } from '../../lib/cart/data.js';
4
+
5
+ export interface CartLineItemProps {
6
+ line: CartLine;
7
+ currency: string;
8
+ }
9
+
10
+ export function CartLineItem({ line, currency }: CartLineItemProps) {
11
+ return (
12
+ <li className="vt-cart-line flex items-center gap-gutter border-b border-border py-unit">
13
+ {line.image ? (
14
+ <img src={line.image} alt="" className="h-16 w-16 rounded-md object-cover" />
15
+ ) : (
16
+ <div className="h-16 w-16 rounded-md bg-muted" aria-hidden="true" />
17
+ )}
18
+ <div className="flex-1">
19
+ <p className="text-fg">{line.title}</p>
20
+ <p className="text-muted-fg">
21
+ {line.quantity} × {formatMoney(line.unitPrice, currency)}
22
+ </p>
23
+ </div>
24
+ <p className="text-price">{formatMoney(line.lineTotal, currency)}</p>
25
+ </li>
26
+ );
27
+ }
@@ -0,0 +1,31 @@
1
+ // Cart totals. Hosts the cart.summary slot (where checkout-stripe mounts the checkout
2
+ // button) and passes it cartId.
3
+ import type { Cart } from '@vitrine-kit/contracts';
4
+ import { Slot } from '@vitrine-kit/core/react';
5
+ import { formatMoney } from '../../lib/cart/data.js';
6
+
7
+ export interface CartSummaryProps {
8
+ cart: Cart;
9
+ }
10
+
11
+ export function CartSummary({ cart }: CartSummaryProps) {
12
+ return (
13
+ <aside className="vt-cart-summary flex flex-col gap-unit rounded-md border border-border p-gutter">
14
+ <div className="flex justify-between text-muted-fg">
15
+ <span>Subtotal</span>
16
+ <span>{formatMoney(cart.subtotal, cart.currency)}</span>
17
+ </div>
18
+ {cart.discountTotal ? (
19
+ <div className="flex justify-between text-muted-fg">
20
+ <span>Discount</span>
21
+ <span>−{formatMoney(cart.discountTotal, cart.currency)}</span>
22
+ </div>
23
+ ) : null}
24
+ <div className="flex justify-between text-fg">
25
+ <span>Total</span>
26
+ <span className="text-price">{formatMoney(cart.total, cart.currency)}</span>
27
+ </div>
28
+ <Slot name="cart.summary" cartId={cart.id} />
29
+ </aside>
30
+ );
31
+ }
@@ -0,0 +1,31 @@
1
+ // Cart contents — presentational. Hosts the cart.items-bottom / cart.below slots.
2
+ import type { Cart } from '@vitrine-kit/contracts';
3
+ import { Slot } from '@vitrine-kit/core/react';
4
+ import { CartLineItem } from './CartLineItem.js';
5
+ import { CartSummary } from './CartSummary.js';
6
+
7
+ export interface CartViewProps {
8
+ cart: Cart;
9
+ }
10
+
11
+ export function CartView({ cart }: CartViewProps) {
12
+ if (cart.lines.length === 0) {
13
+ return <p className="text-muted-fg">Your cart is empty.</p>;
14
+ }
15
+ return (
16
+ <div className="vt-cart grid gap-section md:grid-cols-[2fr_1fr]">
17
+ <div>
18
+ <ul role="list">
19
+ {cart.lines.map((line) => (
20
+ <CartLineItem key={line.id} line={line} currency={cart.currency} />
21
+ ))}
22
+ </ul>
23
+ <Slot name="cart.items-bottom" />
24
+ </div>
25
+ <div className="flex flex-col gap-gutter">
26
+ <CartSummary cart={cart} />
27
+ <Slot name="cart.below" />
28
+ </div>
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,12 @@
1
+ // Cart access via the CommerceBackend contract + price formatting.
2
+ // The cart arithmetic lives in @vitrine-kit/core (critical logic); a ready Cart arrives here.
3
+ import type { Cart, CommerceBackend } from '@vitrine-kit/contracts';
4
+
5
+ export async function loadCart(commerce: CommerceBackend, cartId: string): Promise<Cart | null> {
6
+ return commerce.getCart(cartId);
7
+ }
8
+
9
+ /** Money — minor units (e.g. cents); divide by 100 for 2-decimal currencies. */
10
+ export function formatMoney(amount: number, currency: string, locale = 'en-US'): string {
11
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount / 100);
12
+ }
@@ -0,0 +1,10 @@
1
+ // Registers the cart feature's slots. The client calls this from lib/slots.ts.
2
+ // AddToCart — on the product page (product.purchase), CartIndicator — in the header.
3
+ import { registerSlot } from '@vitrine-kit/core';
4
+ import { AddToCart } from '../../components/cart/AddToCart.js';
5
+ import { CartIndicator } from '../../components/cart/CartIndicator.js';
6
+
7
+ export function registerCartSlots(): void {
8
+ registerSlot({ slot: 'product.purchase', component: AddToCart, order: 10 });
9
+ registerSlot({ slot: 'global.header-actions', component: CartIndicator, order: 20 });
10
+ }
@@ -0,0 +1,18 @@
1
+ # Feature: catalog
2
+
3
+ Browsing the assortment: product grid, product card, category navigation.
4
+
5
+ ## Components
6
+ - `components/catalog/ProductGrid` — grid (semantic list) of `ProductCard`s.
7
+ - `components/catalog/ProductCard` — card (image, title, price).
8
+ - `components/catalog/CategoryNav` — navigation; mounted into the `global.header-nav` slot.
9
+
10
+ ## Data
11
+ - `lib/catalog/data` — `loadProducts`/`loadProduct`/`loadCategories` over the `CatalogSource` contract + `formatPrice` (price in minor units).
12
+ - `lib/catalog/register` — registers the `global.header-nav` slot.
13
+
14
+ ## Styling
15
+ All classes are token-based (`bg-surface`, `text-fg`, `text-price`, `rounded-md`, `gap-gutter`). The design step changes only token values, not structure/a11y.
16
+
17
+ ## Contracts
18
+ Depends only on `@vitrine-kit/contracts` (types, `CatalogSource`) and `@vitrine-kit/core` (`registerSlot`). Portable between Payload and Vendure.
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "../../schemas/feature.schema.json",
3
+ "name": "catalog",
4
+ "title": "Product catalog",
5
+ "kitVersion": "0.4.1",
6
+ "requiresContracts": ">=1.0.0 <2.0.0",
7
+ "tier": [
8
+ "catalog",
9
+ "simple-store",
10
+ "full-store"
11
+ ],
12
+ "registryDependencies": [],
13
+ "corePackages": {
14
+ "@vitrine-kit/core": ">=0.1.0"
15
+ },
16
+ "npm": [],
17
+ "files": [
18
+ {
19
+ "from": "files/components/catalog/",
20
+ "to": "components/catalog/"
21
+ },
22
+ {
23
+ "from": "files/lib/catalog/",
24
+ "to": "lib/catalog/"
25
+ }
26
+ ],
27
+ "config": {
28
+ "set": {
29
+ "features.catalog": true
30
+ }
31
+ },
32
+ "slots": [
33
+ {
34
+ "slot": "global.header-nav",
35
+ "component": "CategoryNav",
36
+ "order": 10
37
+ }
38
+ ],
39
+ "env": [],
40
+ "claudeDoc": "docs/catalog.md",
41
+ "conflicts": [],
42
+ "removable": false
43
+ }
@@ -0,0 +1,26 @@
1
+ // Category navigation — presentational. Data comes as a prop from the slot host
2
+ // (<Slot name="global.header-nav" categories={…} />).
3
+ import type { Category } from '@vitrine-kit/contracts';
4
+
5
+ export interface CategoryNavProps {
6
+ categories: Category[];
7
+ }
8
+
9
+ export function CategoryNav({ categories }: CategoryNavProps) {
10
+ return (
11
+ <nav aria-label="Categories" className="vt-category-nav">
12
+ <ul role="list" className="flex flex-wrap gap-gutter">
13
+ {categories.map((category) => (
14
+ <li key={category.id}>
15
+ <a
16
+ href={`/categories/${category.slug}`}
17
+ className="text-fg transition hover:text-primary focus-visible:outline-none focus-visible:ring-2 ring-ring"
18
+ >
19
+ {category.title}
20
+ </a>
21
+ </li>
22
+ ))}
23
+ </ul>
24
+ </nav>
25
+ );
26
+ }
@@ -0,0 +1,42 @@
1
+ // Product card — presentational, visually neutral (classes → tokens).
2
+ // The design step doesn't touch structure/a11y, only tokens.
3
+ import type { Product } from '@vitrine-kit/contracts';
4
+ import { formatPrice } from '../../lib/catalog/data.js';
5
+
6
+ export interface ProductCardProps {
7
+ product: Product;
8
+ }
9
+
10
+ export function ProductCard({ product }: ProductCardProps) {
11
+ const price = product.priceRange?.min ?? product.variants[0]?.price;
12
+ const currency = product.priceRange?.currency ?? product.variants[0]?.currency ?? 'USD';
13
+ const image = product.images[0];
14
+
15
+ return (
16
+ <article className="vt-product-card rounded-md border border-border bg-surface text-surface-fg">
17
+ <a
18
+ href={`/products/${product.slug}`}
19
+ className="block focus-visible:outline-none focus-visible:ring-2 ring-ring"
20
+ >
21
+ {image ? (
22
+ <img
23
+ src={image.url}
24
+ alt={image.alt ?? product.title}
25
+ width={image.width}
26
+ height={image.height}
27
+ loading="lazy"
28
+ className="aspect-square w-full rounded-t-md object-cover"
29
+ />
30
+ ) : (
31
+ <div className="aspect-square w-full rounded-t-md bg-muted" aria-hidden="true" />
32
+ )}
33
+ <div className="p-gutter">
34
+ <h3 className="font-heading text-fg">{product.title}</h3>
35
+ {price != null ? (
36
+ <p className="text-price">{formatPrice(price, currency)}</p>
37
+ ) : null}
38
+ </div>
39
+ </a>
40
+ </article>
41
+ );
42
+ }
@@ -0,0 +1,25 @@
1
+ // Product grid — presentational, semantic list.
2
+ import type { Product } from '@vitrine-kit/contracts';
3
+ import { ProductCard } from './ProductCard.js';
4
+
5
+ export interface ProductGridProps {
6
+ products: Product[];
7
+ }
8
+
9
+ export function ProductGrid({ products }: ProductGridProps) {
10
+ if (products.length === 0) {
11
+ return <p className="text-muted-fg">No products found.</p>;
12
+ }
13
+ return (
14
+ <ul
15
+ role="list"
16
+ className="vt-product-grid grid grid-cols-2 gap-gutter md:grid-cols-3 lg:grid-cols-4"
17
+ >
18
+ {products.map((product) => (
19
+ <li key={product.id}>
20
+ <ProductCard product={product} />
21
+ </li>
22
+ ))}
23
+ </ul>
24
+ );
25
+ }
@@ -0,0 +1,24 @@
1
+ // Catalog access via the CatalogSource contract. The source (Payload*/Vendure*)
2
+ // is resolved in the page data (template); a ready CatalogSource arrives here —
3
+ // so the feature depends only on the contract and is portable across backends.
4
+ import type { CatalogSource, Category, Product, ProductQuery } from '@vitrine-kit/contracts';
5
+
6
+ export async function loadProducts(
7
+ source: CatalogSource,
8
+ query: ProductQuery = {},
9
+ ): Promise<Product[]> {
10
+ return source.listProducts(query);
11
+ }
12
+
13
+ export async function loadProduct(source: CatalogSource, slug: string): Promise<Product | null> {
14
+ return source.getProduct(slug);
15
+ }
16
+
17
+ export async function loadCategories(source: CatalogSource): Promise<Category[]> {
18
+ return source.listCategories();
19
+ }
20
+
21
+ /** Money — minor units (e.g. cents); divide by 100 for 2-decimal currencies. */
22
+ export function formatPrice(amount: number, currency: string, locale = 'en-US'): string {
23
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount / 100);
24
+ }
@@ -0,0 +1,8 @@
1
+ // Registers the catalog feature's slots. The client calls this from lib/slots.ts.
2
+ // CategoryNav receives categories from the slot host via <Slot> props.
3
+ import { registerSlot } from '@vitrine-kit/core';
4
+ import { CategoryNav } from '../../components/catalog/CategoryNav.js';
5
+
6
+ export function registerCatalogSlots(): void {
7
+ registerSlot({ slot: 'global.header-nav', component: CategoryNav, order: 10 });
8
+ }
@@ -0,0 +1,17 @@
1
+ # Feature: checkout
2
+
3
+ A provider-agnostic checkout scaffold for `simple-store` / `full-store`. Depends on the
4
+ `cart` feature. It does NOT process payments on its own — add one of the provider features:
5
+ `checkout-stripe`, `checkout-paddle`, or `checkout-yookassa` (exactly one).
6
+
7
+ - **Component:** `CheckoutButton` (client component) — `cart.summary` slot. Calls
8
+ `POST /api/checkout` and redirects to the provider URL. It knows nothing about the provider.
9
+ - **API (Next glue):** `POST /api/checkout` — `CommerceBackend.startCheckout`
10
+ delegates to the active provider (`payments.resolve` by `integrations.payments`).
11
+ - **Shared webhook code:** `lib/checkout/fulfill.ts` → `fulfillOrderFromEvent` —
12
+ idempotent order creation from a normalized event (used by all provider webhook
13
+ routes). The critical logic (dedup by `paymentRef`, order snapshot) lives in
14
+ `@vitrine-kit/core` (`shouldCreateOrder`, `buildOrderFromCart`).
15
+
16
+ Flow: cart → `Checkout` → `POST /api/checkout` → redirect to the provider →
17
+ provider webhook → `fulfillOrderFromEvent` → order in the admin, cart `converted`.
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "../../schemas/feature.schema.json",
3
+ "name": "checkout",
4
+ "title": "Checkout",
5
+ "kitVersion": "0.4.1",
6
+ "requiresContracts": ">=1.0.0 <2.0.0",
7
+ "tier": [
8
+ "simple-store",
9
+ "full-store"
10
+ ],
11
+ "registryDependencies": [
12
+ "cart"
13
+ ],
14
+ "corePackages": {
15
+ "@vitrine-kit/core": ">=0.1.0"
16
+ },
17
+ "npm": [],
18
+ "files": [
19
+ {
20
+ "from": "files/components/checkout/",
21
+ "to": "components/checkout/"
22
+ },
23
+ {
24
+ "from": "files/lib/checkout/",
25
+ "to": "lib/checkout/"
26
+ },
27
+ {
28
+ "from": "files/app/",
29
+ "to": "app/"
30
+ }
31
+ ],
32
+ "config": {
33
+ "set": {
34
+ "features.checkout": true
35
+ }
36
+ },
37
+ "slots": [
38
+ {
39
+ "slot": "cart.summary",
40
+ "component": "CheckoutButton",
41
+ "order": 10
42
+ }
43
+ ],
44
+ "claudeDoc": "docs/checkout.md",
45
+ "conflicts": [],
46
+ "removable": true
47
+ }
@@ -0,0 +1,14 @@
1
+ // POST /api/checkout — creates a checkout session with the active payment provider for
2
+ // the current cart (cookie) via CommerceBackend.startCheckout and returns the
3
+ // redirect URL. Provider-agnostic: startCheckout delegates to payments.resolve. Next glue.
4
+ import { cookies } from 'next/headers';
5
+ import { NextResponse } from 'next/server';
6
+ import { getCommerceBackend } from '@/lib/adapter';
7
+
8
+ export async function POST() {
9
+ const cartId = (await cookies()).get('vitrine_cart')?.value;
10
+ if (!cartId) return NextResponse.json({ error: 'cart is empty' }, { status: 400 });
11
+ const commerce = await getCommerceBackend();
12
+ const { redirectUrl } = await commerce.startCheckout(cartId);
13
+ return NextResponse.json({ url: redirectUrl });
14
+ }