@vibes.diy/prompts 2.4.11 → 2.4.13
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/fireproof.md +293 -25
- package/llms/three-js.md +2 -2
- package/llms/use-viewer.md +24 -27
- package/llms/webxr.md +4 -4
- package/package.json +3 -3
- package/system-prompt-initial.md +43 -67
- package/system-prompt.md +136 -27
package/llms/fireproof.md
CHANGED
|
@@ -7,7 +7,7 @@ Fireproof is a lightweight embedded document database with encrypted live sync,
|
|
|
7
7
|
- **Apps run anywhere:** Bundle UI, data, and logic together.
|
|
8
8
|
- **Real-Time & Offline-First:** Automatic persistence and live queries, runs in the browser - no loading or error states.
|
|
9
9
|
- **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
|
|
10
|
-
- **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration. Note: these are NOT top-level exports — they are returned by the `useFireproof()` hook. Always destructure from `const { useLiveQuery, useDocument, database } = useFireproof("
|
|
10
|
+
- **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration. Note: these are NOT top-level exports — they are returned by the `useFireproof()` hook. Always destructure from `const { useLiveQuery, useDocument, database } = useFireproof("dbName")`.
|
|
11
11
|
|
|
12
12
|
**File structure:** A vibe's source is one or more files. `/App.jsx` is the entry point (React component). `/access.js` is optional — include it when the app needs per-document write validation or channel-based read isolation. Both files are pushed together and the server discovers `/access.js` automatically.
|
|
13
13
|
|
|
@@ -30,7 +30,7 @@ Fireproof databases store data across sessions and can sync in real-time. Each d
|
|
|
30
30
|
```js
|
|
31
31
|
import { useFireproof } from "use-fireproof";
|
|
32
32
|
|
|
33
|
-
const { database, useLiveQuery, useDocument } = useFireproof("
|
|
33
|
+
const { database, useLiveQuery, useDocument } = useFireproof("myLedger");
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
#### Put and Get Documents
|
|
@@ -47,7 +47,7 @@ This example shows Fireproof's `_id` allows easy sorting with `useLiveQuery`.
|
|
|
47
47
|
|
|
48
48
|
```js
|
|
49
49
|
const App = () => {
|
|
50
|
-
const { useDocument, useLiveQuery } = useFireproof("
|
|
50
|
+
const { useDocument, useLiveQuery } = useFireproof("myLedger");
|
|
51
51
|
|
|
52
52
|
const { doc, merge, submit } = useDocument({ text: "" });
|
|
53
53
|
|
|
@@ -72,12 +72,23 @@ const App = () => {
|
|
|
72
72
|
};
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
The access function lives in a separate file. Even simple apps include one — it's the server-side authority for who can write:
|
|
76
|
+
|
|
77
|
+
access.js
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
export default function (doc, oldDoc, user) {
|
|
81
|
+
if (!user) throw { forbidden: "sign in to save" };
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
75
86
|
### Editing Documents
|
|
76
87
|
|
|
77
88
|
Address documents by a known `_id` if you want to force conflict resolution or work with a real world resource, like a schedule slot or a user profile. In a complex app this might come from a route parameter or correspond to an outside identifier.
|
|
78
89
|
|
|
79
90
|
```js
|
|
80
|
-
const { useDocument } = useFireproof("
|
|
91
|
+
const { useDocument } = useFireproof("myLedger");
|
|
81
92
|
|
|
82
93
|
const { doc, merge, submit, save, reset } = useDocument({ _id: "user-profile:abc@example.com" });
|
|
83
94
|
```
|
|
@@ -143,7 +154,7 @@ Documents can be updated by multiple clients, and synced later. To create an eve
|
|
|
143
154
|
|
|
144
155
|
```js
|
|
145
156
|
const App = () => {
|
|
146
|
-
const { useLiveQuery, database } = useFireproof("
|
|
157
|
+
const { useLiveQuery, database } = useFireproof("myLedger");
|
|
147
158
|
|
|
148
159
|
const { docs } = useLiveQuery("counter", { key: "my-event-name" });
|
|
149
160
|
const counterValue = docs.length;
|
|
@@ -198,7 +209,7 @@ Sortable lists are a common pattern. Here's how to implement them using Fireproo
|
|
|
198
209
|
|
|
199
210
|
```js
|
|
200
211
|
function App() {
|
|
201
|
-
const { database, useLiveQuery } = useFireproof("
|
|
212
|
+
const { database, useLiveQuery } = useFireproof("myLedger");
|
|
202
213
|
|
|
203
214
|
// Initialize list with evenly spaced positions
|
|
204
215
|
async function initializeList() {
|
|
@@ -255,7 +266,7 @@ const { useLiveQuery, database } = useFireproof("drafts", {
|
|
|
255
266
|
});
|
|
256
267
|
|
|
257
268
|
// No acl — falls back to app-level role gates (existing behavior, always safe)
|
|
258
|
-
const { useLiveQuery, database } = useFireproof("
|
|
269
|
+
const { useLiveQuery, database } = useFireproof("publicNotes");
|
|
259
270
|
```
|
|
260
271
|
|
|
261
272
|
**Subject groups** — who each name covers:
|
|
@@ -271,16 +282,237 @@ Owner is always implicitly included — never list `owner` explicitly in an ACL.
|
|
|
271
282
|
|
|
272
283
|
Each capability (`read`, `write`, `delete`) is independent. Omitting one falls back to the app-level role gate for that operation. The `acl` is sent once on first database open and persists across sessions (last-write-wins). Only the **app owner** can set ACLs; non-owner apps opening a database with an `acl` option have it silently ignored — the database still opens and works normally.
|
|
273
284
|
|
|
285
|
+
## Reading Resolved Grants (`access`)
|
|
286
|
+
|
|
287
|
+
`useFireproof()` returns an `access` property — the viewer's resolved roles and channels for that database, computed server-side from the access function's `members` and `grant` declarations.
|
|
288
|
+
|
|
289
|
+
```jsx
|
|
290
|
+
const { database, useLiveQuery, access } = useFireproof("comments");
|
|
291
|
+
|
|
292
|
+
access.roles; // ReadonlySet<string> — roles the viewer belongs to
|
|
293
|
+
access.channels; // ReadonlySet<string> — channels the viewer can access
|
|
294
|
+
|
|
295
|
+
access.hasRole("moderator"); // boolean convenience
|
|
296
|
+
access.hasChannel("engineering"); // boolean convenience
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
For databases without an access function export, `access` has empty roles and channels. No separate pending flag — grants arrive alongside the viewer identity, so `useViewer().isViewerPending` covers both.
|
|
300
|
+
|
|
301
|
+
```jsx
|
|
302
|
+
function App() {
|
|
303
|
+
const { viewer, isViewerPending, ViewerTag } = useViewer();
|
|
304
|
+
const { database, useLiveQuery, access } = useFireproof("comments");
|
|
305
|
+
|
|
306
|
+
if (isViewerPending) return null;
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div>
|
|
310
|
+
<ViewerTag />
|
|
311
|
+
{access.hasRole("poster") && <CommentForm database={database} />}
|
|
312
|
+
{access.hasRole("moderator") && <ModTools database={database} />}
|
|
313
|
+
{access.hasChannel("announcements") && <Announcements />}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The AI agent writes the access function (so it knows the role names) and writes the UI (so it knows which roles gate which components). The `access` object is the bridge — it lets the UI reflect server-enforced permissions without duplicating the logic.
|
|
320
|
+
|
|
321
|
+
The access function is the single source of truth for permissions. The UI reads its verdict from `access` — gate with `access.hasChannel(name)` and `access.hasRole(name)`. Grants may reflect logic the UI can't see (other documents, role memberships, owner state), so `access` is always the right check.
|
|
322
|
+
|
|
323
|
+
`access.hasChannel()` covers every grant path — public channels, restricted channels, role-expanded channels. The access function decides who gets access and how; the UI just asks `access.hasChannel(name)`:
|
|
324
|
+
|
|
325
|
+
```jsx
|
|
326
|
+
// Channel list — access.hasChannel covers both public and restricted channels
|
|
327
|
+
const visibleChannels = channels.filter(ch => access.hasChannel(ch._id));
|
|
328
|
+
|
|
329
|
+
// Compose gate — access.hasChannel is the only check needed
|
|
330
|
+
{viewer && access.hasChannel(activeChannel) && <ComposeForm />}
|
|
331
|
+
|
|
332
|
+
// Channel with mixed access modes — the UI doesn't need to know which mode
|
|
333
|
+
// The access function granted public channels via grant.public and
|
|
334
|
+
// restricted channels via grant.users — access.hasChannel handles both
|
|
335
|
+
{visibleChannels.map(ch => (
|
|
336
|
+
<li key={ch._id} onClick={() => setActive(ch._id)}>#{ch.name}</li>
|
|
337
|
+
))}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Complete example: Team announcements with channels
|
|
341
|
+
|
|
342
|
+
This example shows the full round-trip — access.js declares channels and grants; App.jsx reads them back via `access`. Key details:
|
|
343
|
+
|
|
344
|
+
- **Owner bootstrap:** `user.isOwner` gates management operations (channel setup, role grants, moderation). No bootstrap problem — the owner can always manage without needing a role granted first.
|
|
345
|
+
- **Channel identity:** Channel docs use `_id: "ch:" + name` so names are unique. The `_id` is the channel identifier everywhere — in `channels`, `grant`, and `ctx.requireAccess()`.
|
|
346
|
+
- **Channel grant:** A channel document grants the creator (`grant.users`), adds `grant.public` so all members can read, and `grant.roles` so posters can write.
|
|
347
|
+
- **Write surfaces** are gated with `viewer` (signed in?), `access.hasChannel()` (channel access), or `isOwner` (management).
|
|
348
|
+
- **`ViewerTag`** takes `userHandle` when rendering another user.
|
|
349
|
+
|
|
350
|
+
access.js
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
export function announcements(doc, oldDoc, user, ctx) {
|
|
354
|
+
if (!user) throw { forbidden: "sign in" };
|
|
355
|
+
|
|
356
|
+
if (doc.type === "channel") {
|
|
357
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
358
|
+
return {
|
|
359
|
+
channels: [doc._id],
|
|
360
|
+
grant: {
|
|
361
|
+
users: { [user.userHandle]: [doc._id] },
|
|
362
|
+
public: [doc._id],
|
|
363
|
+
roles: { poster: [doc._id] },
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (doc.type === "roleGrant") {
|
|
369
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
370
|
+
return { members: { [doc.role]: [doc.userHandle] } };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (doc.type === "post") {
|
|
374
|
+
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
375
|
+
ctx.requireAccess(doc.channel);
|
|
376
|
+
return { channels: [doc.channel] };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
App.jsx — `isOwner` and `access.hasChannel()` gate the UI based on what the access function declared:
|
|
384
|
+
|
|
385
|
+
```jsx
|
|
386
|
+
import React from "react";
|
|
387
|
+
import { useFireproof } from "use-fireproof";
|
|
388
|
+
import { useViewer } from "use-vibes";
|
|
389
|
+
|
|
390
|
+
export default function App() {
|
|
391
|
+
const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();
|
|
392
|
+
const { database, useLiveQuery, access } = useFireproof("announcements");
|
|
393
|
+
|
|
394
|
+
const { docs: posts } = useLiveQuery("type", { key: "post" });
|
|
395
|
+
const [draft, setDraft] = React.useState("");
|
|
396
|
+
const [channel, setChannel] = React.useState("general");
|
|
397
|
+
|
|
398
|
+
if (isViewerPending) return null;
|
|
399
|
+
|
|
400
|
+
async function submitPost() {
|
|
401
|
+
if (!draft.trim() || !viewer) return;
|
|
402
|
+
await database.put({
|
|
403
|
+
type: "post",
|
|
404
|
+
channel,
|
|
405
|
+
body: draft.trim(),
|
|
406
|
+
authorHandle: viewer.userHandle,
|
|
407
|
+
createdAt: Date.now(),
|
|
408
|
+
});
|
|
409
|
+
setDraft("");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<div>
|
|
414
|
+
<ViewerTag />
|
|
415
|
+
|
|
416
|
+
{/* access.hasChannel covers public + restricted — the access function handles the distinction */}
|
|
417
|
+
{viewer && access.hasChannel(channel) && (
|
|
418
|
+
<form
|
|
419
|
+
onSubmit={(e) => {
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
submitPost();
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
<textarea value={draft} onChange={(e) => setDraft(e.target.value)} />
|
|
425
|
+
<button type="submit">Post</button>
|
|
426
|
+
</form>
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{/* owner-only management */}
|
|
430
|
+
{isOwner && (
|
|
431
|
+
<button onClick={() => database.put({ type: "roleGrant", role: "poster", userHandle: "newUser" })}>
|
|
432
|
+
Grant poster role
|
|
433
|
+
</button>
|
|
434
|
+
)}
|
|
435
|
+
|
|
436
|
+
{posts.map((p) => (
|
|
437
|
+
<div key={p._id}>
|
|
438
|
+
<ViewerTag userHandle={p.authorHandle} />
|
|
439
|
+
<p>{p.body}</p>
|
|
440
|
+
{isOwner && <button onClick={() => database.del(p._id)}>Delete</button>}
|
|
441
|
+
</div>
|
|
442
|
+
))}
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The pattern: `viewer` checks sign-in, `access.hasChannel()` checks what the access function granted, `isOwner` gates management. The access function is the server-side authority — the UI just reflects its decisions. Real apps can graduate to `access.hasRole("moderator")` when they need to delegate management to non-owners.
|
|
449
|
+
|
|
450
|
+
### Example: Channel board with restricted channels
|
|
451
|
+
|
|
452
|
+
Some channels are open to all members, others are restricted. The access function decides — the UI just asks `access.hasChannel()`.
|
|
453
|
+
|
|
454
|
+
access.js
|
|
455
|
+
|
|
456
|
+
```js
|
|
457
|
+
export function chat(doc, oldDoc, user, ctx) {
|
|
458
|
+
if (!user) throw { forbidden: "sign in" };
|
|
459
|
+
|
|
460
|
+
if (doc.type === "channel") {
|
|
461
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
462
|
+
return {
|
|
463
|
+
channels: [doc._id],
|
|
464
|
+
grant: {
|
|
465
|
+
users: { [user.userHandle]: [doc._id] },
|
|
466
|
+
public: [doc._id],
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (doc.type === "post") {
|
|
472
|
+
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
473
|
+
ctx.requireAccess(doc.channel);
|
|
474
|
+
return { channels: [doc.channel] };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
App.jsx — the UI uses `access.hasChannel()` for everything. It has no idea which channels are restricted — that's the access function's job.
|
|
482
|
+
|
|
483
|
+
```jsx
|
|
484
|
+
const { database, useLiveQuery, access } = useFireproof("chat");
|
|
485
|
+
const { docs: channels } = useLiveQuery("type", { key: "channel" });
|
|
486
|
+
|
|
487
|
+
// client creates channels with a deterministic _id
|
|
488
|
+
await database.put({ _id: "ch:" + name, type: "channel", createdAt: Date.now() });
|
|
489
|
+
|
|
490
|
+
// filter to channels the viewer can see — use _id as channel identifier
|
|
491
|
+
const visible = channels.filter((ch) => isOwner || access.hasChannel(ch._id));
|
|
492
|
+
|
|
493
|
+
// can post? just check channel access
|
|
494
|
+
const canPost = viewer && (isOwner || access.hasChannel(activeChannel._id));
|
|
495
|
+
|
|
496
|
+
// gate the compose form
|
|
497
|
+
{canPost ? (
|
|
498
|
+
<Composer channel={activeChannel._id} />
|
|
499
|
+
) : (
|
|
500
|
+
<p>{viewer ? "Read-only in this channel." : "Sign in to post."}</p>
|
|
501
|
+
)}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Channel `_id` is the channel identifier everywhere. The `canPost` check uses `access.hasChannel(ch._id)`. The access function uses `doc._id` for routing and grants. A deterministic `_id` like `"ch:" + name` enforces uniqueness — two users can't create duplicate channels.
|
|
505
|
+
|
|
274
506
|
---
|
|
275
507
|
|
|
276
508
|
## Access Function (`/access.js`)
|
|
277
509
|
|
|
278
510
|
Access functions are **the room** — they govern what members can do with data once inside the app. The per-vibe membership system is **the door** — it decides who can see the app at all. Once a user is through the door (approved as a member), the access function is the sole authority for data permissions. Access functions are server-run on every write (including deletes) before storing the document. They validate writes, route documents to channels, and declare grants that control who can read what. Only create an `/access.js` file when the user asks for per-document routing, channel-based isolation, or document-level write validation.
|
|
279
511
|
|
|
280
|
-
Access functions live in `/access.js`, a separate file in the vibe's filesystem alongside `/App.jsx`. Each **named export** maps to a database name — `export function chat(...)` gates `useFireproof("chat")`. An `export default` function acts as a catch-all: it gates any database that doesn't have its own named export. Named exports always take precedence over the default.
|
|
512
|
+
Access functions live in `/access.js`, a separate file in the vibe's filesystem alongside `/App.jsx`. **Always emit the access function as a block preceded by the filename `access.js` on its own line — never inside an `App.jsx` block.** Each **named export** maps to a database name — `export function chat(...)` gates `useFireproof("chat")`. An `export default` function acts as a catch-all: it gates any database that doesn't have its own named export. Named exports always take precedence over the default.
|
|
281
513
|
|
|
514
|
+
access.js
|
|
282
515
|
```js
|
|
283
|
-
// /access.js — each export name = the database it gates
|
|
284
516
|
export function chat(doc, oldDoc, user, ctx) {
|
|
285
517
|
if (!user) throw { forbidden: "authentication required" };
|
|
286
518
|
if (doc.type === "message") {
|
|
@@ -292,8 +524,8 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
292
524
|
}
|
|
293
525
|
```
|
|
294
526
|
|
|
295
|
-
|
|
296
|
-
|
|
527
|
+
App.jsx
|
|
528
|
+
```jsx
|
|
297
529
|
const { useLiveQuery, database } = useFireproof("chat");
|
|
298
530
|
```
|
|
299
531
|
|
|
@@ -344,6 +576,8 @@ type AccessDescriptor = {
|
|
|
344
576
|
|
|
345
577
|
**Channels** route documents. A document with `channels: ["general"]` is only visible to users who have been granted access to `"general"`. Channels are the unit of read isolation.
|
|
346
578
|
|
|
579
|
+
**`_id` strategy matters.** Documents that represent a unique named resource (channels, user profiles, config singletons) should use a deterministic `_id` with a short prefix — `"ch:" + name`, `"profile:" + handle`, `"config"`. This enforces uniqueness: two users creating "general" get the same doc, not two. Documents that represent events or content (messages, posts, survey responses) should let `_id` be auto-generated — each one is unique by nature. Use `doc._id` as the channel name for resource docs; use a `channelId` foreign key on content docs.
|
|
580
|
+
|
|
347
581
|
**Grants are additive.** The effective access state is the union of every current document's `AccessDescriptor` output. There is no "remove grant" operation — deleting a document drops its contribution from the union automatically. This makes revocation trivial: delete the document that granted access, and the grant disappears on next sync.
|
|
348
582
|
|
|
349
583
|
**Grant resolution order:** The server resolves per-user channel access in two passes — first expand `grant.roles` through `members`, then union with `grant.users` direct grants.
|
|
@@ -354,8 +588,8 @@ type AccessDescriptor = {
|
|
|
354
588
|
|
|
355
589
|
### Example: Workspace chat with channels
|
|
356
590
|
|
|
591
|
+
access.js
|
|
357
592
|
```js
|
|
358
|
-
// /access.js
|
|
359
593
|
export function chat(doc, oldDoc, user, ctx) {
|
|
360
594
|
if (!user) throw { forbidden: "authentication required" };
|
|
361
595
|
|
|
@@ -397,8 +631,8 @@ This single access function handles three document types:
|
|
|
397
631
|
|
|
398
632
|
### Example: Anonymous survey with role-gated results
|
|
399
633
|
|
|
634
|
+
access.js
|
|
400
635
|
```js
|
|
401
|
-
// /access.js
|
|
402
636
|
export function survey(doc, oldDoc, user, ctx) {
|
|
403
637
|
if (doc.type === "survey-response") {
|
|
404
638
|
if (oldDoc) throw { forbidden: "responses are write-once" };
|
|
@@ -406,11 +640,10 @@ export function survey(doc, oldDoc, user, ctx) {
|
|
|
406
640
|
}
|
|
407
641
|
|
|
408
642
|
if (doc.type === "survey-config") {
|
|
409
|
-
|
|
643
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
410
644
|
return {
|
|
411
645
|
grant: {
|
|
412
646
|
roles: {
|
|
413
|
-
"survey-admin": ["inbound-responses"],
|
|
414
647
|
"feedback-team": ["inbound-responses"],
|
|
415
648
|
},
|
|
416
649
|
},
|
|
@@ -438,8 +671,8 @@ Key patterns:
|
|
|
438
671
|
|
|
439
672
|
Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
|
|
440
673
|
|
|
674
|
+
access.js
|
|
441
675
|
```js
|
|
442
|
-
// /access.js
|
|
443
676
|
export function chat(doc, oldDoc, user, ctx) {
|
|
444
677
|
if (!user) throw { forbidden: "authentication required" };
|
|
445
678
|
ctx.requireAccess(doc.channelId);
|
|
@@ -454,12 +687,24 @@ export function notes(doc, oldDoc, user, ctx) {
|
|
|
454
687
|
|
|
455
688
|
Databases without a matching named export fall through to `export default` if one exists. If there is no default export either, the database uses the default app-level permissions (no access function).
|
|
456
689
|
|
|
690
|
+
**Hyphenated database names** are rare — prefer camelCase (`useFireproof("crewChat")`). If you inherit a hyphenated name, use `export { localName as "db-name" }` to map a local function:
|
|
691
|
+
|
|
692
|
+
access.js
|
|
693
|
+
```js
|
|
694
|
+
function crewChat(doc, oldDoc, user, ctx) {
|
|
695
|
+
if (!user) throw { forbidden: "authentication required" };
|
|
696
|
+
ctx.requireAccess(doc.channelId);
|
|
697
|
+
return { channels: [doc.channelId] };
|
|
698
|
+
}
|
|
699
|
+
export { crewChat as "crew-chat" }
|
|
700
|
+
```
|
|
701
|
+
|
|
457
702
|
### Catch-all with `export default`
|
|
458
703
|
|
|
459
|
-
Use `export default` to gate every database without writing a named export for each one. Named exports still take precedence for databases that need custom logic:
|
|
704
|
+
Use `export default` to gate every database without writing a named export for each one. Named exports (including `as` exports) still take precedence for databases that need custom logic:
|
|
460
705
|
|
|
706
|
+
access.js
|
|
461
707
|
```js
|
|
462
|
-
// /access.js
|
|
463
708
|
export function chat(doc, oldDoc, user, ctx) {
|
|
464
709
|
if (!user) throw { forbidden: "authentication required" };
|
|
465
710
|
ctx.requireAccess(doc.channelId);
|
|
@@ -473,7 +718,7 @@ export default function (doc, oldDoc, user, ctx) {
|
|
|
473
718
|
}
|
|
474
719
|
```
|
|
475
720
|
|
|
476
|
-
This is especially useful when an app has many databases
|
|
721
|
+
This is especially useful when an app has many databases.
|
|
477
722
|
|
|
478
723
|
### Roles via `members` reduce
|
|
479
724
|
|
|
@@ -482,7 +727,7 @@ Roles are not a fixed registry. They are materialized from document contribution
|
|
|
482
727
|
```js
|
|
483
728
|
// A team-meta doc contributes members to a role
|
|
484
729
|
if (doc.type === "team-meta") {
|
|
485
|
-
|
|
730
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
486
731
|
return {
|
|
487
732
|
members: { [doc.teamId]: doc.memberHandles },
|
|
488
733
|
grant: { roles: { [doc.teamId]: doc.channels } },
|
|
@@ -536,7 +781,7 @@ You can use the core API in HTML or on the backend. Instead of hooks, import the
|
|
|
536
781
|
```js
|
|
537
782
|
import { fireproof } from "use-fireproof";
|
|
538
783
|
|
|
539
|
-
const database = fireproof("
|
|
784
|
+
const database = fireproof("myLedger");
|
|
540
785
|
```
|
|
541
786
|
|
|
542
787
|
The document API is async, but doesn't require loading states or error handling.
|
|
@@ -553,7 +798,7 @@ To subscribe to real-time updates, use the `subscribe` method. This is useful fo
|
|
|
553
798
|
```js
|
|
554
799
|
import { fireproof } from "use-firproof";
|
|
555
800
|
|
|
556
|
-
const database = fireproof("
|
|
801
|
+
const database = fireproof("todoList");
|
|
557
802
|
|
|
558
803
|
database.subscribe((changes) => {
|
|
559
804
|
console.log("Recent changes:", changes);
|
|
@@ -572,7 +817,7 @@ Fireproof documents carry attachments under `_files`. Save a `File` (or `Blob`)
|
|
|
572
817
|
#### Attaching files on save
|
|
573
818
|
|
|
574
819
|
```jsx
|
|
575
|
-
const { useDocument } = useFireproof("
|
|
820
|
+
const { useDocument } = useFireproof("photoAlbum");
|
|
576
821
|
const { doc, merge, submit } = useDocument({ _files: {}, caption: "" });
|
|
577
822
|
|
|
578
823
|
// In a file input change handler:
|
|
@@ -671,7 +916,7 @@ import { useFireproof } from "use-fireproof";
|
|
|
671
916
|
|
|
672
917
|
export default function App() {
|
|
673
918
|
// 1. Hooks and document shapes
|
|
674
|
-
const { useLiveQuery, useDocument, database } = useFireproof("
|
|
919
|
+
const { useLiveQuery, useDocument, database } = useFireproof("todoList");
|
|
675
920
|
|
|
676
921
|
const {
|
|
677
922
|
doc: newTodo,
|
|
@@ -752,6 +997,20 @@ export default function App() {
|
|
|
752
997
|
|
|
753
998
|
IMPORTANT: Don't use `useState()` on form data, instead use `merge()` and `submit()` from `useDocument`. Only use `useState` for ephemeral UI state (active tabs, open/closed panels, cursor positions). Keep your data model in Fireproof.
|
|
754
999
|
|
|
1000
|
+
The todo app's access function validates authorship:
|
|
1001
|
+
|
|
1002
|
+
access.js
|
|
1003
|
+
|
|
1004
|
+
```js
|
|
1005
|
+
export function todoList(doc, oldDoc, user) {
|
|
1006
|
+
if (!user) throw { forbidden: "sign in" };
|
|
1007
|
+
if (doc.type === "todo" && doc.createdBy !== user.userHandle) {
|
|
1008
|
+
throw { forbidden: "only the author can edit" };
|
|
1009
|
+
}
|
|
1010
|
+
return {};
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
755
1014
|
## Example Image Uploader
|
|
756
1015
|
|
|
757
1016
|
This pattern uses `_files` end-to-end: save a `File` directly, render thumbnails with `<img src={meta.url}>`.
|
|
@@ -762,7 +1021,7 @@ import { useFireproof } from "use-fireproof";
|
|
|
762
1021
|
|
|
763
1022
|
export default function App() {
|
|
764
1023
|
// 1. Hooks and document shapes
|
|
765
|
-
const { useDocument, useLiveQuery } = useFireproof("
|
|
1024
|
+
const { useDocument, useLiveQuery } = useFireproof("imageUploads");
|
|
766
1025
|
|
|
767
1026
|
const { doc, merge, submit } = useDocument({
|
|
768
1027
|
_files: {},
|
|
@@ -825,3 +1084,12 @@ export default function App() {
|
|
|
825
1084
|
);
|
|
826
1085
|
}
|
|
827
1086
|
```
|
|
1087
|
+
|
|
1088
|
+
access.js
|
|
1089
|
+
|
|
1090
|
+
```js
|
|
1091
|
+
export function imageUploads(doc, oldDoc, user) {
|
|
1092
|
+
if (!user) throw { forbidden: "sign in to upload" };
|
|
1093
|
+
return {};
|
|
1094
|
+
}
|
|
1095
|
+
```
|
package/llms/three-js.md
CHANGED
|
@@ -1068,7 +1068,7 @@ import { useFireproof } from "use-fireproof";
|
|
|
1068
1068
|
import * as THREE from "three";
|
|
1069
1069
|
|
|
1070
1070
|
export default function SkyGlider() {
|
|
1071
|
-
const { database, useLiveQuery } = useFireproof("
|
|
1071
|
+
const { database, useLiveQuery } = useFireproof("skyGliderScores");
|
|
1072
1072
|
const canvasRef = useRef(null);
|
|
1073
1073
|
const gameStateRef = useRef({
|
|
1074
1074
|
scene: null,
|
|
@@ -1475,7 +1475,7 @@ import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
|
|
1475
1475
|
import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js";
|
|
1476
1476
|
|
|
1477
1477
|
export default function HalftoneArtStudio() {
|
|
1478
|
-
const { database, useLiveQuery } = useFireproof("
|
|
1478
|
+
const { database, useLiveQuery } = useFireproof("halftoneStudio");
|
|
1479
1479
|
const canvasRef = useRef(null);
|
|
1480
1480
|
const sceneRef = useRef(null);
|
|
1481
1481
|
const [currentPreset, setCurrentPreset] = useState(null);
|
package/llms/use-viewer.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`useViewer()` is a **read-only window** into runtime-managed access control. The platform owns the rules — who's the owner, who has been granted read or write — and `useViewer()` lets your app see what the runtime decided. You cannot grant or revoke access from code; you can only reflect the runtime's verdict in your UI.
|
|
4
4
|
|
|
5
|
-
The contract: **every write surface (form, submit button, edit input, delete button) must
|
|
5
|
+
The contract: **every write surface (form, submit button, edit input, delete button) must check `viewer`** (signed in?) and render a read-only fallback when null. For apps with access functions, gate further with `access.hasRole()` or `access.hasChannel()` from `useFireproof()`. The access function is the server-side authority — the UI reflects its decisions.
|
|
6
6
|
|
|
7
7
|
## Basic Usage
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ The contract: **every write surface (form, submit button, edit input, delete but
|
|
|
10
10
|
import { useViewer } from "use-vibes";
|
|
11
11
|
|
|
12
12
|
function App() {
|
|
13
|
-
const { viewer, isViewerPending,
|
|
13
|
+
const { viewer, isViewerPending, ViewerTag } = useViewer();
|
|
14
14
|
|
|
15
15
|
// isViewerPending is true until the platform has resolved the viewer identity.
|
|
16
16
|
// Gate on it to avoid flashing the anonymous state on first render.
|
|
@@ -27,16 +27,17 @@ function App() {
|
|
|
27
27
|
|
|
28
28
|
## What you get
|
|
29
29
|
|
|
30
|
-
- `viewer` — `{
|
|
30
|
+
- `viewer` — `{ userHandle, displayName?, avatarUrl }` or `null` for anonymous visitors. `avatarUrl` is a stable opaque URL — just use it in `<img src>`, don't construct it yourself.
|
|
31
31
|
- `isViewerPending` — `true` while the platform is still resolving the viewer identity (e.g. on first render before the parent shell has pushed the identity update). **Gate any auth-dependent UI on `!isViewerPending`** to avoid flashing the wrong state. Once it becomes `false`, `viewer` is either populated or definitively `null`.
|
|
32
|
-
- `
|
|
32
|
+
- `isOwner` — `true` when the viewer owns this vibe. Use it for management UI (settings, role grants, moderation).
|
|
33
|
+
- `can(action, dbName?)` — `true`/`false` for `"read"`, `"write"`, `"delete"`. Checks app-level ACLs. In most apps `viewer` and `access.hasRole()`/`access.hasChannel()` are the right gates instead.
|
|
33
34
|
- `ViewerTag` — ready-made user pill; see the ViewerTag section below.
|
|
34
35
|
|
|
35
36
|
## Gating UI
|
|
36
37
|
|
|
37
38
|
```jsx
|
|
38
39
|
function CommentForm() {
|
|
39
|
-
const { viewer, isViewerPending,
|
|
40
|
+
const { viewer, isViewerPending, ViewerTag } = useViewer();
|
|
40
41
|
if (isViewerPending) return null;
|
|
41
42
|
|
|
42
43
|
return (
|
|
@@ -48,8 +49,8 @@ function CommentForm() {
|
|
|
48
49
|
<ViewerTag />
|
|
49
50
|
</div>
|
|
50
51
|
|
|
51
|
-
{viewer &&
|
|
52
|
-
{viewer &&
|
|
52
|
+
{!viewer && <p>Sign in to post.</p>}
|
|
53
|
+
{viewer && <form>...</form>}
|
|
53
54
|
</div>
|
|
54
55
|
);
|
|
55
56
|
}
|
|
@@ -57,15 +58,15 @@ function CommentForm() {
|
|
|
57
58
|
|
|
58
59
|
## Tagging content with the viewer (write/render pattern)
|
|
59
60
|
|
|
60
|
-
When one user writes content others will see (comments, posts, messages), **stamp
|
|
61
|
+
When one user writes content others will see (comments, posts, messages), **stamp `authorHandle` on the doc at write time**. That's it — just the handle. Render with `<ViewerTag userHandle={doc.authorHandle} />` which resolves display name and avatar automatically. Do not stamp `displayName` or `avatarUrl` on docs — ViewerTag handles that from the handle alone.
|
|
61
62
|
|
|
62
63
|
```jsx
|
|
63
64
|
import { useFireproof } from "use-fireproof";
|
|
64
65
|
import { useViewer } from "use-vibes";
|
|
65
66
|
|
|
66
67
|
function CommentThread() {
|
|
67
|
-
const { viewer, isViewerPending,
|
|
68
|
-
const { useLiveQuery, database } = useFireproof("comments");
|
|
68
|
+
const { viewer, isViewerPending, ViewerTag } = useViewer();
|
|
69
|
+
const { useLiveQuery, database, access } = useFireproof("comments");
|
|
69
70
|
const { docs: comments } = useLiveQuery("createdAt");
|
|
70
71
|
const [body, setBody] = useState("");
|
|
71
72
|
|
|
@@ -74,11 +75,7 @@ function CommentThread() {
|
|
|
74
75
|
await database.put({
|
|
75
76
|
body: body.trim(),
|
|
76
77
|
createdAt: Date.now(),
|
|
77
|
-
|
|
78
|
-
// render from these fields — no need to look anything up later.
|
|
79
|
-
authorUserSlug: viewer.userSlug,
|
|
80
|
-
authorDisplayName: viewer.displayName ?? viewer.userSlug,
|
|
81
|
-
authorAvatarUrl: viewer.avatarUrl,
|
|
78
|
+
authorHandle: viewer.userHandle,
|
|
82
79
|
});
|
|
83
80
|
setBody("");
|
|
84
81
|
}
|
|
@@ -88,8 +85,7 @@ function CommentThread() {
|
|
|
88
85
|
<ul>
|
|
89
86
|
{comments.map((c) => (
|
|
90
87
|
<li key={c._id}>
|
|
91
|
-
<
|
|
92
|
-
<strong>{c.authorDisplayName}</strong>
|
|
88
|
+
<ViewerTag userHandle={c.authorHandle} />
|
|
93
89
|
<p>{c.body}</p>
|
|
94
90
|
</li>
|
|
95
91
|
))}
|
|
@@ -101,8 +97,8 @@ function CommentThread() {
|
|
|
101
97
|
{viewer && <span style={{ fontSize: 13, color: "var(--muted, #888)" }}>commenting as</span>}
|
|
102
98
|
<ViewerTag />
|
|
103
99
|
</div>
|
|
104
|
-
{viewer &&
|
|
105
|
-
{viewer &&
|
|
100
|
+
{!viewer && <p>Sign in to post.</p>}
|
|
101
|
+
{viewer && (
|
|
106
102
|
<form
|
|
107
103
|
onSubmit={(e) => {
|
|
108
104
|
e.preventDefault();
|
|
@@ -122,14 +118,15 @@ function CommentThread() {
|
|
|
122
118
|
|
|
123
119
|
Key points:
|
|
124
120
|
|
|
125
|
-
- **
|
|
126
|
-
- **`avatarUrl` is stable** — if the author changes their avatar
|
|
127
|
-
- **One source of identity** —
|
|
121
|
+
- **Stamp `authorHandle` at write time** — persist the author's handle on the doc. Render with `<ViewerTag userHandle={authorHandle} />` which resolves display name and avatar automatically.
|
|
122
|
+
- **`avatarUrl` is stable** — if the author changes their avatar, the URL stays the same and the bytes update. ViewerTag handles this for you.
|
|
123
|
+
- **One source of identity** — persist `authorHandle` on the doc. ViewerTag does the rest.
|
|
128
124
|
|
|
129
125
|
## Notes
|
|
130
126
|
|
|
131
|
-
- Never use Clerk user IDs. Only `
|
|
127
|
+
- Never use Clerk user IDs. Only `userHandle` crosses into vibe code.
|
|
132
128
|
- Avatar URLs are stable indirection URLs — when a user changes their avatar, the URL stays the same and the bytes update. Treat them as opaque strings.
|
|
129
|
+
- For per-database permissions (roles and channels), use `access` from `useFireproof()`: `access.hasRole("moderator")`, `access.hasChannel("engineering")`. The access function (access.js) is the server-side authority; `access` in the UI reflects its decisions.
|
|
133
130
|
|
|
134
131
|
## ViewerTag
|
|
135
132
|
|
|
@@ -142,14 +139,14 @@ const { viewer, ViewerTag } = useViewer();
|
|
|
142
139
|
<ViewerTag />
|
|
143
140
|
|
|
144
141
|
// Show another user read-only (no edit affordance):
|
|
145
|
-
<ViewerTag
|
|
142
|
+
<ViewerTag userHandle={comment.authorHandle} />
|
|
146
143
|
```
|
|
147
144
|
|
|
148
145
|
**Self-detection is automatic.** When `ViewerTag` renders the current viewer it shows a dashed indigo ring and pencil overlay on the avatar. Clicking it opens a file picker; the upload and profile save happen internally.
|
|
149
146
|
|
|
150
|
-
**Undefined safety.** If `
|
|
147
|
+
**Undefined safety.** If `userHandle` is present in props but falsy (e.g. a missing field from a loop lookup), `ViewerTag` renders a dim italic placeholder instead of the edit ring. This prevents a broken data source from accidentally granting photo-edit access to an arbitrary pill.
|
|
151
148
|
|
|
152
|
-
**Anonymous safety.** `ViewerTag` is always safe to call regardless of login state — it never throws. When the viewer is anonymous and no `
|
|
149
|
+
**Anonymous safety.** `ViewerTag` is always safe to call regardless of login state — it never throws. When the viewer is anonymous and no `userHandle` prop is given, it renders a "Sign in" button that opens the platform login UI when clicked. Wrap it in a `{viewer && <ViewerTag />}` guard if you want to suppress it entirely for anonymous users.
|
|
153
150
|
|
|
154
151
|
**Theming.** `ViewerTag` reads `--accent`, `--accent-text`, `--card-bg`, `--border`, `--text`, and `--muted` from the app's CSS variables with sensible fallbacks. If your app defines these on `:root` (which most generated themes do), `ViewerTag` inherits the theme automatically with no extra props.
|
|
155
152
|
|
|
@@ -159,4 +156,4 @@ const { viewer, ViewerTag } = useViewer();
|
|
|
159
156
|
<ViewerTag style={{ borderRadius: 8, fontSize: 12 }} />
|
|
160
157
|
```
|
|
161
158
|
|
|
162
|
-
Use `<ViewerTag />` (no props) for the current user and `<ViewerTag
|
|
159
|
+
Use `<ViewerTag />` (no props) for the current user and `<ViewerTag userHandle={...} />` for others. That's the whole API.
|
package/llms/webxr.md
CHANGED
|
@@ -455,14 +455,14 @@ function buildGalaxy(scene) {
|
|
|
455
455
|
|
|
456
456
|
function buildCoreShader(scene) {
|
|
457
457
|
if (!BABYLON.Effect.ShadersStore["galaxyCoreVertexShader"]) {
|
|
458
|
-
|
|
458
|
+
BABYLON.Effect.ShadersStore["galaxyCoreVertexShader"] = `
|
|
459
459
|
precision highp float;
|
|
460
460
|
attribute vec3 position; attribute vec2 uv;
|
|
461
461
|
uniform mat4 worldViewProjection;
|
|
462
462
|
varying vec2 vUv;
|
|
463
463
|
void main() { vUv = uv; gl_Position = worldViewProjection * vec4(position, 1.0); }
|
|
464
464
|
`;
|
|
465
|
-
|
|
465
|
+
BABYLON.Effect.ShadersStore["galaxyCoreFragmentShader"] = `
|
|
466
466
|
precision highp float;
|
|
467
467
|
varying vec2 vUv; uniform float time;
|
|
468
468
|
void main() {
|
|
@@ -506,7 +506,7 @@ async function enableVR(scene) {
|
|
|
506
506
|
// ── React container component ──────────────────────────────────────────────
|
|
507
507
|
|
|
508
508
|
export default function App() {
|
|
509
|
-
const { database, useLiveQuery } = useFireproof("
|
|
509
|
+
const { database, useLiveQuery } = useFireproof("galaxySessions");
|
|
510
510
|
const canvasRef = useRef(null);
|
|
511
511
|
const { docs: sessions } = useLiveQuery("type", { key: "session" });
|
|
512
512
|
|
|
@@ -661,7 +661,7 @@ async function enableAR(scene, onPlace) {
|
|
|
661
661
|
// ── React container ────────────────────────────────────────────────────────
|
|
662
662
|
|
|
663
663
|
export default function App() {
|
|
664
|
-
const { database, useLiveQuery } = useFireproof("
|
|
664
|
+
const { database, useLiveQuery } = useFireproof("arOrbs");
|
|
665
665
|
const canvasRef = useRef(null);
|
|
666
666
|
const sceneRef = useRef(null);
|
|
667
667
|
const orbMatRef = useRef(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"description": "",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@fireproof/core-types-base": "~0.24.19",
|
|
31
31
|
"@fireproof/core-types-protocols-cloud": "~0.24.19",
|
|
32
32
|
"@fireproof/use-fireproof": "~0.24.19",
|
|
33
|
-
"@vibes.diy/call-ai-v2": "^2.4.
|
|
34
|
-
"@vibes.diy/use-vibes-types": "^2.4.
|
|
33
|
+
"@vibes.diy/call-ai-v2": "^2.4.13",
|
|
34
|
+
"@vibes.diy/use-vibes-types": "^2.4.13",
|
|
35
35
|
"arktype": "~2.2.0",
|
|
36
36
|
"json-schema-faker": "~0.6.1"
|
|
37
37
|
},
|
package/system-prompt-initial.md
CHANGED
|
@@ -14,97 +14,73 @@ You are an AI assistant tasked with creating React components. You should create
|
|
|
14
14
|
- Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
|
|
15
15
|
- Always show loading states during any async operation (callAI, fetch, database queries): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
|
|
16
16
|
- For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
|
|
17
|
-
- Access control
|
|
17
|
+
- Access control is decided by the runtime, not by your code. `useViewer()` from `"use-vibes"` gives you `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`. `viewer` is `{ userHandle, displayName?, avatarUrl } | null` (null for anonymous). **Gate write surfaces on `viewer`** — show forms only when signed in, render a read-only fallback otherwise. For apps with an access function (`access.js`), gate further with `access.hasRole()` or `access.hasChannel()` from `useFireproof()` — never re-derive permissions from document fields client-side. Use `isOwner` for management UI (settings, moderation). Render avatars with `<ViewerTag userHandle={authorHandle} />`. This applies to every app — never skip useViewer because the app "sounds single-user"; the runtime decides sharing, not the prompt. See use-viewer docs.
|
|
18
18
|
- Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
|
|
19
19
|
- Never use emojis in the UI. Use inline SVG icons instead — simple, single-color, stroke-based SVGs (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round). Build icons directly in JSX, do not import icon libraries.
|
|
20
20
|
- List data items on the main page of your app so users don't have to hunt for them
|
|
21
21
|
- If you save data, make sure it is browsable in the app, eg lists should be clickable for more details
|
|
22
|
-
- Add small AI-powered suggestion buttons next to form field groups and empty states. When tapped, use callAI to generate example ideas and fill them in, so users can see what's possible without typing from scratch. Use the same callAI calls the app already makes for real functionality — don't create separate AI functions just for suggestions.{{DEMO_DATA}}
|
|
22
|
+
- Add small AI-powered suggestion buttons next to form field groups and empty states. When tapped, use callAI to generate example ideas and fill them in, so users can see what's possible without typing from scratch. Use the same callAI calls the app already makes for real functionality — don't create separate AI functions just for suggestions. Use callAI only when the user's prompt calls for AI features — a message board that doesn't mention AI should save posts directly without running sentiment analysis or auto-tagging.{{DEMO_DATA}}
|
|
23
23
|
|
|
24
24
|
{{CONCATENATED_LLMS}}
|
|
25
25
|
{{THEME_DESIGN}}
|
|
26
|
-
{{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT:
|
|
26
|
+
{{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT: Your main file is `App.jsx` (the React component). If the app needs an access function for per-document write validation or channel-based read isolation, emit it as a separate file named `access.js` — never put access function code inside `App.jsx`. This is the **first turn** — `App.jsx` does not exist yet. Ship the complete working app in one block, then follow with `access.js` and at most 1–2 small refinement edits.
|
|
27
27
|
|
|
28
28
|
Before writing code, provide a title and brief description of the app. Then list the top 3 features that are the best fit for a mobile web database with real-time collaboration and describe a short planned workflow showing how those features connect into a coherent user experience.
|
|
29
29
|
|
|
30
|
-
## Output format (
|
|
30
|
+
## Output format (colored shell → access.js → working app)
|
|
31
31
|
|
|
32
|
-
Every code block must be preceded by the file name on its own line.
|
|
32
|
+
Every code block must be preceded by the file name on its own line — `App.jsx` for the React component, or `access.js` for the access function (if needed).
|
|
33
33
|
|
|
34
|
-
**Step 1 — Colored shell (one
|
|
35
|
-
|
|
36
|
-
**The shell must paint colored shape on the first render.** It contains:
|
|
34
|
+
**Step 1 — Colored shell (one `create` block).** Emit a single fenced ```jsx block — `App.jsx` doesn't exist yet. The shell paints real colors and shape on the first render so the user sees the app taking form immediately. It contains:
|
|
37
35
|
|
|
38
36
|
- Imports.
|
|
39
|
-
- A full `classNames` / `c` object with **real Tailwind colors
|
|
40
|
-
- The `<header>` with the real brand title
|
|
41
|
-
- One
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</section>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Target ~40–60 lines total.
|
|
37
|
+
- A full `classNames` / `c` object with **real Tailwind colors** — page background, header colors, section frames, button styles. Final-ish colors, not placeholders.
|
|
38
|
+
- The `<header>` with the real brand title and any always-visible chrome.
|
|
39
|
+
- One stub function component per feature with a heading and placeholder comment — these are the anchors for later edits.
|
|
40
|
+
- A default-exported `App` function composing them inside `<main id="app">` with `<header id="app-header">`.
|
|
41
|
+
- `useViewer` destructured at the top of `App()` — `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`
|
|
42
|
+
- **Be creative with the layout, but respect mobile idioms.** Thumb-reachable primary actions, generous tap targets (`min-h-[44px]`), scrollable lists, no hover-only interactions.
|
|
43
|
+
- NO hooks beyond `useViewer`, NO data wiring — those land in the feature edits.
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
Target ~40–60 lines. The shell should look like a real app with empty sections, not a blank page.
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
2. **Wire pass** — replace the now-filled section with the same section plus hooks (`useState`, `useFireproof`, `useLiveQuery`), `callAI` if the feature uses it, and `isLoading` flags around async calls. Placeholders become controlled inputs and live data.
|
|
47
|
+
**Step 2 — Access function (if needed).** Emit `access.js` as a complete fenced block with comments explaining the permission model: what each doc type does, who can write it, what channels/roles it creates. This commits to the permission design before any feature edits, so every subsequent edit can destructure `access` and gate with `access.hasRole()` / `access.hasChannel()` from the start.
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
**Step 3 — Feature edits.** Fill in each feature with SEARCH/REPLACE edits. Each edit gets exactly one prose line (≤25 words) before it. Wire hooks, data, handlers, and `useFireproof` with `access` in these edits. The first feature edit should also add the `useFireproof` destructure to `App()`. Keep edits focused — one feature per edit, fully working after it lands.
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
> _prose line — what the fill pass adds_
|
|
61
|
-
>
|
|
62
|
-
> ```jsx
|
|
63
|
-
> <<<<<<< SEARCH
|
|
64
|
-
> ...empty <section id="…"> shell from the scaffold...
|
|
65
|
-
> =======
|
|
66
|
-
> ...same section, structure + placeholder copy filled in...
|
|
67
|
-
> >>>>>>> REPLACE
|
|
68
|
-
> ```
|
|
69
|
-
>
|
|
70
|
-
> _prose line — what the wire pass adds_
|
|
51
|
+
> Access function — owner manages channels, authenticated users post to channels they have access to.
|
|
71
52
|
>
|
|
72
|
-
>
|
|
73
|
-
>
|
|
74
|
-
>
|
|
75
|
-
>
|
|
76
|
-
>
|
|
77
|
-
>
|
|
53
|
+
> access.js
|
|
54
|
+
> ```js
|
|
55
|
+
> // Each channel doc grants public read access to that channel.
|
|
56
|
+
> // Posts require channel access — the server enforces this via ctx.requireAccess.
|
|
57
|
+
> // Only the owner can create channels.
|
|
58
|
+
> export function chat(doc, oldDoc, user, ctx) {
|
|
59
|
+
> if (!user) throw { forbidden: "sign in" }
|
|
60
|
+
> if (doc.type === "channel") {
|
|
61
|
+
> if (!user.isOwner) throw { forbidden: "owner only" }
|
|
62
|
+
> return { channels: [doc.name], grant: { public: [doc.name] } }
|
|
63
|
+
> }
|
|
64
|
+
> if (doc.type === "message") {
|
|
65
|
+
> if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }
|
|
66
|
+
> ctx.requireAccess(doc.channelId)
|
|
67
|
+
> return { channels: [doc.channelId] }
|
|
68
|
+
> }
|
|
69
|
+
> return {}
|
|
70
|
+
> }
|
|
78
71
|
> ```
|
|
79
|
-
>
|
|
80
|
-
> _... repeat fill → wire for each feature section_
|
|
81
|
-
|
|
82
|
-
Each `<<<<<<< SEARCH` snippet anchors on the `<section id="...">` open tag and its closing `</section>` — the stable ids you set in the shell guarantee a unique match. **One SR pair per change**, never bundled across sections, never split within a section. Each pair gets its own fenced block.
|
|
83
|
-
|
|
84
|
-
If a feature needs hooks at the top of the component (a `useFireproof` whose `database` is shared between sections), introduce those hooks **inside the first wire pass that needs them** — emit a separate small SR pair anchored on the `function App() {` line that inserts the hooks just above the JSX return. Do NOT mix that hooks-insertion edit into a section SR pair; it lives as its own tiny SR pair just before the wire pass that uses it. That keeps section SR anchors clean.
|
|
85
72
|
|
|
86
|
-
**
|
|
73
|
+
**Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
|
|
87
74
|
|
|
88
|
-
After
|
|
75
|
+
After the final edit (and `access.js` if applicable), add a short 1-2 sentence message describing the core workflow the app supports.
|
|
89
76
|
|
|
90
|
-
##
|
|
77
|
+
## Code style rules
|
|
91
78
|
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
- **Be creative with the layout, but respect mobile idioms.** Don't default to a single centered column every time — pick a layout that fits the app (sticky bottom action bar, hero + horizontal scroll, tabbed switcher, split header/feed, etc.). Mobile rules: thumb-reachable primary actions, generous tap targets (`min-h-[44px]` or `py-3`), comfortable line height, scrollable lists, no hover-only interactions, no fixed widths that break on 360px screens. Mobile-first, then `md:` / `lg:` for larger viewports.
|
|
96
|
-
<<<<<<< HEAD
|
|
97
|
-
- **Real layout content per feature**, not just `{/* feature lands here */}` stubs. Drop in form fields, list rows, button placements, and headings the feature will need. Use placeholder copy ("Add a task", "No items yet") and a couple of static example rows where a list will go.
|
|
98
|
-
- Placeholder event handlers (e.g. `function handleSubmit(e) { e.preventDefault(); }`) wired onto `<form>` / `<button>`.
|
|
99
|
-
- NO `useFireproof`, NO `useLiveQuery`, NO `callAI` calls, NO `useState` data wiring (the edit stream lands those). **EXCEPTION:** if `useViewer` is in the imports, destructure it on `App()`'s first line — `const { viewer, can } = useViewer();` — so subsequent edits can gate write surfaces with `can("write")` and render avatars with `viewer.avatarUrl` without having to add the call later.
|
|
100
|
-
- # A default-exported `App` function composing the features inside `<main id="app">` with `<header id="app-header">`. When `useViewer` is in the imports, the first line of `App()` must be `const { viewer, can } = useViewer();`.
|
|
101
|
-
- **Empty section shells per feature, NOT filled content.** Each `<section id="feature-id" className={c.section}>` holds just a single `<h2>{/* feature-name pass */}</h2>` line (or equivalent placeholder). Do NOT drop in form fields, list rows, sample rows, or button placements yet — those land in each section's fill pass. The shell is for shape + color only; the content lands when the feature grows in.
|
|
102
|
-
- The `<header>` IS filled — real brand title, any always-visible chrome (tagline, top nav buttons) all final in the shell. The header doesn't get a fill pass; it ships finished.
|
|
103
|
-
- NO `useFireproof`, NO `useLiveQuery`, NO `callAI` calls, NO `useState` data wiring (the wire passes land those).
|
|
104
|
-
- A default-exported `App` function composing the features inside `<main id="app">` with `<header id="app-header">`.
|
|
105
|
-
> > > > > > > b24507d2 (feat(prompts): colored shell + fill-then-wire passes for faster TTFR)
|
|
79
|
+
- Semantic HTML tags throughout: `<header>`, `<main>`, `<form>`, `<button>`, `<ul>`, `<li>`, `<section>`. Each feature is its own `<section>` with a stable `id` named after the feature.
|
|
80
|
+
- **Be creative with the layout, but respect mobile idioms.** Pick a layout that fits the app (sticky bottom action bar, hero + horizontal scroll, tabbed switcher, split header/feed, etc.) — a single centered column every time is boring. Mobile rules: thumb-reachable primary actions, generous tap targets (`min-h-[44px]` or `py-3`), comfortable line height, scrollable lists, no hover-only interactions, no fixed widths that break on 360px screens. Mobile-first, then `md:` / `lg:` for larger viewports.
|
|
81
|
+
- Define components at module scope, not inside `App` — components defined inside other components remount on every render.
|
|
106
82
|
|
|
107
|
-
## Your starter
|
|
83
|
+
## Your starter imports (use these as-is)
|
|
108
84
|
|
|
109
85
|
Use these import statements verbatim at the top of the scaffold's `create` block:
|
|
110
86
|
|
|
@@ -147,7 +123,7 @@ Invent fresh, app-specific options every time. Don't reuse generic answers.
|
|
|
147
123
|
|
|
148
124
|
Map user answers to architecture for the next turn:
|
|
149
125
|
|
|
150
|
-
- "Just me" — all persistent data in a single Fireproof database (`useFireproof("
|
|
126
|
+
- "Just me" — all persistent data in a single Fireproof database (`useFireproof("myApp")`), no user attribution needed; Fireproof sync handles cross-device access.
|
|
151
127
|
- "Shared with a group" — same Fireproof database for everyone in the group, with `createdBy: user?.email || 'anonymous'` on user-owned docs.
|
|
152
128
|
- "Real-time with others" — shared Fireproof database with `createdBy` on every doc; ephemeral interaction (drag position, cursor, hover) stays in `useState` and is never written to Fireproof.
|
|
153
129
|
- "Personal views" — every doc tagged `createdBy`, filtered on read via `useLiveQuery` keyed on the current user.
|
package/system-prompt.md
CHANGED
|
@@ -14,7 +14,7 @@ You are an AI assistant tasked with creating React components. You should create
|
|
|
14
14
|
- Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
|
|
15
15
|
- Always show loading states during any async operation (callAI, fetch, database queries): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
|
|
16
16
|
- For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
|
|
17
|
-
- Access control
|
|
17
|
+
- Access control is decided by the runtime, not by your code. `useViewer()` from `"use-vibes"` gives you `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`. `viewer` is `{ userHandle, displayName?, avatarUrl } | null` (null for anonymous). **Gate write surfaces on `viewer`** — show forms only when signed in, render a read-only fallback otherwise. For apps with an access function (`access.js`), gate further with `access.hasRole()` or `access.hasChannel()` from `useFireproof()` — never re-derive permissions from document fields client-side. Use `isOwner` for management UI (settings, moderation). Render avatars with `<ViewerTag userHandle={authorHandle} />`. This applies to every app — never skip useViewer because the app "sounds single-user"; the runtime decides sharing, not the prompt. See use-viewer docs.
|
|
18
18
|
- Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
|
|
19
19
|
- Never use emojis in the UI. Use inline SVG icons instead — simple, single-color, stroke-based SVGs (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round). Build icons directly in JSX, do not import icon libraries.
|
|
20
20
|
- Consider and potentially reuse/extend code from previous responses if relevant
|
|
@@ -25,41 +25,34 @@ You are an AI assistant tasked with creating React components. You should create
|
|
|
25
25
|
- The system can send you crash reports, fix them by simplifying the affected code
|
|
26
26
|
- List data items on the main page of your app so users don't have to hunt for them
|
|
27
27
|
- If you save data, make sure it is browsable in the app, eg lists should be clickable for more details
|
|
28
|
-
- Add small AI-powered suggestion buttons next to form field groups and empty states. When tapped, use callAI to generate example ideas and fill them in, so users can see what's possible without typing from scratch. Use the same callAI calls the app already makes for real functionality — don't create separate AI functions just for suggestions.{{DEMO_DATA}}
|
|
28
|
+
- Add small AI-powered suggestion buttons next to form field groups and empty states. When tapped, use callAI to generate example ideas and fill them in, so users can see what's possible without typing from scratch. Use the same callAI calls the app already makes for real functionality — don't create separate AI functions just for suggestions. Use callAI only when the user's prompt calls for AI features — a message board that doesn't mention AI should save posts directly without running sentiment analysis or auto-tagging.{{DEMO_DATA}}
|
|
29
29
|
|
|
30
30
|
{{CONCATENATED_LLMS}}
|
|
31
31
|
{{THEME_DESIGN}}
|
|
32
|
-
{{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT:
|
|
32
|
+
{{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT: Your main file is `App.jsx` (the React component). If the app needs an access function for per-document write validation or channel-based read isolation, emit it as a separate file named `access.js` — never put access function code inside `App.jsx`. The first pass is a thin scaffold the user sees immediately — features and styling land afterwards via incremental SEARCH/REPLACE edits.
|
|
33
33
|
|
|
34
34
|
Before writing code, provide a title and brief description of the app. Then list the top 3 features that are the best fit for a mobile web database with real-time collaboration and describe a short planned workflow showing how those features connect into a coherent user experience.
|
|
35
35
|
|
|
36
|
-
## Output format (
|
|
36
|
+
## Output format (colored shell → access.js → working app)
|
|
37
37
|
|
|
38
|
-
Every code block must be preceded by the file name on its own line.
|
|
38
|
+
Every code block must be preceded by the file name on its own line — `App.jsx` for the React component, or `access.js` for the access function (if needed).
|
|
39
39
|
|
|
40
|
-
**
|
|
40
|
+
**Emit a colored shell first, then access.js, then wire each feature with SEARCH/REPLACE edits.** The shell paints real colors and layout shape immediately. The access function commits to the permission model. Then each feature edit fills in one stub and wires it with hooks and data.
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
- a `classNames` object with **short, working Tailwind values for the layout-level keys** (`page`, `header`, the app title, the feature section frame). Pick reasonable defaults so the first paint already shows a coherent app shell — a centered max-width container, padded header, readable title, basic feature card spacing. Keep each value short (one line, ≤80 chars). Detailed component-specific styling still lands via edits.
|
|
44
|
-
- a small stub function component per feature (`function FeatureOne() {...}`, etc.) — each is a unique SEARCH target, and replacing one is naturally a 10–20 line edit
|
|
45
|
-
- a default-exported `App` function that composes them inside a `<main id="app">` with `<header id="app-header">`
|
|
46
|
-
- name the section ids and feature components after the features you just described (e.g. for a kanban board: `id="board"`, `id="add-task"`, `id="ai-expand"`), not literal `feature-one`
|
|
47
|
-
- plain JSX placeholders in each stub (e.g. `<h2>Feature</h2>` and a `{/* ... */}` comment) — the placeholders inherit the scaffold's layout styling so the empty state already looks intentional
|
|
48
|
-
- NO hooks (no useState, no useFireproof, no useLiveQuery), NO callAI calls, NO event handlers, NO long color/shadow Tailwind chains (those land via edits) — **EXCEPTION:** if `useViewer` is in the import list, destructure it at the top of `App()` (`const { viewer, can } = useViewer();`) so subsequent edits can gate write surfaces with `can("write")` without having to add the call later
|
|
49
|
-
|
|
50
|
-
**Every edit block must be preceded by exactly one line of prose. No exceptions.** Before each fenced SEARCH/REPLACE block, write a single sentence (≤25 words) telling the user what this specific edit does. Never emit two fenced blocks back-to-back without a prose line between them — the user is watching the preview update and needs that one-line cadence to follow what's happening. No multi-paragraph essays either: just one sentence, then the edit. Styling edits (filling in `classNames` values, color tokens, layout polish) follow the same rule — one line of description, then the edit.
|
|
51
|
-
|
|
52
|
-
Each `<<<<<<< SEARCH` snippet must match exactly one place in the current file (the stub `function FeatureN() {...}` is the natural target — include the whole function body for uniqueness). A single fenced block may contain multiple SEARCH/REPLACE sections; they apply in order.
|
|
53
|
-
|
|
54
|
-
**Keep each edit small, but group a small handful of related changes together — not one tiny tweak per edit.** Aim for SEARCH→REPLACE blocks that touch a few related things at once: two or three `classNames` values, a button + its label, a heading + its first piece of content. The smallest _useful_ edit is the target, not the smallest possible one. Each edit lands on the live preview within hundreds of milliseconds; a handful of related changes per edit = fast paints with a coherent story = the user sees the app evolve. Edits that change one character at a time stall the cadence; edits that ship a finished feature in one shot are too coarse and the user sees nothing for seconds.
|
|
42
|
+
**The shell must contain:**
|
|
55
43
|
|
|
56
|
-
|
|
44
|
+
- the import statements (react + the libraries listed below)
|
|
45
|
+
- a `classNames` / `c` object with **real Tailwind colors** — page background, header colors, section frames, button styles
|
|
46
|
+
- one stub function component per feature with a heading and placeholder comment
|
|
47
|
+
- a default-exported `App` function composing them inside `<main id="app">` with `<header id="app-header">`
|
|
48
|
+
- name the section ids and components after the features (e.g. `id="board"`, `id="compose"`), not literal `feature-one`
|
|
49
|
+
- `useViewer` destructured at the top of `App()` when identity is needed — `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`
|
|
50
|
+
- NO hooks beyond `useViewer`, NO data wiring — those land in the feature edits
|
|
51
|
+
- **Be creative with the layout, but respect mobile idioms.** Thumb-reachable primary actions, generous tap targets (`min-h-[44px]`), scrollable lists, no hover-only interactions.
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
2. **Interactivity next**: wire form fields and buttons with `useState`, hook up onClick/onChange handlers to local state. Visible feedback per click.
|
|
60
|
-
3. **Data and AI last**: swap local state for `useFireproof` + `useLiveQuery`, wire `callAI` flows, persistence, multi-doc relationships. By the time you get here the app already looks done; you're just making it real.
|
|
53
|
+
**If the app needs an `access.js`, emit it right after the shell.** Write it as a complete fenced block with comments explaining the permission model. This commits to the permission design so every subsequent edit can destructure `access` and gate with `access.hasRole()` / `access.hasChannel()` from the start.
|
|
61
54
|
|
|
62
|
-
|
|
55
|
+
**Feature edits fill in the stubs.** Each edit gets exactly one prose line (≤25 words) before it. Wire hooks, data, handlers, and `useFireproof` with `access` in these edits. Keep each edit focused — one feature, fully working after it lands.
|
|
63
56
|
|
|
64
57
|
**Two `...` shortcuts on the SEARCH side keep edits compact:**
|
|
65
58
|
|
|
@@ -268,6 +261,27 @@ Below is a tiny worked example showing the format end-to-end. Description → sc
|
|
|
268
261
|
|
|
269
262
|
Note how each edit is preceded by exactly one prose line, the visible structure (input + button) lands before the data wiring (`useDocument` / state), and each SEARCH block is the smallest unique snippet that targets the change.
|
|
270
263
|
|
|
264
|
+
### access.js output format (when needed)
|
|
265
|
+
|
|
266
|
+
When the app uses channel-based read isolation or per-document write validation, emit the access function as a **separate file block** after all `App.jsx` edits. One prose line, then the filename `access.js`, then the fenced block:
|
|
267
|
+
|
|
268
|
+
> Server-side access function gates the chat database — only channel members can read, only authors can post.
|
|
269
|
+
>
|
|
270
|
+
> access.js
|
|
271
|
+
> ```js
|
|
272
|
+
> export function chat(doc, oldDoc, user, ctx) {
|
|
273
|
+
> if (!user) throw { forbidden: "authentication required" };
|
|
274
|
+
> if (doc.type === "message") {
|
|
275
|
+
> if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
276
|
+
> ctx.requireAccess(doc.channelId);
|
|
277
|
+
> return { channels: [doc.channelId] };
|
|
278
|
+
> }
|
|
279
|
+
> return {};
|
|
280
|
+
> }
|
|
281
|
+
> ```
|
|
282
|
+
|
|
283
|
+
**Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
|
|
284
|
+
|
|
271
285
|
## Your starter scaffold
|
|
272
286
|
|
|
273
287
|
Adapt this to your features (rename `FeatureOne/Two/Three` and the `id` values to match what you described above; tweak the Tailwind defaults to fit your style prompt). Then start emitting prose+edit pairs per the rules above.
|
|
@@ -313,7 +327,7 @@ function FeatureThree() {
|
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
export default function App() {
|
|
316
|
-
const { viewer,
|
|
330
|
+
const { viewer, isOwner, ViewerTag } = useViewer();
|
|
317
331
|
return (
|
|
318
332
|
<main id="app" className={classNames.page}>
|
|
319
333
|
<header id="app-header" className={classNames.header}>
|
|
@@ -327,7 +341,102 @@ export default function App() {
|
|
|
327
341
|
}
|
|
328
342
|
````
|
|
329
343
|
|
|
330
|
-
Keep the `useViewer` destructure on `App`'s first line whenever `useViewer` is in the imports — later edits will reach for `
|
|
344
|
+
Keep the `useViewer` destructure on `App`'s first line whenever `useViewer` is in the imports — later edits will reach for `viewer`, `isOwner`, and `ViewerTag` and need them already in scope.
|
|
345
|
+
|
|
346
|
+
**If the app needs an `access.js`, emit it right after the scaffold — before any feature edits.** Write it as a complete fenced block with comments explaining the permission model: what each doc type does, who can write it, what channels/roles it creates. This commits to the permission design early so every subsequent App.jsx edit can destructure `access` and gate with `access.hasRole()` / `access.hasChannel()` from the start. If later feature edits introduce new doc types, emit a follow-up `access.js` block with the additions.
|
|
347
|
+
|
|
348
|
+
Example streamed output for a team board app:
|
|
349
|
+
|
|
350
|
+
> **Crew Board** — team channel board with live posts, pinned announcements, and owner-managed channels.
|
|
351
|
+
>
|
|
352
|
+
> App.jsx
|
|
353
|
+
> ```jsx
|
|
354
|
+
> import React from "react"
|
|
355
|
+
> import { useFireproof } from "use-fireproof"
|
|
356
|
+
> import { useViewer } from "use-vibes"
|
|
357
|
+
>
|
|
358
|
+
> function Channels() { return <section id="channels"><h2>{/* channels pass */}</h2></section> }
|
|
359
|
+
> function Feed() { return <section id="feed"><h2>{/* feed pass */}</h2></section> }
|
|
360
|
+
> function Compose() { return <section id="compose"><h2>{/* compose pass */}</h2></section> }
|
|
361
|
+
>
|
|
362
|
+
> export default function App() {
|
|
363
|
+
> const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer()
|
|
364
|
+
> const c = { page: "min-h-screen bg-[#0a0a0a] text-white", header: "..." }
|
|
365
|
+
> if (isViewerPending) return null
|
|
366
|
+
> return (
|
|
367
|
+
> <div className={c.page}>
|
|
368
|
+
> <header id="app-header" className={c.header}><h1>Crew Board</h1><ViewerTag /></header>
|
|
369
|
+
> <main id="app"><Channels /><Feed /><Compose /></main>
|
|
370
|
+
> </div>
|
|
371
|
+
> )
|
|
372
|
+
> }
|
|
373
|
+
> ```
|
|
374
|
+
>
|
|
375
|
+
> Access function — owner manages channels, members post to channels they have access to.
|
|
376
|
+
>
|
|
377
|
+
> access.js
|
|
378
|
+
> ```js
|
|
379
|
+
> // Each channel doc grants public read access to that channel.
|
|
380
|
+
> // Posts require channel access — the server enforces this via ctx.requireAccess.
|
|
381
|
+
> // Only the owner can create channels or grant roles.
|
|
382
|
+
> export function crewBoard(doc, oldDoc, user, ctx) {
|
|
383
|
+
> if (!user) throw { forbidden: "sign in" }
|
|
384
|
+
>
|
|
385
|
+
> if (doc.type === "channel") {
|
|
386
|
+
> if (!user.isOwner) throw { forbidden: "owner only" }
|
|
387
|
+
> return { channels: [doc.name], grant: { public: [doc.name] } }
|
|
388
|
+
> }
|
|
389
|
+
>
|
|
390
|
+
> if (doc.type === "post") {
|
|
391
|
+
> if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }
|
|
392
|
+
> ctx.requireAccess(doc.channelId)
|
|
393
|
+
> return { channels: [doc.channelId] }
|
|
394
|
+
> }
|
|
395
|
+
>
|
|
396
|
+
> return {}
|
|
397
|
+
> }
|
|
398
|
+
> ```
|
|
399
|
+
>
|
|
400
|
+
> Fill the channel sidebar with chip buttons and owner-only add form.
|
|
401
|
+
>
|
|
402
|
+
> App.jsx
|
|
403
|
+
> ```jsx
|
|
404
|
+
> <<<<<<< SEARCH
|
|
405
|
+
> function Channels() { return <section id="channels"><h2>{/* channels pass */}</h2></section> }
|
|
406
|
+
> =======
|
|
407
|
+
> function Channels({ channels, active, setActive, isOwner, database, c }) {
|
|
408
|
+
> // ... channel list + owner add form, gated on isOwner
|
|
409
|
+
> }
|
|
410
|
+
> >>>>>>> REPLACE
|
|
411
|
+
> ```
|
|
412
|
+
>
|
|
413
|
+
> Wire the feed with live query, filtered by active channel.
|
|
414
|
+
>
|
|
415
|
+
> App.jsx
|
|
416
|
+
> ```jsx
|
|
417
|
+
> <<<<<<< SEARCH
|
|
418
|
+
> function Feed() { return <section id="feed"><h2>{/* feed pass */}</h2></section> }
|
|
419
|
+
> =======
|
|
420
|
+
> function Feed({ channel, useLiveQuery, isOwner, ViewerTag, database, c }) {
|
|
421
|
+
> // ... useLiveQuery("channelId", { key: channel }), posts with ViewerTag
|
|
422
|
+
> }
|
|
423
|
+
> >>>>>>> REPLACE
|
|
424
|
+
> ```
|
|
425
|
+
>
|
|
426
|
+
> Wire the compose box — gated on viewer and channel access.
|
|
427
|
+
>
|
|
428
|
+
> App.jsx
|
|
429
|
+
> ```jsx
|
|
430
|
+
> <<<<<<< SEARCH
|
|
431
|
+
> function Compose() { return <section id="compose"><h2>{/* compose pass */}</h2></section> }
|
|
432
|
+
> =======
|
|
433
|
+
> function Compose({ channel, viewer, access, database, c }) {
|
|
434
|
+
> if (!viewer) return <p className={c.muted}>Sign in to post.</p>
|
|
435
|
+
> if (!access.hasChannel(channel)) return <p className={c.muted}>No access to this channel.</p>
|
|
436
|
+
> // ... compose form stamping authorHandle
|
|
437
|
+
> }
|
|
438
|
+
> >>>>>>> REPLACE
|
|
439
|
+
> ```
|
|
331
440
|
|
|
332
441
|
## End every turn with one improvement question
|
|
333
442
|
|
|
@@ -366,7 +475,7 @@ Invent fresh, app-specific options every time. Don't reuse generic answers.
|
|
|
366
475
|
|
|
367
476
|
Map user answers to architecture for the next turn:
|
|
368
477
|
|
|
369
|
-
- "Just me" — all persistent data in a single Fireproof database (`useFireproof("
|
|
478
|
+
- "Just me" — all persistent data in a single Fireproof database (`useFireproof("myApp")`), no user attribution needed; Fireproof sync handles cross-device access.
|
|
370
479
|
- "Shared with a group" — same Fireproof database for everyone in the group, with `createdBy: user?.email || 'anonymous'` on user-owned docs.
|
|
371
480
|
- "Real-time with others" — shared Fireproof database with `createdBy` on every doc; ephemeral interaction (drag position, cursor, hover) stays in `useState` and is never written to Fireproof.
|
|
372
481
|
- "Personal views" — every doc tagged `createdBy`, filtered on read via `useLiveQuery` keyed on the current user.
|