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,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The design system on one screen.
|
|
3
|
-
*
|
|
4
|
-
* Every token the library owns, rendered as the thing it controls rather than
|
|
5
|
-
* listed as text: a colour is a swatch, a type style is set in that style, a
|
|
6
|
-
* shadow is a raised card, a radius is a corner. You review the system by
|
|
7
|
-
* looking at it, not by reading a table of values.
|
|
8
|
-
*
|
|
9
|
-
* These are NOT components. They are the layer underneath every component, and
|
|
10
|
-
* they come first in the nav for that reason: change one value here and all
|
|
11
|
-
* hundred and seventeen components repaint at once.
|
|
12
|
-
*/
|
|
13
|
-
export const dynamic = "force-dynamic"
|
|
14
|
-
|
|
15
|
-
export default function DesignSystemPage() {
|
|
16
|
-
return (
|
|
17
|
-
<div className="space-y-16">
|
|
18
|
-
<header>
|
|
19
|
-
<h1 className="text-h1">Design system</h1>
|
|
20
|
-
<p className="text-body text-muted-foreground mt-3 max-w-prose">
|
|
21
|
-
The values every component reads. Nothing here is a Cartbase
|
|
22
|
-
invention: the namespaces are Tailwind’s and the colour names
|
|
23
|
-
are shadcn’s, so a developer or a coding agent recognises them
|
|
24
|
-
without documentation. A theme replaces these values and never these
|
|
25
|
-
names.
|
|
26
|
-
</p>
|
|
27
|
-
</header>
|
|
28
|
-
|
|
29
|
-
<Colour />
|
|
30
|
-
<Typography />
|
|
31
|
-
<Weight />
|
|
32
|
-
<Spacing />
|
|
33
|
-
<Radius />
|
|
34
|
-
<Elevation />
|
|
35
|
-
<Motion />
|
|
36
|
-
<Layout />
|
|
37
|
-
</div>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/* ── Colour ─────────────────────────────────────────────────────────────── */
|
|
42
|
-
|
|
43
|
-
const SURFACES = [
|
|
44
|
-
{ name: "background", fg: "foreground", use: "The page itself." },
|
|
45
|
-
{ name: "card", fg: "card-foreground", use: "Anything raised off the page." },
|
|
46
|
-
{ name: "popover", fg: "popover-foreground", use: "Menus, dropdowns, tooltips." },
|
|
47
|
-
{ name: "muted", fg: "muted-foreground", use: "Recessed areas and secondary text." },
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
const ROLES = [
|
|
51
|
-
{ name: "primary", fg: "primary-foreground", use: "The main action. Add to cart is this." },
|
|
52
|
-
{ name: "secondary", fg: "secondary-foreground", use: "The action beside the main one." },
|
|
53
|
-
{ name: "accent", fg: "accent-foreground", use: "Highlights and hover grounds." },
|
|
54
|
-
{ name: "destructive", fg: "destructive-foreground", use: "Remove, cancel, delete." },
|
|
55
|
-
{ name: "success", fg: "success-foreground", use: "Paid, delivered, in stock." },
|
|
56
|
-
{ name: "warning", fg: "warning-foreground", use: "Low stock, pending, attention." },
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
const LINES = [
|
|
60
|
-
{ name: "border", use: "Every dividing line." },
|
|
61
|
-
{ name: "input", use: "Form field outlines." },
|
|
62
|
-
{ name: "ring", use: "Keyboard focus." },
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
function Colour() {
|
|
66
|
-
return (
|
|
67
|
-
<Section
|
|
68
|
-
title="Colour"
|
|
69
|
-
blurb="Named by role, never by hue. Every surface carries a paired foreground so text on it is never a guess."
|
|
70
|
-
>
|
|
71
|
-
<SubTitle>Surfaces</SubTitle>
|
|
72
|
-
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-10">
|
|
73
|
-
{SURFACES.map((s) => (
|
|
74
|
-
<Swatch key={s.name} name={s.name} fg={s.fg} use={s.use} />
|
|
75
|
-
))}
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<SubTitle>Roles</SubTitle>
|
|
79
|
-
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 mb-10">
|
|
80
|
-
{ROLES.map((s) => (
|
|
81
|
-
<Swatch key={s.name} name={s.name} fg={s.fg} use={s.use} />
|
|
82
|
-
))}
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<SubTitle>Lines and focus</SubTitle>
|
|
86
|
-
<div className="grid gap-4 sm:grid-cols-3">
|
|
87
|
-
{LINES.map((l) => (
|
|
88
|
-
<div key={l.name} className="rounded-lg border border-border p-4">
|
|
89
|
-
<div
|
|
90
|
-
className="h-10 rounded-md mb-3"
|
|
91
|
-
style={{ backgroundColor: `var(--${l.name})` }}
|
|
92
|
-
/>
|
|
93
|
-
<code className="text-caption font-mono">{l.name}</code>
|
|
94
|
-
<p className="text-caption text-muted-foreground mt-1">{l.use}</p>
|
|
95
|
-
</div>
|
|
96
|
-
))}
|
|
97
|
-
</div>
|
|
98
|
-
</Section>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function Swatch({ name, fg, use }: { name: string; fg: string; use: string }) {
|
|
103
|
-
return (
|
|
104
|
-
<div className="rounded-lg border border-border overflow-hidden">
|
|
105
|
-
<div
|
|
106
|
-
className="p-5"
|
|
107
|
-
style={{ backgroundColor: `var(--${name})`, color: `var(--${fg})` }}
|
|
108
|
-
>
|
|
109
|
-
<span className="text-label">Aa</span>
|
|
110
|
-
</div>
|
|
111
|
-
<div className="p-3">
|
|
112
|
-
<code className="text-caption font-mono">{name}</code>
|
|
113
|
-
<p className="text-caption text-muted-foreground mt-1">{use}</p>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* ── Typography ─────────────────────────────────────────────────────────── */
|
|
120
|
-
|
|
121
|
-
const TYPE = [
|
|
122
|
-
{ cls: "text-h1", label: "h1", sample: "Everyday Essentials" },
|
|
123
|
-
{ cls: "text-h2", label: "h2", sample: "New this season" },
|
|
124
|
-
{ cls: "text-h3", label: "h3", sample: "Merino Crew Neck" },
|
|
125
|
-
{ cls: "text-h4", label: "h4", sample: "Materials and care" },
|
|
126
|
-
{ cls: "text-h5", label: "h5", sample: "Shipping" },
|
|
127
|
-
{ cls: "text-h6", label: "h6", sample: "Returns within 30 days" },
|
|
128
|
-
{
|
|
129
|
-
cls: "text-body-large",
|
|
130
|
-
label: "body-large",
|
|
131
|
-
sample: "Knitted from extra-fine merino at a gauge light enough to layer.",
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
cls: "text-body",
|
|
135
|
-
label: "body",
|
|
136
|
-
sample: "Knitted from extra-fine merino at a gauge light enough to layer.",
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
cls: "text-body-small",
|
|
140
|
-
label: "body-small",
|
|
141
|
-
sample: "Knitted from extra-fine merino at a gauge light enough to layer.",
|
|
142
|
-
},
|
|
143
|
-
{ cls: "text-label", label: "label", sample: "Add to cart" },
|
|
144
|
-
{ cls: "text-caption", label: "caption", sample: "Free delivery over 100 EUR" },
|
|
145
|
-
]
|
|
146
|
-
|
|
147
|
-
function Typography() {
|
|
148
|
-
return (
|
|
149
|
-
<Section
|
|
150
|
-
title="Type"
|
|
151
|
-
blurb="Eleven styles on a 1.2 ratio from a 16px base. The name is a style, never a tag: put text-h2 on an h1 when the page needs a smaller heading, and the document outline stays correct for search engines and screen readers."
|
|
152
|
-
>
|
|
153
|
-
<div className="space-y-6">
|
|
154
|
-
{TYPE.map((t) => (
|
|
155
|
-
<div key={t.cls} className="flex items-baseline gap-6 border-b border-border pb-5">
|
|
156
|
-
<code className="text-caption font-mono text-muted-foreground w-28 shrink-0">
|
|
157
|
-
{t.label}
|
|
158
|
-
</code>
|
|
159
|
-
<p className={`${t.cls} min-w-0`}>{t.sample}</p>
|
|
160
|
-
</div>
|
|
161
|
-
))}
|
|
162
|
-
</div>
|
|
163
|
-
</Section>
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function Weight() {
|
|
168
|
-
return (
|
|
169
|
-
<Section
|
|
170
|
-
title="Weight"
|
|
171
|
-
blurb="Three, not four. Bold and semibold were doing the same job in different places, which is how a scale rots."
|
|
172
|
-
>
|
|
173
|
-
<div className="space-y-4">
|
|
174
|
-
{[
|
|
175
|
-
["regular", "font-regular"],
|
|
176
|
-
["medium", "font-medium"],
|
|
177
|
-
["semibold", "font-semibold"],
|
|
178
|
-
].map(([label, cls]) => (
|
|
179
|
-
<div key={label} className="flex items-baseline gap-6">
|
|
180
|
-
<code className="text-caption font-mono text-muted-foreground w-28 shrink-0">
|
|
181
|
-
{label}
|
|
182
|
-
</code>
|
|
183
|
-
<p className={`text-h5 ${cls}`}>Merino Crew Neck</p>
|
|
184
|
-
</div>
|
|
185
|
-
))}
|
|
186
|
-
</div>
|
|
187
|
-
</Section>
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/* ── Spacing, radius, elevation, motion, layout ─────────────────────────── */
|
|
192
|
-
|
|
193
|
-
const SPACE_STEPS = [1, 2, 3, 4, 6, 8, 12, 16, 24]
|
|
194
|
-
|
|
195
|
-
function Spacing() {
|
|
196
|
-
return (
|
|
197
|
-
<Section
|
|
198
|
-
title="Spacing"
|
|
199
|
-
blurb="One 4px step, multiplied. Every gap, padding and margin in the library is a multiple of it, so widening the whole rhythm is a single number."
|
|
200
|
-
>
|
|
201
|
-
<div className="space-y-2">
|
|
202
|
-
{SPACE_STEPS.map((n) => (
|
|
203
|
-
<div key={n} className="flex items-center gap-4">
|
|
204
|
-
<code className="text-caption font-mono text-muted-foreground w-14 shrink-0">
|
|
205
|
-
{n}
|
|
206
|
-
</code>
|
|
207
|
-
<div
|
|
208
|
-
className="h-4 bg-primary rounded-sm"
|
|
209
|
-
style={{ width: `calc(var(--spacing) * ${n})` }}
|
|
210
|
-
/>
|
|
211
|
-
<span className="text-caption text-muted-foreground">{n * 4}px</span>
|
|
212
|
-
</div>
|
|
213
|
-
))}
|
|
214
|
-
</div>
|
|
215
|
-
</Section>
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function Radius() {
|
|
220
|
-
return (
|
|
221
|
-
<Section
|
|
222
|
-
title="Radius"
|
|
223
|
-
blurb="Derived from one base value, so a theme turns the whole library sharp or soft with one number."
|
|
224
|
-
>
|
|
225
|
-
{/* Class names are written out, never built from a variable: Tailwind
|
|
226
|
-
scans source text, so `rounded-${r}` would generate nothing. */}
|
|
227
|
-
<div className="flex flex-wrap gap-6">
|
|
228
|
-
{[
|
|
229
|
-
["sm", "rounded-sm"],
|
|
230
|
-
["md", "rounded-md"],
|
|
231
|
-
["lg", "rounded-lg"],
|
|
232
|
-
["xl", "rounded-xl"],
|
|
233
|
-
["2xl", "rounded-2xl"],
|
|
234
|
-
].map(([label, cls]) => (
|
|
235
|
-
<div key={label} className="text-center">
|
|
236
|
-
<div className={`w-24 h-24 bg-muted border border-border ${cls}`} />
|
|
237
|
-
<code className="text-caption font-mono text-muted-foreground mt-2 block">
|
|
238
|
-
{label}
|
|
239
|
-
</code>
|
|
240
|
-
</div>
|
|
241
|
-
))}
|
|
242
|
-
</div>
|
|
243
|
-
</Section>
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function Elevation() {
|
|
248
|
-
return (
|
|
249
|
-
<Section
|
|
250
|
-
title="Elevation"
|
|
251
|
-
blurb="Named for what is raised, not for how big the blur is. shadow-card says what it is for; shadow-md says nothing and cannot be retuned per theme."
|
|
252
|
-
>
|
|
253
|
-
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
|
254
|
-
{[
|
|
255
|
-
["card", "shadow-card"],
|
|
256
|
-
["card-hover", "shadow-card-hover"],
|
|
257
|
-
["popover", "shadow-popover"],
|
|
258
|
-
["drawer", "shadow-drawer"],
|
|
259
|
-
].map(([label, cls]) => (
|
|
260
|
-
<div key={label}>
|
|
261
|
-
<div className={`h-24 bg-card rounded-lg ${cls}`} />
|
|
262
|
-
<code className="text-caption font-mono text-muted-foreground mt-3 block">
|
|
263
|
-
{label}
|
|
264
|
-
</code>
|
|
265
|
-
</div>
|
|
266
|
-
))}
|
|
267
|
-
</div>
|
|
268
|
-
</Section>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function Motion() {
|
|
273
|
-
return (
|
|
274
|
-
<Section
|
|
275
|
-
title="Motion"
|
|
276
|
-
blurb="Three curves. Standard for things that move within the page, entrance for things arriving, exit for things leaving. Hover a bar to see it."
|
|
277
|
-
>
|
|
278
|
-
<div className="space-y-4">
|
|
279
|
-
{[
|
|
280
|
-
["standard", "ease-standard"],
|
|
281
|
-
["entrance", "ease-entrance"],
|
|
282
|
-
["exit", "ease-exit"],
|
|
283
|
-
].map(([label, cls]) => (
|
|
284
|
-
<div key={label} className="flex items-center gap-6 group">
|
|
285
|
-
<code className="text-caption font-mono text-muted-foreground w-28 shrink-0">
|
|
286
|
-
{label}
|
|
287
|
-
</code>
|
|
288
|
-
<div className="flex-1 h-8 bg-muted rounded-md relative overflow-hidden">
|
|
289
|
-
<div
|
|
290
|
-
className={`absolute inset-y-1 left-1 w-8 bg-primary rounded-sm transition-all duration-500 ${cls} group-hover:left-[calc(100%-2.25rem)]`}
|
|
291
|
-
/>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
))}
|
|
295
|
-
</div>
|
|
296
|
-
</Section>
|
|
297
|
-
)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function Layout() {
|
|
301
|
-
return (
|
|
302
|
-
<Section
|
|
303
|
-
title="Layout"
|
|
304
|
-
blurb="Two widths. The page holds the whole store, prose holds anything meant to be read as a paragraph."
|
|
305
|
-
>
|
|
306
|
-
<div className="space-y-4">
|
|
307
|
-
{[
|
|
308
|
-
["page", "var(--layout-page-width)", "The store's outer bound."],
|
|
309
|
-
["prose", "var(--layout-prose-width)", "Reading measure for descriptions and policies."],
|
|
310
|
-
].map(([name, val, use]) => (
|
|
311
|
-
<div key={name}>
|
|
312
|
-
<div className="flex items-baseline gap-3">
|
|
313
|
-
<code className="text-caption font-mono">{name}</code>
|
|
314
|
-
<span className="text-caption text-muted-foreground">{use}</span>
|
|
315
|
-
</div>
|
|
316
|
-
<div
|
|
317
|
-
className="h-3 bg-primary/20 border border-primary/40 rounded-sm mt-2 max-w-full"
|
|
318
|
-
style={{ width: val }}
|
|
319
|
-
/>
|
|
320
|
-
</div>
|
|
321
|
-
))}
|
|
322
|
-
</div>
|
|
323
|
-
</Section>
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/* ── Frame ──────────────────────────────────────────────────────────────── */
|
|
328
|
-
|
|
329
|
-
function Section({
|
|
330
|
-
title,
|
|
331
|
-
blurb,
|
|
332
|
-
children,
|
|
333
|
-
}: {
|
|
334
|
-
title: string
|
|
335
|
-
blurb: string
|
|
336
|
-
children: React.ReactNode
|
|
337
|
-
}) {
|
|
338
|
-
return (
|
|
339
|
-
<section>
|
|
340
|
-
<h2 className="text-h3">{title}</h2>
|
|
341
|
-
<p className="text-body-small text-muted-foreground mt-2 mb-8 max-w-prose">{blurb}</p>
|
|
342
|
-
{children}
|
|
343
|
-
</section>
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function SubTitle({ children }: { children: React.ReactNode }) {
|
|
348
|
-
return (
|
|
349
|
-
<h3 className="text-caption uppercase tracking-wide text-muted-foreground mb-3">
|
|
350
|
-
{children}
|
|
351
|
-
</h3>
|
|
352
|
-
)
|
|
353
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The workbench.
|
|
3
|
-
*
|
|
4
|
-
* Everything in the library has an address here, grouped the way a merchant
|
|
5
|
-
* thinks about a storefront rather than the way our folders are arranged.
|
|
6
|
-
* The nav is always present, so you can move between any two components
|
|
7
|
-
* without going back to an index, and every component has a permanent URL so
|
|
8
|
-
* it can be named in conversation and opened directly.
|
|
9
|
-
*/
|
|
10
|
-
import Link from "next/link"
|
|
11
|
-
import { GROUPS } from "./_lib/registry"
|
|
12
|
-
import { StatusDot } from "./_components/status"
|
|
13
|
-
|
|
14
|
-
export default function GalleryLayout({ children }: { children: React.ReactNode }) {
|
|
15
|
-
return (
|
|
16
|
-
<div className="flex min-h-screen">
|
|
17
|
-
<aside className="w-64 shrink-0 border-r border-border bg-muted/20">
|
|
18
|
-
<div className="sticky top-0 max-h-screen overflow-y-auto px-4 py-6">
|
|
19
|
-
<Link href="/gallery" className="block mb-6">
|
|
20
|
-
<p className="text-[11px] uppercase tracking-widest text-muted-foreground">
|
|
21
|
-
Cartbase
|
|
22
|
-
</p>
|
|
23
|
-
<p className="font-semibold">Storefront library</p>
|
|
24
|
-
</Link>
|
|
25
|
-
|
|
26
|
-
<nav className="space-y-6">
|
|
27
|
-
{/*
|
|
28
|
-
The design system sits above every group because it is a
|
|
29
|
-
different kind of thing: not a component, but the values all
|
|
30
|
-
117 of them read. Change one here and everything below repaints.
|
|
31
|
-
*/}
|
|
32
|
-
<div>
|
|
33
|
-
<p className="text-[11px] uppercase tracking-widest text-muted-foreground mb-2 px-2">
|
|
34
|
-
Foundation
|
|
35
|
-
</p>
|
|
36
|
-
<Link
|
|
37
|
-
href="/gallery/design-system"
|
|
38
|
-
className="flex items-center gap-2 px-2 py-1 rounded text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
39
|
-
>
|
|
40
|
-
<span className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-primary" />
|
|
41
|
-
<span className="truncate">Design system</span>
|
|
42
|
-
</Link>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
{GROUPS.map((group) => (
|
|
46
|
-
<div key={group.slug}>
|
|
47
|
-
<p className="text-[11px] uppercase tracking-widest text-muted-foreground mb-2 px-2">
|
|
48
|
-
{group.name}
|
|
49
|
-
</p>
|
|
50
|
-
<ul className="space-y-0.5">
|
|
51
|
-
{group.entries.map((entry) => (
|
|
52
|
-
<li key={entry.slug}>
|
|
53
|
-
<Link
|
|
54
|
-
href={`/gallery/${entry.slug}`}
|
|
55
|
-
className="flex items-center gap-2 px-2 py-1 rounded text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
56
|
-
>
|
|
57
|
-
<StatusDot status={entry.status} />
|
|
58
|
-
<span className="truncate">{entry.name}</span>
|
|
59
|
-
</Link>
|
|
60
|
-
</li>
|
|
61
|
-
))}
|
|
62
|
-
</ul>
|
|
63
|
-
</div>
|
|
64
|
-
))}
|
|
65
|
-
</nav>
|
|
66
|
-
|
|
67
|
-
<div className="mt-8 pt-6 border-t border-border">
|
|
68
|
-
<Link
|
|
69
|
-
href="/"
|
|
70
|
-
className="block px-2 py-1 rounded text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
71
|
-
>
|
|
72
|
-
Open the shop
|
|
73
|
-
</Link>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</aside>
|
|
77
|
-
|
|
78
|
-
<div className="min-w-0 flex-1">
|
|
79
|
-
<div className="max-w-5xl px-10 py-10">{children}</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The board. Everything the library has and everything it owes, at a glance,
|
|
3
|
-
* so the size of the job is never a surprise.
|
|
4
|
-
*/
|
|
5
|
-
import Link from "next/link"
|
|
6
|
-
import { GROUPS, counts } from "./_lib/registry"
|
|
7
|
-
import { StatusDot } from "./_components/status"
|
|
8
|
-
|
|
9
|
-
export const dynamic = "force-dynamic"
|
|
10
|
-
|
|
11
|
-
export default function GalleryIndex() {
|
|
12
|
-
const c = counts()
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<div>
|
|
16
|
-
<header className="mb-10">
|
|
17
|
-
<h1 className="text-3xl font-semibold">The library</h1>
|
|
18
|
-
<p className="text-muted-foreground mt-3 max-w-2xl">
|
|
19
|
-
Every part of a storefront we ship, or still owe. Open one and we
|
|
20
|
-
shape it together; when it is agreed it turns green and stops being
|
|
21
|
-
negotiable.
|
|
22
|
-
</p>
|
|
23
|
-
</header>
|
|
24
|
-
|
|
25
|
-
<div className="grid grid-cols-3 gap-4 mb-12">
|
|
26
|
-
<Tally label="Agreed" value={c.done} tone="done" />
|
|
27
|
-
<Tally label="Built, not reviewed" value={c.built} tone="built" />
|
|
28
|
-
<Tally label="Not built" value={c.missing} tone="missing" />
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div className="space-y-10">
|
|
32
|
-
{GROUPS.map((group) => (
|
|
33
|
-
<section key={group.slug}>
|
|
34
|
-
<h2 className="text-lg font-semibold">{group.name}</h2>
|
|
35
|
-
<p className="text-sm text-muted-foreground mt-1 mb-4 max-w-2xl">{group.blurb}</p>
|
|
36
|
-
<ul className="grid gap-2 sm:grid-cols-2">
|
|
37
|
-
{group.entries.map((entry) => (
|
|
38
|
-
<li key={entry.slug}>
|
|
39
|
-
<Link
|
|
40
|
-
href={`/gallery/${entry.slug}`}
|
|
41
|
-
className="flex items-start gap-3 rounded-lg border border-border p-3 hover:bg-muted/50"
|
|
42
|
-
>
|
|
43
|
-
<span className="mt-1.5">
|
|
44
|
-
<StatusDot status={entry.status} />
|
|
45
|
-
</span>
|
|
46
|
-
<span className="min-w-0">
|
|
47
|
-
<span className="block font-medium text-sm">{entry.name}</span>
|
|
48
|
-
<span className="block text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
|
49
|
-
{entry.summary}
|
|
50
|
-
</span>
|
|
51
|
-
</span>
|
|
52
|
-
</Link>
|
|
53
|
-
</li>
|
|
54
|
-
))}
|
|
55
|
-
</ul>
|
|
56
|
-
</section>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function Tally({
|
|
64
|
-
label,
|
|
65
|
-
value,
|
|
66
|
-
tone,
|
|
67
|
-
}: {
|
|
68
|
-
label: string
|
|
69
|
-
value: number
|
|
70
|
-
tone: "done" | "built" | "missing"
|
|
71
|
-
}) {
|
|
72
|
-
return (
|
|
73
|
-
<div className="rounded-lg border border-border p-4">
|
|
74
|
-
<div className="flex items-center gap-2">
|
|
75
|
-
<StatusDot status={tone} />
|
|
76
|
-
<span className="text-xs uppercase tracking-wide text-muted-foreground">{label}</span>
|
|
77
|
-
</div>
|
|
78
|
-
<p className="text-2xl font-semibold mt-1">{value}</p>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|