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.
Files changed (32) hide show
  1. package/README.md +13 -8
  2. package/dist/index.js +28 -20
  3. package/package.json +3 -3
  4. package/template/app/AGENTS.md +29 -0
  5. package/template/app/CLAUDE.md +19 -8
  6. package/template/app/docs/BUILD-A-STOREFRONT.md +233 -226
  7. package/template/app/docs/README.md +1 -0
  8. package/template/app/docs/components.md +1134 -1090
  9. package/template/app/docs/deploy.md +15 -10
  10. package/template/app/docs/integrations.md +15 -3
  11. package/template/app/docs/platform.md +1 -1
  12. package/template/app/docs/store.md +47 -0
  13. package/template/app/docs/variables.md +3 -3
  14. package/template/app/next.config.ts +10 -14
  15. package/template/app/package.json +4 -3
  16. package/template/app/src/app/checkout/checkout-page-client.tsx +0 -20
  17. package/template/app/src/app/globals.css +1 -1
  18. package/template/app/src/app/layout.tsx +73 -18
  19. package/template/app/src/app/order/[id]/confirmed/page.tsx +92 -78
  20. package/template/app/src/lib/config.ts +30 -21
  21. package/template/app/next-env.d.ts +0 -6
  22. package/template/app/smoke.mjs +0 -158
  23. package/template/app/src/app/checkout/mypos-demo-tab.tsx +0 -101
  24. package/template/app/src/app/gallery/[slug]/page.tsx +0 -172
  25. package/template/app/src/app/gallery/_components/specimens.tsx +0 -254
  26. package/template/app/src/app/gallery/_components/status.tsx +0 -47
  27. package/template/app/src/app/gallery/_lib/catalog.ts +0 -59
  28. package/template/app/src/app/gallery/_lib/registry.ts +0 -401
  29. package/template/app/src/app/gallery/design-system/page.tsx +0 -353
  30. package/template/app/src/app/gallery/layout.tsx +0 -83
  31. package/template/app/src/app/gallery/page.tsx +0 -81
  32. package/template/app/tsconfig.tsbuildinfo +0 -1
@@ -1,254 +0,0 @@
1
- /**
2
- * What each component looks like — split into the two things that are NOT
3
- * the same, and that this file previously collapsed into one row.
4
- *
5
- * **VARIANTS are chosen.** They are deliberate design options that ship as
6
- * part of the component's public surface. `variant="outline"` is a string a
7
- * theme writes and a merchant's agent reads, so each one needs a stable id
8
- * that we can never rename without breaking every storefront using it. A
9
- * variant is a promise.
10
- *
11
- * **STATES are received.** The same component, the same options, different
12
- * data: a long title, a missing image, a sold-out product, a reduced price.
13
- * Nobody picks these — the catalogue produces them. They ship as nothing at
14
- * all. They exist here because a component that only survives tidy data is
15
- * not finished, and the only way to know is to see the untidy data next to
16
- * the tidy data.
17
- *
18
- * The rule that follows: **adding a variant is an API decision, adding a
19
- * state is a test.** One is expensive and permanent, the other is free.
20
- */
21
- import type { ReactNode } from "react"
22
- import { Thumbnail } from "@cartbase/storefront/products/thumbnail"
23
- import { ProductPreview } from "@cartbase/storefront/products/product-preview"
24
- import { PreviewPrice } from "@cartbase/storefront/products/preview-price"
25
- import { ImageGallery } from "@cartbase/storefront/products/image-gallery"
26
- import { getProductPrice } from "@cartbase/storefront/lib/get-product-price"
27
- import { Button } from "@cartbase/storefront/primitives/ui/button"
28
- import { Input } from "@cartbase/storefront/primitives/ui/input"
29
- import { Label } from "@cartbase/storefront/primitives/ui/label"
30
- import type { StoreProduct } from "@cartbase/storefront/api/products"
31
- import type { Catalog } from "../_lib/catalog"
32
-
33
- export type SpecimenContext = { catalog: Catalog }
34
-
35
- /** A deliberate option. `id` is the value that ships in the component's API. */
36
- export type Variant = {
37
- id: string
38
- label: string
39
- note?: string
40
- wide?: boolean
41
- render: ReactNode
42
- }
43
-
44
- /** A data condition the component must survive. Ships as nothing. */
45
- export type StateCase = {
46
- label: string
47
- note?: string
48
- wide?: boolean
49
- render: ReactNode
50
- }
51
-
52
- export type Specimen = {
53
- /** How a merchant or theme selects a variant, e.g. `variant="outline"`. */
54
- api?: string
55
- variants?: Variant[]
56
- states?: StateCase[]
57
- }
58
-
59
- export type SpecimenFn = (ctx: SpecimenContext) => Specimen
60
-
61
- function Absent({ what }: { what: string }) {
62
- return (
63
- <p className="text-xs text-muted-foreground italic">
64
- This store has no product for the “{what}” case.
65
- </p>
66
- )
67
- }
68
-
69
- function Price({ product }: { product: StoreProduct }) {
70
- const { cheapestPrice } = getProductPrice({ product })
71
- if (!cheapestPrice) return <span className="text-xs text-muted-foreground">No price</span>
72
- return <PreviewPrice price={cheapestPrice} />
73
- }
74
-
75
- export const SPECIMENS: Record<string, SpecimenFn | undefined> = {
76
- "product-card": ({ catalog: { specimens } }) => ({
77
- api: 'isFeatured',
78
- variants: [
79
- {
80
- id: "default",
81
- label: "Default",
82
- note: "What every grid renders unless told otherwise.",
83
- render: specimens.typical ? <ProductPreview product={specimens.typical} /> : <Absent what="typical" />,
84
- },
85
- {
86
- id: "featured",
87
- label: "Featured",
88
- note: "A wider thumbnail ratio, for a hand-picked position.",
89
- render: specimens.typical ? (
90
- <ProductPreview product={specimens.typical} isFeatured />
91
- ) : (
92
- <Absent what="typical" />
93
- ),
94
- },
95
- ],
96
- states: [
97
- {
98
- label: "Long title",
99
- note: "78 characters. Where card layouts usually break.",
100
- render: specimens.longTitle ? (
101
- <ProductPreview product={specimens.longTitle} />
102
- ) : (
103
- <Absent what="long title" />
104
- ),
105
- },
106
- {
107
- label: "No image",
108
- note: "The fallback has to look deliberate, not broken.",
109
- render: specimens.noImage ? <ProductPreview product={specimens.noImage} /> : <Absent what="no image" />,
110
- },
111
- {
112
- label: "Reduced price",
113
- note: "Coming from a real sale price list, not a decorative field.",
114
- render: specimens.onSale ? <ProductPreview product={specimens.onSale} /> : <Absent what="reduced" />,
115
- },
116
- {
117
- label: "Sold out",
118
- note: "Nothing in the card says so today. That is a finding, not a state.",
119
- render: specimens.soldOut ? <ProductPreview product={specimens.soldOut} /> : <Absent what="sold out" />,
120
- },
121
- ],
122
- }),
123
-
124
- "product-grid": ({ catalog: { all } }) => ({
125
- states: [
126
- {
127
- label: "Eight products together",
128
- note: "The real test of the card is its neighbours: different title lengths and image ratios in one row.",
129
- wide: true,
130
- render: (
131
- <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
132
- {all.slice(0, 8).map((p) => (
133
- <ProductPreview key={p.id} product={p} />
134
- ))}
135
- </div>
136
- ),
137
- },
138
- ],
139
- }),
140
-
141
- thumbnail: ({ catalog: { specimens } }) => {
142
- const p = specimens.typical
143
- return {
144
- api: 'size="square" | "full" | "small"',
145
- variants: [
146
- { id: "square", label: "Square", render: <Thumbnail thumbnail={p?.thumbnail ?? null} images={p?.images} size="square" /> },
147
- { id: "full", label: "Full", render: <Thumbnail thumbnail={p?.thumbnail ?? null} images={p?.images} size="full" /> },
148
- { id: "small", label: "Small", render: <Thumbnail thumbnail={p?.thumbnail ?? null} images={p?.images} size="small" /> },
149
- ],
150
- states: [
151
- {
152
- label: "No image at all",
153
- note: "Falls through to the placeholder.",
154
- render: <Thumbnail thumbnail={null} images={[]} size="square" />,
155
- },
156
- ],
157
- }
158
- },
159
-
160
- "preview-price": ({ catalog: { specimens } }) => ({
161
- states: [
162
- {
163
- label: "Plain",
164
- render: specimens.typical ? <Price product={specimens.typical} /> : <Absent what="typical" />,
165
- },
166
- {
167
- label: "Reduced",
168
- note: "Server computed the sale price below the original.",
169
- render: specimens.onSale ? <Price product={specimens.onSale} /> : <Absent what="reduced" />,
170
- },
171
- ],
172
- }),
173
-
174
- "image-gallery": ({ catalog: { specimens } }) => ({
175
- states: [
176
- {
177
- label: "Four images",
178
- wide: true,
179
- render: specimens.gallery ? (
180
- <ImageGallery images={specimens.gallery.images ?? []} />
181
- ) : (
182
- <Absent what="deep gallery" />
183
- ),
184
- },
185
- {
186
- label: "One image",
187
- wide: true,
188
- render: specimens.typical ? (
189
- <ImageGallery images={(specimens.typical.images ?? []).slice(0, 1)} />
190
- ) : (
191
- <Absent what="typical" />
192
- ),
193
- },
194
- { label: "No images", wide: true, render: <ImageGallery images={[]} /> },
195
- ],
196
- }),
197
-
198
- button: () => ({
199
- api: 'variant="…" size="…"',
200
- variants: [
201
- { id: "default", label: "Default", note: "Add to cart is this one.", render: <Button>Add to cart</Button> },
202
- { id: "secondary", label: "Secondary", render: <Button variant="secondary">Secondary</Button> },
203
- { id: "outline", label: "Outline", render: <Button variant="outline">Outline</Button> },
204
- { id: "ghost", label: "Ghost", render: <Button variant="ghost">Ghost</Button> },
205
- { id: "destructive", label: "Destructive", render: <Button variant="destructive">Remove</Button> },
206
- { id: "sm", label: "Small", render: <Button size="sm">Small</Button> },
207
- { id: "lg", label: "Large", render: <Button size="lg">Large</Button> },
208
- ],
209
- states: [
210
- {
211
- label: "Disabled",
212
- note: "What a sold-out add-to-cart looks like.",
213
- render: <Button disabled>Add to cart</Button>,
214
- },
215
- {
216
- label: "Full width",
217
- note: "How it sits on a product page.",
218
- render: <Button className="w-full">Add to cart</Button>,
219
- },
220
- ],
221
- }),
222
-
223
- input: () => ({
224
- states: [
225
- {
226
- label: "Empty",
227
- render: (
228
- <div className="space-y-2">
229
- <Label htmlFor="g-email">Email</Label>
230
- <Input id="g-email" placeholder="you@example.com" />
231
- </div>
232
- ),
233
- },
234
- {
235
- label: "Filled",
236
- render: (
237
- <div className="space-y-2">
238
- <Label htmlFor="g-name">Full name</Label>
239
- <Input id="g-name" defaultValue="Maria Petrova" />
240
- </div>
241
- ),
242
- },
243
- {
244
- label: "Disabled",
245
- render: (
246
- <div className="space-y-2">
247
- <Label htmlFor="g-locked">Country</Label>
248
- <Input id="g-locked" defaultValue="Bulgaria" disabled />
249
- </div>
250
- ),
251
- },
252
- ],
253
- }),
254
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Status, shown the same way everywhere: a dot in the nav, a word on the page.
3
- *
4
- * Three states and no fourth, so the board can never be ambiguous about what
5
- * is finished. `missing` is deliberately loud — an empty slot with a name is
6
- * the roadmap, and hiding it would make the library look more complete than
7
- * it is.
8
- */
9
- import type { Status } from "../_lib/registry"
10
-
11
- const COLOUR: Record<Status, string> = {
12
- done: "bg-emerald-500",
13
- built: "bg-amber-400",
14
- missing: "bg-border",
15
- }
16
-
17
- const WORD: Record<Status, string> = {
18
- done: "Agreed",
19
- built: "Not reviewed",
20
- missing: "Not built",
21
- }
22
-
23
- const EXPLAIN: Record<Status, string> = {
24
- done: "We looked at it together, agreed it, and its styling values are tokens.",
25
- built: "The code exists and renders, but we have never reviewed it.",
26
- missing: "It does not exist. Building it is the work.",
27
- }
28
-
29
- export function StatusDot({ status }: { status: Status }) {
30
- return (
31
- <span
32
- className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${COLOUR[status]}`}
33
- title={WORD[status]}
34
- />
35
- )
36
- }
37
-
38
- export function StatusLabel({ status }: { status: Status }) {
39
- return (
40
- <span className="inline-flex items-center gap-2" title={EXPLAIN[status]}>
41
- <StatusDot status={status} />
42
- <span className="text-xs uppercase tracking-wide text-muted-foreground">
43
- {WORD[status]}
44
- </span>
45
- </span>
46
- )
47
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * The gallery's data. Real products from the live store, selected by handle so
3
- * each specimen is a KNOWN state rather than whatever happened to be first.
4
- *
5
- * The handles below are seeded by scripts/seed-demo-catalog.ts, which marks
6
- * each one with the state it exists to prove. If a handle goes missing the
7
- * gallery degrades to "not in this store" rather than crashing, because the
8
- * app must render against ANY store — that is the whole point of pointing it
9
- * at a store through env instead of bundling fixtures.
10
- */
11
- import { listProducts, type StoreProduct } from "@cartbase/storefront/api/products"
12
- import { PRICING_CONTEXT } from "@/lib/config"
13
- import { getServerClient } from "@/lib/server-client"
14
-
15
- /** Handles whose state we deliberately review. */
16
- export const SPECIMEN_HANDLES = {
17
- /** Ordinary case: short title, image, a few variants. */
18
- typical: "merino-crew",
19
- /** A merchandising title long enough to break a card's layout. */
20
- longTitle: "brushed-overshirt",
21
- /** No thumbnail and no gallery — the fallback has to be deliberate. */
22
- noImage: "lightweight-parka",
23
- /** Zero stock at every variant. */
24
- soldOut: "fisherman-sweater",
25
- /** Carries a real `sale` price list, so calculated < original. */
26
- onSale: "chambray-work-shirt",
27
- /** Four colours by three sizes: the option matrix that breaks pickers. */
28
- manyVariants: "oxford-shirt",
29
- /** A single expensive item with a deep gallery. */
30
- gallery: "leather-weekend-bag",
31
- } as const
32
-
33
- export type SpecimenKey = keyof typeof SPECIMEN_HANDLES
34
-
35
- export type Catalog = {
36
- /** Everything the key's sales channel exposes, newest first. */
37
- all: StoreProduct[]
38
- /** The named states above, resolved. Missing handles map to null. */
39
- specimens: Record<SpecimenKey, StoreProduct | null>
40
- /** True when the store answered but has nothing to show. */
41
- empty: boolean
42
- }
43
-
44
- /**
45
- * One fetch for the whole gallery. Products come back with
46
- * `variant.calculated_price` decorated because we always pass a pricing
47
- * context — without it prices render undecided and every price specimen lies.
48
- */
49
- export async function loadCatalog(): Promise<Catalog> {
50
- const client = await getServerClient()
51
- const { products } = await listProducts(client, { limit: 100, ...PRICING_CONTEXT })
52
-
53
- const byHandle = new Map(products.map((p) => [p.handle, p]))
54
- const specimens = Object.fromEntries(
55
- Object.entries(SPECIMEN_HANDLES).map(([key, handle]) => [key, byHandle.get(handle) ?? null])
56
- ) as Record<SpecimenKey, StoreProduct | null>
57
-
58
- return { all: products, specimens, empty: products.length === 0 }
59
- }