@vibes.diy/prompts 14.3.79 → 14.3.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/llms/access.md +105 -60
- package/llms/backend.js +2 -2
- package/llms/backend.js.map +1 -1
- package/llms/backend.md +51 -43
- package/llms/bluesky.md +16 -14
- package/llms/board-games.d.ts +2 -0
- package/llms/board-games.js +29 -0
- package/llms/board-games.js.map +1 -0
- package/llms/board-games.md +477 -0
- package/llms/callai.md +91 -20
- package/llms/connections.md +5 -4
- package/llms/create-vibe.md +5 -4
- package/llms/fireproof.md +261 -109
- package/llms/image-gen.md +45 -7
- package/llms/index.d.ts +2 -1
- package/llms/index.js +3 -0
- package/llms/index.js.map +1 -1
- package/llms/p5.md +9 -11
- package/llms/spotify.md +5 -2
- package/llms/three-js.md +163 -166
- package/llms/use-vibe.md +15 -8
- package/llms/use-viewer.md +61 -45
- package/llms/voxel.md +32 -37
- package/llms/youtube.md +5 -2
- package/package.json +4 -4
- package/prompts.d.ts +4 -2
- package/prompts.js +47 -24
- package/prompts.js.map +1 -1
- package/system-prompt-initial-oneshot.md +13 -12
- package/system-prompt-initial.md +12 -11
- package/system-prompt.md +29 -47
- package/themes/colorsets.js +18 -2
- package/themes/colorsets.js.map +1 -1
- package/themes/looks/pouf/brief.md +65 -47
- package/themes/looks/pouf/catalog.generated.d.ts +1 -1
- package/themes/looks/pouf/catalog.generated.js +103 -10
- package/themes/looks/pouf/catalog.generated.js.map +1 -1
- package/themes/looks/pouf/variety.js +2 -2
- package/themes/looks/pouf/variety.js.map +1 -1
package/llms/access.md
CHANGED
|
@@ -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 && (
|
|
@@ -550,9 +549,15 @@ import { useVibe } from "use-vibes";
|
|
|
550
549
|
{docs.map((doc) => (
|
|
551
550
|
<li key={doc._id}>
|
|
552
551
|
{doc.text}
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
552
|
+
<IconButton
|
|
553
|
+
size="sm"
|
|
554
|
+
variant={doc.favorite ? "solid" : "quiet"}
|
|
555
|
+
tone="warn"
|
|
556
|
+
aria-pressed={Boolean(doc.favorite)}
|
|
557
|
+
label={doc.favorite ? "Unfavorite" : "Favorite"}
|
|
558
|
+
icon={<Icon name="star" size="sm" />}
|
|
559
|
+
onClick={() => database.put({ ...doc, favorite: !doc.favorite })}
|
|
560
|
+
/>
|
|
556
561
|
</li>
|
|
557
562
|
))}
|
|
558
563
|
</ul>
|
|
@@ -564,7 +569,7 @@ import { useVibe } from "use-vibes";
|
|
|
564
569
|
>>>>>>> REPLACE
|
|
565
570
|
```
|
|
566
571
|
|
|
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
|
|
572
|
+
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
573
|
|
|
569
574
|
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
575
|
|
|
@@ -636,12 +641,12 @@ App.jsx — `useVibe().can` gates every write surface (posts AND owner-only mana
|
|
|
636
641
|
import React from "react";
|
|
637
642
|
import { useFireproof } from "use-fireproof";
|
|
638
643
|
import { useViewer, useVibe } from "use-vibes";
|
|
639
|
-
import { Button, Textarea } from "@vibes.diy/
|
|
644
|
+
import { Button, Field, RowCard, Skeleton, Stack, Text, Textarea } from "@vibes.diy/pouf";
|
|
640
645
|
|
|
641
646
|
export default function App() {
|
|
642
647
|
const { viewer, isViewerPending, ViewerTag } = useViewer();
|
|
643
648
|
const { database, useLiveQuery, access } = useFireproof("announcements");
|
|
644
|
-
const { can } = useVibe("announcements");
|
|
649
|
+
const { can, ready } = useVibe("announcements");
|
|
645
650
|
|
|
646
651
|
const { docs: posts } = useLiveQuery("type", { key: "post" });
|
|
647
652
|
const [draft, setDraft] = React.useState("");
|
|
@@ -654,7 +659,9 @@ export default function App() {
|
|
|
654
659
|
// ctx.requireRole("owner"), so this verdict is false for everyone but the owner.
|
|
655
660
|
const canGrant = can.create({ type: "roleGrant", role: "poster", userHandle: "newUser" });
|
|
656
661
|
|
|
657
|
-
|
|
662
|
+
// Hold the page while identity and the access function resolve: every verdict
|
|
663
|
+
// reads pending until then, so the write surface waits on a quiet checking state.
|
|
664
|
+
if (isViewerPending || !ready) return <Skeleton />;
|
|
658
665
|
|
|
659
666
|
async function submitPost() {
|
|
660
667
|
if (!draft.trim() || !viewer) return;
|
|
@@ -669,7 +676,7 @@ export default function App() {
|
|
|
669
676
|
}
|
|
670
677
|
|
|
671
678
|
return (
|
|
672
|
-
<
|
|
679
|
+
<Stack gap={4}>
|
|
673
680
|
{/* No current-user pill / sign-in button here — that's system chrome in the
|
|
674
681
|
Vibes Switch (the panel the logo opens). ViewerTag below renders post authors. */}
|
|
675
682
|
|
|
@@ -681,32 +688,34 @@ export default function App() {
|
|
|
681
688
|
submitPost();
|
|
682
689
|
}}
|
|
683
690
|
>
|
|
684
|
-
<
|
|
685
|
-
|
|
691
|
+
<Stack gap={3}>
|
|
692
|
+
<Field label="Announcement">{(id, describedBy) => <Textarea id={id} describedBy={describedBy} value={draft} onChange={setDraft} />}</Field>
|
|
693
|
+
<Button type="submit">Post</Button>
|
|
694
|
+
</Stack>
|
|
686
695
|
</form>
|
|
687
696
|
) : (
|
|
688
|
-
viewer && <
|
|
697
|
+
viewer && <Text muted>Posting opens up once the owner adds you as a poster.</Text>
|
|
689
698
|
)}
|
|
690
699
|
|
|
691
700
|
{/* owner-only management — gated on can.* */}
|
|
692
701
|
{canGrant.ok && (
|
|
693
|
-
<Button variant="
|
|
702
|
+
<Button type="button" variant="quiet" onClick={() => database.put({ type: "roleGrant", role: "poster", userHandle: "newUser" })}>
|
|
694
703
|
Grant poster role
|
|
695
704
|
</Button>
|
|
696
705
|
)}
|
|
697
706
|
|
|
698
707
|
{posts.map((p) => (
|
|
699
|
-
<
|
|
708
|
+
<RowCard key={p._id}>
|
|
700
709
|
<ViewerTag userHandle={p.authorHandle} />
|
|
701
|
-
<
|
|
710
|
+
<Text>{p.body}</Text>
|
|
702
711
|
{can.delete(p).ok && (
|
|
703
|
-
<Button variant="
|
|
712
|
+
<Button type="button" variant="quiet" tone="down" size="sm" onClick={() => database.del(p._id)}>
|
|
704
713
|
Delete
|
|
705
714
|
</Button>
|
|
706
715
|
)}
|
|
707
|
-
</
|
|
716
|
+
</RowCard>
|
|
708
717
|
))}
|
|
709
|
-
</
|
|
718
|
+
</Stack>
|
|
710
719
|
);
|
|
711
720
|
}
|
|
712
721
|
```
|
|
@@ -722,12 +731,12 @@ const canRevoke = can.delete(grantDoc);
|
|
|
722
731
|
{(canAppoint.ok || canRevoke.ok) && (
|
|
723
732
|
<ModeratorPanel>
|
|
724
733
|
{canAppoint.ok && (
|
|
725
|
-
<Button onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</Button>
|
|
734
|
+
<Button type="button" onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</Button>
|
|
726
735
|
)}
|
|
727
736
|
{canRevoke.ok ? (
|
|
728
|
-
<Button variant="
|
|
737
|
+
<Button type="button" variant="quiet" tone="down" onClick={() => database.del(grantDoc._id)}>Revoke</Button>
|
|
729
738
|
) : (
|
|
730
|
-
<
|
|
739
|
+
<Text muted>{canRevoke.reason}</Text>
|
|
731
740
|
)}
|
|
732
741
|
</ModeratorPanel>
|
|
733
742
|
)}
|
|
@@ -792,7 +801,7 @@ App.jsx — `access.hasChannel()` filters which channels are visible (display);
|
|
|
792
801
|
=======
|
|
793
802
|
{/* filter to channels the viewer can see — use _id as channel identifier (display only) */}
|
|
794
803
|
{channels.filter((ch) => access.hasChannel(ch._id)).map((ch) => (
|
|
795
|
-
<
|
|
804
|
+
<Button key={ch._id} type="button" size="sm" variant={ch._id === channel ? "solid" : "quiet"} tone="info" onClick={() => setChannel(ch._id)}>{ch.name}</Button>
|
|
796
805
|
))}
|
|
797
806
|
|
|
798
807
|
{/* gate the write surface on useVibe().can */}
|
|
@@ -863,18 +872,18 @@ App.jsx — `access.hasChannel()` shows only the lists the viewer belongs to; `u
|
|
|
863
872
|
|
|
864
873
|
```jsx
|
|
865
874
|
<<<<<<< SEARCH
|
|
866
|
-
{viewer && <button onClick={addNote}
|
|
875
|
+
{viewer && <Button type="button" onClick={addNote}>Add a note</Button>}
|
|
867
876
|
=======
|
|
868
877
|
{/* create list — any signed-in visitor can start their own */}
|
|
869
878
|
{can.create({ type: "list", author: me?.userHandle }).ok && (
|
|
870
|
-
<button onClick={() => database.put({ type: "list", author: me.userHandle, name: "new list" })}>
|
|
871
|
-
|
|
872
|
-
</
|
|
879
|
+
<Button type="button" onClick={() => database.put({ type: "list", author: me.userHandle, name: "new list" })}>
|
|
880
|
+
New list
|
|
881
|
+
</Button>
|
|
873
882
|
)}
|
|
874
883
|
|
|
875
884
|
{visible.map((list) => (
|
|
876
885
|
<section key={list._id}>
|
|
877
|
-
<
|
|
886
|
+
<Heading level={2}>{list.name}</Heading>
|
|
878
887
|
{/* add-item form is shown only when the access fn would accept the write */}
|
|
879
888
|
{canAddItem(list) && <AddItemForm listId={list._id} />}
|
|
880
889
|
{/* invite peer — only shown to members of this list */}
|
|
@@ -987,7 +996,7 @@ App.jsx — the same board list serves the creator and the invited recipient. `B
|
|
|
987
996
|
```jsx partial
|
|
988
997
|
function BoardWorkspace({ ensureDefaultBoard }) {
|
|
989
998
|
const { useLiveQuery } = useFireproof("boards");
|
|
990
|
-
const { me, can } = useVibe("boards");
|
|
999
|
+
const { me, can, ready } = useVibe("boards");
|
|
991
1000
|
const [pickedBoardId, setPickedBoardId] = React.useState();
|
|
992
1001
|
const { docs: boards } = useLiveQuery("type", { key: "board" });
|
|
993
1002
|
const ownBoardId = me?.userHandle ? `default-${me.userHandle}` : undefined;
|
|
@@ -1002,15 +1011,21 @@ function BoardWorkspace({ ensureDefaultBoard }) {
|
|
|
1002
1011
|
boards[0];
|
|
1003
1012
|
return (
|
|
1004
1013
|
<section>
|
|
1005
|
-
{ownBoardId && !boards.some((board) => board._id === ownBoardId) && can.create(ownBoard).ok && (
|
|
1006
|
-
<button onClick={createOwnBoard}>Create my board</
|
|
1014
|
+
{ownBoardId && !boards.some((board) => board._id === ownBoardId) && (!ready || can.create(ownBoard).ok) && (
|
|
1015
|
+
<Button type="button" onClick={createOwnBoard} disabled={!ready}>Create my board</Button>
|
|
1007
1016
|
)}
|
|
1008
|
-
<label>
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1017
|
+
<Field label="Board">
|
|
1018
|
+
{(id, describedBy) => (
|
|
1019
|
+
<Select
|
|
1020
|
+
id={id}
|
|
1021
|
+
describedBy={describedBy}
|
|
1022
|
+
value={board?._id ?? ""}
|
|
1023
|
+
onChange={setPickedBoardId}
|
|
1024
|
+
disabled={boards.length === 0}
|
|
1025
|
+
options={boards.map((board) => ({ value: board._id, label: board.name ?? board._id }))}
|
|
1026
|
+
/>
|
|
1027
|
+
)}
|
|
1028
|
+
</Field>
|
|
1014
1029
|
{board && <BoardContents board={board} />}
|
|
1015
1030
|
</section>
|
|
1016
1031
|
);
|
|
@@ -1028,11 +1043,11 @@ const sharedWithMe = boards.filter((board) => board.creatorHandle !== me?.userHa
|
|
|
1028
1043
|
// …
|
|
1029
1044
|
{sharedWithMe.length > 0 && (
|
|
1030
1045
|
<section>
|
|
1031
|
-
<
|
|
1046
|
+
<Heading level={2}>Shared with you</Heading>
|
|
1032
1047
|
{sharedWithMe.map((board) => (
|
|
1033
|
-
<
|
|
1034
|
-
{board.name ?? board._id} <ViewerTag userHandle={board.creatorHandle} />
|
|
1035
|
-
</
|
|
1048
|
+
<RowCard key={board._id} onClick={() => setPickedBoardId(board._id)}>
|
|
1049
|
+
<Text>{board.name ?? board._id}</Text> <ViewerTag userHandle={board.creatorHandle} />
|
|
1050
|
+
</RowCard>
|
|
1036
1051
|
))}
|
|
1037
1052
|
</section>
|
|
1038
1053
|
)}
|
|
@@ -1137,13 +1152,13 @@ App.jsx — show only channels the viewer is in (`access.hasChannel`), gate the
|
|
|
1137
1152
|
{viewer && <Compose onSend={send} />}
|
|
1138
1153
|
=======
|
|
1139
1154
|
{/* sidebar lists only the channels the viewer is in */}
|
|
1140
|
-
<nav>{myChannels.map((ch) => <
|
|
1155
|
+
<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
1156
|
|
|
1142
1157
|
{/* compose is shown only when the viewer is a member of the selected channel */}
|
|
1143
1158
|
{channelId && canPost.ok ? (
|
|
1144
1159
|
<Compose onSend={(text) => database.put({ type: "message", channelId, userHandle: me.userHandle, text })} />
|
|
1145
1160
|
) : (
|
|
1146
|
-
<
|
|
1161
|
+
<Text muted>{canPost.reason || "Pick a channel"}</Text>
|
|
1147
1162
|
)}
|
|
1148
1163
|
|
|
1149
1164
|
{/* any member of this channel may invite a peer */}
|
|
@@ -1203,7 +1218,7 @@ App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated
|
|
|
1203
1218
|
|
|
1204
1219
|
```jsx
|
|
1205
1220
|
<<<<<<< SEARCH
|
|
1206
|
-
<
|
|
1221
|
+
<Field label="Your answer">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={answer} onChange={setAnswer} />}</Field>
|
|
1207
1222
|
=======
|
|
1208
1223
|
{/* anyone (signed in OR anonymous) can submit — only stamp authorHandle when present */}
|
|
1209
1224
|
{canSubmit.ok ? (
|
|
@@ -1211,10 +1226,10 @@ App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated
|
|
|
1211
1226
|
e.preventDefault();
|
|
1212
1227
|
database.put({ type: "survey-response", question: "q1", answer, ...(me && { authorHandle: me.userHandle }) });
|
|
1213
1228
|
}}>
|
|
1214
|
-
<
|
|
1215
|
-
<
|
|
1229
|
+
<Field label="Your answer">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={answer} onChange={setAnswer} />}</Field>
|
|
1230
|
+
<Button type="submit">Submit</Button>
|
|
1216
1231
|
</form>
|
|
1217
|
-
) : <
|
|
1232
|
+
) : <Text muted>{canSubmit.reason}</Text>}
|
|
1218
1233
|
|
|
1219
1234
|
{/* owner-only admin to wire up the feedback-team role */}
|
|
1220
1235
|
{canConfigure.ok && <ConfigPanel />}
|
|
@@ -1242,8 +1257,38 @@ export function guestbook(doc, oldDoc, user, ctx) {
|
|
|
1242
1257
|
|
|
1243
1258
|
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
1259
|
|
|
1260
|
+
The app before the edit, a note form wrapped in a sign-in wall:
|
|
1261
|
+
|
|
1262
|
+
```jsx
|
|
1263
|
+
import { useState } from "react";
|
|
1264
|
+
import { useFireproof } from "use-fireproof";
|
|
1265
|
+
import { Field, Input, Stack } from "@vibes.diy/pouf";
|
|
1266
|
+
import { SignInGate } from "./SignInGate.jsx";
|
|
1267
|
+
|
|
1268
|
+
export default function App() {
|
|
1269
|
+
const { database } = useFireproof("notes");
|
|
1270
|
+
const [message, setMessage] = useState("");
|
|
1271
|
+
return (
|
|
1272
|
+
<Stack>
|
|
1273
|
+
<SignInGate>
|
|
1274
|
+
<Field label="Your note">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={message} onChange={setMessage} />}</Field>
|
|
1275
|
+
</SignInGate>
|
|
1276
|
+
</Stack>
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1245
1281
|
App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true even when nobody is signed in:
|
|
1246
1282
|
|
|
1283
|
+
```jsx
|
|
1284
|
+
<<<<<<< SEARCH
|
|
1285
|
+
import { Field, Input, Stack } from "@vibes.diy/pouf";
|
|
1286
|
+
=======
|
|
1287
|
+
import { Button, Field, Input, RowCard, Stack, Text } from "@vibes.diy/pouf";
|
|
1288
|
+
import { useVibe } from "use-vibes";
|
|
1289
|
+
>>>>>>> REPLACE
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1247
1292
|
```jsx
|
|
1248
1293
|
<<<<<<< SEARCH
|
|
1249
1294
|
const { database } = useFireproof("notes");
|
|
@@ -1260,7 +1305,7 @@ App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true e
|
|
|
1260
1305
|
```jsx
|
|
1261
1306
|
<<<<<<< SEARCH
|
|
1262
1307
|
<SignInGate>
|
|
1263
|
-
<
|
|
1308
|
+
<Field label="Your note">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={message} onChange={setMessage} />}</Field>
|
|
1264
1309
|
</SignInGate>
|
|
1265
1310
|
=======
|
|
1266
1311
|
{canSign.ok ? (
|
|
@@ -1270,11 +1315,11 @@ App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true e
|
|
|
1270
1315
|
database.put({ type: "entry", message, createdAt: Date.now(), ...(me && { authorHandle: me.userHandle }) });
|
|
1271
1316
|
setMessage("");
|
|
1272
1317
|
}}>
|
|
1273
|
-
<
|
|
1274
|
-
<
|
|
1318
|
+
<Field label="Your note">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={message} onChange={setMessage} placeholder="Leave a note" />}</Field>
|
|
1319
|
+
<Button type="submit">Sign</Button>
|
|
1275
1320
|
</form>
|
|
1276
|
-
) : <
|
|
1321
|
+
) : <Text muted>{canSign.reason}</Text>}
|
|
1277
1322
|
|
|
1278
|
-
{entries.map((e) => <
|
|
1323
|
+
{entries.map((e) => <RowCard key={e._id}><Text>{e.message} — {e.authorHandle || "anonymous"}</Text></RowCard>)}
|
|
1279
1324
|
>>>>>>> REPLACE
|
|
1280
1325
|
```
|
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
|
}
|
package/llms/bluesky.md
CHANGED
|
@@ -264,6 +264,7 @@ account of the visitor's involved.
|
|
|
264
264
|
### `App.jsx`
|
|
265
265
|
```jsx app=bluesky-browser
|
|
266
266
|
import React, { useState } from "react";
|
|
267
|
+
import { Button, Field, Input, Row, RowCard, Stack } from "@vibes.diy/pouf";
|
|
267
268
|
|
|
268
269
|
export default function App() {
|
|
269
270
|
const [handle, setHandle] = useState("bsky.app");
|
|
@@ -296,8 +297,10 @@ export default function App() {
|
|
|
296
297
|
|
|
297
298
|
return (
|
|
298
299
|
<div>
|
|
299
|
-
<
|
|
300
|
-
|
|
300
|
+
<Row gap={2} align="top">
|
|
301
|
+
<Field label="Bluesky handle">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={handle} onChange={setHandle} placeholder="alice.bsky.social" />}</Field>
|
|
302
|
+
<Button type="button" onClick={() => load()}>Look up</Button>
|
|
303
|
+
</Row>
|
|
301
304
|
{problem && <p role="alert">{problem}</p>}
|
|
302
305
|
|
|
303
306
|
{profile && (
|
|
@@ -314,20 +317,19 @@ export default function App() {
|
|
|
314
317
|
|
|
315
318
|
<section>
|
|
316
319
|
<h3>Following</h3>
|
|
317
|
-
<
|
|
320
|
+
<Stack gap={2}>
|
|
318
321
|
{people.map((person) => (
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
</li>
|
|
322
|
+
<RowCard
|
|
323
|
+
key={person.did}
|
|
324
|
+
onClick={() => {
|
|
325
|
+
setHandle(person.handle);
|
|
326
|
+
load(person.handle);
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
{person.displayName} (@{person.handle})
|
|
330
|
+
</RowCard>
|
|
329
331
|
))}
|
|
330
|
-
</
|
|
332
|
+
</Stack>
|
|
331
333
|
</section>
|
|
332
334
|
|
|
333
335
|
<section>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const boardGamesConfig = {
|
|
2
|
+
name: "board-games",
|
|
3
|
+
label: "Board, tile and card games",
|
|
4
|
+
description: "Board, tile and card games played one tap, guess or card at a time: bingo cards dealt per player from a shared pool, " +
|
|
5
|
+
"daily word guesses with an on-screen keyboard and a share grid, crosswords, word searches, grouping puzzles, letter hives, " +
|
|
6
|
+
"sudoku, minesweeper, 2048, checkers, chess, go, hex, mancala, solitaire and other card games with hidden hands, trivia rooms " +
|
|
7
|
+
"and dice scoresheets with house rules. Covers SVG boards in the app's theme tokens (square, hex, ring, graph and stacked " +
|
|
8
|
+
"shapes), seeded deals and daily seeds, win-line detection, turns, undo, AI opponents, and who sees what — public tables, " +
|
|
9
|
+
"private hands, members-only clubs.",
|
|
10
|
+
cues: [
|
|
11
|
+
"bingo",
|
|
12
|
+
"word game",
|
|
13
|
+
"wordle",
|
|
14
|
+
"crossword",
|
|
15
|
+
"word search",
|
|
16
|
+
"sudoku",
|
|
17
|
+
"chess",
|
|
18
|
+
"checkers",
|
|
19
|
+
"solitaire",
|
|
20
|
+
"card game",
|
|
21
|
+
"board game",
|
|
22
|
+
"deck of cards",
|
|
23
|
+
"puzzle board",
|
|
24
|
+
"minesweeper",
|
|
25
|
+
"mancala",
|
|
26
|
+
"tic-tac-toe",
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=board-games.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board-games.js","sourceRoot":"","sources":["../../jsr/llms/board-games.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,gBAAgB,GAAc;IACzC,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,4BAA4B;IACnC,WAAW,EACT,uHAAuH;QACvH,6HAA6H;QAC7H,+HAA+H;QAC/H,2HAA2H;QAC3H,2HAA2H;QAC3H,oCAAoC;IACtC,IAAI,EAAE;QACJ,OAAO;QACP,WAAW;QACX,QAAQ;QACR,WAAW;QACX,aAAa;QACb,QAAQ;QACR,OAAO;QACP,UAAU;QACV,WAAW;QACX,WAAW;QACX,YAAY;QACZ,eAAe;QACf,cAAc;QACd,aAAa;QACb,SAAS;QACT,aAAa;KACd;CACF,CAAC"}
|