@vibes.diy/prompts 14.3.79 → 14.3.81
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 +120 -85
- 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/calendar.js +19 -2
- package/llms/calendar.js.map +1 -1
- package/llms/calendar.md +119 -1
- 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 +44 -27
- 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 +30 -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 +15 -12
- package/system-prompt-initial.md +14 -11
- package/system-prompt.md +36 -54
- 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/access.md
CHANGED
|
@@ -34,7 +34,7 @@ writes.
|
|
|
34
34
|
|
|
35
35
|
Express every refusal about **who may act** through the supplied validators — `ctx.requireAccess`, `ctx.requireRole`, `ctx.requireBackend` — so that admin mode passes them and the function still runs, applying your own channels and grants to the repaired document; a refusal about the document's own **shape**, like an unknown type or a missing field, stays a plain `throw` and is right to, while a write-once check sits in between and is worth naming in a comment, since a schema repair has to get past it.
|
|
36
36
|
|
|
37
|
-
A component-owned image database (`media:<hostDbName>`) uses platform-authored rules by default: each generated image is readable by whoever can read the record it was generated for, and by the person who generated it. An explicit string-named export such as `export { posterMedia as "media:posters" }` overrides that one media database. Neither `export default` nor a function merely named `media` overrides it.
|
|
37
|
+
A component-owned image database (`media:<hostDbName>`) uses platform-authored rules by default: each generated image is readable by whoever can read the record it was generated for, and by the person who generated it. An explicit string-named export such as `export { posterMedia as "media:posters" }` overrides that one media database. Neither `export default` nor a function merely named `media` overrides it. Your function governs your app's own records, and every picture `<ImgGen>` makes is saved in that media database as its creator's own record, so pictures need no branch of their own in yours.
|
|
38
38
|
|
|
39
39
|
### AccessDescriptor return type
|
|
40
40
|
|
|
@@ -79,18 +79,17 @@ App.jsx — the checkbox is the consent call; the picker is a doc write:
|
|
|
79
79
|
const { can } = useVibe("logs"); // the write gate is always the server's own verdict
|
|
80
80
|
|
|
81
81
|
// checking is the consent preflight; unchecking is the off switch. Never one verb for both edges.
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
|
|
82
|
+
<Row gap={2} align="center">
|
|
83
|
+
<Checkbox
|
|
84
|
+
label="Shared with followers"
|
|
85
85
|
checked={followersEnabled}
|
|
86
|
-
onChange={(
|
|
86
|
+
onChange={(checked) => (checked === true ? requestFollowersAccess() : setVisibility("private"))}
|
|
87
87
|
/>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</
|
|
93
|
-
{showFollowers && <ul>{followers.map((f) => <li key={f.handle}>{f.handle}</li>)}</ul>}
|
|
88
|
+
<Button type="button" size="sm" variant="quiet" onClick={() => setShowFollowers(true)}>
|
|
89
|
+
See followers
|
|
90
|
+
</Button>
|
|
91
|
+
</Row>
|
|
92
|
+
{showFollowers && <Stack gap={1}>{followers.map((f) => <Text key={f.handle}>{f.handle}</Text>)}</Stack>}
|
|
94
93
|
|
|
95
94
|
{/* the app's own list — one share doc per person picked */}
|
|
96
95
|
{can.create({ type: "share", authorHandle: me?.userHandle, buddyHandle: "x" }).ok && (
|
|
@@ -176,9 +175,9 @@ would copy draft prompts and prior versions. Unpublishing removes that record's
|
|
|
176
175
|
public grant. Explicit media bindings fail closed on older access outputs until
|
|
177
176
|
the publish-time backfill evaluates them under the new rule.
|
|
178
177
|
|
|
179
|
-
###
|
|
178
|
+
### Pictures on shared records
|
|
180
179
|
|
|
181
|
-
|
|
180
|
+
Anyone who can read a record can make and keep pictures for it: each generation is saved in the media database as that person's own record, so a shared record stays author-edited while everyone's pictures land. Write-once docs can simply `if (oldDoc === null) {} else throw`.
|
|
182
181
|
|
|
183
182
|
### `_id` strategy matters
|
|
184
183
|
|
|
@@ -317,14 +316,12 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
317
316
|
if (!user) throw { forbidden: "authentication required" };
|
|
318
317
|
|
|
319
318
|
if (doc.type === "message") {
|
|
320
|
-
// Author fixed at create
|
|
321
|
-
// only append one legitimate ImgGen version — the platform predicate
|
|
322
|
-
// ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
|
|
319
|
+
// Author fixed at create and kept for good; the author edits their own.
|
|
323
320
|
if (oldDoc === null) {
|
|
324
321
|
if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
325
322
|
} else if (doc.userHandle !== oldDoc.userHandle) {
|
|
326
323
|
throw { forbidden: "cannot change author" };
|
|
327
|
-
} else if (
|
|
324
|
+
} else if (oldDoc.userHandle !== user.userHandle) {
|
|
328
325
|
throw { forbidden: "not author" };
|
|
329
326
|
}
|
|
330
327
|
ctx.requireAccess(doc.channelId);
|
|
@@ -335,7 +332,7 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
335
332
|
}
|
|
336
333
|
```
|
|
337
334
|
|
|
338
|
-
`ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member/role doc must also return `channels` (route it to an owner-readable admin channel like `["admin:grants"]`) — a channel-less result is rejected as "unreadable write". On updates
|
|
335
|
+
`ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member/role doc must also return `channels` (route it to an owner-readable admin channel like `["admin:grants"]`) — a channel-less result is rejected as "unreadable write". On updates, keep ownership immutable (`if (oldDoc && doc.<authorField> !== oldDoc.<authorField>) throw`, where `<authorField>` is your doc's author field — `authorHandle`/`userHandle`/`senderHandle`) and let the author edit their own (as `chat.message` above shows). Pictures other viewers make for a shared doc are their own records in the media database, so an author-only update keeps every picture flowing.
|
|
339
336
|
|
|
340
337
|
**A follow-up edit that adds a NEW doc type updates `access.js` FIRST.** The app runs live while your edits stream in, and writes are enforced against the access.js that was in force before this turn until the whole turn completes — so a `db.put` of a doc type the old function rejects fails immediately (`unknown document type`), even though your access.js update lands later in the same reply. Emit the access.js edit adding the new type's branch before the App.jsx edits that write it. And never fire-and-forget background writes of a newly added type: gate seed/auto-write effects on `useVibe(dbName)` — `if (!ready || !can.create(sampleDoc).ok) return;` with `ready`/`can` in the effect deps, checking a representative sample of **each** doc type the effect writes — so a not-yet-allowed write is skipped quietly instead of surfacing rejection errors the user didn't cause.
|
|
341
338
|
|
|
@@ -378,14 +375,12 @@ export function wall(doc, oldDoc, user, ctx) {
|
|
|
378
375
|
}
|
|
379
376
|
|
|
380
377
|
if (doc.type === "post") {
|
|
381
|
-
// Author fixed at create
|
|
382
|
-
// only append one legitimate ImgGen version — the platform predicate
|
|
383
|
-
// ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
|
|
378
|
+
// Author fixed at create and kept for good; the author edits their own.
|
|
384
379
|
if (oldDoc === null) {
|
|
385
380
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
386
381
|
} else if (doc.authorHandle !== oldDoc.authorHandle) {
|
|
387
382
|
throw { forbidden: "cannot change author" };
|
|
388
|
-
} else if (
|
|
383
|
+
} else if (oldDoc.authorHandle !== user.userHandle) {
|
|
389
384
|
throw { forbidden: "not author" };
|
|
390
385
|
}
|
|
391
386
|
return { channels: [doc.channelId] };
|
|
@@ -421,7 +416,7 @@ export function board(doc, oldDoc, user, ctx) {
|
|
|
421
416
|
}
|
|
422
417
|
```
|
|
423
418
|
|
|
424
|
-
`ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open feed anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/share/request doc must also return `channels` — a channel-less result is rejected. On updates
|
|
419
|
+
`ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open feed anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/share/request doc must also return `channels` — a channel-less result is rejected. On updates, keep ownership immutable (`doc.authorHandle !== oldDoc.authorHandle` → throw) and let the author edit their own (as `wall.post` above shows). Pictures other viewers make for a shared doc are their own records in the media database, so an author-only update keeps every picture flowing.
|
|
425
420
|
|
|
426
421
|
**Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own entries, post a note, drop a pin, join a shared canvas. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
|
|
427
422
|
|
|
@@ -550,9 +545,15 @@ import { useVibe } from "use-vibes";
|
|
|
550
545
|
{docs.map((doc) => (
|
|
551
546
|
<li key={doc._id}>
|
|
552
547
|
{doc.text}
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
548
|
+
<IconButton
|
|
549
|
+
size="sm"
|
|
550
|
+
variant={doc.favorite ? "solid" : "quiet"}
|
|
551
|
+
tone="warn"
|
|
552
|
+
aria-pressed={Boolean(doc.favorite)}
|
|
553
|
+
label={doc.favorite ? "Unfavorite" : "Favorite"}
|
|
554
|
+
icon={<Icon name="star" size="sm" />}
|
|
555
|
+
onClick={() => database.put({ ...doc, favorite: !doc.favorite })}
|
|
556
|
+
/>
|
|
556
557
|
</li>
|
|
557
558
|
))}
|
|
558
559
|
</ul>
|
|
@@ -564,7 +565,7 @@ import { useVibe } from "use-vibes";
|
|
|
564
565
|
>>>>>>> REPLACE
|
|
565
566
|
```
|
|
566
567
|
|
|
567
|
-
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
|
|
568
|
+
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 link between them — it lets the UI reflect server-enforced permissions without duplicating the logic.
|
|
568
569
|
|
|
569
570
|
The access function is the single source of truth for permissions. Gate write surfaces with `useVibe(dbName).can`, which runs this same access function. The `access` object (`access.hasRole(name)`, `access.hasChannel(name)`) reflects the viewer's resolved roles/channels for DISPLAY — role badges, showing/hiding read-only sections — not as the write gate.
|
|
570
571
|
|
|
@@ -611,14 +612,12 @@ export function announcements(doc, oldDoc, user, ctx) {
|
|
|
611
612
|
}
|
|
612
613
|
|
|
613
614
|
if (doc.type === "post") {
|
|
614
|
-
// Author fixed at create
|
|
615
|
-
// only append one legitimate ImgGen version — the platform predicate
|
|
616
|
-
// ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
|
|
615
|
+
// Author fixed at create and kept for good; the author edits their own.
|
|
617
616
|
if (oldDoc === null) {
|
|
618
617
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
619
618
|
} else if (doc.authorHandle !== oldDoc.authorHandle) {
|
|
620
619
|
throw { forbidden: "cannot change author" };
|
|
621
|
-
} else if (
|
|
620
|
+
} else if (oldDoc.authorHandle !== user.userHandle) {
|
|
622
621
|
throw { forbidden: "not author" };
|
|
623
622
|
}
|
|
624
623
|
ctx.requireAccess(doc.channel);
|
|
@@ -636,12 +635,12 @@ App.jsx — `useVibe().can` gates every write surface (posts AND owner-only mana
|
|
|
636
635
|
import React from "react";
|
|
637
636
|
import { useFireproof } from "use-fireproof";
|
|
638
637
|
import { useViewer, useVibe } from "use-vibes";
|
|
639
|
-
import { Button, Textarea } from "@vibes.diy/
|
|
638
|
+
import { Button, Field, RowCard, Skeleton, Stack, Text, Textarea } from "@vibes.diy/pouf";
|
|
640
639
|
|
|
641
640
|
export default function App() {
|
|
642
641
|
const { viewer, isViewerPending, ViewerTag } = useViewer();
|
|
643
642
|
const { database, useLiveQuery, access } = useFireproof("announcements");
|
|
644
|
-
const { can } = useVibe("announcements");
|
|
643
|
+
const { can, ready } = useVibe("announcements");
|
|
645
644
|
|
|
646
645
|
const { docs: posts } = useLiveQuery("type", { key: "post" });
|
|
647
646
|
const [draft, setDraft] = React.useState("");
|
|
@@ -654,7 +653,9 @@ export default function App() {
|
|
|
654
653
|
// ctx.requireRole("owner"), so this verdict is false for everyone but the owner.
|
|
655
654
|
const canGrant = can.create({ type: "roleGrant", role: "poster", userHandle: "newUser" });
|
|
656
655
|
|
|
657
|
-
|
|
656
|
+
// Hold the page while identity and the access function resolve: every verdict
|
|
657
|
+
// reads pending until then, so the write surface waits on a quiet checking state.
|
|
658
|
+
if (isViewerPending || !ready) return <Skeleton />;
|
|
658
659
|
|
|
659
660
|
async function submitPost() {
|
|
660
661
|
if (!draft.trim() || !viewer) return;
|
|
@@ -669,7 +670,7 @@ export default function App() {
|
|
|
669
670
|
}
|
|
670
671
|
|
|
671
672
|
return (
|
|
672
|
-
<
|
|
673
|
+
<Stack gap={4}>
|
|
673
674
|
{/* No current-user pill / sign-in button here — that's system chrome in the
|
|
674
675
|
Vibes Switch (the panel the logo opens). ViewerTag below renders post authors. */}
|
|
675
676
|
|
|
@@ -681,32 +682,34 @@ export default function App() {
|
|
|
681
682
|
submitPost();
|
|
682
683
|
}}
|
|
683
684
|
>
|
|
684
|
-
<
|
|
685
|
-
|
|
685
|
+
<Stack gap={3}>
|
|
686
|
+
<Field label="Announcement">{(id, describedBy) => <Textarea id={id} describedBy={describedBy} value={draft} onChange={setDraft} />}</Field>
|
|
687
|
+
<Button type="submit">Post</Button>
|
|
688
|
+
</Stack>
|
|
686
689
|
</form>
|
|
687
690
|
) : (
|
|
688
|
-
viewer && <
|
|
691
|
+
viewer && <Text muted>Posting opens up once the owner adds you as a poster.</Text>
|
|
689
692
|
)}
|
|
690
693
|
|
|
691
694
|
{/* owner-only management — gated on can.* */}
|
|
692
695
|
{canGrant.ok && (
|
|
693
|
-
<Button variant="
|
|
696
|
+
<Button type="button" variant="quiet" onClick={() => database.put({ type: "roleGrant", role: "poster", userHandle: "newUser" })}>
|
|
694
697
|
Grant poster role
|
|
695
698
|
</Button>
|
|
696
699
|
)}
|
|
697
700
|
|
|
698
701
|
{posts.map((p) => (
|
|
699
|
-
<
|
|
702
|
+
<RowCard key={p._id}>
|
|
700
703
|
<ViewerTag userHandle={p.authorHandle} />
|
|
701
|
-
<
|
|
704
|
+
<Text>{p.body}</Text>
|
|
702
705
|
{can.delete(p).ok && (
|
|
703
|
-
<Button variant="
|
|
706
|
+
<Button type="button" variant="quiet" tone="down" size="sm" onClick={() => database.del(p._id)}>
|
|
704
707
|
Delete
|
|
705
708
|
</Button>
|
|
706
709
|
)}
|
|
707
|
-
</
|
|
710
|
+
</RowCard>
|
|
708
711
|
))}
|
|
709
|
-
</
|
|
712
|
+
</Stack>
|
|
710
713
|
);
|
|
711
714
|
}
|
|
712
715
|
```
|
|
@@ -722,12 +725,12 @@ const canRevoke = can.delete(grantDoc);
|
|
|
722
725
|
{(canAppoint.ok || canRevoke.ok) && (
|
|
723
726
|
<ModeratorPanel>
|
|
724
727
|
{canAppoint.ok && (
|
|
725
|
-
<Button onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</Button>
|
|
728
|
+
<Button type="button" onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</Button>
|
|
726
729
|
)}
|
|
727
730
|
{canRevoke.ok ? (
|
|
728
|
-
<Button variant="
|
|
731
|
+
<Button type="button" variant="quiet" tone="down" onClick={() => database.del(grantDoc._id)}>Revoke</Button>
|
|
729
732
|
) : (
|
|
730
|
-
<
|
|
733
|
+
<Text muted>{canRevoke.reason}</Text>
|
|
731
734
|
)}
|
|
732
735
|
</ModeratorPanel>
|
|
733
736
|
)}
|
|
@@ -750,14 +753,12 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
750
753
|
}
|
|
751
754
|
|
|
752
755
|
if (doc.type === "post") {
|
|
753
|
-
// Author fixed at create
|
|
754
|
-
// only append one legitimate ImgGen version — the platform predicate
|
|
755
|
-
// ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
|
|
756
|
+
// Author fixed at create and kept for good; the author edits their own.
|
|
756
757
|
if (oldDoc === null) {
|
|
757
758
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
758
759
|
} else if (doc.authorHandle !== oldDoc.authorHandle) {
|
|
759
760
|
throw { forbidden: "cannot change author" };
|
|
760
|
-
} else if (
|
|
761
|
+
} else if (oldDoc.authorHandle !== user.userHandle) {
|
|
761
762
|
throw { forbidden: "not author" };
|
|
762
763
|
}
|
|
763
764
|
// Any signed-in author may post to this open channel. Do NOT call
|
|
@@ -792,7 +793,7 @@ App.jsx — `access.hasChannel()` filters which channels are visible (display);
|
|
|
792
793
|
=======
|
|
793
794
|
{/* filter to channels the viewer can see — use _id as channel identifier (display only) */}
|
|
794
795
|
{channels.filter((ch) => access.hasChannel(ch._id)).map((ch) => (
|
|
795
|
-
<
|
|
796
|
+
<Button key={ch._id} type="button" size="sm" variant={ch._id === channel ? "solid" : "quiet"} tone="info" onClick={() => setChannel(ch._id)}>{ch.name}</Button>
|
|
796
797
|
))}
|
|
797
798
|
|
|
798
799
|
{/* gate the write surface on useVibe().can */}
|
|
@@ -863,18 +864,18 @@ App.jsx — `access.hasChannel()` shows only the lists the viewer belongs to; `u
|
|
|
863
864
|
|
|
864
865
|
```jsx
|
|
865
866
|
<<<<<<< SEARCH
|
|
866
|
-
{viewer && <button onClick={addNote}
|
|
867
|
+
{viewer && <Button type="button" onClick={addNote}>Add a note</Button>}
|
|
867
868
|
=======
|
|
868
869
|
{/* create list — any signed-in visitor can start their own */}
|
|
869
870
|
{can.create({ type: "list", author: me?.userHandle }).ok && (
|
|
870
|
-
<button onClick={() => database.put({ type: "list", author: me.userHandle, name: "new list" })}>
|
|
871
|
-
|
|
872
|
-
</
|
|
871
|
+
<Button type="button" onClick={() => database.put({ type: "list", author: me.userHandle, name: "new list" })}>
|
|
872
|
+
New list
|
|
873
|
+
</Button>
|
|
873
874
|
)}
|
|
874
875
|
|
|
875
876
|
{visible.map((list) => (
|
|
876
877
|
<section key={list._id}>
|
|
877
|
-
<
|
|
878
|
+
<Heading level={2}>{list.name}</Heading>
|
|
878
879
|
{/* add-item form is shown only when the access fn would accept the write */}
|
|
879
880
|
{canAddItem(list) && <AddItemForm listId={list._id} />}
|
|
880
881
|
{/* invite peer — only shown to members of this list */}
|
|
@@ -987,7 +988,7 @@ App.jsx — the same board list serves the creator and the invited recipient. `B
|
|
|
987
988
|
```jsx partial
|
|
988
989
|
function BoardWorkspace({ ensureDefaultBoard }) {
|
|
989
990
|
const { useLiveQuery } = useFireproof("boards");
|
|
990
|
-
const { me, can } = useVibe("boards");
|
|
991
|
+
const { me, can, ready } = useVibe("boards");
|
|
991
992
|
const [pickedBoardId, setPickedBoardId] = React.useState();
|
|
992
993
|
const { docs: boards } = useLiveQuery("type", { key: "board" });
|
|
993
994
|
const ownBoardId = me?.userHandle ? `default-${me.userHandle}` : undefined;
|
|
@@ -1002,15 +1003,21 @@ function BoardWorkspace({ ensureDefaultBoard }) {
|
|
|
1002
1003
|
boards[0];
|
|
1003
1004
|
return (
|
|
1004
1005
|
<section>
|
|
1005
|
-
{ownBoardId && !boards.some((board) => board._id === ownBoardId) && can.create(ownBoard).ok && (
|
|
1006
|
-
<button onClick={createOwnBoard}>Create my board</
|
|
1006
|
+
{ownBoardId && !boards.some((board) => board._id === ownBoardId) && (!ready || can.create(ownBoard).ok) && (
|
|
1007
|
+
<Button type="button" onClick={createOwnBoard} disabled={!ready}>Create my board</Button>
|
|
1007
1008
|
)}
|
|
1008
|
-
<label>
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1009
|
+
<Field label="Board">
|
|
1010
|
+
{(id, describedBy) => (
|
|
1011
|
+
<Select
|
|
1012
|
+
id={id}
|
|
1013
|
+
describedBy={describedBy}
|
|
1014
|
+
value={board?._id ?? ""}
|
|
1015
|
+
onChange={setPickedBoardId}
|
|
1016
|
+
disabled={boards.length === 0}
|
|
1017
|
+
options={boards.map((board) => ({ value: board._id, label: board.name ?? board._id }))}
|
|
1018
|
+
/>
|
|
1019
|
+
)}
|
|
1020
|
+
</Field>
|
|
1014
1021
|
{board && <BoardContents board={board} />}
|
|
1015
1022
|
</section>
|
|
1016
1023
|
);
|
|
@@ -1028,11 +1035,11 @@ const sharedWithMe = boards.filter((board) => board.creatorHandle !== me?.userHa
|
|
|
1028
1035
|
// …
|
|
1029
1036
|
{sharedWithMe.length > 0 && (
|
|
1030
1037
|
<section>
|
|
1031
|
-
<
|
|
1038
|
+
<Heading level={2}>Shared with you</Heading>
|
|
1032
1039
|
{sharedWithMe.map((board) => (
|
|
1033
|
-
<
|
|
1034
|
-
{board.name ?? board._id} <ViewerTag userHandle={board.creatorHandle} />
|
|
1035
|
-
</
|
|
1040
|
+
<RowCard key={board._id} onClick={() => setPickedBoardId(board._id)}>
|
|
1041
|
+
<Text>{board.name ?? board._id}</Text> <ViewerTag userHandle={board.creatorHandle} />
|
|
1042
|
+
</RowCard>
|
|
1036
1043
|
))}
|
|
1037
1044
|
</section>
|
|
1038
1045
|
)}
|
|
@@ -1085,14 +1092,12 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
1085
1092
|
}
|
|
1086
1093
|
|
|
1087
1094
|
if (doc.type === "message") {
|
|
1088
|
-
// Author fixed at create
|
|
1089
|
-
// only append one legitimate ImgGen version — the platform predicate
|
|
1090
|
-
// ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
|
|
1095
|
+
// Author fixed at create and kept for good; the author edits their own.
|
|
1091
1096
|
if (oldDoc === null) {
|
|
1092
1097
|
if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
1093
1098
|
} else if (doc.userHandle !== oldDoc.userHandle) {
|
|
1094
1099
|
throw { forbidden: "cannot change author" };
|
|
1095
|
-
} else if (
|
|
1100
|
+
} else if (oldDoc.userHandle !== user.userHandle) {
|
|
1096
1101
|
throw { forbidden: "not author" };
|
|
1097
1102
|
}
|
|
1098
1103
|
ctx.requireAccess(doc.channelId);
|
|
@@ -1137,13 +1142,13 @@ App.jsx — show only channels the viewer is in (`access.hasChannel`), gate the
|
|
|
1137
1142
|
{viewer && <Compose onSend={send} />}
|
|
1138
1143
|
=======
|
|
1139
1144
|
{/* sidebar lists only the channels the viewer is in */}
|
|
1140
|
-
<nav>{myChannels.map((ch) => <
|
|
1145
|
+
<nav>{myChannels.map((ch) => <Button key={ch._id} type="button" size="sm" variant={ch._id === channelId ? "solid" : "quiet"} tone="info" onClick={() => setChannelId(ch._id)}>{ch.name}</Button>)}</nav>
|
|
1141
1146
|
|
|
1142
1147
|
{/* compose is shown only when the viewer is a member of the selected channel */}
|
|
1143
1148
|
{channelId && canPost.ok ? (
|
|
1144
1149
|
<Compose onSend={(text) => database.put({ type: "message", channelId, userHandle: me.userHandle, text })} />
|
|
1145
1150
|
) : (
|
|
1146
|
-
<
|
|
1151
|
+
<Text muted>{canPost.reason || "Pick a channel"}</Text>
|
|
1147
1152
|
)}
|
|
1148
1153
|
|
|
1149
1154
|
{/* any member of this channel may invite a peer */}
|
|
@@ -1203,7 +1208,7 @@ App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated
|
|
|
1203
1208
|
|
|
1204
1209
|
```jsx
|
|
1205
1210
|
<<<<<<< SEARCH
|
|
1206
|
-
<
|
|
1211
|
+
<Field label="Your answer">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={answer} onChange={setAnswer} />}</Field>
|
|
1207
1212
|
=======
|
|
1208
1213
|
{/* anyone (signed in OR anonymous) can submit — only stamp authorHandle when present */}
|
|
1209
1214
|
{canSubmit.ok ? (
|
|
@@ -1211,10 +1216,10 @@ App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated
|
|
|
1211
1216
|
e.preventDefault();
|
|
1212
1217
|
database.put({ type: "survey-response", question: "q1", answer, ...(me && { authorHandle: me.userHandle }) });
|
|
1213
1218
|
}}>
|
|
1214
|
-
<
|
|
1215
|
-
<
|
|
1219
|
+
<Field label="Your answer">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={answer} onChange={setAnswer} />}</Field>
|
|
1220
|
+
<Button type="submit">Submit</Button>
|
|
1216
1221
|
</form>
|
|
1217
|
-
) : <
|
|
1222
|
+
) : <Text muted>{canSubmit.reason}</Text>}
|
|
1218
1223
|
|
|
1219
1224
|
{/* owner-only admin to wire up the feedback-team role */}
|
|
1220
1225
|
{canConfigure.ok && <ConfigPanel />}
|
|
@@ -1242,8 +1247,38 @@ export function guestbook(doc, oldDoc, user, ctx) {
|
|
|
1242
1247
|
|
|
1243
1248
|
In `App.jsx`, gate the form on `useVibe("guestbook").can.create({ type: "entry" }).ok` (true for anon here) and stamp `authorHandle: me?.userHandle` only when signed in. Without `allowAnonymous: true` the runtime rejects the null-user write even though the function didn't throw — so the guestbook would silently require login, the exact miss to avoid.
|
|
1244
1249
|
|
|
1250
|
+
The app before the edit, a note form wrapped in a sign-in wall:
|
|
1251
|
+
|
|
1252
|
+
```jsx
|
|
1253
|
+
import { useState } from "react";
|
|
1254
|
+
import { useFireproof } from "use-fireproof";
|
|
1255
|
+
import { Field, Input, Stack } from "@vibes.diy/pouf";
|
|
1256
|
+
import { SignInGate } from "./SignInGate.jsx";
|
|
1257
|
+
|
|
1258
|
+
export default function App() {
|
|
1259
|
+
const { database } = useFireproof("notes");
|
|
1260
|
+
const [message, setMessage] = useState("");
|
|
1261
|
+
return (
|
|
1262
|
+
<Stack>
|
|
1263
|
+
<SignInGate>
|
|
1264
|
+
<Field label="Your note">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={message} onChange={setMessage} />}</Field>
|
|
1265
|
+
</SignInGate>
|
|
1266
|
+
</Stack>
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1245
1271
|
App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true even when nobody is signed in:
|
|
1246
1272
|
|
|
1273
|
+
```jsx
|
|
1274
|
+
<<<<<<< SEARCH
|
|
1275
|
+
import { Field, Input, Stack } from "@vibes.diy/pouf";
|
|
1276
|
+
=======
|
|
1277
|
+
import { Button, Field, Input, RowCard, Stack, Text } from "@vibes.diy/pouf";
|
|
1278
|
+
import { useVibe } from "use-vibes";
|
|
1279
|
+
>>>>>>> REPLACE
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1247
1282
|
```jsx
|
|
1248
1283
|
<<<<<<< SEARCH
|
|
1249
1284
|
const { database } = useFireproof("notes");
|
|
@@ -1260,7 +1295,7 @@ App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true e
|
|
|
1260
1295
|
```jsx
|
|
1261
1296
|
<<<<<<< SEARCH
|
|
1262
1297
|
<SignInGate>
|
|
1263
|
-
<
|
|
1298
|
+
<Field label="Your note">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={message} onChange={setMessage} />}</Field>
|
|
1264
1299
|
</SignInGate>
|
|
1265
1300
|
=======
|
|
1266
1301
|
{canSign.ok ? (
|
|
@@ -1270,11 +1305,11 @@ App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true e
|
|
|
1270
1305
|
database.put({ type: "entry", message, createdAt: Date.now(), ...(me && { authorHandle: me.userHandle }) });
|
|
1271
1306
|
setMessage("");
|
|
1272
1307
|
}}>
|
|
1273
|
-
<
|
|
1274
|
-
<
|
|
1308
|
+
<Field label="Your note">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={message} onChange={setMessage} placeholder="Leave a note" />}</Field>
|
|
1309
|
+
<Button type="submit">Sign</Button>
|
|
1275
1310
|
</form>
|
|
1276
|
-
) : <
|
|
1311
|
+
) : <Text muted>{canSign.reason}</Text>}
|
|
1277
1312
|
|
|
1278
|
-
{entries.map((e) => <
|
|
1313
|
+
{entries.map((e) => <RowCard key={e._id}><Text>{e.message} — {e.authorHandle || "anonymous"}</Text></RowCard>)}
|
|
1279
1314
|
>>>>>>> REPLACE
|
|
1280
1315
|
```
|
package/llms/backend.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const backendConfig = {
|
|
2
2
|
name: "backend",
|
|
3
3
|
label: "backend-js",
|
|
4
|
-
description: "server-side backend.js file: answer HTTP/webhook requests at the app's /_api URL, react to committed data changes, and run scheduled jobs — its writes go through the app's own access rules; reads a web page, feed or API the person names and hands it to the page through /_api; sends a record
|
|
5
|
-
cues: ["zapier", "catch hook"],
|
|
4
|
+
description: "server-side backend.js file: answer HTTP/webhook requests at the app's /_api URL, react to committed data changes, and run scheduled jobs — its writes go through the app's own access rules; reads a web page, feed or API the person names and hands it to the page through /_api; emails, notifies and reminds people through ctx.notify by handle — a weekly digest, a summary for the owner, a Monday email to the manager — and invites a recipient who is not in the app yet; and sends a record to a tool the person names (Slack, Google Sheets, a CRM, Zapier) through a Zapier Catch Hook the owner connects",
|
|
5
|
+
cues: ["email", "notify", "remind", "digest", "zapier", "catch hook", "webhook", "slack", "sheets", "crm"],
|
|
6
6
|
};
|
|
7
7
|
//# sourceMappingURL=backend.js.map
|
package/llms/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../jsr/llms/backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../jsr/llms/backend.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,YAAY;IACnB,WAAW,EACT,ylBAAylB;IAO3lB,IAAI,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC;CAC3G,CAAC"}
|
package/llms/backend.md
CHANGED
|
@@ -24,7 +24,9 @@ create or mention `backend.js`.
|
|
|
24
24
|
"form POST", "Stripe/GitHub/Sonos sends us…". Something **external → us**.
|
|
25
25
|
- **`onChange`** — "when X is created", "after a write", "notify", "send an
|
|
26
26
|
email", "aggregate", "moderate", "sync to…". An **internal write → a side
|
|
27
|
-
effect
|
|
27
|
+
effect**: `ctx.notify` for every person the app emails, notifies or reminds,
|
|
28
|
+
and a Zapier Catch Hook for a tool the person names (Slack, Google Sheets, a
|
|
29
|
+
CRM).
|
|
28
30
|
- **`scheduled`** — "every N minutes", "poll", "check periodically", "cron",
|
|
29
31
|
"cleanup", "digest". **Timer-driven**, no user in the loop.
|
|
30
32
|
|
|
@@ -308,32 +310,38 @@ const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", b
|
|
|
308
310
|
CORS headers (`api.github.com` is seeded; additions are a PR to the platform
|
|
309
311
|
repo).
|
|
310
312
|
|
|
311
|
-
**
|
|
312
|
-
`
|
|
313
|
-
|
|
314
|
-
email
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
313
|
+
**Emailing, notifying or reminding a person is `ctx.notify`.**
|
|
314
|
+
`ctx.notify` reaches a person in the app by handle — the owner, a member, a
|
|
315
|
+
manager — on their bell, their push and their email, so "email the manager
|
|
316
|
+
every Monday", "email me a weekly digest" and "send the owner a summary" are
|
|
317
|
+
each a `scheduled` handler that calls `ctx.notify`. A recipient who is not in
|
|
318
|
+
the app yet joins it through the app's invite: the person picker takes their
|
|
319
|
+
handle, or takes their email address and mails them an invite, and once they
|
|
320
|
+
arrive every notification reaches them. Keep the record in the app too, so the app's
|
|
321
|
+
own screen shows what went out.
|
|
322
|
+
|
|
323
|
+
**A Catch Hook carries a record to a tool the person names.** When the ask
|
|
324
|
+
names Slack, Google Sheets, a CRM, Airtable, Notion, Clio, HubSpot or Zapier
|
|
325
|
+
itself, `hooks.zapier.com` is on the curated list for a `POST` to a Catch Hook, so
|
|
326
|
+
the app hands the new record to that tool without holding its credentials.
|
|
327
|
+
The owner makes a Zap whose trigger is "Webhooks by Zapier → Catch Hook",
|
|
328
|
+
saves the hook URL as a secret (`vibes-diy secrets set ZAPIER_HOOK_URL ...`),
|
|
329
|
+
and the backend posts JSON to `ctx.secrets.ZAPIER_HOOK_URL`. The URL **is**
|
|
330
|
+
the credential, so it stays in the secret, out of every document and response
|
|
331
|
+
body. Write that path gated on the secret
|
|
332
|
+
(`if (!ctx.secrets.ZAPIER_HOOK_URL) return;`), so the app works fully before
|
|
333
|
+
any Zap exists, and say in your reply that connecting a Zap to the hook turns
|
|
334
|
+
that delivery on. Treat any 2xx as accepted and mark the record sent; on
|
|
335
|
+
anything else **throw** — a handler that returns normally has acknowledged
|
|
336
|
+
the event, so the platform will not call it again for that write, while a
|
|
337
|
+
thrown `onChange` is re-delivered with backoff (a handful of tries over a few
|
|
338
|
+
minutes) and a run that never succeeds lands in the app's logs.
|
|
332
339
|
|
|
333
340
|
```js
|
|
334
341
|
export async function onChange(event, ctx) {
|
|
335
342
|
const doc = event.doc;
|
|
336
343
|
if (event.deleted || doc.type !== "intake" || doc.sentToZapier) return;
|
|
344
|
+
if (!ctx.secrets.ZAPIER_HOOK_URL) return; // no Zap connected yet: the record stays in the app
|
|
337
345
|
const res = await ctx.fetch(ctx.secrets.ZAPIER_HOOK_URL, {
|
|
338
346
|
method: "POST",
|
|
339
347
|
headers: { "content-type": "application/json" },
|
|
@@ -589,7 +597,7 @@ export async function scheduled(event, ctx) {
|
|
|
589
597
|
```jsx
|
|
590
598
|
import React from "react";
|
|
591
599
|
import { useFireproof } from "use-fireproof";
|
|
592
|
-
import { Badge,
|
|
600
|
+
import { Badge, Heading, RowCard, Stack, Text } from "@vibes.diy/pouf";
|
|
593
601
|
|
|
594
602
|
export default function App() {
|
|
595
603
|
const { useLiveQuery } = useFireproof("nodeReleases");
|
|
@@ -600,26 +608,26 @@ export default function App() {
|
|
|
600
608
|
|
|
601
609
|
return (
|
|
602
610
|
<main className="min-h-screen bg-background text-foreground font-sans mx-auto max-w-2xl p-6">
|
|
603
|
-
<
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
{
|
|
611
|
-
|
|
612
|
-
<
|
|
613
|
-
<
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
</
|
|
619
|
-
</
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
</
|
|
611
|
+
<Stack gap={5}>
|
|
612
|
+
<Heading level={1}>Node.js releases</Heading>
|
|
613
|
+
{status?.state === "egress-denied" && (
|
|
614
|
+
<div role="status">
|
|
615
|
+
<Badge tone="info">showing saved data — live refresh unavailable</Badge>
|
|
616
|
+
</div>
|
|
617
|
+
)}
|
|
618
|
+
<Stack gap={3}>
|
|
619
|
+
{releases.map((release) => (
|
|
620
|
+
<RowCard key={release._id}>
|
|
621
|
+
<a className="font-bold underline" href={release.url} target="_blank" rel="noreferrer">
|
|
622
|
+
{release.title}
|
|
623
|
+
</a>
|
|
624
|
+
<Text size="sm" muted>
|
|
625
|
+
{release.publishedAt.slice(0, 10)}
|
|
626
|
+
</Text>
|
|
627
|
+
</RowCard>
|
|
628
|
+
))}
|
|
629
|
+
</Stack>
|
|
630
|
+
</Stack>
|
|
623
631
|
</main>
|
|
624
632
|
);
|
|
625
633
|
}
|