@vibes.diy/prompts 14.3.188 → 14.3.190
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/llms/backend.md +20 -0
- package/llms/custom-look.d.ts +2 -0
- package/llms/custom-look.js +28 -0
- package/llms/custom-look.js.map +1 -0
- package/llms/custom-look.md +623 -0
- package/llms/image-gen.md +33 -0
- package/llms/index.d.ts +2 -1
- package/llms/index.js +3 -0
- package/llms/index.js.map +1 -1
- package/llms/pouf-calendar.js.map +1 -1
- package/llms/pouf-landing.js.map +1 -1
- package/llms/pouf-quiz.js.map +1 -1
- package/llms/pouf-testimonials.js.map +1 -1
- package/llms/pouf-weather.js.map +1 -1
- package/llms/stripe-subscriptions.md +28 -4
- package/llms/stripe.js +1 -1
- package/llms/stripe.js.map +1 -1
- package/llms/stripe.md +20 -1
- package/llms/web-audio.md +54 -0
- package/package.json +5 -5
- package/prompts.js +7 -3
- package/prompts.js.map +1 -1
- package/themes/colorsets.js +25 -1
- package/themes/colorsets.js.map +1 -1
- package/themes/looks/index.d.ts +2 -0
- package/themes/looks/index.js +6 -1
- package/themes/looks/index.js.map +1 -1
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# Custom look — wear the style the person names
|
|
2
|
+
|
|
3
|
+
When the person names an outside style, brand or design system — "make it look
|
|
4
|
+
like a Claude artifact", "use Material", "plain shadcn", "neo-brutalist like Gumroad",
|
|
5
|
+
"terminal", "newspaper", "iOS settings", "our brand colours are #…" — the app
|
|
6
|
+
wears THAT style, built from the recipes below.
|
|
7
|
+
|
|
8
|
+
## How a custom look works
|
|
9
|
+
|
|
10
|
+
**The app writes the style's palette onto the platform's theme variables in its own `:root` block.**
|
|
11
|
+
Every component then paints through those variables, so the owner's Style tab
|
|
12
|
+
still restyles the whole app later: a Style pick overrides the same names.
|
|
13
|
+
|
|
14
|
+
The block lives in ONE `<style>` element, the first child of the app's root
|
|
15
|
+
element, holding a module-level `THEME_CSS` string in this order:
|
|
16
|
+
|
|
17
|
+
1. the Google Fonts `@import` (an `@import` is only read at the top of a stylesheet);
|
|
18
|
+
2. `:root { … }` — the light palette and every structural token;
|
|
19
|
+
3. `@media (prefers-color-scheme: dark) { :root { … } }` — the dark palette,
|
|
20
|
+
for a style that has one. A style that is light-only or dark-only writes the
|
|
21
|
+
one `:root` and sets `color-scheme` to match.
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
const THEME_CSS = `
|
|
25
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
26
|
+
:root {
|
|
27
|
+
color-scheme: light;
|
|
28
|
+
--background: #ffffff; --surface: #ffffff; --surface-overlay: #ffffff;
|
|
29
|
+
--primary: #18181b; --on-primary: #fafafa;
|
|
30
|
+
/* …the rest of the style's tokens… */
|
|
31
|
+
}
|
|
32
|
+
@media (prefers-color-scheme: dark) {
|
|
33
|
+
:root { color-scheme: dark; --background: #09090b; /* …colours only… */ }
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export default function App() {
|
|
38
|
+
return (
|
|
39
|
+
<div className="min-h-screen bg-background text-foreground font-sans">
|
|
40
|
+
<style>{THEME_CSS}</style>
|
|
41
|
+
{/* the app */}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### The variables
|
|
48
|
+
|
|
49
|
+
Write literal values (hex, `rgb()`, `oklch()`) for these names, exactly:
|
|
50
|
+
|
|
51
|
+
- **Colours** (light, and again in the dark arm): `--background` (the page),
|
|
52
|
+
`--surface` (cards, rows, fields), `--surface-overlay` (opaque panels that
|
|
53
|
+
float: menus, dialogs), `--primary`, `--secondary`, `--accent`, `--neutral`
|
|
54
|
+
(the quiet fill), `--text-primary`, `--text-secondary`, `--border`.
|
|
55
|
+
- **Inks** — each fill names the text colour that sits on it, at 4.5:1 or
|
|
56
|
+
better: `--on-primary`, `--on-secondary`, `--on-accent`.
|
|
57
|
+
- **Structure** (once, in the light `:root`): `--font-family`,
|
|
58
|
+
`--font-family-mono`, `--font-size-base`, `--radius-sm`, `--radius`,
|
|
59
|
+
`--radius-lg`, `--border-width`, `--vibes-spacing`, `--vibes-shadow-sm`,
|
|
60
|
+
`--vibes-shadow`, `--vibes-shadow-lg`, `--letter-spacing`,
|
|
61
|
+
`--letter-spacing-wide`, `--font-weight`, `--text-transform`.
|
|
62
|
+
- **A display face**, when the style pairs two families: `--font-serif`,
|
|
63
|
+
read by the `font-serif` utility.
|
|
64
|
+
|
|
65
|
+
The platform's shadcn names (`--foreground`, `--card`, `--card-foreground`,
|
|
66
|
+
`--popover`, `--popover-foreground`, `--primary-foreground`,
|
|
67
|
+
`--secondary-foreground`, `--muted`, `--muted-foreground`,
|
|
68
|
+
`--accent-foreground`, `--destructive`, `--input`, `--ring`) already point at
|
|
69
|
+
the names above, so they follow the block on their own; the block writes the
|
|
70
|
+
canonical names and leaves the aliases to the platform.
|
|
71
|
+
|
|
72
|
+
### Painting with them
|
|
73
|
+
|
|
74
|
+
Plain Tailwind utilities read the variables:
|
|
75
|
+
|
|
76
|
+
| Utility | Reads |
|
|
77
|
+
| ------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
78
|
+
| `bg-background text-foreground` | `--background`, `--text-primary` |
|
|
79
|
+
| `bg-card text-card-foreground` / `bg-popover text-popover-foreground` | `--surface` / `--surface-overlay`, `--text-primary` |
|
|
80
|
+
| `text-muted-foreground`, `bg-muted` | `--text-secondary`, the quiet fill |
|
|
81
|
+
| `bg-primary text-primary-foreground` (and `secondary`, `accent`) | the fill and its `--on-*` ink |
|
|
82
|
+
| `border border-border` | `--border-width`, `--border` |
|
|
83
|
+
| `rounded-sm` / `rounded-md` / `rounded-lg` | `--radius-sm` / `--radius` / `--radius-lg` |
|
|
84
|
+
| `shadow-xs` / `shadow-md` / `shadow-lg` | `--vibes-shadow-sm` / `--vibes-shadow` / `-lg` |
|
|
85
|
+
| `font-sans` / `font-mono` / `font-serif` | `--font-family` / `--font-family-mono` / `--font-serif` |
|
|
86
|
+
| `tracking-tight` / `tracking-wide`, `font-bold` | `--letter-spacing` / `-wide`, `--font-weight` |
|
|
87
|
+
|
|
88
|
+
`font-sans` goes once on the root element and every child inherits the face.
|
|
89
|
+
|
|
90
|
+
**Every component is plain JSX with Tailwind utilities, written in the app itself.**
|
|
91
|
+
A custom-look app imports no look package (`@vibes.diy/pouf`, `@vibes.diy/look`):
|
|
92
|
+
its buttons, cards, rows and fields are a few lines of JSX each, shaped by the
|
|
93
|
+
recipe below. Each extracted component file paints with the same utilities.
|
|
94
|
+
|
|
95
|
+
**Fonts load from Google Fonts with `&display=swap`.** Name the families in
|
|
96
|
+
`--font-family` (and `--font-serif` / `--font-family-mono`) with a system
|
|
97
|
+
fallback after them, and load exactly those families in the `@import`.
|
|
98
|
+
|
|
99
|
+
**The look is inspired by its source; the words on screen are the app's own.**
|
|
100
|
+
Headings, the title, empty states and buttons speak about the app (a reading
|
|
101
|
+
list, a habit, a gig), so another company's name, logo and marks stay out of
|
|
102
|
+
the UI. "Inspired by" lives in the code comments, if anywhere.
|
|
103
|
+
|
|
104
|
+
**Any other named style** ("like Linear", "Bauhaus", "Y2K") gets the same
|
|
105
|
+
treatment: start from the nearest recipe below, then set the colours, faces,
|
|
106
|
+
radius and shadows the style is known for.
|
|
107
|
+
|
|
108
|
+
## Claude-artifact-inspired
|
|
109
|
+
|
|
110
|
+
**Asked as:** "like a Claude artifact", "Claude style", "Anthropic-y", "warm and calm".
|
|
111
|
+
Warm neutral ground, one terracotta accent, generous whitespace, quiet borders,
|
|
112
|
+
a serif display face over a clean sans body.
|
|
113
|
+
|
|
114
|
+
```css
|
|
115
|
+
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&family=Inter:wght@400;500;600&display=swap');
|
|
116
|
+
:root {
|
|
117
|
+
color-scheme: light;
|
|
118
|
+
--background: #f5f3ec; --surface: #fdfcf8; --surface-overlay: #fdfcf8;
|
|
119
|
+
--primary: #b4532f; --on-primary: #ffffff;
|
|
120
|
+
--secondary: #ebe7dc; --on-secondary: #3b382f;
|
|
121
|
+
--accent: #f3e2d6; --on-accent: #7a3417; --neutral: #ebe7dc;
|
|
122
|
+
--text-primary: #2b2a25; --text-secondary: #6b675b; --border: #e2ded2;
|
|
123
|
+
--font-family: 'Inter', system-ui, sans-serif; --font-serif: 'Source Serif 4', Georgia, serif;
|
|
124
|
+
--font-family-mono: ui-monospace, Menlo, monospace; --font-size-base: 1rem;
|
|
125
|
+
--radius-sm: 6px; --radius: 10px; --radius-lg: 14px; --border-width: 1px; --vibes-spacing: 1rem;
|
|
126
|
+
--vibes-shadow-sm: 0 1px 1px rgb(60 50 30 / 0.04); --vibes-shadow: 0 1px 3px rgb(60 50 30 / 0.06);
|
|
127
|
+
--vibes-shadow-lg: 0 8px 24px rgb(60 50 30 / 0.08);
|
|
128
|
+
--letter-spacing: -0.01em; --letter-spacing-wide: 0.04em; --font-weight: 600; --text-transform: none;
|
|
129
|
+
}
|
|
130
|
+
@media (prefers-color-scheme: dark) {
|
|
131
|
+
:root {
|
|
132
|
+
color-scheme: dark;
|
|
133
|
+
--background: #262522; --surface: #2f2e2a; --surface-overlay: #34332f;
|
|
134
|
+
--primary: #d97757; --on-primary: #1f1e1b; --secondary: #3a3833; --on-secondary: #ece9e0;
|
|
135
|
+
--accent: #4a3328; --on-accent: #f5c9b3; --neutral: #3a3833;
|
|
136
|
+
--text-primary: #f0eee6; --text-secondary: #b1ada1; --border: #3e3c36;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **Type:** headings `font-serif` at 600 (`text-3xl` page title, `text-xl`
|
|
142
|
+
section), body Inter 400 at `text-base` with `leading-relaxed`, labels 500.
|
|
143
|
+
- **Shape:** soft 10px corners, 1px quiet borders, near-invisible shadows,
|
|
144
|
+
roomy padding (`p-6`, `gap-6`), a narrow reading column (`max-w-2xl mx-auto`).
|
|
145
|
+
|
|
146
|
+
```jsx
|
|
147
|
+
function ReadingCard({ book, onFinish }) {
|
|
148
|
+
return (
|
|
149
|
+
<article className="bg-card text-card-foreground border border-border rounded-md p-6 flex flex-col gap-3">
|
|
150
|
+
<h3 className="font-serif text-xl">{book.title}</h3>
|
|
151
|
+
<p className="text-muted-foreground leading-relaxed">{book.note}</p>
|
|
152
|
+
<div className="flex items-center gap-3 pt-2">
|
|
153
|
+
<button type="button" className="bg-primary text-primary-foreground rounded-md px-4 py-2 font-medium" onClick={onFinish}>
|
|
154
|
+
Mark as read
|
|
155
|
+
</button>
|
|
156
|
+
<span className="bg-accent text-accent-foreground rounded-sm px-2 py-1 text-sm">{book.author}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</article>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## shadcn/ui plain
|
|
164
|
+
|
|
165
|
+
**Asked as:** "plain shadcn", "shadcn default", "like a Vercel app", "clean zinc".
|
|
166
|
+
Neutral zinc, near-black primary, crisp 1px borders, compact controls.
|
|
167
|
+
|
|
168
|
+
```css
|
|
169
|
+
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
|
|
170
|
+
:root {
|
|
171
|
+
color-scheme: light;
|
|
172
|
+
--background: #ffffff; --surface: #ffffff; --surface-overlay: #ffffff;
|
|
173
|
+
--primary: #18181b; --on-primary: #fafafa;
|
|
174
|
+
--secondary: #f4f4f5; --on-secondary: #18181b;
|
|
175
|
+
--accent: #f4f4f5; --on-accent: #18181b; --neutral: #f4f4f5;
|
|
176
|
+
--text-primary: #09090b; --text-secondary: #71717a; --border: #e4e4e7;
|
|
177
|
+
--font-family: 'Geist', system-ui, sans-serif; --font-family-mono: 'Geist Mono', ui-monospace, monospace;
|
|
178
|
+
--font-size-base: 0.9375rem;
|
|
179
|
+
--radius-sm: 4px; --radius: 6px; --radius-lg: 8px; --border-width: 1px; --vibes-spacing: 1rem;
|
|
180
|
+
--vibes-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05); --vibes-shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
181
|
+
--vibes-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
182
|
+
--letter-spacing: -0.015em; --letter-spacing-wide: 0.02em; --font-weight: 600; --text-transform: none;
|
|
183
|
+
}
|
|
184
|
+
@media (prefers-color-scheme: dark) {
|
|
185
|
+
:root {
|
|
186
|
+
color-scheme: dark;
|
|
187
|
+
--background: #09090b; --surface: #111113; --surface-overlay: #18181b;
|
|
188
|
+
--primary: #fafafa; --on-primary: #18181b; --secondary: #27272a; --on-secondary: #fafafa;
|
|
189
|
+
--accent: #27272a; --on-accent: #fafafa; --neutral: #27272a;
|
|
190
|
+
--text-primary: #fafafa; --text-secondary: #a1a1aa; --border: #27272a;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- **Type:** Geist 400 body at 15px, 600 titles with `tracking-tight`
|
|
196
|
+
(`text-2xl` page, `text-base` card), `text-sm` controls, Geist Mono for numbers.
|
|
197
|
+
- **Shape:** 6px corners, 1px borders on every card and field, `shadow-xs` on
|
|
198
|
+
cards, `h-9` buttons, tight rhythm (`gap-4`, `p-4`).
|
|
199
|
+
|
|
200
|
+
```jsx
|
|
201
|
+
function SettingCard({ title, hint, onSave }) {
|
|
202
|
+
return (
|
|
203
|
+
<div className="bg-card text-card-foreground border border-border rounded-lg shadow-xs">
|
|
204
|
+
<div className="p-4 flex flex-col gap-1">
|
|
205
|
+
<h3 className="font-semibold tracking-tight">{title}</h3>
|
|
206
|
+
<p className="text-sm text-muted-foreground">{hint}</p>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="border-t border-border px-4 py-3 flex justify-end gap-2">
|
|
209
|
+
<button type="button" className="h-9 px-4 rounded-md border border-border text-sm font-medium hover:bg-accent">Cancel</button>
|
|
210
|
+
<button type="button" className="h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium" onClick={onSave}>
|
|
211
|
+
Save
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Material 3
|
|
220
|
+
|
|
221
|
+
**Asked as:** "Material", "Material 3", "Material You", "like an Android app", "Google style".
|
|
222
|
+
Tonal surfaces in a purple family, pill buttons (filled and tonal), elevation
|
|
223
|
+
shown as a deeper tint, large rounded cards.
|
|
224
|
+
|
|
225
|
+
```css
|
|
226
|
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
|
|
227
|
+
:root {
|
|
228
|
+
color-scheme: light;
|
|
229
|
+
--background: #fef7ff; --surface: #f3edf7; --surface-overlay: #ece6f0;
|
|
230
|
+
--primary: #6750a4; --on-primary: #ffffff;
|
|
231
|
+
--secondary: #e8def8; --on-secondary: #1d192b;
|
|
232
|
+
--accent: #ffd8e4; --on-accent: #31111d; --neutral: #e7e0ec;
|
|
233
|
+
--text-primary: #1d1b20; --text-secondary: #49454f; --border: #cac4d0;
|
|
234
|
+
--font-family: 'Roboto', system-ui, sans-serif; --font-family-mono: ui-monospace, monospace; --font-size-base: 1rem;
|
|
235
|
+
--radius-sm: 8px; --radius: 12px; --radius-lg: 28px; --border-width: 1px; --vibes-spacing: 1rem;
|
|
236
|
+
--vibes-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3); --vibes-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
|
|
237
|
+
--vibes-shadow-lg: 0 2px 3px rgb(0 0 0 / 0.3), 0 6px 10px 4px rgb(0 0 0 / 0.15);
|
|
238
|
+
--letter-spacing: 0em; --letter-spacing-wide: 0.01em; --font-weight: 500; --text-transform: none;
|
|
239
|
+
}
|
|
240
|
+
@media (prefers-color-scheme: dark) {
|
|
241
|
+
:root {
|
|
242
|
+
color-scheme: dark;
|
|
243
|
+
--background: #141218; --surface: #211f26; --surface-overlay: #2b2930;
|
|
244
|
+
--primary: #d0bcff; --on-primary: #381e72; --secondary: #4a4458; --on-secondary: #e8def8;
|
|
245
|
+
--accent: #633b48; --on-accent: #ffd8e4; --neutral: #36343b;
|
|
246
|
+
--text-primary: #e6e0e9; --text-secondary: #cac4d0; --border: #49454f;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- **Type:** Roboto; titles 400 at `text-2xl`, list headlines 500 at
|
|
252
|
+
`text-base`, supporting text 400 `text-sm text-muted-foreground`, button
|
|
253
|
+
labels 500 `text-sm`.
|
|
254
|
+
- **Shape:** cards on `bg-card` with no border and `rounded-lg` (28px) for
|
|
255
|
+
sheets or `rounded-md` (12px) for list cards; buttons `rounded-full h-10 px-6`;
|
|
256
|
+
elevation is the tonal step (`bg-background` → `bg-card` → `bg-popover`) and
|
|
257
|
+
`shadow-xs` only on a floating action.
|
|
258
|
+
|
|
259
|
+
```jsx
|
|
260
|
+
function HabitCard({ habit, onCheck }) {
|
|
261
|
+
return (
|
|
262
|
+
<div className="bg-card text-card-foreground rounded-md p-4 flex items-center gap-4">
|
|
263
|
+
<div className="h-10 w-10 rounded-full bg-secondary text-secondary-foreground grid place-items-center font-medium">
|
|
264
|
+
{habit.streak}
|
|
265
|
+
</div>
|
|
266
|
+
<div className="flex-1">
|
|
267
|
+
<p className="font-medium">{habit.name}</p>
|
|
268
|
+
<p className="text-sm text-muted-foreground">{habit.streak} day streak</p>
|
|
269
|
+
</div>
|
|
270
|
+
<button type="button" className="h-10 px-6 rounded-full bg-secondary text-secondary-foreground text-sm font-medium">Skip</button>
|
|
271
|
+
<button type="button" className="h-10 px-6 rounded-full bg-primary text-primary-foreground text-sm font-medium" onClick={onCheck}>
|
|
272
|
+
Done
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Neo-brutalist (Gumroad-like)
|
|
280
|
+
|
|
281
|
+
**Asked as:** "neo-brutalist", "neo-brutalism", "like Gumroad", "chunky bold borders and hard shadows". A colour word alone ("make it brutalist") is a colourway on the platform's own look, not this recipe.
|
|
282
|
+
Thick black borders, hard offset shadows, flat high-chroma fills on a cream ground.
|
|
283
|
+
Light-only.
|
|
284
|
+
|
|
285
|
+
```css
|
|
286
|
+
@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@500;700;900&display=swap');
|
|
287
|
+
:root {
|
|
288
|
+
color-scheme: light;
|
|
289
|
+
--background: #f4f4f0; --surface: #ffffff; --surface-overlay: #ffffff;
|
|
290
|
+
--primary: #ff90e8; --on-primary: #000000;
|
|
291
|
+
--secondary: #ffc900; --on-secondary: #000000;
|
|
292
|
+
--accent: #23a094; --on-accent: #000000; --neutral: #e8e8e2;
|
|
293
|
+
--text-primary: #000000; --text-secondary: #3d3d3d; --border: #000000;
|
|
294
|
+
--font-family: 'Archivo', system-ui, sans-serif; --font-family-mono: ui-monospace, monospace; --font-size-base: 1rem;
|
|
295
|
+
--radius-sm: 2px; --radius: 4px; --radius-lg: 8px; --border-width: 2px; --vibes-spacing: 1rem;
|
|
296
|
+
--vibes-shadow-sm: 2px 2px 0 var(--border); --vibes-shadow: 4px 4px 0 var(--border); --vibes-shadow-lg: 8px 8px 0 var(--border);
|
|
297
|
+
--letter-spacing: -0.02em; --letter-spacing-wide: 0.08em; --font-weight: 900; --text-transform: uppercase;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- **Type:** Archivo; headings 900 with `tracking-tight` at `text-4xl`, body
|
|
302
|
+
500, labels 700 `text-xs tracking-wide [text-transform:var(--text-transform)]`.
|
|
303
|
+
- **Shape:** every card and button carries `border border-border` (2px black)
|
|
304
|
+
and `shadow-md` (hard 4px offset, no blur); fills are flat; a press slides the
|
|
305
|
+
element into its shadow (`active:translate-x-1 active:translate-y-1 active:shadow-none`).
|
|
306
|
+
|
|
307
|
+
```jsx
|
|
308
|
+
function GigRow({ gig, onTickets }) {
|
|
309
|
+
return (
|
|
310
|
+
<div className="bg-card border border-border rounded-md shadow-md p-4 flex items-center gap-4">
|
|
311
|
+
<div className="bg-secondary text-secondary-foreground border border-border rounded-sm px-3 py-2 text-center">
|
|
312
|
+
<p className="text-xs font-bold tracking-wide [text-transform:var(--text-transform)]">{gig.month}</p>
|
|
313
|
+
<p className="text-2xl font-bold">{gig.day}</p>
|
|
314
|
+
</div>
|
|
315
|
+
<div className="flex-1">
|
|
316
|
+
<p className="text-xl font-bold tracking-tight">{gig.venue}</p>
|
|
317
|
+
<p className="text-muted-foreground">{gig.city}</p>
|
|
318
|
+
</div>
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
className="bg-primary text-primary-foreground border border-border rounded-md shadow-md px-5 py-3 font-bold active:translate-x-1 active:translate-y-1 active:shadow-none"
|
|
322
|
+
onClick={onTickets}
|
|
323
|
+
>
|
|
324
|
+
Tickets
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Terminal / mono
|
|
332
|
+
|
|
333
|
+
**Asked as:** "terminal", "hacker", "command line", "monospace", "green screen", "retro computer".
|
|
334
|
+
Phosphor green on near-black, one amber highlight, square corners, everything mono.
|
|
335
|
+
Dark-only.
|
|
336
|
+
|
|
337
|
+
```css
|
|
338
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
|
339
|
+
:root {
|
|
340
|
+
color-scheme: dark;
|
|
341
|
+
--background: #0a0f0b; --surface: #101812; --surface-overlay: #141e17;
|
|
342
|
+
--primary: #4ade80; --on-primary: #03140a;
|
|
343
|
+
--secondary: #16251a; --on-secondary: #a7f3c0;
|
|
344
|
+
--accent: #fbbf24; --on-accent: #1a1200; --neutral: #16251a;
|
|
345
|
+
--text-primary: #c6f6d5; --text-secondary: #6ea883; --border: #1f3527;
|
|
346
|
+
--font-family: 'JetBrains Mono', ui-monospace, monospace; --font-family-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
347
|
+
--font-size-base: 0.9375rem;
|
|
348
|
+
--radius-sm: 0px; --radius: 0px; --radius-lg: 0px; --border-width: 1px; --vibes-spacing: 1rem;
|
|
349
|
+
--vibes-shadow-sm: 0 0 #0000; --vibes-shadow: 0 0 #0000; --vibes-shadow-lg: 0 0 0 1px var(--border);
|
|
350
|
+
--letter-spacing: 0em; --letter-spacing-wide: 0.1em; --font-weight: 700; --text-transform: uppercase;
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
- **Type:** JetBrains Mono everywhere; one size step for headings
|
|
355
|
+
(`text-lg font-bold`), labels `text-xs tracking-wide` in caps, a `>` or `$`
|
|
356
|
+
prompt glyph before headings.
|
|
357
|
+
- **Shape:** square corners, 1px rules, no shadows, dense rows (`py-2`), status
|
|
358
|
+
shown as bracketed words (`[ OK ]`, `[WARN]`) in the fill colours.
|
|
359
|
+
|
|
360
|
+
```jsx
|
|
361
|
+
function ServiceLine({ service, onRestart }) {
|
|
362
|
+
const up = service.status === "up";
|
|
363
|
+
return (
|
|
364
|
+
<div className="flex items-center gap-3 border-b border-border py-2">
|
|
365
|
+
<span className={up ? "bg-primary text-primary-foreground px-1" : "bg-accent text-accent-foreground px-1"}>
|
|
366
|
+
{up ? "[ OK ]" : "[WARN]"}
|
|
367
|
+
</span>
|
|
368
|
+
<span className="flex-1">{service.name}</span>
|
|
369
|
+
<span className="text-muted-foreground">{service.latency}ms</span>
|
|
370
|
+
<button type="button" className="border border-border px-2 hover:bg-primary hover:text-primary-foreground" onClick={onRestart}>
|
|
371
|
+
restart
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Editorial / newspaper
|
|
379
|
+
|
|
380
|
+
**Asked as:** "newspaper", "editorial", "magazine", "broadsheet", "like the morning paper".
|
|
381
|
+
Newsprint ground, black ink, one red for kickers, ruled columns, serif type.
|
|
382
|
+
Light-only.
|
|
383
|
+
|
|
384
|
+
```css
|
|
385
|
+
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&display=swap');
|
|
386
|
+
:root {
|
|
387
|
+
color-scheme: light;
|
|
388
|
+
--background: #f6f2e8; --surface: #fbf8f1; --surface-overlay: #fbf8f1;
|
|
389
|
+
--primary: #141414; --on-primary: #f6f2e8;
|
|
390
|
+
--secondary: #e9e2d0; --on-secondary: #141414;
|
|
391
|
+
--accent: #a51d1d; --on-accent: #ffffff; --neutral: #e9e2d0;
|
|
392
|
+
--text-primary: #141414; --text-secondary: #57524a; --border: #1c1c1c;
|
|
393
|
+
--font-family: 'Source Serif 4', Georgia, serif; --font-serif: 'Playfair Display', Georgia, serif;
|
|
394
|
+
--font-family-mono: ui-monospace, monospace; --font-size-base: 1.0625rem;
|
|
395
|
+
--radius-sm: 0px; --radius: 0px; --radius-lg: 0px; --border-width: 1px; --vibes-spacing: 1rem;
|
|
396
|
+
--vibes-shadow-sm: 0 0 #0000; --vibes-shadow: 0 0 #0000; --vibes-shadow-lg: 0 0 #0000;
|
|
397
|
+
--letter-spacing: -0.01em; --letter-spacing-wide: 0.12em; --font-weight: 700; --text-transform: uppercase;
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
- **Type:** headlines `font-serif` 900 (`text-5xl` masthead, `text-2xl`
|
|
402
|
+
story), body Source Serif 400 at 17px `leading-relaxed`, kickers `text-xs
|
|
403
|
+
font-bold tracking-wide` in caps and `text-accent`.
|
|
404
|
+
- **Shape:** square corners, hairline column rules (`border-t border-border`),
|
|
405
|
+
a double rule under the masthead (`border-b-4 border-double`), no shadows,
|
|
406
|
+
columns with `md:grid-cols-3 gap-8`.
|
|
407
|
+
|
|
408
|
+
```jsx
|
|
409
|
+
function RecipeStory({ recipe, onSave }) {
|
|
410
|
+
return (
|
|
411
|
+
<article className="border-t border-border pt-4 flex flex-col gap-2">
|
|
412
|
+
<p className="text-xs font-bold tracking-wide uppercase text-accent">{recipe.course}</p>
|
|
413
|
+
<h2 className="font-serif text-2xl font-bold leading-tight">{recipe.title}</h2>
|
|
414
|
+
<p className="leading-relaxed first-letter:font-serif first-letter:text-4xl first-letter:float-left first-letter:mr-1">
|
|
415
|
+
{recipe.intro}
|
|
416
|
+
</p>
|
|
417
|
+
<p className="text-sm italic text-muted-foreground">Serves {recipe.serves}</p>
|
|
418
|
+
<button type="button" className="self-start bg-primary text-primary-foreground px-4 py-2 text-sm font-bold tracking-wide uppercase" onClick={onSave}>
|
|
419
|
+
Clip it
|
|
420
|
+
</button>
|
|
421
|
+
</article>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## iOS settings
|
|
427
|
+
|
|
428
|
+
**Asked as:** "iOS settings", "like an iPhone app", "Apple style", "grouped list", "native settings".
|
|
429
|
+
Grouped inset lists on a grey ground, hairline separators, blue actions, green switches.
|
|
430
|
+
|
|
431
|
+
```css
|
|
432
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
|
433
|
+
:root {
|
|
434
|
+
color-scheme: light;
|
|
435
|
+
--background: #f2f2f7; --surface: #ffffff; --surface-overlay: #ffffff;
|
|
436
|
+
--primary: #0066d6; --on-primary: #ffffff;
|
|
437
|
+
--secondary: #e5e5ea; --on-secondary: #000000;
|
|
438
|
+
--accent: #34c759; --on-accent: #000000; --neutral: #e5e5ea;
|
|
439
|
+
--text-primary: #000000; --text-secondary: #6c6c70; --border: #c6c6c8;
|
|
440
|
+
--font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
|
|
441
|
+
--font-family-mono: ui-monospace, Menlo, monospace; --font-size-base: 1.0625rem;
|
|
442
|
+
--radius-sm: 6px; --radius: 10px; --radius-lg: 14px; --border-width: 0.5px; --vibes-spacing: 1rem;
|
|
443
|
+
--vibes-shadow-sm: 0 0 #0000; --vibes-shadow: 0 0 #0000; --vibes-shadow-lg: 0 8px 30px rgb(0 0 0 / 0.12);
|
|
444
|
+
--letter-spacing: -0.01em; --letter-spacing-wide: 0.02em; --font-weight: 600; --text-transform: uppercase;
|
|
445
|
+
}
|
|
446
|
+
@media (prefers-color-scheme: dark) {
|
|
447
|
+
:root {
|
|
448
|
+
color-scheme: dark;
|
|
449
|
+
--background: #000000; --surface: #1c1c1e; --surface-overlay: #2c2c2e;
|
|
450
|
+
--primary: #0a84ff; --on-primary: #000000; --secondary: #2c2c2e; --on-secondary: #ffffff;
|
|
451
|
+
--accent: #30d158; --on-accent: #000000; --neutral: #2c2c2e;
|
|
452
|
+
--text-primary: #ffffff; --text-secondary: #a1a1a6; --border: #38383a;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- **Type:** the system face (Inter where it is absent); rows at 17px 400,
|
|
458
|
+
large title `text-3xl font-bold`, group headers `text-xs
|
|
459
|
+
text-muted-foreground uppercase` above each group, footers `text-xs` below.
|
|
460
|
+
- **Shape:** each group is one `bg-card rounded-md` block inset from the screen
|
|
461
|
+
edge (`mx-4`), rows `min-h-11 px-4`, hairline separators inset to the label
|
|
462
|
+
(`border-b border-border` on all rows but the last), chevrons in
|
|
463
|
+
`text-muted-foreground`, no shadows.
|
|
464
|
+
|
|
465
|
+
```jsx
|
|
466
|
+
function SettingsGroup({ title, rows }) {
|
|
467
|
+
return (
|
|
468
|
+
<section className="mx-4 flex flex-col gap-1">
|
|
469
|
+
<p className="px-4 text-xs uppercase text-muted-foreground">{title}</p>
|
|
470
|
+
<div className="bg-card text-card-foreground rounded-md overflow-hidden">
|
|
471
|
+
{rows.map((row, i) => (
|
|
472
|
+
<label key={row.id} className="flex items-center min-h-11 pl-4">
|
|
473
|
+
<div className={`flex flex-1 items-center gap-3 pr-4 py-2 ${i < rows.length - 1 ? "border-b border-border" : ""}`}>
|
|
474
|
+
<span className="flex-1">{row.label}</span>
|
|
475
|
+
{row.kind === "toggle" ? (
|
|
476
|
+
<input type="checkbox" checked={row.on} onChange={row.onToggle} className="h-6 w-10 accent-accent" />
|
|
477
|
+
) : (
|
|
478
|
+
<span className="text-muted-foreground">{row.value} ›</span>
|
|
479
|
+
)}
|
|
480
|
+
</div>
|
|
481
|
+
</label>
|
|
482
|
+
))}
|
|
483
|
+
</div>
|
|
484
|
+
</section>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Brand kit from the maker
|
|
490
|
+
|
|
491
|
+
**Asked as:** "our brand colours are #…", "use our palette", "our font is …", "match our logo".
|
|
492
|
+
Map what the person pasted onto the variables, step by step.
|
|
493
|
+
Their hex values go into the `:root` exactly as they wrote them.
|
|
494
|
+
|
|
495
|
+
1. **Ground.** The lightest brand colour (a cream, an off-white) is
|
|
496
|
+
`--background`. With no light colour in the kit, `--background` is `#ffffff`
|
|
497
|
+
or a 3% tint of the main colour.
|
|
498
|
+
2. **Surface.** `--surface` and `--surface-overlay` sit one step lighter than
|
|
499
|
+
the ground (`#ffffff` on a cream).
|
|
500
|
+
3. **Primary.** The boldest brand colour is `--primary`; `--on-primary` is
|
|
501
|
+
`#ffffff` or a near-black, whichever reads at 4.5:1 or better.
|
|
502
|
+
4. **Secondary and accent.** The next brand colour is `--secondary` (with its
|
|
503
|
+
ink); a pale tint of the primary is `--accent`, for highlights and badges.
|
|
504
|
+
5. **Ink.** `--text-primary` is the darkest brand colour, or a near-black
|
|
505
|
+
leaning toward the primary's hue; `--text-secondary` sits halfway to the
|
|
506
|
+
ground and still reads at 4.5:1; `--border` is a light step of the ground.
|
|
507
|
+
6. **Font.** The named font is `--font-family` (or `--font-serif` for a display
|
|
508
|
+
face), loaded in the `@import`. A font outside Google Fonts gets the closest
|
|
509
|
+
Google family in its place.
|
|
510
|
+
7. **Shape.** Take radius and weight from the brand's feel: rounded and soft
|
|
511
|
+
for a friendly brand, square and tight for a formal one.
|
|
512
|
+
|
|
513
|
+
Worked example — "a bake sale order form; our colours are #1b4d3e, #f4a259 and
|
|
514
|
+
#fdf6ec, and our font is Fraunces":
|
|
515
|
+
|
|
516
|
+
### `App.jsx`
|
|
517
|
+
```jsx
|
|
518
|
+
import React, { useState } from "react";
|
|
519
|
+
import { useFireproof } from "use-fireproof";
|
|
520
|
+
|
|
521
|
+
// Brand kit: #fdf6ec ground, #1b4d3e primary, #f4a259 secondary, Fraunces display.
|
|
522
|
+
const THEME_CSS = `
|
|
523
|
+
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,800&family=Inter:wght@400;500;600&display=swap');
|
|
524
|
+
:root {
|
|
525
|
+
color-scheme: light;
|
|
526
|
+
--background: #fdf6ec; --surface: #ffffff; --surface-overlay: #ffffff;
|
|
527
|
+
--primary: #1b4d3e; --on-primary: #ffffff;
|
|
528
|
+
--secondary: #f4a259; --on-secondary: #2a1a08;
|
|
529
|
+
--accent: #f8dcc0; --on-accent: #1b4d3e; --neutral: #f3eadc;
|
|
530
|
+
--text-primary: #1f2a26; --text-secondary: #5c6661; --border: #e8dccb;
|
|
531
|
+
--font-family: 'Inter', system-ui, sans-serif; --font-serif: 'Fraunces', Georgia, serif;
|
|
532
|
+
--font-family-mono: ui-monospace, monospace; --font-size-base: 1rem;
|
|
533
|
+
--radius-sm: 8px; --radius: 14px; --radius-lg: 22px; --border-width: 1px; --vibes-spacing: 1rem;
|
|
534
|
+
--vibes-shadow-sm: 0 1px 2px rgb(27 77 62 / 0.06); --vibes-shadow: 0 2px 8px rgb(27 77 62 / 0.08);
|
|
535
|
+
--vibes-shadow-lg: 0 12px 32px rgb(27 77 62 / 0.12);
|
|
536
|
+
--letter-spacing: -0.01em; --letter-spacing-wide: 0.06em; --font-weight: 600; --text-transform: none;
|
|
537
|
+
}
|
|
538
|
+
`;
|
|
539
|
+
|
|
540
|
+
const TREATS = [
|
|
541
|
+
{ id: "scone", name: "Apricot scone", price: 3 },
|
|
542
|
+
{ id: "brownie", name: "Brown butter brownie", price: 4 },
|
|
543
|
+
{ id: "loaf", name: "Lemon loaf slice", price: 3 },
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
function TreatRow({ treat, count, onAdd }) {
|
|
547
|
+
return (
|
|
548
|
+
<div className="bg-card text-card-foreground border border-border rounded-md shadow-sm p-4 flex items-center gap-4">
|
|
549
|
+
<div className="flex-1">
|
|
550
|
+
<p className="font-serif text-lg">{treat.name}</p>
|
|
551
|
+
<p className="text-sm text-muted-foreground">${treat.price} each</p>
|
|
552
|
+
</div>
|
|
553
|
+
{count > 0 && <span className="bg-accent text-accent-foreground rounded-sm px-2 py-1 text-sm">{count}</span>}
|
|
554
|
+
<button type="button" className="bg-secondary text-secondary-foreground rounded-md px-4 py-2 font-medium" onClick={onAdd}>
|
|
555
|
+
Add
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export default function App() {
|
|
562
|
+
const { database, useLiveQuery } = useFireproof("bake-sale");
|
|
563
|
+
const { docs: orders } = useLiveQuery("type", { key: "order" });
|
|
564
|
+
const [name, setName] = useState("");
|
|
565
|
+
const [basket, setBasket] = useState({});
|
|
566
|
+
|
|
567
|
+
const add = (id) => setBasket((b) => ({ ...b, [id]: (b[id] ?? 0) + 1 }));
|
|
568
|
+
const total = TREATS.reduce((sum, t) => sum + (basket[t.id] ?? 0) * t.price, 0);
|
|
569
|
+
|
|
570
|
+
const placeOrder = async (e) => {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
const typed = name.trim();
|
|
573
|
+
const items = basket;
|
|
574
|
+
setName("");
|
|
575
|
+
setBasket({});
|
|
576
|
+
const { ok } = await database.put({ type: "order", name: typed, items, total, created: Date.now() });
|
|
577
|
+
if (!ok) {
|
|
578
|
+
setName((now) => now || typed);
|
|
579
|
+
setBasket((now) => (Object.keys(now).length === 0 ? items : now));
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const c = {
|
|
584
|
+
page: "min-h-screen bg-background text-foreground font-sans",
|
|
585
|
+
column: "max-w-xl mx-auto p-6 flex flex-col gap-6",
|
|
586
|
+
title: "font-serif text-4xl tracking-tight",
|
|
587
|
+
field: "bg-card border border-border rounded-md px-4 py-3",
|
|
588
|
+
order: "flex justify-between border-b border-border py-2 text-sm",
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<div className={c.page}>
|
|
593
|
+
<style>{THEME_CSS}</style>
|
|
594
|
+
<main className={c.column}>
|
|
595
|
+
<h1 className={c.title}>Saturday bake sale</h1>
|
|
596
|
+
<form onSubmit={placeOrder} className="flex flex-col gap-4">
|
|
597
|
+
{TREATS.map((t) => (
|
|
598
|
+
<TreatRow key={t.id} treat={t} count={basket[t.id] ?? 0} onAdd={() => add(t.id)} />
|
|
599
|
+
))}
|
|
600
|
+
<input className={c.field} placeholder="Name for the order" value={name} onChange={(e) => setName(e.target.value)} />
|
|
601
|
+
<button
|
|
602
|
+
type="submit"
|
|
603
|
+
className="bg-primary text-primary-foreground rounded-md px-6 py-3 font-semibold disabled:opacity-50"
|
|
604
|
+
disabled={!name.trim() || total === 0}
|
|
605
|
+
>
|
|
606
|
+
Order for ${total}
|
|
607
|
+
</button>
|
|
608
|
+
</form>
|
|
609
|
+
<section className="flex flex-col">
|
|
610
|
+
<h2 className="font-serif text-xl">Orders so far</h2>
|
|
611
|
+
{orders.length === 0 && <p className="text-muted-foreground">The first order lands here.</p>}
|
|
612
|
+
{orders.map((o) => (
|
|
613
|
+
<div key={o._id} className={c.order}>
|
|
614
|
+
<span>{o.name}</span>
|
|
615
|
+
<span className="text-muted-foreground">${o.total}</span>
|
|
616
|
+
</div>
|
|
617
|
+
))}
|
|
618
|
+
</section>
|
|
619
|
+
</main>
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
```
|
package/llms/image-gen.md
CHANGED
|
@@ -135,6 +135,39 @@ When one photo becomes several looks at once (four costumes, a style sampler), u
|
|
|
135
135
|
|
|
136
136
|
Load a previously generated image with its saved host record's `_id`: `<ImgGen _id={doc._id} database={database} />`. A literal such as `_id="my-image-id"` refers to a host record already saved under that exact `_id` in this database. If the host has a `prompt` but no stored image yet, the component generates one.
|
|
137
137
|
|
|
138
|
+
## Page Pictures Every Visitor Shares
|
|
139
|
+
|
|
140
|
+
Pictures written into the page itself — a hero, one illustration per chapter or section, a banner — are the same for every visitor, so draw each one once and let everyone see that picture. A picture's readers are its record's readers, and a record nobody saved has none but the picture's maker, so give every page picture a saved record. List the pictures as records with their own `_id` and `prompt`, have the app save any that are missing, and mount each `<ImgGen>` on its record's `_id`. Keep them in a database of their own that `access.js` leaves open to every reader (when `access.js` has a default export, give it a branch for these records that grants public read). The first picture drawn is then the one every later visitor sees, at no cost to them.
|
|
141
|
+
|
|
142
|
+
### `App.jsx`
|
|
143
|
+
```jsx
|
|
144
|
+
import React, { useEffect } from "react";
|
|
145
|
+
import { useFireproof } from "use-fireproof";
|
|
146
|
+
import { ImgGen } from "use-vibes";
|
|
147
|
+
|
|
148
|
+
const PAGE_ART = [
|
|
149
|
+
{ _id: "art-hero", type: "art", title: "Harbor at dawn", prompt: "A misty harbor at dawn, warm watercolor" },
|
|
150
|
+
{ _id: "art-market", type: "art", title: "Morning market", prompt: "A morning fish market under striped awnings, watercolor" },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
export default function App() {
|
|
154
|
+
const { database } = useFireproof("pageArt");
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
for (const art of PAGE_ART) database.get(art._id).catch(() => database.put(art));
|
|
157
|
+
}, [database]);
|
|
158
|
+
return (
|
|
159
|
+
<main>
|
|
160
|
+
{PAGE_ART.map((art) => (
|
|
161
|
+
<figure key={art._id}>
|
|
162
|
+
<ImgGen _id={art._id} database={database} prompt={art.prompt} alt={art.title} showControls={false} />
|
|
163
|
+
<figcaption>{art.title}</figcaption>
|
|
164
|
+
</figure>
|
|
165
|
+
))}
|
|
166
|
+
</main>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
138
171
|
## Illustrations That Are Part of an Item's Identity
|
|
139
172
|
|
|
140
173
|
When an item's picture is intrinsic to what it is — a card in a deck, a creature in a bestiary, a product in a catalog, a character in a cast — render `<ImgGen>` **by default** for every such item that lacks an image, so the illustration appears as soon as the item does. Because ImgGen auto-generates when the target doc has a `prompt` but no `_files` yet, mounting it directly on the item is all it takes:
|