@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.
@@ -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: