@vibes.diy/prompts 14.3.85 → 14.3.86
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.js +1 -1
- package/llms/access.md +40 -3
- package/llms/use-viewer.md +1 -1
- package/package.json +5 -5
- package/system-prompt-initial-oneshot.md +2 -2
- package/system-prompt-initial.md +2 -2
- package/system-prompt.md +2 -2
- package/themes/fonts.d.ts +1 -0
- package/themes/fonts.js +13 -0
- package/themes/fonts.js.map +1 -1
package/llms/access.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const accessConfig = {
|
|
2
2
|
name: "access",
|
|
3
3
|
label: "access-js",
|
|
4
|
-
description: "server-side access.js file — the record rules every app ships with: who reads and writes each record, the creator granted at creation, invites into an object, membership/roles, approval-to-join, moderation, private per-user data, DMs. Name it on every first build
|
|
4
|
+
description: "server-side access.js file — the record rules every app ships with: who reads and writes each record, the creator granted at creation, invites into an object, membership/roles, approval-to-join, moderation, private per-user data, DMs. Name it on every first build, an app the person keeps to themselves included: its rules give each person their own record tree and a publish step",
|
|
5
5
|
cues: [
|
|
6
6
|
"private",
|
|
7
7
|
"privately",
|
package/llms/access.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Access Control (`access.js`) — permission design
|
|
2
2
|
|
|
3
|
-
This doc is in view because the app is getting its `access.js`: a new app's first build writes its rules with its screens, and a later edit adds or changes them. Either way the records carry their keys (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record), and this build writes the rules that key on them. The rules and those keys are plumbing with nothing on screen of their own; the invite screen joins the build when the ask names other people, sharing, a group or a person by name, and is otherwise offered as an option after the build. Every app gets record rules
|
|
3
|
+
This doc is in view because the app is getting its `access.js`: a new app's first build writes its rules with its screens, and a later edit adds or changes them. Either way the records carry their keys (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record), and this build writes the rules that key on them. The rules and those keys are plumbing with nothing on screen of their own; the invite screen joins the build when the ask names other people, sharing, a group or a person by name, and is otherwise offered as an option after the build. Every app gets record rules, and the group shape taught below is the resting default. A person the ask names is let in by a record — see _A person the ask names is let in by a record_ below — so the build states the membership record's shape and the invite handler that writes it.
|
|
4
4
|
|
|
5
5
|
**When an edit adds the rules to an app that already exists, the edit owes only what the rules need.** Read `App.jsx` for the object type, the field that names its creator and the parent id its children carry; name the channels from those records; write the complete `access.js`; then change `App.jsx` only where the rules require it — stable creator membership, typed content queries, any write surface not yet gated on `can`, and the invite screen when the ask names other people, sharing or a group — with SEARCH/REPLACE edits. A file the rules do not touch is not emitted, and stays as it is.
|
|
6
6
|
|
|
@@ -235,6 +235,7 @@ Each named export gates its own database. A single `/access.js` can gate all dat
|
|
|
235
235
|
```js
|
|
236
236
|
export function chat(doc, oldDoc, user, ctx) {
|
|
237
237
|
if (!user) throw { forbidden: "authentication required" };
|
|
238
|
+
if (oldDoc && oldDoc.channelId !== doc.channelId) throw { forbidden: "a record stays in its channel" };
|
|
238
239
|
ctx.requireAccess(doc.channelId);
|
|
239
240
|
return { channels: [doc.channelId] };
|
|
240
241
|
}
|
|
@@ -256,6 +257,7 @@ Databases without a matching named export fall through to `export default` if on
|
|
|
256
257
|
```js
|
|
257
258
|
function crewChat(doc, oldDoc, user, ctx) {
|
|
258
259
|
if (!user) throw { forbidden: "authentication required" };
|
|
260
|
+
if (oldDoc && oldDoc.channelId !== doc.channelId) throw { forbidden: "a record stays in its channel" };
|
|
259
261
|
ctx.requireAccess(doc.channelId);
|
|
260
262
|
return { channels: [doc.channelId] };
|
|
261
263
|
}
|
|
@@ -271,6 +273,7 @@ Use `export default` to gate every database without writing a named export for e
|
|
|
271
273
|
```js
|
|
272
274
|
export function chat(doc, oldDoc, user, ctx) {
|
|
273
275
|
if (!user) throw { forbidden: "authentication required" };
|
|
276
|
+
if (oldDoc && oldDoc.channelId !== doc.channelId) throw { forbidden: "a record stays in its channel" };
|
|
274
277
|
ctx.requireAccess(doc.channelId);
|
|
275
278
|
return { channels: [doc.channelId] };
|
|
276
279
|
}
|
|
@@ -291,6 +294,8 @@ This is especially useful when an app has many databases.
|
|
|
291
294
|
|
|
292
295
|
Use `oldDoc` (the previous version of the document) to enforce invariants across updates: `if (oldDoc === null) { /* create-only logic */ }` for new documents, and `if (oldDoc && doc.version <= oldDoc.version) { throw { forbidden: "version must increase" } }` for monotonic versions.
|
|
293
296
|
|
|
297
|
+
An access function that dispatches on `doc.type` keeps each record's type: right after the sign-in check, `if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" };`. The branch that runs is chosen by the record being written, so this one line is what keeps an edit from rewriting someone else's record as another kind and skipping the checks its own kind applies. Deletes pass it: the delete gate hands the function the stored record, type and all, marked `_deleted`. The worked examples here that dispatch on more than one type all enforce it.
|
|
298
|
+
|
|
294
299
|
### Documents only the app's own backend writes
|
|
295
300
|
|
|
296
301
|
Some documents belong to the machinery rather than to a person: a scheduled run's state doc, a counter a `scheduled` sweep advances, a notification the backend generates. Gate those on the **origin** with `ctx.requireBackend(...)` — one line, no conditional:
|
|
@@ -395,6 +400,7 @@ _Each example below closes its type dispatch with the default terminal `throw {
|
|
|
395
400
|
```js partial
|
|
396
401
|
export function wall(doc, oldDoc, user, ctx) {
|
|
397
402
|
if (!user) throw { forbidden: "sign in" };
|
|
403
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
398
404
|
|
|
399
405
|
if (doc.type === "channel") {
|
|
400
406
|
ctx.requireRole("owner");
|
|
@@ -472,7 +478,29 @@ export function board(doc, oldDoc, user, ctx) {
|
|
|
472
478
|
|
|
473
479
|
**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.
|
|
474
480
|
|
|
475
|
-
**When the person says "
|
|
481
|
+
**Every app ships `access.js`. When the person says "just for me", "no sharing" or "keep it single-user", each signed-in person gets their own record tree, and a publish step shares a chosen item with everyone who can open the app.** The rules key on the writer's own handle, so they read the same for the person who made the app and for anyone they invite later. The app's nature is sensitive, and who can open the app is its sharing setting, not the rules: a published item is world-readable in `access.js` and reaches whoever can open the app — its members while it stays private, everyone once the owner opens it up. Publish copy says that plainly ("Published — everyone who can open this app sees it"):
|
|
482
|
+
|
|
483
|
+
### `access.js`
|
|
484
|
+
```js partial
|
|
485
|
+
export function sketches(doc, oldDoc, user, ctx) {
|
|
486
|
+
if (!user) throw { forbidden: "sign in" };
|
|
487
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
488
|
+
if (doc.type === "sketch") {
|
|
489
|
+
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
490
|
+
if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
491
|
+
const mine = `user:${user.userHandle}`; // this person's own tree
|
|
492
|
+
const ch = `sketch:${doc._id}`; // the item's own channel
|
|
493
|
+
const grant = { users: { [user.userHandle]: [mine, ch] } };
|
|
494
|
+
if (doc.visibility === "public") grant.public = [ch]; // published: everyone who can open the app
|
|
495
|
+
return { channels: [mine, ch], grant };
|
|
496
|
+
}
|
|
497
|
+
throw { forbidden: "unknown document type" };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Summary: each person's sketches are their own, and a sketch they publish is open to everyone who can open the app. A sketch carries its writer's handle in authorHandle and starts unpublished.
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**An ask for "no access control" or "anyone can edit" ships `access.js` too, with rules that admit everyone:** every signed-in visitor writes the shared records and edits their own, and `allowAnonymous: true` opens the same writes to guests when the ask needs no sign-in. When an ask says both, keeping it to themselves decides.
|
|
476
504
|
|
|
477
505
|
**When the person asks for private per-user permissions — "each person sees only their own", "private to every user" — every visitor gets their own private space on a single per-user channel.** A sleep log, a houseplant watering schedule, a savings jar, a notes app or a workout log where the data is one person's own routes every doc that visitor creates to the one channel keyed on their handle — `user:${user.userHandle}` — self-granted so only they read it, with `authorHandle: user.userHandle` fixed at create and held immutable on update (`oldDoc.authorHandle === user.userHandle`). Their whole collection lives on that single private channel, reachable from first load, each visitor's space entirely their own:
|
|
478
506
|
|
|
@@ -502,7 +530,7 @@ Every item a visitor creates lands on their own `user:<handle>` channel, private
|
|
|
502
530
|
|
|
503
531
|
**When the prompt asks to share, invite, or collaborate — bring in a partner, a buddy, a friend, a team, or a group that co-edits — each shared thing becomes its own object others can be invited into.** A seed-swap shelf you invite your sibling to, a board a group co-edits, a document you open to a collaborator: route that shared thing to its **own object channel** (`list:<id>`/`board:<id>`) and self-grant it at creation (`grant: { users: { [user.userHandle]: [ch] } }`), with the `authorHandle` create + `oldDoc` author checks so each item stays on its object. Then let the creator invite a chosen friend in: a `share` doc the creator authors grants that friend the same channel (`grant: { users: { [doc.invitee]: [ch] } }`), so they collaborate on that one list — sharing a single list or a whole space is the same grant at a different node of the object graph. A wall, guestbook, or map where each visitor adds their _own_ items is author-owned writes + public read: any signed-in visitor authors their own and everyone reads.
|
|
504
532
|
|
|
505
|
-
**When a user's work is private by default, show it — and offer a way to publish.** If everything a user does routes to a channel only they can read (a per-user `user:<handle>`, a private journal/notes/tracker with no `grant.public`), the UI must say so: a small, persistent "Only you can see this" / "Private to you" cue near their content, so no one wonders who's watching their unfinished work. Then, where sharing fits the app, give them a publish control — but publish at the granularity of a **channel**, not a doc: channels are the unit of read isolation, so adding `grant.public` to the shared `user:<handle>` channel would expose _every_ private item on it, not just the one they meant to share. To publish a single item, route it to its **own** channel and flip only that channel's read-grant from a `visibility` field the access fn reads — exactly the per-item-channel shape the worked example below uses (`const ch = \`entry:${doc._id}\`; const grant = { users: { [user.userHandle]: [ch] } }; if (doc.visibility === "public") grant.public = [ch];`) — for anonymous visitors, or grant a shared app channel for all granted members. Gate the control on `useVibe(dbName).can`, and reflect the result back in the affordance ("Published — anyone can see this", with an unpublish to flip it back). Making the user's _whole_ space public is fine when that's the intent; silently leaking their other private items by publishing one is the trap to avoid. **
|
|
533
|
+
**When a user's work is private by default, show it — and offer a way to publish.** If everything a user does routes to a channel only they can read (a per-user `user:<handle>`, a private journal/notes/tracker with no `grant.public`), the UI must say so: a small, persistent "Only you can see this" / "Private to you" cue near their content, so no one wonders who's watching their unfinished work. Then, where sharing fits the app, give them a publish control — but publish at the granularity of a **channel**, not a doc: channels are the unit of read isolation, so adding `grant.public` to the shared `user:<handle>` channel would expose _every_ private item on it, not just the one they meant to share. To publish a single item, route it to its **own** channel and flip only that channel's read-grant from a `visibility` field the access fn reads — exactly the per-item-channel shape the worked example below uses (`const ch = \`entry:${doc._id}\`; const grant = { users: { [user.userHandle]: [ch] } }; if (doc.visibility === "public") grant.public = [ch];`) — for anonymous visitors, or grant a shared app channel for all granted members. Gate the control on `useVibe(dbName).can`, and reflect the result back in the affordance ("Published — anyone can see this", with an unpublish to flip it back). Making the user's _whole_ space public is fine when that's the intent; silently leaking their other private items by publishing one is the trap to avoid. **When the person asked, in their own words, to keep it to themselves — "only I", "just me", "nobody else" — the publish control is the one sharing surface:** one tap on an item they choose, shared with everyone who can open the app. A privacy-sensitive domain on its own (a journal, a health tracker) is not that ask: it gets the two controls from _Two controls_ above, off until the person says so. Keep publish opt-in and one tap for user-selected items. **Every app that is neither privacy-sensitive nor an only-me ask ships with a sharing path already in it: either an always-on shared channel its records land on, or a publish control.** **An open-nature app — an image or meme wall, a bingo card, a party game — puts its records on an always-on `grant.public` channel from the first rules it ships, builds no publish step, and admits anonymous readers**, so everyone with the link sees what people make the moment they make it. The always-on channel is the least-surprise shape for anything playful and low-stakes (an image or meme wall, a group list, a guestbook): a `grant.public` channel, a members' channel, or an `audience` arm, so a thing someone makes is seen the moment they make it. The publish control is the shape for work people draft first: the per-item channel whose read-grant flips from a `visibility` field, behind one tap. A publish control that names its destination — "Publish to the wall", "Share to the group" — is the best shape of the second, not a requirement. An ordinary app with neither is a bug: every record lands on a channel only its writer can read, and nobody asked for that.
|
|
506
534
|
|
|
507
535
|
**A share control is a link only where the record's channel admits anonymous readers; everywhere else it is the person picker, which takes a handle or an email and shows the invite as pending until they arrive.** A link — Copy link, the browser's share sheet, a QR code — is a share only when the person who opens it can read the thing: a record on a `grant.public` channel, always on or flipped by its `visibility` field. A record whose channel is reached only by grants is unreadable through a link, so sharing it means letting a person in: the `HandleInput` picker writes the share doc that grants their handle, exactly as the buddy picker above does, and the same picker takes an email address for someone who is not on the platform yet — it mails them an invite and hands the app an opaque `invite:<id>` subject to store as the invitee, which binds to their handle when they arrive, so the app never holds the address and `ViewerTag` shows them as invited until then and by their handle after. The one link that works on a grant-only thing is the one that lands on a `request` doc: it invites a knock, not a read. An app with a publish control changes the share control's shape with the record's state — the picker before the flip, the link after it.
|
|
508
536
|
|
|
@@ -512,6 +540,7 @@ Every item a visitor creates lands on their own `user:<handle>` channel, private
|
|
|
512
540
|
```js partial
|
|
513
541
|
export function pieces(doc, oldDoc, user, ctx) {
|
|
514
542
|
if (!user) throw { forbidden: "sign in" };
|
|
543
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
515
544
|
|
|
516
545
|
// A piece: a public object anyone proposes; everyone can read and adopt it.
|
|
517
546
|
if (doc.type === "piece") {
|
|
@@ -641,6 +670,7 @@ This example shows the full round-trip — access.js declares channels and grant
|
|
|
641
670
|
```js
|
|
642
671
|
export function announcements(doc, oldDoc, user, ctx) {
|
|
643
672
|
if (!user) throw { forbidden: "sign in" };
|
|
673
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
644
674
|
|
|
645
675
|
if (doc.type === "channel") {
|
|
646
676
|
ctx.requireRole("owner");
|
|
@@ -802,6 +832,7 @@ Channels everyone can read, and any signed-in user can post to. The channel doc
|
|
|
802
832
|
```js partial
|
|
803
833
|
export function chat(doc, oldDoc, user, ctx) {
|
|
804
834
|
if (!user) throw { forbidden: "sign in" };
|
|
835
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
805
836
|
|
|
806
837
|
if (doc.type === "channel") {
|
|
807
838
|
ctx.requireRole("owner");
|
|
@@ -871,6 +902,7 @@ Reach for this by default, and certainly whenever the prompt says **invite, join
|
|
|
871
902
|
```js partial
|
|
872
903
|
export default function (doc, oldDoc, user, ctx) {
|
|
873
904
|
if (!user) throw { forbidden: "sign in" };
|
|
905
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
874
906
|
const ch = (id) => `list:${id}`;
|
|
875
907
|
|
|
876
908
|
if (doc.type === "list") {
|
|
@@ -896,6 +928,9 @@ export default function (doc, oldDoc, user, ctx) {
|
|
|
896
928
|
// Any current member invites a peer by handle — grants them the list channel.
|
|
897
929
|
// Route the share doc to the list channel itself (every member already holds
|
|
898
930
|
// it), so members see who was added without a second channel to grant.
|
|
931
|
+
// listId is immutable too, so only a member of the list a share is already on
|
|
932
|
+
// changes it — a member of list X never rewrites list Y's share as one on X.
|
|
933
|
+
if (oldDoc && oldDoc.listId !== doc.listId) throw { forbidden: "a share stays on its list" };
|
|
899
934
|
ctx.requireAccess(ch(doc.listId));
|
|
900
935
|
return { channels: [ch(doc.listId)], grant: { users: { [doc.invitee]: [ch(doc.listId)] } } };
|
|
901
936
|
}
|
|
@@ -960,6 +995,7 @@ Reach for this variant whenever the app is built around a *thing* people come ba
|
|
|
960
995
|
```js partial
|
|
961
996
|
export function boards(doc, oldDoc, user, ctx) {
|
|
962
997
|
if (!user?.userHandle) throw { forbidden: "sign in" };
|
|
998
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
963
999
|
const safeId = (id) => {
|
|
964
1000
|
if (typeof id !== "string" || !/^[A-Za-z0-9_-]+$/.test(id)) throw { forbidden: "bad id" };
|
|
965
1001
|
return id;
|
|
@@ -1143,6 +1179,7 @@ That bounds what the build's own closing line says: the rules and the screen it
|
|
|
1143
1179
|
```js partial
|
|
1144
1180
|
export function chat(doc, oldDoc, user, ctx) {
|
|
1145
1181
|
if (!user) throw { forbidden: "authentication required" };
|
|
1182
|
+
if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
|
|
1146
1183
|
|
|
1147
1184
|
if (doc.type === "channel-meta") {
|
|
1148
1185
|
if (doc.ownerHandle !== user.userHandle) throw { forbidden: "not owner" };
|
package/llms/use-viewer.md
CHANGED
|
@@ -387,4 +387,4 @@ async function addMember(handle) {
|
|
|
387
387
|
{canInvite && <HandleInput onChange={addMember} placeholder="Add a friend…" />}
|
|
388
388
|
```
|
|
389
389
|
|
|
390
|
-
Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app the person asked to keep to themselves — "just for me", "no sharing"
|
|
390
|
+
Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app the person asked to keep to themselves — "just for me", "no sharing" — builds no picker: each person keeps their own record tree and shares a chosen item with its publish step, and the records stay shaped so a picker can join later. Everywhere else the picker belongs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "14.3.
|
|
3
|
+
"version": "14.3.86",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"exports": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@adviser/cement": "~0.5.34",
|
|
42
|
-
"@vibes.diy/api-types": "14.3.
|
|
43
|
-
"@vibes.diy/call-ai-v2": "14.3.
|
|
44
|
-
"@vibes.diy/identity": "14.3.
|
|
45
|
-
"@vibes.diy/use-vibes-types": "14.3.
|
|
42
|
+
"@vibes.diy/api-types": "14.3.86",
|
|
43
|
+
"@vibes.diy/call-ai-v2": "14.3.86",
|
|
44
|
+
"@vibes.diy/identity": "14.3.86",
|
|
45
|
+
"@vibes.diy/use-vibes-types": "14.3.86",
|
|
46
46
|
"arktype": "~2.2.6",
|
|
47
47
|
"json-schema-faker": "~0.6.3"
|
|
48
48
|
},
|
|
@@ -54,7 +54,7 @@ A value the person already has in mind stays a direct control when setting or ad
|
|
|
54
54
|
- For an app with the callai skill, let an example chip put an example sentence into the same intake box. Submitting it runs the same parser and automatic save path as the person's own words. Extraction of the person's facts into structured records is a default use of callAI, including when the request describes an ordinary useful app. Reserve additional analysis (such as sentiment scoring or inferred tags) for a feature the person requests. A message board can save the person's message directly when its text already is the record.{{DEMO_DATA}}
|
|
55
55
|
|
|
56
56
|
{{CONCATENATED_LLMS}}
|
|
57
|
-
IMPORTANT: Your main file is `App.jsx` (the React component). This is the **first turn** — `App.jsx` does not exist yet, so write the whole thing at once: emit the complete, working `App.jsx` as a single full-file block (every feature wired, hooks and data in place — not a stub, not a placeholder shell), then the app's `access.js` (every app ships one
|
|
57
|
+
IMPORTANT: Your main file is `App.jsx` (the React component). This is the **first turn** — `App.jsx` does not exist yet, so write the whole thing at once: emit the complete, working `App.jsx` as a single full-file block (every feature wired, hooks and data in place — not a stub, not a placeholder shell), then the app's `access.js` (every app ships one, an app the person keeps to themselves included), then the companion feature files (each as its own complete full-file block — splitting features out is the default for any app with more than a couple of them), then `backend.js` when the app needs server work, then a `seed.json` block with the app's launch content — the user's concrete example data when the prompt gave you some, otherwise the vivid seeded world described below, on every first build of an app that holds records (specified or open-ended, shared or private-by-nature, the owner's catalog or the person's own entries: the file is the demo the screenshot renders and is never applied to the live app unasked, so a specified request still opens empty and ready for the person's first entry); only a stateless toy that stores nothing skips it. That is the entire first turn — the whole app: one `App.jsx` block, its `access.js` when it has one, companion feature files if any, `backend.js` when it needs one, then `seed.json` (order: App.jsx → access.js when present → companion files → backend.js → seed.json). Do NOT split the build into a scaffold plus edits, and do NOT use `SEARCH`/`REPLACE` on this turn; that targeted small-edit format is only for follow-up turns, once `App.jsx` already exists.
|
|
58
58
|
|
|
59
59
|
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.
|
|
60
60
|
|
|
@@ -105,7 +105,7 @@ Every feature you described above should work when this one block lands. Don't l
|
|
|
105
105
|
```
|
|
106
106
|
- **Load Google Fonts with `&display=swap` (or `&display=optional`), never `&display=block`.** Append it to the Fonts URL so text paints immediately in a fallback instead of staying invisible for seconds on slow connections (flash of invisible text) — e.g. `https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap`.
|
|
107
107
|
- **The bottom-right corner belongs to the platform — never pin your own control there.** The Vibes Switch (the logo) floats over every app in that corner, so a `fixed` element anchored to both `bottom` and `right` lands underneath it: no floating add/compose button, no chat bubble, no scroll-to-top disc in that spot. Anchor a floating action bottom-left or bottom-center instead, or fold it into the layout — a header button, or a full-width sticky bar (the platform already reserves scroll clearance below your app for one).
|
|
108
|
-
**This turn writes the app's `access.js` with the app** — the rules that decide who sees and changes each record
|
|
108
|
+
**This turn writes the app's `access.js` with the app, every app** — the rules that decide who sees and changes each record: an app the person keeps to themselves gives each signed-in person their own record tree keyed on the writer's handle, with a publish step that shares a chosen item with everyone who can open the app, and an ask for no access control gets rules that admit everyone; the access skill doc carries its emit format, placement and worked examples, and the file ends with its one plain-language `// Summary:` line saying what people can do with each record. When the app needs server work (a schedule, an outside read, a notification), this turn writes its `backend.js` too, in the format the backend skill doc teaches. UI copy names the sharing those rules give: "only you can see this" belongs on records the rules keep to their writer, and an open board says everyone with the link can see it. Gate every write surface on `useVibe(dbName).can` — the liveness gate every app carries. The rules and the record shapes they key on are plumbing, with nothing on screen of their own. The object list is part of that plumbing: `App.jsx` reads the objects by their `type`, which returns the ones the person made and the ones a membership lets them into, and marks each shared one with `ViewerTag` for its maker — in the same list, or in a section that renders once there is at least one; opening a shared object reads and writes its records with that object's id, each write gated on `can`. The invite screen — the platform's `HandleInput` writing a membership into the person's object — joins this turn's `App.jsx` when the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name; every other first build offers it as one of the closing `▸` options, in the app's own words ("Let a friend add pages to your sketchbook").
|
|
109
109
|
|
|
110
110
|
After your last code block, add a short 1-2 sentence message describing the core workflow the app supports.
|
|
111
111
|
|
package/system-prompt-initial.md
CHANGED
|
@@ -105,11 +105,11 @@ The sandbox serves raw ES modules, so `App.jsx` can import local `.js`/`.jsx` fi
|
|
|
105
105
|
|
|
106
106
|
Target ~40–60 lines. The shell should look like a real app with empty sections, not a blank page.
|
|
107
107
|
|
|
108
|
-
**Then `access.js`.** Right after the shell, emit the app's complete `access.js` (see below) — every app ships one
|
|
108
|
+
**Then `access.js`.** Right after the shell, emit the app's complete `access.js` (see below) — every app ships one, an app the person keeps to themselves included — so every feature edit writes doc types the rules already admit.
|
|
109
109
|
|
|
110
110
|
**Step 2 — Feature edits.** Wire each feature with SEARCH/REPLACE edits. Each edit gets exactly one prose line (≤25 words) before it. Wire hooks, data, handlers, and `useFireproof` 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.
|
|
111
111
|
|
|
112
|
-
**This turn writes the app's `access.js` with the app** — the rules that decide who sees and changes each record
|
|
112
|
+
**This turn writes the app's `access.js` with the app, every app** — the rules that decide who sees and changes each record: an app the person keeps to themselves gives each signed-in person their own record tree keyed on the writer's handle, with a publish step that shares a chosen item with everyone who can open the app, and an ask for no access control gets rules that admit everyone; the access skill doc carries its emit format, placement and worked examples, and the file ends with its one plain-language `// Summary:` line saying what people can do with each record. When the app needs server work (a schedule, an outside read, a notification), this turn writes its `backend.js` too, in the format the backend skill doc teaches. UI copy names the sharing those rules give: "only you can see this" belongs on records the rules keep to their writer, and an open board says everyone with the link can see it. Gate every write surface on `useVibe(dbName).can` — the liveness gate every app carries. The rules and the record shapes they key on are plumbing, with nothing on screen of their own. The object list is part of that plumbing: `App.jsx` reads the objects by their `type`, which returns the ones the person made and the ones a membership lets them into, and marks each shared one with `ViewerTag` for its maker — in the same list, or in a section that renders once there is at least one; opening a shared object reads and writes its records with that object's id, each write gated on `can`. The invite screen — the platform's `HandleInput` writing a membership into the person's object — joins this turn's `App.jsx` when the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name; every other first build offers it as one of the closing `▸` options, in the app's own words ("Let a friend add pages to your sketchbook").
|
|
113
113
|
|
|
114
114
|
After your last code block, add a short 1-2 sentence message describing the core workflow the app supports.
|
|
115
115
|
|
package/system-prompt.md
CHANGED
|
@@ -111,7 +111,7 @@ The sandbox serves raw ES modules, so `App.jsx` can import local `.js`/`.jsx` fi
|
|
|
111
111
|
- **Load Google Fonts with `&display=swap` (or `&display=optional`), never `&display=block`.** Append it to the Fonts URL so text paints immediately in a fallback instead of staying invisible for seconds on slow connections (flash of invisible text) — e.g. `https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap`.
|
|
112
112
|
- **The bottom-right corner belongs to the platform — never pin your own control there.** The Vibes Switch (the logo) floats over every app in that corner, so a `fixed` element anchored to both `bottom` and `right` lands underneath it: no floating add/compose button, no chat bubble, no scroll-to-top disc in that spot. Anchor a floating action bottom-left or bottom-center instead, or fold it into the layout — a header button, or a full-width sticky bar (the platform already reserves scroll clearance below your app for one).
|
|
113
113
|
|
|
114
|
-
**Every app gets an `access.js
|
|
114
|
+
**Every app gets an `access.js`**, an app the person keeps to themselves included, and the access skill doc carries the emit format, placement, and worked examples whenever a turn writes one. When an edit adds `access.js` to an app that has none, the edit owes the complete `access.js` first, then only the `App.jsx` SEARCH/REPLACE edits the rules need — any ungated write surface, and the invite screen when the request names other people, sharing or a group; every other file stays untouched, and `seed.json` is re-emitted only when it does not fit the rules — a record lacking a field they key on, a type they do not authorize, or a child ordered before the parent that grants its channel. On any turn, emit `access.js` before any `App.jsx` edit that writes a doc type it gates. Gate every write surface on `useVibe(dbName).can` regardless of whether the app has an `access.js` yet. When the instruction names a person to let in, the build's part is the rule and the invite handler that write a membership record under an id fixed by the object and the handle, as the access skill doc's _A person the ask names is let in by a record_ shows — the record for that person is saved through that same shape once the rules are live, so the files carry the shape and the database carries the name.
|
|
115
115
|
|
|
116
116
|
**Keep `access.js` in step with the data model.** The app's current `access.js` and `seed.json` are always in view (the `APP_STATE` block). When an edit adds a new written doc `type` — or a field the rules key on — update `access.js` in the same reply so the new writes are allowed; an existing terminal branch that rejects unknown types will reject them at runtime. Whenever the function changes, rewrite its closing `// Summary:` line in the same block so it still says, in plain words, who can see and change what and what a new record carries.
|
|
117
117
|
|
|
@@ -560,7 +560,7 @@ docs in your database, and never build follow UI state machines.
|
|
|
560
560
|
|
|
561
561
|
**`access.js` must be one self-contained exported function.** The server extracts and evaluates ONLY the exported function's source — module-scope code (a `const CHANNEL = "team:hub"` above the function, a shared helper, an import) does not exist at runtime, and referencing it throws `ReferenceError: 'CHANNEL' is not defined` on every write, breaking the app. Declare constants and helpers inside the function body, as every worked example here does.
|
|
562
562
|
|
|
563
|
-
**Every app gets record rules, and the group shape is the default.** Whatever the prompt's pronouns, build the app so a second person can be let in later: a sourdough feeding log one person keeps is the same shape with one object in it, and a baking friend they invite next month is a grant on an object that already exists. The rules and record shapes are plumbing with nothing on screen of their own; the invite screen joins the app when the request names other people, sharing, a group or a person by name, and is otherwise one of the closing `▸` options.
|
|
563
|
+
**Every app gets record rules, and the group shape is the default.** Whatever the prompt's pronouns, build the app so a second person can be let in later: a sourdough feeding log one person keeps is the same shape with one object in it, and a baking friend they invite next month is a grant on an object that already exists. The rules and record shapes are plumbing with nothing on screen of their own; the invite screen joins the app when the request names other people, sharing, a group or a person by name, and is otherwise one of the closing `▸` options. An app the person keeps to themselves — "just for me", "keep it single-user", "no sharing" — ships `access.js` too: each signed-in person gets their own record tree on a channel keyed on the writer's own handle, and a publish step shares a chosen item with everyone who can open the app — its members while the app stays private, everyone once the owner opens it up; an ask for "no access control" or "anyone can edit" ships rules that admit everyone. The shape that carries both is author-owned writes on the object's own channel, self-granted at creation: stamp `authorHandle: user.userHandle` on each doc, key the object on its own `creatorHandle`, and let the author check accept the first write, so a signed-in visitor creates from first load with nobody letting them in. Save `ctx.requireAccess(channel)` (and roster / `requireRole` gates) for records that already exist — asking for _pre-existing_ membership to create is what puts "not in channel / request to join" in front of the maker on the very app they just made, or hides the compose input behind a "join" wall. For a tool that wants no sign-in at all, `allowAnonymous: true` in `access.js` opts guests in, and the write surface is then gated the usual way (`ready` for pending identity/access resolution, then `can.create(candidate).ok` for the affordance itself) — once `allowAnonymous` opts them in, the access fn accepts `user === null` and `can.create()` returns the real verdict rather than `authentication required`. The footgun is gating the affordance on `can.create()` _without_ setting `allowAnonymous` — then it stays `authentication required` and the control vanishes for the very guests it's for.
|
|
564
564
|
|
|
565
565
|
**A group-shaped ask — more than one person acting on the same records: several people adding, voting, sharing or joining, or a named group such as a quilting circle, a beekeeping club or a team keeping records together — gives each signed-in person their own object plus a way to invite others into it with the platform's `HandleInput` (from `useViewer()`), and keys its rules on that object's own creator (`doc.creatorHandle === user.userHandle`), which places the app owner exactly where everyone else stands.**
|
|
566
566
|
|
package/themes/fonts.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface VibeFont {
|
|
|
4
4
|
readonly family: string;
|
|
5
5
|
readonly importUrl?: string;
|
|
6
6
|
readonly mono?: boolean;
|
|
7
|
+
readonly mood: string;
|
|
7
8
|
}
|
|
8
9
|
export declare const VIBE_FONTS: readonly VibeFont[];
|
|
9
10
|
export declare function getVibeFont(slug: string | undefined): VibeFont | undefined;
|
package/themes/fonts.js
CHANGED
|
@@ -4,65 +4,76 @@ export const VIBE_FONTS = [
|
|
|
4
4
|
slug: "system",
|
|
5
5
|
label: "System",
|
|
6
6
|
family: SYSTEM_STACK,
|
|
7
|
+
mood: "plain, native, invisible; utilities and quick tools",
|
|
7
8
|
},
|
|
8
9
|
{
|
|
9
10
|
slug: "inter",
|
|
10
11
|
label: "Inter",
|
|
11
12
|
family: `'Inter', ${SYSTEM_STACK}`,
|
|
12
13
|
importUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap",
|
|
14
|
+
mood: "clean, modern, neutral; productivity and dashboards",
|
|
13
15
|
},
|
|
14
16
|
{
|
|
15
17
|
slug: "dm-sans",
|
|
16
18
|
label: "DM Sans",
|
|
17
19
|
family: `'DM Sans', ${SYSTEM_STACK}`,
|
|
18
20
|
importUrl: "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap",
|
|
21
|
+
mood: "friendly low-contrast geometric; everyday apps",
|
|
19
22
|
},
|
|
20
23
|
{
|
|
21
24
|
slug: "nunito",
|
|
22
25
|
label: "Nunito",
|
|
23
26
|
family: `'Nunito', ${SYSTEM_STACK}`,
|
|
24
27
|
importUrl: "https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap",
|
|
28
|
+
mood: "rounded, warm, gentle; cosy and friendly",
|
|
25
29
|
},
|
|
26
30
|
{
|
|
27
31
|
slug: "space-grotesk",
|
|
28
32
|
label: "Space Grotesk",
|
|
29
33
|
family: `'Space Grotesk', ${SYSTEM_STACK}`,
|
|
30
34
|
importUrl: "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap",
|
|
35
|
+
mood: "quirky, techy, contemporary; creative projects",
|
|
31
36
|
},
|
|
32
37
|
{
|
|
33
38
|
slug: "rajdhani",
|
|
34
39
|
label: "Rajdhani",
|
|
35
40
|
family: `'Rajdhani', ${SYSTEM_STACK}`,
|
|
36
41
|
importUrl: "https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;700&display=swap",
|
|
42
|
+
mood: "squared, condensed, sci-fi; scores, stats and gear",
|
|
37
43
|
},
|
|
38
44
|
{
|
|
39
45
|
slug: "bebas-neue",
|
|
40
46
|
label: "Bebas Neue",
|
|
41
47
|
family: `'Bebas Neue', ${SYSTEM_STACK}`,
|
|
42
48
|
importUrl: "https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap",
|
|
49
|
+
mood: "tall condensed capitals only; posters, events, scoreboards",
|
|
43
50
|
},
|
|
44
51
|
{
|
|
45
52
|
slug: "playfair-display",
|
|
46
53
|
label: "Playfair Display",
|
|
47
54
|
family: "'Playfair Display', Georgia, 'Times New Roman', serif",
|
|
48
55
|
importUrl: "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;700&display=swap",
|
|
56
|
+
mood: "elegant high-contrast serif; editorial, food, celebrations",
|
|
49
57
|
},
|
|
50
58
|
{
|
|
51
59
|
slug: "lora",
|
|
52
60
|
label: "Lora",
|
|
53
61
|
family: "'Lora', Georgia, 'Times New Roman', serif",
|
|
54
62
|
importUrl: "https://fonts.googleapis.com/css2?family=Lora:wght@400;500;700&display=swap",
|
|
63
|
+
mood: "warm calligraphic serif; journals, stories, reading",
|
|
55
64
|
},
|
|
56
65
|
{
|
|
57
66
|
slug: "source-serif-4",
|
|
58
67
|
label: "Source Serif",
|
|
59
68
|
family: "'Source Serif 4', Georgia, 'Times New Roman', serif",
|
|
60
69
|
importUrl: "https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;500;700&display=swap",
|
|
70
|
+
mood: "sober reading serif; notes, documents, study",
|
|
61
71
|
},
|
|
62
72
|
{
|
|
63
73
|
slug: "helvetica",
|
|
64
74
|
label: "Helvetica",
|
|
65
75
|
family: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
|
76
|
+
mood: "classic Swiss, neutral, timeless; lists and schedules",
|
|
66
77
|
},
|
|
67
78
|
{
|
|
68
79
|
slug: "jetbrains-mono",
|
|
@@ -70,6 +81,7 @@ export const VIBE_FONTS = [
|
|
|
70
81
|
family: "'JetBrains Mono', ui-monospace, Menlo, monospace",
|
|
71
82
|
importUrl: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap",
|
|
72
83
|
mono: true,
|
|
84
|
+
mood: "monospaced, technical, precise; code, logs and developer tools",
|
|
73
85
|
},
|
|
74
86
|
{
|
|
75
87
|
slug: "vt323",
|
|
@@ -77,6 +89,7 @@ export const VIBE_FONTS = [
|
|
|
77
89
|
family: "'VT323', ui-monospace, Menlo, monospace",
|
|
78
90
|
importUrl: "https://fonts.googleapis.com/css2?family=VT323&display=swap",
|
|
79
91
|
mono: true,
|
|
92
|
+
mood: "retro pixel terminal, monospaced; games, arcade, nostalgia",
|
|
80
93
|
},
|
|
81
94
|
];
|
|
82
95
|
const BY_SLUG = new Map(VIBE_FONTS.map((f) => [f.slug, f]));
|
package/themes/fonts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fonts.js","sourceRoot":"","sources":["../../jsr/themes/fonts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fonts.js","sourceRoot":"","sources":["../../jsr/themes/fonts.ts"],"names":[],"mappings":"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"}
|