@vibes.diy/prompts 14.3.78 → 14.3.80
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/access.md +105 -60
- package/llms/backend.js +2 -2
- package/llms/backend.js.map +1 -1
- package/llms/backend.md +51 -43
- package/llms/bluesky.md +16 -14
- package/llms/board-games.d.ts +2 -0
- package/llms/board-games.js +29 -0
- package/llms/board-games.js.map +1 -0
- package/llms/board-games.md +477 -0
- package/llms/callai.md +91 -20
- package/llms/connections.md +5 -4
- package/llms/create-vibe.md +5 -4
- package/llms/fireproof.md +261 -109
- package/llms/image-gen.md +45 -7
- package/llms/index.d.ts +2 -1
- package/llms/index.js +3 -0
- package/llms/index.js.map +1 -1
- package/llms/p5.md +9 -11
- package/llms/spotify.md +5 -2
- package/llms/three-js.md +163 -166
- package/llms/use-vibe.md +15 -8
- package/llms/use-viewer.md +61 -45
- package/llms/voxel.md +32 -37
- package/llms/youtube.md +5 -2
- package/package.json +4 -4
- package/prompts.d.ts +4 -2
- package/prompts.js +47 -24
- package/prompts.js.map +1 -1
- package/system-prompt-initial-oneshot.md +13 -12
- package/system-prompt-initial.md +12 -11
- package/system-prompt.md +29 -47
- package/themes/colorsets.js +18 -2
- package/themes/colorsets.js.map +1 -1
- package/themes/looks/pouf/brief.md +65 -47
- package/themes/looks/pouf/catalog.generated.d.ts +1 -1
- package/themes/looks/pouf/catalog.generated.js +103 -10
- package/themes/looks/pouf/catalog.generated.js.map +1 -1
- package/themes/looks/pouf/variety.js +2 -2
- package/themes/looks/pouf/variety.js.map +1 -1
package/llms/connections.md
CHANGED
|
@@ -75,18 +75,19 @@ for the rest.
|
|
|
75
75
|
### `App.jsx`
|
|
76
76
|
```jsx app=instagram-connection
|
|
77
77
|
import { useConnection } from "use-vibes";
|
|
78
|
+
import { Button, Text } from "@vibes.diy/pouf";
|
|
78
79
|
|
|
79
80
|
function ConnectInstagram() {
|
|
80
81
|
const { state, consented, available, accountLabel, link, refresh } = useConnection("instagram");
|
|
81
82
|
if (state === "pending") return null; // still resolving — never flash a Connect button
|
|
82
83
|
// A connection that already works keeps working, whatever `available` says.
|
|
83
84
|
if (consented && (state === "live" || state === "expiring"))
|
|
84
|
-
return <
|
|
85
|
+
return <Text>Connected as {accountLabel}</Text>;
|
|
85
86
|
// Everything below asks the visitor to start or redo a connection, which is
|
|
86
87
|
// the only thing `available` can stop.
|
|
87
|
-
if (!available) return <
|
|
88
|
-
if (state === "dead") return <
|
|
89
|
-
return <button onClick={link}>Connect Instagram</
|
|
88
|
+
if (!available) return <Text muted>Connecting Instagram is not available right now.</Text>;
|
|
89
|
+
if (state === "dead") return <Text muted>Your Instagram connection expired. Reconnect it in Settings.</Text>;
|
|
90
|
+
return <Button type="button" onClick={link}>Connect Instagram</Button>;
|
|
90
91
|
}
|
|
91
92
|
```
|
|
92
93
|
|
package/llms/create-vibe.md
CHANGED
|
@@ -28,7 +28,7 @@ when they're ready.
|
|
|
28
28
|
```jsx
|
|
29
29
|
import React from "react";
|
|
30
30
|
import { createVibe } from "use-vibes";
|
|
31
|
-
import { Button } from "@vibes.diy/
|
|
31
|
+
import { Button } from "@vibes.diy/pouf";
|
|
32
32
|
|
|
33
33
|
export default function App() {
|
|
34
34
|
// `ready` + `spec` come from the interview (see the full example below).
|
|
@@ -69,7 +69,7 @@ Let the user edit each slide inline and present full-screen.`;
|
|
|
69
69
|
import React from "react";
|
|
70
70
|
import { callAI } from "call-ai";
|
|
71
71
|
import { createVibe } from "use-vibes";
|
|
72
|
-
import { Button } from "@vibes.diy/
|
|
72
|
+
import { Button } from "@vibes.diy/pouf";
|
|
73
73
|
|
|
74
74
|
export default function App() {
|
|
75
75
|
const [answers, setAnswers] = React.useState({});
|
|
@@ -95,7 +95,7 @@ export default function App() {
|
|
|
95
95
|
<main id="app">
|
|
96
96
|
{/* ...interview... */}
|
|
97
97
|
{!spec ? (
|
|
98
|
-
<Button size="lg"
|
|
98
|
+
<Button size="lg" loading={building} onClick={finish}>
|
|
99
99
|
{building ? "Preparing…" : "I'm ready"}
|
|
100
100
|
</Button>
|
|
101
101
|
) : (
|
|
@@ -131,6 +131,7 @@ blocked), offer a fallback link so the hand-off still works:
|
|
|
131
131
|
|
|
132
132
|
```jsx
|
|
133
133
|
import { createVibe, buildCreateVibeUrl } from "use-vibes";
|
|
134
|
+
import { Button } from "@vibes.diy/pouf";
|
|
134
135
|
|
|
135
136
|
function HandoffButton({ spec }) {
|
|
136
137
|
const [blocked, setBlocked] = React.useState(false);
|
|
@@ -139,7 +140,7 @@ function HandoffButton({ spec }) {
|
|
|
139
140
|
Open the builder
|
|
140
141
|
</a>
|
|
141
142
|
) : (
|
|
142
|
-
<button onClick={() => setBlocked(!createVibe(spec))}>Create my pitch deck</
|
|
143
|
+
<Button type="button" onClick={() => setBlocked(!createVibe(spec))}>Create my pitch deck</Button>
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
```
|
package/llms/fireproof.md
CHANGED
|
@@ -29,14 +29,17 @@ When the server later refuses a write the visitor can act on — a signed-in vis
|
|
|
29
29
|
function NoteForm({ text }) {
|
|
30
30
|
const { database, writeRejection, clearWriteRejection } = useFireproof("notes");
|
|
31
31
|
return (
|
|
32
|
-
<
|
|
33
|
-
<
|
|
32
|
+
<Stack gap={2}>
|
|
33
|
+
<Row>
|
|
34
|
+
<Button type="button" onClick={() => database.put({ type: "note", text })}>Save</Button>
|
|
35
|
+
</Row>
|
|
34
36
|
{writeRejection && (
|
|
35
|
-
<
|
|
36
|
-
{writeRejection.message}
|
|
37
|
-
|
|
37
|
+
<Row gap={2} align="center" wrap>
|
|
38
|
+
<p role="status">{writeRejection.message}</p>
|
|
39
|
+
<Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button>
|
|
40
|
+
</Row>
|
|
38
41
|
)}
|
|
39
|
-
</
|
|
42
|
+
</Stack>
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
45
|
```
|
|
@@ -53,6 +56,7 @@ import React, { useState } from "react";
|
|
|
53
56
|
import { callAI } from "call-ai";
|
|
54
57
|
import { useFireproof } from "use-fireproof";
|
|
55
58
|
import { useVibe } from "use-vibes";
|
|
59
|
+
import { Button, Field, Heading, Input, Row, RowCard, Stack, Text, Textarea } from "@vibes.diy/pouf";
|
|
56
60
|
|
|
57
61
|
const taskSchema = {
|
|
58
62
|
type: "object",
|
|
@@ -100,19 +104,23 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
|
100
104
|
const { useDocument } = useFireproof(database);
|
|
101
105
|
const { doc, merge, save } = useDocument(task);
|
|
102
106
|
const [editing, setEditing] = useState(false);
|
|
103
|
-
return <
|
|
104
|
-
<
|
|
107
|
+
return <RowCard><Stack gap={2}>
|
|
108
|
+
<Text>{task.title}</Text><Text muted>{task.details}</Text><Text muted>{task.dueDate}</Text>
|
|
105
109
|
{can.edit(task).ok && <>
|
|
106
|
-
<
|
|
107
|
-
|
|
110
|
+
<Row gap={2} wrap>
|
|
111
|
+
<Button type="button" size="sm" variant="quiet" disabled={disabled} onClick={() => onCorrect(task)}>Change by sentence</Button>
|
|
112
|
+
<Button type="button" size="sm" variant="quiet" disabled={disabled} onClick={() => setEditing(true)}>Edit details</Button>
|
|
113
|
+
</Row>
|
|
108
114
|
{editing && <form onSubmit={async (event) => { event.preventDefault(); if (disabled) return; await save(); onSaved(task); setEditing(false); }}>
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
<Stack gap={3}>
|
|
116
|
+
<Field label="Task">{(id, describedBy) => <Input id={id} describedBy={describedBy} disabled={disabled} value={doc.title} onChange={(title) => merge({ title })} />}</Field>
|
|
117
|
+
<Field label="Details">{(id, describedBy) => <Textarea id={id} describedBy={describedBy} disabled={disabled} value={doc.details} onChange={(details) => merge({ details })} />}</Field>
|
|
118
|
+
<Field label="Due date">{(id, describedBy) => <Input id={id} describedBy={describedBy} disabled={disabled} type="date" value={doc.dueDate} onChange={(dueDate) => merge({ dueDate })} />}</Field>
|
|
119
|
+
<Row><Button type="submit" size="sm" variant="quiet" disabled={disabled}>Save details</Button></Row>
|
|
120
|
+
</Stack>
|
|
113
121
|
</form>}
|
|
114
122
|
</>}
|
|
115
|
-
</
|
|
123
|
+
</Stack></RowCard>;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
export default function App() {
|
|
@@ -188,21 +196,27 @@ export default function App() {
|
|
|
188
196
|
setMessage(error.message);
|
|
189
197
|
} finally { setBusy(false); }
|
|
190
198
|
}
|
|
191
|
-
return <main>
|
|
192
|
-
<
|
|
199
|
+
return <main className="min-h-screen bg-background text-foreground font-sans p-6"><Stack gap={5}>
|
|
200
|
+
<Heading level={1}>Tasks in your own words</Heading>
|
|
193
201
|
<form onSubmit={submitSentence}>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
<Stack gap={3}>
|
|
203
|
+
<Field label="Describe tasks, a correction, or ask a question">{(id, describedBy) => <Textarea id={id} describedBy={describedBy} value={sentence} disabled={busy || pending !== undefined} onChange={setSentence} />}</Field>
|
|
204
|
+
{current !== undefined && <Row gap={2} align="center" wrap><Text muted>Changing: {current.title}</Text><Button type="button" size="sm" variant="quiet" disabled={busy || pending !== undefined} onClick={() => { setCurrent(undefined); setSentence(""); }}>Add a new task</Button></Row>}
|
|
205
|
+
<Row gap={2} wrap>
|
|
206
|
+
<Button type="submit" disabled={!ready || busy || sentence.trim().length === 0}>{busy ? "Adding your changes…" : pending !== undefined ? "Retry remaining tasks" : current === undefined ? "Add tasks" : "Apply correction"}</Button>
|
|
207
|
+
<Button type="button" variant="quiet" disabled={!ready || busy || pending !== undefined || sentence.trim().length === 0} onClick={askRecords}>Ask about tasks</Button>
|
|
208
|
+
</Row>
|
|
209
|
+
</Stack>
|
|
198
210
|
</form>
|
|
199
|
-
<
|
|
211
|
+
<Text>{answer}</Text>
|
|
200
212
|
<p role="status">{message}</p>
|
|
201
|
-
{writeRejection && <p role="status">{writeRejection.message}
|
|
202
|
-
|
|
203
|
-
|
|
213
|
+
{writeRejection && <Row gap={2} align="center" wrap><p role="status">{writeRejection.message}</p><Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button></Row>}
|
|
214
|
+
<Row gap={2} wrap>
|
|
215
|
+
{lastWrite !== undefined && <Button type="button" size="sm" variant="quiet" disabled={busy} onClick={undoLast}>Undo</Button>}
|
|
216
|
+
{pending !== undefined && !busy && <Button type="button" size="sm" variant="quiet" tone="down" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</Button>}
|
|
217
|
+
</Row>
|
|
204
218
|
{docs.map((task) => <TaskCard key={task._id} task={task} database={database} can={can} disabled={busy || pending !== undefined} onSaved={(previous) => setLastWrite({ created: [], replaced: previous })} onCorrect={(doc) => { if (busy || pending !== undefined) return; setCurrent(doc); setSentence(""); }} />)}
|
|
205
|
-
</main>;
|
|
219
|
+
</Stack></main>;
|
|
206
220
|
}
|
|
207
221
|
```
|
|
208
222
|
|
|
@@ -337,9 +351,15 @@ To update an existing document from a click handler or callback, use `database.p
|
|
|
337
351
|
=======
|
|
338
352
|
<li key={doc._id}>
|
|
339
353
|
{doc.text}
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
354
|
+
<IconButton
|
|
355
|
+
size="sm"
|
|
356
|
+
variant={doc.favorite ? "solid" : "quiet"}
|
|
357
|
+
tone="warn"
|
|
358
|
+
aria-pressed={Boolean(doc.favorite)}
|
|
359
|
+
label={doc.favorite ? "Unfavorite" : "Favorite"}
|
|
360
|
+
icon={<Icon name="star" size="sm" />}
|
|
361
|
+
onClick={() => database.put({ ...doc, favorite: !doc.favorite })}
|
|
362
|
+
/>
|
|
343
363
|
</li>
|
|
344
364
|
>>>>>>> REPLACE
|
|
345
365
|
```
|
|
@@ -348,7 +368,17 @@ Never call hooks inside handlers — `const { doc, save } = useDocument({ _id: i
|
|
|
348
368
|
|
|
349
369
|
### Continuous Controls — `merge()` on every event, `save()` once on commit
|
|
350
370
|
|
|
351
|
-
A continuous control (slider, drag, color picker, live-typed text bound to one doc) fires many events in quick succession. **Never call `database.put()` or `save()` on every `onChange`** — that floods the server with rapid concurrent writes to the same `_id`, which collide on the per-document sequence and get rejected (`Failed to put document …`). Instead, `merge()`
|
|
371
|
+
A continuous control (slider, drag, color picker, live-typed text bound to one doc) fires many events in quick succession. **Never call `database.put()` or `save()` on every `onChange`** — that floods the server with rapid concurrent writes to the same `_id`, which collide on the per-document sequence and get rejected (`Failed to put document …`). Instead, `merge()` on each event to keep the UI live (it updates the local draft and, when the doc has an `_id`, broadcasts the merged snapshot to connected peers, but persists nothing), and `save()` once when the interaction commits (`onPointerUp`, `onBlur`, or a debounced trailing call; a `Slider`'s `onChange` fires on every step, so it is not a commit, and the wrapper around it hears the release). Editing a single doc with a slider:
|
|
372
|
+
|
|
373
|
+
### `App.jsx`
|
|
374
|
+
```jsx
|
|
375
|
+
<<<<<<< SEARCH
|
|
376
|
+
import { useFireproof } from "use-fireproof";
|
|
377
|
+
=======
|
|
378
|
+
import { useFireproof } from "use-fireproof";
|
|
379
|
+
import { Slider } from "@vibes.diy/pouf/interactive";
|
|
380
|
+
>>>>>>> REPLACE
|
|
381
|
+
```
|
|
352
382
|
|
|
353
383
|
### `App.jsx`
|
|
354
384
|
```jsx
|
|
@@ -359,6 +389,7 @@ A continuous control (slider, drag, color picker, live-typed text bound to one d
|
|
|
359
389
|
|
|
360
390
|
// One shared mix doc; slider updates the UI on every event, persists on commit
|
|
361
391
|
const { doc: mix, merge: mergeMix, save: saveMix } = useDocument({ _id: "mix:current", level: 50 });
|
|
392
|
+
const SLIDER_KEYS = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"]);
|
|
362
393
|
>>>>>>> REPLACE
|
|
363
394
|
```
|
|
364
395
|
|
|
@@ -367,14 +398,16 @@ A continuous control (slider, drag, color picker, live-typed text bound to one d
|
|
|
367
398
|
<<<<<<< SEARCH
|
|
368
399
|
<h3>Recent Documents</h3>
|
|
369
400
|
=======
|
|
370
|
-
<
|
|
371
|
-
type="range"
|
|
372
|
-
min="0"
|
|
373
|
-
max="100"
|
|
374
|
-
value={mix.level}
|
|
375
|
-
onChange={(e) => mergeMix({ level: Number(e.target.value) })} // live, no write
|
|
401
|
+
<div
|
|
376
402
|
onPointerUp={() => saveMix().catch((err) => console.error("save failed", err))} // one write on commit
|
|
377
|
-
|
|
403
|
+
onKeyUp={(e) => {
|
|
404
|
+
// only the keys that move the slider commit; Tab or focusing it writes nothing
|
|
405
|
+
if (SLIDER_KEYS.has(e.key)) saveMix().catch((err) => console.error("save failed", err));
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{/* live, no write */}
|
|
409
|
+
<Slider label="Level" min={0} max={100} value={[mix.level]} onChange={([level]) => mergeMix({ level })} />
|
|
410
|
+
</div>
|
|
378
411
|
|
|
379
412
|
<h3>Recent Documents</h3>
|
|
380
413
|
>>>>>>> REPLACE
|
|
@@ -382,6 +415,127 @@ A continuous control (slider, drag, color picker, live-typed text bound to one d
|
|
|
382
415
|
|
|
383
416
|
If you genuinely need to persist mid-drag, debounce the `save()` so at most one write is in flight, and still `.catch()` the rejection. Better yet, prefer one document per event (see the Counter Pattern) over hammering a single hot document.
|
|
384
417
|
|
|
418
|
+
### Strokes — `merge()` the gesture live, save its cells once on pointer-up
|
|
419
|
+
|
|
420
|
+
A drag that touches many documents (painting pixels, drawing on a shared board) splits the same way: **the stroke is live, the cells are durable.** Keep the in-progress stroke in a per-person draft (`_id: "stroke:" + handle`) and `merge()` each new cell into it on `onPointerMove`; peers see it as it is drawn, because `merge()` broadcasts the draft and their `useLiveQuery` returns it as a row. On `onPointerUp`, `put()` one document per touched cell, then `merge()` the stroke back to empty. Never `put()` from `onPointerMove`: one quick drag becomes dozens of writes in flight.
|
|
421
|
+
|
|
422
|
+
Keep each cell its own document with a deterministic `_id` (`pixel-<x>-<y>`). Never fold the canvas into one document every visitor saves: the server keeps whichever save arrives last, so one visitor's save erases everyone else's strokes. An unsaved draft drops out for peers about 12 seconds after it stops updating.
|
|
423
|
+
|
|
424
|
+
A draft reaches only the peers already subscribed to its channel, and a channel's grant comes from a saved document. Create a fixed-id room record on that channel (see Required starting records) so even the first stroke on an empty canvas is seen live.
|
|
425
|
+
|
|
426
|
+
### `App.jsx`
|
|
427
|
+
```jsx
|
|
428
|
+
import React, { useEffect, useRef } from "react";
|
|
429
|
+
import { useFireproof } from "use-fireproof";
|
|
430
|
+
import { useViewer, useVibe } from "use-vibes";
|
|
431
|
+
import { Button } from "@vibes.diy/pouf";
|
|
432
|
+
|
|
433
|
+
const SIZE = 32;
|
|
434
|
+
const CELL = 16;
|
|
435
|
+
const COLOR = "#3a86ff";
|
|
436
|
+
|
|
437
|
+
export default function App() {
|
|
438
|
+
const { viewer, isViewerPending, requestLogin } = useViewer();
|
|
439
|
+
if (isViewerPending) return null;
|
|
440
|
+
if (!viewer) return <Button type="button" onClick={requestLogin}>Sign in to paint</Button>;
|
|
441
|
+
return <Canvas key={viewer.userHandle} me={viewer.userHandle} />; // fixed draft _id per painter
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function Canvas({ me }) {
|
|
445
|
+
// Many people write the same pixel docs: fail fast rather than replay a stale queued write.
|
|
446
|
+
const { database, useLiveQuery, useDocument, writeRejection } = useFireproof("pixels", { offlineQueue: false });
|
|
447
|
+
const { ready, can } = useVibe("pixels");
|
|
448
|
+
const canPaint = ready && can.create({ _id: "pixel-0-0", type: "pixel", x: 0, y: 0, color: COLOR, paintedBy: me }).ok;
|
|
449
|
+
const { docs: pixels } = useLiveQuery("type", { key: "pixel" });
|
|
450
|
+
const { docs: strokes } = useLiveQuery("type", { key: "stroke" });
|
|
451
|
+
const { doc: stroke, merge: mergeStroke } = useDocument({ _id: "stroke:" + me, type: "stroke", userHandle: me, color: COLOR, cells: [] });
|
|
452
|
+
const cellsRef = useRef([]);
|
|
453
|
+
const canvasRef = useRef(null);
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
// The room record grants the channel, so peers subscribe before the first stroke.
|
|
457
|
+
const room = { _id: "canvas:main", type: "canvas" };
|
|
458
|
+
if (ready && can.create(room).ok) database.put(room);
|
|
459
|
+
}, [ready, can, database]);
|
|
460
|
+
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
const ctx = canvasRef.current.getContext("2d");
|
|
463
|
+
ctx.fillStyle = "#ffffff";
|
|
464
|
+
ctx.fillRect(0, 0, SIZE * CELL, SIZE * CELL);
|
|
465
|
+
const fill = (x, y, color) => {
|
|
466
|
+
ctx.fillStyle = color;
|
|
467
|
+
ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
|
|
468
|
+
};
|
|
469
|
+
for (const p of pixels) fill(p.x, p.y, p.color);
|
|
470
|
+
for (const s of strokes) if (s.userHandle !== me) for (const [x, y] of s.cells ?? []) fill(x, y, s.color);
|
|
471
|
+
for (const [x, y] of stroke.cells) fill(x, y, stroke.color);
|
|
472
|
+
}, [pixels, strokes, stroke, me]);
|
|
473
|
+
|
|
474
|
+
function extendStroke(e) {
|
|
475
|
+
if (!canPaint) return;
|
|
476
|
+
const r = canvasRef.current.getBoundingClientRect();
|
|
477
|
+
const x = Math.floor(((e.clientX - r.left) / r.width) * SIZE);
|
|
478
|
+
const y = Math.floor(((e.clientY - r.top) / r.height) * SIZE);
|
|
479
|
+
if (x < 0 || y < 0 || x >= SIZE || y >= SIZE) return;
|
|
480
|
+
if (cellsRef.current.some((c) => c[0] === x && c[1] === y)) return;
|
|
481
|
+
cellsRef.current = [...cellsRef.current, [x, y]];
|
|
482
|
+
mergeStroke({ cells: cellsRef.current }); // live for everyone, no write
|
|
483
|
+
}
|
|
484
|
+
async function endStroke() {
|
|
485
|
+
const cells = cellsRef.current;
|
|
486
|
+
cellsRef.current = [];
|
|
487
|
+
const writes = [];
|
|
488
|
+
for (const [x, y] of cells) writes.push(database.put({ _id: `pixel-${x}-${y}`, type: "pixel", x, y, color: COLOR, paintedBy: me }));
|
|
489
|
+
await Promise.all(writes); // the durable commit: one write per touched cell
|
|
490
|
+
if (cellsRef.current.length === 0) mergeStroke({ cells: [] }); // unless a newer stroke began
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<div>
|
|
495
|
+
{writeRejection && <p role="status">{writeRejection.message}</p>}
|
|
496
|
+
<canvas
|
|
497
|
+
ref={canvasRef}
|
|
498
|
+
width={SIZE * CELL}
|
|
499
|
+
height={SIZE * CELL}
|
|
500
|
+
className="w-full max-w-lg aspect-square touch-none"
|
|
501
|
+
onPointerDown={(e) => {
|
|
502
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
503
|
+
extendStroke(e);
|
|
504
|
+
}}
|
|
505
|
+
onPointerMove={(e) => e.buttons && extendStroke(e)}
|
|
506
|
+
onPointerUp={endStroke}
|
|
507
|
+
onPointerCancel={endStroke}
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The access rules see the broadcast draft too, so `stroke` needs its own branch that binds the `_id` to the sender:
|
|
515
|
+
|
|
516
|
+
### `access.js`
|
|
517
|
+
```js
|
|
518
|
+
export default function (doc, oldDoc, user, ctx) {
|
|
519
|
+
if (!user) throw { forbidden: "sign in to paint" };
|
|
520
|
+
const inGrid = (n) => Number.isInteger(n) && n >= 0 && n < 32;
|
|
521
|
+
if (doc.type === "canvas") {
|
|
522
|
+
if (doc._id !== "canvas:main") throw { forbidden: "one canvas" };
|
|
523
|
+
return { channels: ["canvas"], grant: { public: ["canvas"] } };
|
|
524
|
+
}
|
|
525
|
+
if (doc.type === "stroke") {
|
|
526
|
+
if (doc._id !== "stroke:" + user.userHandle || doc.userHandle !== user.userHandle) throw { forbidden: "not your stroke" };
|
|
527
|
+
if (!Array.isArray(doc.cells) || doc.cells.length > 1024 || !doc.cells.every((c) => Array.isArray(c) && c.length === 2 && inGrid(c[0]) && inGrid(c[1]))) throw { forbidden: "bad stroke" };
|
|
528
|
+
return { channels: ["canvas"], grant: { public: ["canvas"] } };
|
|
529
|
+
}
|
|
530
|
+
if (doc.type === "pixel") {
|
|
531
|
+
if (!inGrid(doc.x) || !inGrid(doc.y) || doc._id !== `pixel-${doc.x}-${doc.y}`) throw { forbidden: "pixel out of bounds" };
|
|
532
|
+
if (doc.paintedBy !== user.userHandle) throw { forbidden: "not your paint" };
|
|
533
|
+
return { channels: ["canvas"], grant: { public: ["canvas"] } };
|
|
534
|
+
}
|
|
535
|
+
throw { forbidden: "unknown document type" };
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
385
539
|
### Query Data
|
|
386
540
|
|
|
387
541
|
Data is queried by sorted indexes defined by the application. Sort by strings, numbers, or booleans, as well as arrays for grouping. Use numbers when possible for sorting continuous data. You can use the `_id` field for temporal sorting so you don't have to write code to get simple recent document lists, as in the basic example above.
|
|
@@ -620,7 +774,7 @@ Building an image uploader with `_files`:
|
|
|
620
774
|
```jsx
|
|
621
775
|
import React from "react";
|
|
622
776
|
import { useFireproof } from "use-fireproof";
|
|
623
|
-
import { Button, Card,
|
|
777
|
+
import { Button, Card, ErrorNote, Field, Figure, Grid, Heading, Input, Stack, Text } from "@vibes.diy/pouf";
|
|
624
778
|
|
|
625
779
|
export default function App() {
|
|
626
780
|
const { useDocument, useLiveQuery } = useFireproof("imageUploads");
|
|
@@ -654,46 +808,45 @@ export default function App() {
|
|
|
654
808
|
};
|
|
655
809
|
|
|
656
810
|
const c = {
|
|
657
|
-
page: "bg-background text-foreground font-sans",
|
|
658
|
-
meta: "text-muted-foreground",
|
|
811
|
+
page: "min-h-screen p-6 max-w-lg mx-auto bg-background text-foreground font-sans",
|
|
659
812
|
};
|
|
660
813
|
|
|
661
814
|
return (
|
|
662
|
-
<div className={
|
|
663
|
-
<
|
|
664
|
-
|
|
665
|
-
<
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
</
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
</
|
|
815
|
+
<div className={c.page}>
|
|
816
|
+
<Stack gap={5}>
|
|
817
|
+
<Heading level={1}>Image Uploader</Heading>
|
|
818
|
+
<form onSubmit={onSubmit}>
|
|
819
|
+
<Stack gap={3}>
|
|
820
|
+
{/* A file picker hands over files, not a value, so it stays the browser's own control inside the Field. */}
|
|
821
|
+
<Field label="Photo">{(id, describedBy) => <input id={id} aria-describedby={describedBy} type="file" accept="image/*" onChange={onPickFile} />}</Field>
|
|
822
|
+
<Field label="Caption">
|
|
823
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} value={doc.caption} onChange={(caption) => merge({ caption })} />}
|
|
824
|
+
</Field>
|
|
825
|
+
<Button type="submit" size="lg">
|
|
826
|
+
Upload
|
|
827
|
+
</Button>
|
|
828
|
+
{saveError && <ErrorNote>{saveError}</ErrorNote>}
|
|
829
|
+
</Stack>
|
|
830
|
+
</form>
|
|
831
|
+
|
|
832
|
+
<Heading level={2}>Recent Uploads</Heading>
|
|
833
|
+
<Grid cols={2} gap={4}>
|
|
834
|
+
{docs.map((d) => (
|
|
835
|
+
<Card key={d._id}>
|
|
836
|
+
<Stack gap={2}>
|
|
837
|
+
{d._files?.photo?.url && <Figure src={d._files.photo.url} alt={d.caption || "upload"} width={400} height={300} />}
|
|
838
|
+
<Text muted>{d.caption || "No caption"}</Text>
|
|
839
|
+
</Stack>
|
|
840
|
+
</Card>
|
|
841
|
+
))}
|
|
842
|
+
</Grid>
|
|
843
|
+
</Stack>
|
|
691
844
|
</div>
|
|
692
845
|
);
|
|
693
846
|
}
|
|
694
847
|
```
|
|
695
848
|
|
|
696
|
-
For multi-file uploads (
|
|
849
|
+
For multi-file uploads (the file picker with `multiple`), build the `_files` map keyed by filename and iterate with `Object.entries(doc._files)` to render each entry.
|
|
697
850
|
|
|
698
851
|
Adding multi-file support:
|
|
699
852
|
|
|
@@ -756,7 +909,8 @@ Code listing for a bird sightings log App.jsx. Note the code ordering: hooks, th
|
|
|
756
909
|
```js
|
|
757
910
|
import React from "react";
|
|
758
911
|
import { useFireproof } from "use-fireproof";
|
|
759
|
-
import { Button,
|
|
912
|
+
import { Button, Field, Heading, Input, Row, RowCard, Spacer, Stack, Text } from "@vibes.diy/pouf";
|
|
913
|
+
import { Checkbox } from "@vibes.diy/pouf/interactive";
|
|
760
914
|
|
|
761
915
|
export default function App() {
|
|
762
916
|
// 1. Hooks and document shapes
|
|
@@ -779,8 +933,8 @@ export default function App() {
|
|
|
779
933
|
});
|
|
780
934
|
|
|
781
935
|
// 2. Event handlers
|
|
782
|
-
const handleInputChange = (
|
|
783
|
-
mergeNewSighting({ species
|
|
936
|
+
const handleInputChange = (species) => {
|
|
937
|
+
mergeNewSighting({ species });
|
|
784
938
|
};
|
|
785
939
|
|
|
786
940
|
const handleSubmit = (e) => {
|
|
@@ -788,48 +942,46 @@ export default function App() {
|
|
|
788
942
|
submitNewSighting();
|
|
789
943
|
};
|
|
790
944
|
|
|
791
|
-
// 3. ClassNames — right before JSX, for the plain elements only; the
|
|
945
|
+
// 3. ClassNames — right before JSX, for the plain elements only; the pouf
|
|
792
946
|
// components carry their own surface, ink and shape.
|
|
793
947
|
const c = {
|
|
794
|
-
page: "bg-background text-foreground font-sans",
|
|
795
|
-
meta: "text-muted-foreground",
|
|
948
|
+
page: "min-h-screen max-w-md mx-auto p-6 bg-background text-foreground font-sans",
|
|
796
949
|
};
|
|
797
950
|
|
|
798
951
|
// 4. JSX return
|
|
799
952
|
return (
|
|
800
|
-
<div className={
|
|
801
|
-
<
|
|
802
|
-
|
|
803
|
-
<
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
</ul>
|
|
953
|
+
<div className={c.page}>
|
|
954
|
+
<Stack gap={5}>
|
|
955
|
+
<Heading level={1}>Bird Sightings</Heading>
|
|
956
|
+
<form onSubmit={handleSubmit}>
|
|
957
|
+
<Row gap={2} align="top">
|
|
958
|
+
<Field label="Species">
|
|
959
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} onChange={handleInputChange} value={newSighting.species} />}
|
|
960
|
+
</Field>
|
|
961
|
+
<Button type="submit">Add</Button>
|
|
962
|
+
</Row>
|
|
963
|
+
</form>
|
|
964
|
+
<Stack gap={3}>
|
|
965
|
+
{sightings.map((doc) => (
|
|
966
|
+
<RowCard key={doc._id}>
|
|
967
|
+
<Row gap={3} align="center">
|
|
968
|
+
<Checkbox
|
|
969
|
+
label={doc.species}
|
|
970
|
+
checked={doc.confirmed}
|
|
971
|
+
onChange={(confirmed) => database.put({ ...doc, confirmed: confirmed === true })}
|
|
972
|
+
/>
|
|
973
|
+
<Spacer />
|
|
974
|
+
<Button type="button" variant="quiet" tone="down" size="sm" onClick={() => database.del(doc._id)}>
|
|
975
|
+
Delete
|
|
976
|
+
</Button>
|
|
977
|
+
</Row>
|
|
978
|
+
<Text size="sm" muted>
|
|
979
|
+
{new Date(doc.createdAt).toISOString()}
|
|
980
|
+
</Text>
|
|
981
|
+
</RowCard>
|
|
982
|
+
))}
|
|
983
|
+
</Stack>
|
|
984
|
+
</Stack>
|
|
833
985
|
</div>
|
|
834
986
|
);
|
|
835
987
|
}
|