create-cartbase 0.1.5 → 0.1.7
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.
- package/README.md +13 -8
- package/dist/index.js +28 -20
- package/package.json +3 -3
- package/template/app/AGENTS.md +29 -0
- package/template/app/CLAUDE.md +19 -8
- package/template/app/docs/BUILD-A-STOREFRONT.md +233 -226
- package/template/app/docs/README.md +1 -0
- package/template/app/docs/components.md +1134 -1090
- package/template/app/docs/deploy.md +15 -10
- package/template/app/docs/integrations.md +15 -3
- package/template/app/docs/platform.md +1 -1
- package/template/app/docs/store.md +47 -0
- package/template/app/docs/variables.md +3 -3
- package/template/app/next.config.ts +10 -14
- package/template/app/package.json +4 -3
- package/template/app/src/app/checkout/checkout-page-client.tsx +0 -20
- package/template/app/src/app/globals.css +1 -1
- package/template/app/src/app/layout.tsx +73 -18
- package/template/app/src/app/order/[id]/confirmed/page.tsx +92 -78
- package/template/app/src/lib/config.ts +30 -21
- package/template/app/next-env.d.ts +0 -6
- package/template/app/smoke.mjs +0 -158
- package/template/app/src/app/checkout/mypos-demo-tab.tsx +0 -101
- package/template/app/src/app/gallery/[slug]/page.tsx +0 -172
- package/template/app/src/app/gallery/_components/specimens.tsx +0 -254
- package/template/app/src/app/gallery/_components/status.tsx +0 -47
- package/template/app/src/app/gallery/_lib/catalog.ts +0 -59
- package/template/app/src/app/gallery/_lib/registry.ts +0 -401
- package/template/app/src/app/gallery/design-system/page.tsx +0 -353
- package/template/app/src/app/gallery/layout.tsx +0 -83
- package/template/app/src/app/gallery/page.tsx +0 -81
- package/template/app/tsconfig.tsbuildinfo +0 -1
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
/** Shared storefront configuration (BUILD-A-STOREFRONT.md —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
export const
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
/** Shared storefront configuration (BUILD-A-STOREFRONT.md — the one input). */
|
|
2
|
+
|
|
3
|
+
import { cartbaseApiOrigin } from "@cartbase/storefront/lib/platform"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The store's publishable key names the store and scopes its catalog: the
|
|
7
|
+
* ONE value a storefront needs (one key, 2026-09-07). The origin is the
|
|
8
|
+
* platform's, overridable for a local or staging platform; the client id is
|
|
9
|
+
* the platform's own door (hosted builds carry it) and never something to
|
|
10
|
+
* copy by hand.
|
|
11
|
+
*/
|
|
12
|
+
export const BARTER_PUBLISHABLE_KEY =
|
|
13
|
+
process.env.NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY || undefined
|
|
14
|
+
export const BARTER_URL = cartbaseApiOrigin(process.env.NEXT_PUBLIC_CARTBASE_URL)
|
|
15
|
+
export const BARTER_CLIENT_ID = process.env.NEXT_PUBLIC_CARTBASE_CLIENT_ID || undefined
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cart-id cookie — owned by the app (the SDK never persists the cart);
|
|
19
|
+
* the NAME is owned by the library so every consumer emits the same wire
|
|
20
|
+
* fingerprint (platform-fingerprints card). `readCartCookie` is the
|
|
21
|
+
* backward-compat reader for the legacy `_barter_cart_id` name.
|
|
22
|
+
*/
|
|
23
|
+
export { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
|
|
24
|
+
export const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
25
|
+
|
|
26
|
+
/** Locale cookie read by the clients' `getLocale`. */
|
|
27
|
+
export const LOCALE_COOKIE = "_barter_locale"
|
|
28
|
+
|
|
29
|
+
/** Pricing context for every catalog surface (runbook step 5). EUR only. */
|
|
30
|
+
export const PRICING_CONTEXT = { currency_code: "eur" } as const
|
package/template/app/smoke.mjs
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adoption-proof smoke: drives the REAL example-storefront UI from home to
|
|
3
|
-
* a completed pp_manual checkout against the barter backend.
|
|
4
|
-
*
|
|
5
|
-
* Prereqs: backend on :4777, example app on :4778 (see examples/storefront
|
|
6
|
-
* package.json). Run: `node examples/storefront/smoke.mjs` from the repo
|
|
7
|
-
* root (Playwright is a root devDependency). Exits 0 only when the
|
|
8
|
-
* confirmation page shows an order number and totals.
|
|
9
|
-
*/
|
|
10
|
-
import { chromium } from "@playwright/test"
|
|
11
|
-
|
|
12
|
-
const BASE = process.env.SMOKE_BASE_URL ?? "http://127.0.0.1:4778"
|
|
13
|
-
const trail = []
|
|
14
|
-
const step = (msg) => {
|
|
15
|
-
trail.push(msg)
|
|
16
|
-
console.log(`[smoke] ${msg}`)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const browser = await chromium.launch()
|
|
20
|
-
const page = await browser.newPage()
|
|
21
|
-
page.setDefaultTimeout(30_000)
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
// ── Home ────────────────────────────────────────────────────────────
|
|
25
|
-
await page.goto(`${BASE}/`, { waitUntil: "domcontentloaded" })
|
|
26
|
-
step("home loaded")
|
|
27
|
-
|
|
28
|
-
// Consent banner (builtin modal is the server default) — accept.
|
|
29
|
-
const acceptButton = page.getByRole("button", { name: "Accept", exact: true })
|
|
30
|
-
try {
|
|
31
|
-
await acceptButton.waitFor({ state: "visible", timeout: 10_000 })
|
|
32
|
-
await acceptButton.click()
|
|
33
|
-
step("consent banner accepted")
|
|
34
|
-
} catch {
|
|
35
|
-
step("no consent banner rendered (disabled or external mode)")
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await page.locator('a[href^="/products/"]').first().waitFor()
|
|
39
|
-
step("home renders product cards")
|
|
40
|
-
|
|
41
|
-
// ── PDP (seeded handle) ─────────────────────────────────────────────
|
|
42
|
-
await page.goto(`${BASE}/products/linen-shirt`, {
|
|
43
|
-
waitUntil: "domcontentloaded",
|
|
44
|
-
})
|
|
45
|
-
await page.getByTestId("product-container").waitFor()
|
|
46
|
-
step("PDP linen-shirt rendered")
|
|
47
|
-
|
|
48
|
-
// Pick the REAL Size option's "M" (the shared dev tenant accretes junk
|
|
49
|
-
// options; variant match needs exactly the Size choice). Dev-mode
|
|
50
|
-
// hydration can lag the SSR paint — retry until the selection sticks.
|
|
51
|
-
const sizeRow = page
|
|
52
|
-
.locator('div.flex.flex-col:has(> span:text-is("Select Size"))')
|
|
53
|
-
.first()
|
|
54
|
-
const mButton = sizeRow.getByRole("button", { name: "M", exact: true })
|
|
55
|
-
let selected = false
|
|
56
|
-
for (let i = 0; i < 30 && !selected; i++) {
|
|
57
|
-
await mButton.click()
|
|
58
|
-
await page.waitForTimeout(700)
|
|
59
|
-
selected = ((await mButton.getAttribute("class")) ?? "").includes(
|
|
60
|
-
"border-primary"
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
if (!selected) throw new Error("Size M never became selected")
|
|
64
|
-
step("selected Size M")
|
|
65
|
-
|
|
66
|
-
const addButton = page.getByTestId("add-product-button").first()
|
|
67
|
-
await addButton.click()
|
|
68
|
-
step("clicked Add to cart")
|
|
69
|
-
|
|
70
|
-
// ── Cart drawer → checkout ──────────────────────────────────────────
|
|
71
|
-
// The drawer auto-opens only on a rise from a nonzero base (first add
|
|
72
|
-
// never auto-opens — components.md); open it via the header cart
|
|
73
|
-
// button once the badge confirms the server-side add landed.
|
|
74
|
-
await page
|
|
75
|
-
.getByTestId("nav-cart-link")
|
|
76
|
-
.locator("span")
|
|
77
|
-
.filter({ hasText: /^\d+$/ })
|
|
78
|
-
.waitFor({ timeout: 20_000 })
|
|
79
|
-
step("header cart badge shows the added item")
|
|
80
|
-
await page.getByTestId("nav-cart-link").click()
|
|
81
|
-
const checkoutLink = page.getByRole("link", { name: "Checkout" })
|
|
82
|
-
await checkoutLink.click()
|
|
83
|
-
step("cart drawer opened via header cart button")
|
|
84
|
-
await page.waitForURL("**/checkout")
|
|
85
|
-
step("navigated to /checkout")
|
|
86
|
-
|
|
87
|
-
// ── Address form ────────────────────────────────────────────────────
|
|
88
|
-
const fill = async (name, value) => {
|
|
89
|
-
await page.locator(`[name="${name}"]`).fill(value)
|
|
90
|
-
}
|
|
91
|
-
await fill("email", "smoke-adoption@example.test")
|
|
92
|
-
await fill("shipping_address.first_name", "Ivan")
|
|
93
|
-
await fill("shipping_address.last_name", "Petrov")
|
|
94
|
-
await fill("shipping_address.address_1", "ul. Vitosha 15")
|
|
95
|
-
await fill("shipping_address.postal_code", "1000")
|
|
96
|
-
await fill("shipping_address.city", "Sofia")
|
|
97
|
-
await fill("shipping_address.phone", "+359888123456")
|
|
98
|
-
await page.locator('[name="shipping_address.phone"]').blur()
|
|
99
|
-
step("address form filled (BG address, EUR cart)")
|
|
100
|
-
|
|
101
|
-
// ── Shipping method ─────────────────────────────────────────────────
|
|
102
|
-
await page.getByText("Flat Rate (Bulgaria)", { exact: false }).first().click()
|
|
103
|
-
step('selected shipping "Flat Rate (Bulgaria)"')
|
|
104
|
-
|
|
105
|
-
// ── Payment: manual/offline tab (no Stripe env) ─────────────────────
|
|
106
|
-
const codTab = page.getByRole("button", { name: "Cash on delivery" })
|
|
107
|
-
await codTab.waitFor({ state: "visible" })
|
|
108
|
-
await codTab.click()
|
|
109
|
-
step("selected the merchant method (offline tab)")
|
|
110
|
-
|
|
111
|
-
// ── Place order ─────────────────────────────────────────────────────
|
|
112
|
-
const submit = page.getByTestId("submit-order-button")
|
|
113
|
-
await submit.waitFor({ state: "visible" })
|
|
114
|
-
// The Buy button enables once the debounced address autosave lands.
|
|
115
|
-
await page.waitForFunction(
|
|
116
|
-
() =>
|
|
117
|
-
!document.querySelector('[data-testid="submit-order-button"]')?.disabled,
|
|
118
|
-
undefined,
|
|
119
|
-
{ timeout: 20_000 }
|
|
120
|
-
)
|
|
121
|
-
await submit.click()
|
|
122
|
-
step("clicked Place order")
|
|
123
|
-
|
|
124
|
-
// ── Confirmation ────────────────────────────────────────────────────
|
|
125
|
-
await page.waitForURL("**/order/**/confirmed", { timeout: 60_000 })
|
|
126
|
-
step(`confirmation URL: ${new URL(page.url()).pathname}`)
|
|
127
|
-
|
|
128
|
-
const orderNumberText = await page
|
|
129
|
-
.getByText(/#\d+/)
|
|
130
|
-
.first()
|
|
131
|
-
.innerText({ timeout: 30_000 })
|
|
132
|
-
const displayNumber = orderNumberText.match(/#(\d+)/)?.[1]
|
|
133
|
-
if (!displayNumber) throw new Error("no order display number rendered")
|
|
134
|
-
step(`order number rendered: #${displayNumber}`)
|
|
135
|
-
|
|
136
|
-
// Totals: the order family renders a Total row with a EUR amount.
|
|
137
|
-
const totalRow = page
|
|
138
|
-
.locator("div,li")
|
|
139
|
-
.filter({ hasText: /^Total/ })
|
|
140
|
-
.filter({ hasText: "€" })
|
|
141
|
-
.first()
|
|
142
|
-
const totalText = (await totalRow.innerText()).replace(/\s+/g, " ").trim()
|
|
143
|
-
if (!/€\s?\d/.test(totalText)) throw new Error("no EUR total rendered")
|
|
144
|
-
step(`totals rendered: ${totalText}`)
|
|
145
|
-
|
|
146
|
-
console.log("\n[smoke] PASS — completed checkout, order #" + displayNumber)
|
|
147
|
-
await browser.close()
|
|
148
|
-
process.exit(0)
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.error("\n[smoke] FAIL after steps:\n - " + trail.join("\n - "))
|
|
151
|
-
console.error(err)
|
|
152
|
-
try {
|
|
153
|
-
await page.screenshot({ path: "examples/storefront/smoke-failure.png" })
|
|
154
|
-
console.error("[smoke] screenshot: examples/storefront/smoke-failure.png")
|
|
155
|
-
} catch {}
|
|
156
|
-
await browser.close()
|
|
157
|
-
process.exit(1)
|
|
158
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from "react"
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* DEMO ONLY (2026-08-04, provider research): mounts the myPOS Embedded
|
|
7
|
-
* sandbox (their public docs demo store) inside the real checkout's
|
|
8
|
-
* payment section, so the visual seam of a third-party embedded provider
|
|
9
|
-
* can be judged against our own tabs. Enabled by
|
|
10
|
-
* NEXT_PUBLIC_MYPOS_DEMO=1; never ship this to a real store.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const SDK_SRC = "https://developers.mypos.com/repository/mypos-embedded-sdk.js"
|
|
14
|
-
const CONTAINER_ID = "mypos-demo-container"
|
|
15
|
-
|
|
16
|
-
declare global {
|
|
17
|
-
interface Window {
|
|
18
|
-
MyPOSEmbedded?: {
|
|
19
|
-
createPayment: (
|
|
20
|
-
containerId: string,
|
|
21
|
-
params: Record<string, unknown>,
|
|
22
|
-
callbacks: Record<string, unknown>
|
|
23
|
-
) => void
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function MyposDemoTab({ amount, currency }: { amount: number; currency: string }) {
|
|
29
|
-
const mounted = useRef(false)
|
|
30
|
-
const [error, setError] = useState<string | null>(null)
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (mounted.current) return
|
|
34
|
-
mounted.current = true
|
|
35
|
-
|
|
36
|
-
const mount = () => {
|
|
37
|
-
const sdk = window.MyPOSEmbedded
|
|
38
|
-
if (!sdk || typeof sdk.createPayment !== "function") {
|
|
39
|
-
setError("myPOS SDK loaded but its global was not found (check the console).")
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
sdk.createPayment(
|
|
43
|
-
CONTAINER_ID,
|
|
44
|
-
{
|
|
45
|
-
// Public sandbox store from the myPOS docs sample.
|
|
46
|
-
sid: "000000000000010",
|
|
47
|
-
ipcLanguage: "en",
|
|
48
|
-
walletNumber: "61938166610",
|
|
49
|
-
amount,
|
|
50
|
-
currency: currency.toUpperCase(),
|
|
51
|
-
orderID: `demo_${Math.random().toString(36).slice(2, 11)}`,
|
|
52
|
-
urlNotify: "https://example.com/payment-notify",
|
|
53
|
-
urlOk: window.location.href,
|
|
54
|
-
urlCancel: window.location.href,
|
|
55
|
-
keyIndex: 1,
|
|
56
|
-
cartItems: [{ article: "Cartbase demo order", quantity: 1, price: amount, currency: currency.toUpperCase() }],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
isSandbox: true,
|
|
60
|
-
onSuccess: (data: unknown) => {
|
|
61
|
-
// Real integration: this is where complete-cart would run,
|
|
62
|
-
// AFTER server-side verification via urlNotify.
|
|
63
|
-
// eslint-disable-next-line no-console
|
|
64
|
-
console.log("[mypos-demo] sandbox success", data)
|
|
65
|
-
window.alert("myPOS sandbox reported success (demo only, no order is placed).")
|
|
66
|
-
},
|
|
67
|
-
onError: () => {
|
|
68
|
-
// eslint-disable-next-line no-console
|
|
69
|
-
console.log("[mypos-demo] sandbox payment failed")
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const existing = document.querySelector(`script[src="${SDK_SRC}"]`)
|
|
76
|
-
if (existing && window.MyPOSEmbedded) {
|
|
77
|
-
mount()
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
const script = document.createElement("script")
|
|
81
|
-
script.src = SDK_SRC
|
|
82
|
-
script.async = true
|
|
83
|
-
script.onload = mount
|
|
84
|
-
script.onerror = () => setError("Could not load the myPOS SDK script.")
|
|
85
|
-
document.head.appendChild(script)
|
|
86
|
-
}, [amount, currency])
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<div>
|
|
90
|
-
{error ? (
|
|
91
|
-
<p className="text-sm text-red-600">{error}</p>
|
|
92
|
-
) : (
|
|
93
|
-
<div id={CONTAINER_ID} className="min-h-[620px]" />
|
|
94
|
-
)}
|
|
95
|
-
<p className="mt-2 text-xs text-muted-foreground">
|
|
96
|
-
myPOS sandbox demo. The frame above is myPOS's embedded form with its
|
|
97
|
-
own pay button; everything around it is the Cartbase checkout.
|
|
98
|
-
</p>
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One component, at its own permanent address.
|
|
3
|
-
*
|
|
4
|
-
* This is the page we sit on together: you say "let's do the hero", we open
|
|
5
|
-
* /gallery/hero, and everything about that one thing is here.
|
|
6
|
-
*
|
|
7
|
-
* Variants and states are shown as two separate blocks on purpose. They look
|
|
8
|
-
* similar and they are not: a variant is a permanent option in the
|
|
9
|
-
* component's API with an id a theme writes, a state is data the component
|
|
10
|
-
* has to survive and ships as nothing. Putting them in one row, which this
|
|
11
|
-
* page used to do, teaches the wrong thing.
|
|
12
|
-
*/
|
|
13
|
-
import Link from "next/link"
|
|
14
|
-
import { notFound } from "next/navigation"
|
|
15
|
-
import { findEntry, GROUPS } from "../_lib/registry"
|
|
16
|
-
import { loadCatalog } from "../_lib/catalog"
|
|
17
|
-
import { SPECIMENS, type StateCase, type Variant } from "../_components/specimens"
|
|
18
|
-
import { StatusLabel } from "../_components/status"
|
|
19
|
-
|
|
20
|
-
export const dynamic = "force-dynamic"
|
|
21
|
-
|
|
22
|
-
export function generateStaticParams() {
|
|
23
|
-
return GROUPS.flatMap((g) => g.entries.map((e) => ({ slug: e.slug })))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default async function ComponentPage({
|
|
27
|
-
params,
|
|
28
|
-
}: {
|
|
29
|
-
params: Promise<{ slug: string }>
|
|
30
|
-
}) {
|
|
31
|
-
const { slug } = await params
|
|
32
|
-
const entry = findEntry(slug)
|
|
33
|
-
if (!entry) notFound()
|
|
34
|
-
|
|
35
|
-
const build = SPECIMENS[slug]
|
|
36
|
-
const specimen = build ? build({ catalog: await loadCatalog() }) : null
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<article>
|
|
40
|
-
<header className="mb-10">
|
|
41
|
-
<Link
|
|
42
|
-
href="/gallery"
|
|
43
|
-
className="text-xs uppercase tracking-widest text-muted-foreground hover:text-foreground"
|
|
44
|
-
>
|
|
45
|
-
{entry.group.name}
|
|
46
|
-
</Link>
|
|
47
|
-
<div className="flex flex-wrap items-baseline gap-4 mt-2">
|
|
48
|
-
<h1 className="text-3xl font-semibold">{entry.name}</h1>
|
|
49
|
-
<StatusLabel status={entry.status} />
|
|
50
|
-
</div>
|
|
51
|
-
<p className="text-muted-foreground mt-3 max-w-2xl">{entry.summary}</p>
|
|
52
|
-
</header>
|
|
53
|
-
|
|
54
|
-
{specimen ? (
|
|
55
|
-
<div className="space-y-14">
|
|
56
|
-
{specimen.variants?.length ? (
|
|
57
|
-
<Block
|
|
58
|
-
title="Variants"
|
|
59
|
-
blurb="Deliberate options. Each id below ships in the component's API, so a theme can select it and we can never rename it without breaking every storefront using it."
|
|
60
|
-
api={specimen.api}
|
|
61
|
-
>
|
|
62
|
-
{specimen.variants.map((v) => (
|
|
63
|
-
<Cell key={v.id} label={v.label} note={v.note} wide={v.wide} id={v.id}>
|
|
64
|
-
{v.render}
|
|
65
|
-
</Cell>
|
|
66
|
-
))}
|
|
67
|
-
</Block>
|
|
68
|
-
) : (
|
|
69
|
-
<Block
|
|
70
|
-
title="Variants"
|
|
71
|
-
blurb="This component has none. It renders one way, and everything below is data it has to survive."
|
|
72
|
-
>
|
|
73
|
-
<p className="col-span-full text-sm text-muted-foreground">
|
|
74
|
-
No options to choose from.
|
|
75
|
-
</p>
|
|
76
|
-
</Block>
|
|
77
|
-
)}
|
|
78
|
-
|
|
79
|
-
{specimen.states?.length ? (
|
|
80
|
-
<Block
|
|
81
|
-
title="States"
|
|
82
|
-
blurb="Not choices. The same component receiving different data, which the catalogue produces whether we plan for it or not. These ship as nothing; they exist so we can see what breaks."
|
|
83
|
-
>
|
|
84
|
-
{specimen.states.map((s: StateCase) => (
|
|
85
|
-
<Cell key={s.label} label={s.label} note={s.note} wide={s.wide}>
|
|
86
|
-
{s.render}
|
|
87
|
-
</Cell>
|
|
88
|
-
))}
|
|
89
|
-
</Block>
|
|
90
|
-
) : null}
|
|
91
|
-
</div>
|
|
92
|
-
) : entry.status === "missing" ? (
|
|
93
|
-
<Empty
|
|
94
|
-
title={`${entry.name} does not exist yet`}
|
|
95
|
-
body="Nothing in the library renders this. Say the word and we design it here first, then it gets built into the package."
|
|
96
|
-
/>
|
|
97
|
-
) : (
|
|
98
|
-
<Empty
|
|
99
|
-
title={`${entry.name} exists but has never been reviewed`}
|
|
100
|
-
body="The code renders in the shop today. It has no specimen here yet, which means we have never put its variants and states side by side and decided whether it is right."
|
|
101
|
-
/>
|
|
102
|
-
)}
|
|
103
|
-
</article>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function Block({
|
|
108
|
-
title,
|
|
109
|
-
blurb,
|
|
110
|
-
api,
|
|
111
|
-
children,
|
|
112
|
-
}: {
|
|
113
|
-
title: string
|
|
114
|
-
blurb: string
|
|
115
|
-
api?: string
|
|
116
|
-
children: React.ReactNode
|
|
117
|
-
}) {
|
|
118
|
-
return (
|
|
119
|
-
<section>
|
|
120
|
-
<div className="mb-6">
|
|
121
|
-
<h2 className="text-lg font-semibold">{title}</h2>
|
|
122
|
-
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">{blurb}</p>
|
|
123
|
-
{api ? (
|
|
124
|
-
<code className="inline-block mt-3 text-xs bg-muted px-2 py-1 rounded font-mono">
|
|
125
|
-
{api}
|
|
126
|
-
</code>
|
|
127
|
-
) : null}
|
|
128
|
-
</div>
|
|
129
|
-
<div className="grid gap-8 md:grid-cols-2">{children}</div>
|
|
130
|
-
</section>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function Cell({
|
|
135
|
-
label,
|
|
136
|
-
note,
|
|
137
|
-
id,
|
|
138
|
-
wide,
|
|
139
|
-
children,
|
|
140
|
-
}: {
|
|
141
|
-
label: string
|
|
142
|
-
note?: string
|
|
143
|
-
id?: string
|
|
144
|
-
wide?: boolean
|
|
145
|
-
children: React.ReactNode
|
|
146
|
-
}) {
|
|
147
|
-
return (
|
|
148
|
-
<div className={wide ? "col-span-full min-w-0" : "min-w-0"}>
|
|
149
|
-
<div className="mb-3 flex items-baseline gap-2 flex-wrap">
|
|
150
|
-
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
151
|
-
{label}
|
|
152
|
-
</span>
|
|
153
|
-
{id ? (
|
|
154
|
-
<code className="text-[11px] font-mono text-muted-foreground/70">{id}</code>
|
|
155
|
-
) : null}
|
|
156
|
-
</div>
|
|
157
|
-
{note ? <p className="text-xs text-muted-foreground/80 -mt-2 mb-3">{note}</p> : null}
|
|
158
|
-
<div className="rounded-lg border border-border p-4 bg-background">{children}</div>
|
|
159
|
-
</div>
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function Empty({ title, body }: { title: string; body: string }) {
|
|
164
|
-
return (
|
|
165
|
-
<div className="rounded-lg border border-dashed border-border p-10 text-center">
|
|
166
|
-
<p className="font-medium">{title}</p>
|
|
167
|
-
<p className="text-sm text-muted-foreground mt-2 max-w-md mx-auto">{body}</p>
|
|
168
|
-
</div>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export type { Variant }
|