@vibes.diy/prompts 14.3.117 → 14.3.119
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/package.json +5 -5
- package/system-prompt-initial-oneshot.md +162 -12
- package/system-prompt-initial.md +162 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "14.3.
|
|
3
|
+
"version": "14.3.119",
|
|
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.119",
|
|
43
|
+
"@vibes.diy/call-ai-v2": "14.3.119",
|
|
44
|
+
"@vibes.diy/identity": "14.3.119",
|
|
45
|
+
"@vibes.diy/use-vibes-types": "14.3.119",
|
|
46
46
|
"arktype": "~2.2.6",
|
|
47
47
|
"json-schema-faker": "~0.6.3"
|
|
48
48
|
},
|
|
@@ -121,22 +121,172 @@ After your last code block, add a short 1-2 sentence message describing the core
|
|
|
121
121
|
Use these import statements verbatim at the top of the `App.jsx` block:
|
|
122
122
|
|
|
123
123
|
{{IMPORT_STATEMENTS}}
|
|
124
|
+
### The object graph — one saved shape from the first screen through sharing
|
|
125
|
+
|
|
126
|
+
Make records tangible: saved rows or cards let people inspect and correct the same documents that sentence intake and direct editing write. Give every user-authored record `authorHandle: me.userHandle`, every shared object `creatorHandle: me.userHandle`, and every child its displayed parent's id. These fields give this turn's `access.js` and later sharing changes the keys they need from the first turn.
|
|
127
|
+
|
|
128
|
+
Here is one connected example for a weaving studio. Adapt the nouns and fields to the person's app. A `loom` is the stable shared object, a `weaving` is content on that loom, and a `bobbin` records one person's choice for one weaving. The database name is `"studio"` in both `useFireproof`, `useVibe`, and the named access export. The app establishes the person's own default loom before mounting its child controls; the first screen opens inside that loom; readable looms, including invited ones, are selected from the type query. The selected id is resolved against current readable results each render, so a loom that stops being readable leaves both picker and content view.
|
|
129
|
+
|
|
130
|
+
```jsx partial
|
|
131
|
+
function Studio() {
|
|
132
|
+
const { database, useLiveQuery } = useFireproof("studio");
|
|
133
|
+
const { me, can, ready } = useVibe("studio");
|
|
134
|
+
const { requestLogin, ViewerTag } = useViewer();
|
|
135
|
+
const [pickedId, setPickedId] = React.useState();
|
|
136
|
+
const [establishedId, setEstablishedId] = React.useState();
|
|
137
|
+
const [initializationError, setInitializationError] = React.useState("");
|
|
138
|
+
const { docs: looms } = useLiveQuery("type", { key: "loom" });
|
|
139
|
+
const ownId = me?.userHandle ? `default-${me.userHandle}` : undefined;
|
|
140
|
+
const own = ownId && {
|
|
141
|
+
_id: ownId, type: "loom", name: "My weaving studio",
|
|
142
|
+
creatorHandle: me.userHandle, authorHandle: me.userHandle, isPublic: false,
|
|
143
|
+
};
|
|
144
|
+
// The person's own loom comes first, saved or not yet saved; an invited loom opens when picked.
|
|
145
|
+
const loom = looms.find((doc) => doc._id === pickedId)
|
|
146
|
+
?? looms.find((doc) => doc._id === ownId) ?? own ?? looms[0];
|
|
147
|
+
// A stable parent carries the creator's lasting grant, even with zero children.
|
|
148
|
+
// Await this before content OR an invitation, whichever comes first.
|
|
149
|
+
const ensureDefault = async (id) => {
|
|
150
|
+
if (!ready || id !== ownId) return;
|
|
151
|
+
const { docs } = await database.query("_id", { key: id });
|
|
152
|
+
if (docs.length === 0) {
|
|
153
|
+
if (!can.create(own).ok) throw new Error("Studio creation needs permission");
|
|
154
|
+
await database.put(own);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
if (!ready || !ownId) return;
|
|
159
|
+
let current = true;
|
|
160
|
+
ensureDefault(ownId).then(() => {
|
|
161
|
+
if (current) { setEstablishedId(ownId); setInitializationError(""); }
|
|
162
|
+
}).catch(() => {
|
|
163
|
+
if (current) setInitializationError("Your studio needs permission to open.");
|
|
164
|
+
});
|
|
165
|
+
return () => { current = false; };
|
|
166
|
+
}, [ready, ownId, can, database]);
|
|
167
|
+
// Hooks stay above this hold. Identity resolves before sign-in copy or controls.
|
|
168
|
+
if (!ready) return <p role="status">Checking studio access…</p>;
|
|
169
|
+
const parentEstablished = loom && (loom._id !== ownId
|
|
170
|
+
|| looms.some((doc) => doc._id === ownId) || establishedId === ownId);
|
|
171
|
+
return <>
|
|
172
|
+
{looms.length > 0 && <Field label="Studio">
|
|
173
|
+
{(id, describedBy) => <Select id={id} describedBy={describedBy} value={loom?._id ?? ""}
|
|
174
|
+
onChange={setPickedId} options={looms.map((doc) => ({ value: doc._id, label: doc.name }))} />}
|
|
175
|
+
</Field>}
|
|
176
|
+
{loom?.creatorHandle && loom.creatorHandle !== me?.userHandle &&
|
|
177
|
+
<p>Shared by <ViewerTag userHandle={loom.creatorHandle} /></p>}
|
|
178
|
+
{initializationError && <p role="status">{initializationError}</p>}
|
|
179
|
+
{loom && !parentEstablished && !initializationError && <p role="status">Opening your studio…</p>}
|
|
180
|
+
{parentEstablished && <LoomContents key={loom._id} loom={loom} ensureDefault={ensureDefault} />}
|
|
181
|
+
{!me && <Button type="button" variant="quiet" onClick={() => requestLogin()}>Sign in to keep a studio of your own</Button>}
|
|
182
|
+
</>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function LoomContents({ loom, ensureDefault }) {
|
|
186
|
+
const { database, useLiveQuery } = useFireproof("studio");
|
|
187
|
+
const { me, can, ready } = useVibe("studio");
|
|
188
|
+
const [title, setTitle] = React.useState("");
|
|
189
|
+
// A parent query also returns supporting records; choose content before rendering,
|
|
190
|
+
// counting, correcting, or passing records to Ask.
|
|
191
|
+
const { docs } = useLiveQuery("loomId", { key: loom._id });
|
|
192
|
+
const weavings = docs.filter((doc) => doc.type === "weaving");
|
|
193
|
+
const draft = {
|
|
194
|
+
type: "weaving", loomId: loom._id, authorHandle: me?.userHandle,
|
|
195
|
+
title: title.trim(), finished: false,
|
|
196
|
+
};
|
|
197
|
+
const verdict = can.create(draft);
|
|
198
|
+
const allowed = ready && verdict.ok;
|
|
199
|
+
const add = async (event) => {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
if (!ready || !draft.title || !can.create(draft).ok) return;
|
|
202
|
+
await ensureDefault(loom._id);
|
|
203
|
+
await database.put(draft); // the exact value the control checked
|
|
204
|
+
setTitle("");
|
|
205
|
+
};
|
|
206
|
+
return <section>
|
|
207
|
+
<h1>{loom.name}</h1>
|
|
208
|
+
<form onSubmit={add}>
|
|
209
|
+
<Field label="Weaving name">
|
|
210
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} value={title} onChange={setTitle} />}
|
|
211
|
+
</Field>
|
|
212
|
+
<Button type="submit" disabled={!allowed || !draft.title}>Add weaving</Button>
|
|
213
|
+
</form>
|
|
214
|
+
{ready && me && !verdict.ok && !verdict.pending &&
|
|
215
|
+
<p role="status">Weavers invited to this studio can add weavings here.</p>}
|
|
216
|
+
{weavings.map((weaving) => <WeavingRow key={weaving._id} weaving={weaving} />)}
|
|
217
|
+
</section>;
|
|
218
|
+
}
|
|
124
219
|
|
|
125
|
-
|
|
220
|
+
function WeavingRow({ weaving }) {
|
|
221
|
+
const { database, useLiveQuery } = useFireproof("studio");
|
|
222
|
+
const { me, can, ready } = useVibe("studio");
|
|
223
|
+
const [editedTitle, setEditedTitle] = React.useState(weaving.title);
|
|
224
|
+
React.useEffect(() => { setEditedTitle(weaving.title); }, [weaving.title]);
|
|
225
|
+
// One bobbin per person per weaving: construct its deterministic id BEFORE
|
|
226
|
+
// asking can.create, with all the fields that the access rule will inspect.
|
|
227
|
+
const bobbin = {
|
|
228
|
+
_id: `bobbin-${me?.userHandle}-${weaving._id}`, type: "bobbin",
|
|
229
|
+
loomId: weaving.loomId, weavingId: weaving._id,
|
|
230
|
+
authorHandle: me?.userHandle,
|
|
231
|
+
};
|
|
232
|
+
// Read the saved choice back so the control shows it and removes it with can.delete.
|
|
233
|
+
const { docs: chosen } = useLiveQuery("_id", { key: bobbin._id });
|
|
234
|
+
const saved = chosen[0];
|
|
235
|
+
const choiceAllowed = ready && (saved ? can.delete(saved).ok : can.create(bobbin).ok);
|
|
236
|
+
const next = { ...weaving, title: editedTitle.trim() };
|
|
237
|
+
const editAllowed = ready && can.edit(next).ok;
|
|
238
|
+
const finishedNext = { ...weaving, finished: !weaving.finished };
|
|
239
|
+
const toggleChoice = async () => {
|
|
240
|
+
if (!ready) return;
|
|
241
|
+
if (saved) {
|
|
242
|
+
if (!can.delete(saved).ok) return;
|
|
243
|
+
await database.del(saved._id);
|
|
244
|
+
} else {
|
|
245
|
+
if (!can.create(bobbin).ok) return;
|
|
246
|
+
await database.put(bobbin); // the exact value the control checked
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const rename = async () => {
|
|
250
|
+
if (!ready || !next.title || !can.edit(next).ok) return;
|
|
251
|
+
await database.put(next);
|
|
252
|
+
};
|
|
253
|
+
const finish = async () => {
|
|
254
|
+
if (!ready || !can.edit(finishedNext).ok) return;
|
|
255
|
+
await database.put(finishedNext);
|
|
256
|
+
};
|
|
257
|
+
const remove = async () => {
|
|
258
|
+
if (!ready || !can.delete(weaving).ok) return;
|
|
259
|
+
await database.del(weaving._id);
|
|
260
|
+
};
|
|
261
|
+
return <article>
|
|
262
|
+
<span>{weaving.title}</span>
|
|
263
|
+
<Field label="Weaving name">
|
|
264
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} value={editedTitle} onChange={setEditedTitle} />}
|
|
265
|
+
</Field>
|
|
266
|
+
<Button type="button" size="sm" disabled={!editAllowed || !next.title} onClick={rename}>Save name</Button>
|
|
267
|
+
<Button type="button" size="sm" variant={saved ? "solid" : "quiet"} disabled={!choiceAllowed} onClick={toggleChoice}>
|
|
268
|
+
{saved ? "Chosen — tap to unchoose" : "Choose this weaving"}
|
|
269
|
+
</Button>
|
|
270
|
+
<Button type="button" size="sm" variant="quiet" disabled={!ready || !can.edit(finishedNext).ok} onClick={finish}>
|
|
271
|
+
{weaving.finished ? "Keep weaving" : "Mark finished"}
|
|
272
|
+
</Button>
|
|
273
|
+
<Button type="button" size="sm" variant="quiet" tone="down" disabled={!ready || !can.delete(weaving).ok} onClick={remove}>
|
|
274
|
+
Delete weaving
|
|
275
|
+
</Button>
|
|
276
|
+
{ready && me && !editAllowed && <p role="status">The weaver who added this one can rename or finish it.</p>}
|
|
277
|
+
</article>;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
126
280
|
|
|
127
|
-
The
|
|
281
|
+
The control and its handler check the **same complete candidate** that `database.put` persists, including `_id` whenever a rule keys on it. Direct corrections and sentence corrections build the next record by spreading the saved one, so its `_id`, author and parent stay fixed, check `can.edit(next)`, and save that value. `can.edit` and `can.delete` evaluate the record they are given as both its old and new version: they say whether this person may write that record, and the server compares against the saved version to enforce the fixed fields, which the spread keeps true. This example's rules key every write on a signed-in handle, so a signed-out visitor reads what they can and the ask comes after it, offering a studio of their own; when an app's records work for a signed-out visitor, the ask waits until they have made something worth keeping (see `use-viewer.md`). Loading, signing in and a signed-in person's resolved refusal (`!ok && !pending`) each get one fitting line in the app's words (see `access.md`). A one-per-person choice reads its saved record back by its deterministic id: unchosen, the control checks `can.create` on the full candidate and puts it; chosen, it checks `can.delete` on the saved record and deletes it. The rule also admits an identical rewrite, since a second device can write the same id.
|
|
128
282
|
|
|
129
|
-
|
|
130
|
-
- **Stamp the writer on every user-authored doc.** Put the author's handle in the canonical field `authorHandle: me?.userHandle` (`me` from `useVibe(dbName)` — the client-side identity) on each doc a person creates, even when the UI never displays it. "Only the author can edit" keys on the author the doc records.
|
|
131
|
-
- **Carry the parent id on every child doc.** A comment, item, reaction, or member doc stores the id of the thing it belongs to (`listId`, `boardId`, `postId`, `rideId`, …), written at create and left alone afterwards. Queries need it, and the sharing rules route reads/writes off it.
|
|
132
|
-
- **Stamp the creator on the object itself.** The list, board, trip or space a person makes carries `creatorHandle: me.userHandle` — the child docs' `authorHandle` says who wrote each record, and this says whose object they are in.
|
|
133
|
-
- **List objects by their type.** A query on `type` returns every object the rules let the person read — the ones they made and the ones a membership lets them into — so the list is whole from the first build, and `creatorHandle` says who made each one.
|
|
134
|
-
- **Give each person a default object derived from their own handle.** `` `default-${me.userHandle}` `` is the id a child doc names when the person picked no object, so a brand-new visitor's first write lands with nothing for them to create first. It needs no document of its own, and an app one person keeps for themselves is simply this with one object in it. The person arrives inside that default object: the first screen is the app at work — the roster, the shelf, the board — with the object's name a setting that starts sensible ("My roster") and is theirs to change, and the form for a second object sits beside the list once there is one to list. On arrival the object's name is the app's name, so the header carries that name once: the title is the object's name until the person renames it or makes a second object, and only then does the object's name earn a line of its own.
|
|
135
|
-
- **Deterministic `_id`s for one-per-user-per-thing docs.** A single vote/RSVP/favorite per person per parent uses an `_id` that embeds the handle and parent — `favorite-<handle>-<parentId>` — so re-writes replace rather than duplicate, and one-per-user rules have something to key on.
|
|
136
|
-
- **Store sharing choices as doc fields, not client state.** Any "share with friends" / "make public" toggle the UI offers is persisted as a boolean on the doc (`isPublic`, `sharedWithFollowers`), never held only in React state — the access rules read the field, not the UI.
|
|
137
|
-
- **Keep secrets and contact details behind the rules.** A doc type holding a capability token, a phone number or other contact details is one this turn's `access.js` keeps to the people it belongs to.
|
|
283
|
+
Write this turn's `access.js` against precisely these shapes: a loom grants its creator the loom channel and its admin channel; content and choices route to their stored `loomId`; membership grants that same loom channel. Keep each record's type, author and parent fixed across edits. A bobbin's `_id` equals `bobbin-<authorHandle>-<weavingId>`; the client takes both parent fields from the displayed weaving, and the rule holds them fixed on subsequent writes. Follow `access.md`'s stable-default-object and membership pattern: read the fixed default id before creating it, preserve existing name/settings, and await that parent before the first content write or invitation. This is ordinary initialization through live access rules, independent of importing example records. The creator's membership survives deletion of their last content row.
|
|
138
284
|
|
|
139
|
-
|
|
285
|
+
- **Keep the opening view useful.** The default id is `default-<handle>` for each person. The person arrives inside that default object: the first screen is the app at work with a sensible name they can change, and a second-object form joins the object list when useful. On arrival the object's name is the app's name, so the header carries that name once; a separate object-name line becomes useful after a rename or a second object. Show the whole readable type-query result in the picker, including objects someone else made; `creatorHandle` identifies their makers.
|
|
286
|
+
- **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + loom._id + ":" + handle, type: "member", loomId: loom._id, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }`. Construct that value after the picker resolves the handle, gate it with `can.create`, await `ensureDefault(loom._id)`, then persist the same value. The `member` branch admits exactly that shape, with creator-admin authority and fixed subject/parent/writer. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When sharing was not asked for, offer inviting as a closing option.
|
|
287
|
+
- **Persist the sharing choice.** A public/follower toggle writes the field its rules read (`isPublic`, `sharedWithFollowers`), with the full edited candidate checked before saving. Its who-can-see line sits beside that control and describes the channel the rules actually share. Follow `access.md` for follower consent and channel granularity.
|
|
288
|
+
- **Protect supporting records.** A membership record containing contact details or a capability token belongs only to its intended readers; choose its private channel accordingly while its grant admits the recipient to the object's channel. Keep contact/token-bearing types distinct from ordinary content and out of content queries and Ask inputs.
|
|
289
|
+
- **Keep examples and the file protocol consistent.** The first build writes the files and privacy rules the surrounding prompt requires. `seed.json` uses these same types, field names and parent ids, with `"@owner"` in writer fields and literal owner-derived default ids from `<app-owner>` where needed. The access rules admit every example record, and the final `// Summary:` accurately describes those rules; private records begin private. Follow the existing attachment/file protocol for uploaded files and their visibility.
|
|
140
290
|
|
|
141
291
|
### The prompt's content is launch content — emit `seed.json`
|
|
142
292
|
|
package/system-prompt-initial.md
CHANGED
|
@@ -125,22 +125,172 @@ After your last code block, add a short 1-2 sentence message describing the core
|
|
|
125
125
|
Use these import statements verbatim at the top of the `App.jsx` block:
|
|
126
126
|
|
|
127
127
|
{{IMPORT_STATEMENTS}}
|
|
128
|
+
### The object graph — one saved shape from the first screen through sharing
|
|
129
|
+
|
|
130
|
+
Make records tangible: saved rows or cards let people inspect and correct the same documents that sentence intake and direct editing write. Give every user-authored record `authorHandle: me.userHandle`, every shared object `creatorHandle: me.userHandle`, and every child its displayed parent's id. These fields give this turn's `access.js` and later sharing changes the keys they need from the first turn.
|
|
131
|
+
|
|
132
|
+
Here is one connected example for a weaving studio. Adapt the nouns and fields to the person's app. A `loom` is the stable shared object, a `weaving` is content on that loom, and a `bobbin` records one person's choice for one weaving. The database name is `"studio"` in both `useFireproof`, `useVibe`, and the named access export. The app establishes the person's own default loom before mounting its child controls; the first screen opens inside that loom; readable looms, including invited ones, are selected from the type query. The selected id is resolved against current readable results each render, so a loom that stops being readable leaves both picker and content view.
|
|
133
|
+
|
|
134
|
+
```jsx partial
|
|
135
|
+
function Studio() {
|
|
136
|
+
const { database, useLiveQuery } = useFireproof("studio");
|
|
137
|
+
const { me, can, ready } = useVibe("studio");
|
|
138
|
+
const { requestLogin, ViewerTag } = useViewer();
|
|
139
|
+
const [pickedId, setPickedId] = React.useState();
|
|
140
|
+
const [establishedId, setEstablishedId] = React.useState();
|
|
141
|
+
const [initializationError, setInitializationError] = React.useState("");
|
|
142
|
+
const { docs: looms } = useLiveQuery("type", { key: "loom" });
|
|
143
|
+
const ownId = me?.userHandle ? `default-${me.userHandle}` : undefined;
|
|
144
|
+
const own = ownId && {
|
|
145
|
+
_id: ownId, type: "loom", name: "My weaving studio",
|
|
146
|
+
creatorHandle: me.userHandle, authorHandle: me.userHandle, isPublic: false,
|
|
147
|
+
};
|
|
148
|
+
// The person's own loom comes first, saved or not yet saved; an invited loom opens when picked.
|
|
149
|
+
const loom = looms.find((doc) => doc._id === pickedId)
|
|
150
|
+
?? looms.find((doc) => doc._id === ownId) ?? own ?? looms[0];
|
|
151
|
+
// A stable parent carries the creator's lasting grant, even with zero children.
|
|
152
|
+
// Await this before content OR an invitation, whichever comes first.
|
|
153
|
+
const ensureDefault = async (id) => {
|
|
154
|
+
if (!ready || id !== ownId) return;
|
|
155
|
+
const { docs } = await database.query("_id", { key: id });
|
|
156
|
+
if (docs.length === 0) {
|
|
157
|
+
if (!can.create(own).ok) throw new Error("Studio creation needs permission");
|
|
158
|
+
await database.put(own);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
if (!ready || !ownId) return;
|
|
163
|
+
let current = true;
|
|
164
|
+
ensureDefault(ownId).then(() => {
|
|
165
|
+
if (current) { setEstablishedId(ownId); setInitializationError(""); }
|
|
166
|
+
}).catch(() => {
|
|
167
|
+
if (current) setInitializationError("Your studio needs permission to open.");
|
|
168
|
+
});
|
|
169
|
+
return () => { current = false; };
|
|
170
|
+
}, [ready, ownId, can, database]);
|
|
171
|
+
// Hooks stay above this hold. Identity resolves before sign-in copy or controls.
|
|
172
|
+
if (!ready) return <p role="status">Checking studio access…</p>;
|
|
173
|
+
const parentEstablished = loom && (loom._id !== ownId
|
|
174
|
+
|| looms.some((doc) => doc._id === ownId) || establishedId === ownId);
|
|
175
|
+
return <>
|
|
176
|
+
{looms.length > 0 && <Field label="Studio">
|
|
177
|
+
{(id, describedBy) => <Select id={id} describedBy={describedBy} value={loom?._id ?? ""}
|
|
178
|
+
onChange={setPickedId} options={looms.map((doc) => ({ value: doc._id, label: doc.name }))} />}
|
|
179
|
+
</Field>}
|
|
180
|
+
{loom?.creatorHandle && loom.creatorHandle !== me?.userHandle &&
|
|
181
|
+
<p>Shared by <ViewerTag userHandle={loom.creatorHandle} /></p>}
|
|
182
|
+
{initializationError && <p role="status">{initializationError}</p>}
|
|
183
|
+
{loom && !parentEstablished && !initializationError && <p role="status">Opening your studio…</p>}
|
|
184
|
+
{parentEstablished && <LoomContents key={loom._id} loom={loom} ensureDefault={ensureDefault} />}
|
|
185
|
+
{!me && <Button type="button" variant="quiet" onClick={() => requestLogin()}>Sign in to keep a studio of your own</Button>}
|
|
186
|
+
</>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function LoomContents({ loom, ensureDefault }) {
|
|
190
|
+
const { database, useLiveQuery } = useFireproof("studio");
|
|
191
|
+
const { me, can, ready } = useVibe("studio");
|
|
192
|
+
const [title, setTitle] = React.useState("");
|
|
193
|
+
// A parent query also returns supporting records; choose content before rendering,
|
|
194
|
+
// counting, correcting, or passing records to Ask.
|
|
195
|
+
const { docs } = useLiveQuery("loomId", { key: loom._id });
|
|
196
|
+
const weavings = docs.filter((doc) => doc.type === "weaving");
|
|
197
|
+
const draft = {
|
|
198
|
+
type: "weaving", loomId: loom._id, authorHandle: me?.userHandle,
|
|
199
|
+
title: title.trim(), finished: false,
|
|
200
|
+
};
|
|
201
|
+
const verdict = can.create(draft);
|
|
202
|
+
const allowed = ready && verdict.ok;
|
|
203
|
+
const add = async (event) => {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
if (!ready || !draft.title || !can.create(draft).ok) return;
|
|
206
|
+
await ensureDefault(loom._id);
|
|
207
|
+
await database.put(draft); // the exact value the control checked
|
|
208
|
+
setTitle("");
|
|
209
|
+
};
|
|
210
|
+
return <section>
|
|
211
|
+
<h1>{loom.name}</h1>
|
|
212
|
+
<form onSubmit={add}>
|
|
213
|
+
<Field label="Weaving name">
|
|
214
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} value={title} onChange={setTitle} />}
|
|
215
|
+
</Field>
|
|
216
|
+
<Button type="submit" disabled={!allowed || !draft.title}>Add weaving</Button>
|
|
217
|
+
</form>
|
|
218
|
+
{ready && me && !verdict.ok && !verdict.pending &&
|
|
219
|
+
<p role="status">Weavers invited to this studio can add weavings here.</p>}
|
|
220
|
+
{weavings.map((weaving) => <WeavingRow key={weaving._id} weaving={weaving} />)}
|
|
221
|
+
</section>;
|
|
222
|
+
}
|
|
128
223
|
|
|
129
|
-
|
|
224
|
+
function WeavingRow({ weaving }) {
|
|
225
|
+
const { database, useLiveQuery } = useFireproof("studio");
|
|
226
|
+
const { me, can, ready } = useVibe("studio");
|
|
227
|
+
const [editedTitle, setEditedTitle] = React.useState(weaving.title);
|
|
228
|
+
React.useEffect(() => { setEditedTitle(weaving.title); }, [weaving.title]);
|
|
229
|
+
// One bobbin per person per weaving: construct its deterministic id BEFORE
|
|
230
|
+
// asking can.create, with all the fields that the access rule will inspect.
|
|
231
|
+
const bobbin = {
|
|
232
|
+
_id: `bobbin-${me?.userHandle}-${weaving._id}`, type: "bobbin",
|
|
233
|
+
loomId: weaving.loomId, weavingId: weaving._id,
|
|
234
|
+
authorHandle: me?.userHandle,
|
|
235
|
+
};
|
|
236
|
+
// Read the saved choice back so the control shows it and removes it with can.delete.
|
|
237
|
+
const { docs: chosen } = useLiveQuery("_id", { key: bobbin._id });
|
|
238
|
+
const saved = chosen[0];
|
|
239
|
+
const choiceAllowed = ready && (saved ? can.delete(saved).ok : can.create(bobbin).ok);
|
|
240
|
+
const next = { ...weaving, title: editedTitle.trim() };
|
|
241
|
+
const editAllowed = ready && can.edit(next).ok;
|
|
242
|
+
const finishedNext = { ...weaving, finished: !weaving.finished };
|
|
243
|
+
const toggleChoice = async () => {
|
|
244
|
+
if (!ready) return;
|
|
245
|
+
if (saved) {
|
|
246
|
+
if (!can.delete(saved).ok) return;
|
|
247
|
+
await database.del(saved._id);
|
|
248
|
+
} else {
|
|
249
|
+
if (!can.create(bobbin).ok) return;
|
|
250
|
+
await database.put(bobbin); // the exact value the control checked
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const rename = async () => {
|
|
254
|
+
if (!ready || !next.title || !can.edit(next).ok) return;
|
|
255
|
+
await database.put(next);
|
|
256
|
+
};
|
|
257
|
+
const finish = async () => {
|
|
258
|
+
if (!ready || !can.edit(finishedNext).ok) return;
|
|
259
|
+
await database.put(finishedNext);
|
|
260
|
+
};
|
|
261
|
+
const remove = async () => {
|
|
262
|
+
if (!ready || !can.delete(weaving).ok) return;
|
|
263
|
+
await database.del(weaving._id);
|
|
264
|
+
};
|
|
265
|
+
return <article>
|
|
266
|
+
<span>{weaving.title}</span>
|
|
267
|
+
<Field label="Weaving name">
|
|
268
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} value={editedTitle} onChange={setEditedTitle} />}
|
|
269
|
+
</Field>
|
|
270
|
+
<Button type="button" size="sm" disabled={!editAllowed || !next.title} onClick={rename}>Save name</Button>
|
|
271
|
+
<Button type="button" size="sm" variant={saved ? "solid" : "quiet"} disabled={!choiceAllowed} onClick={toggleChoice}>
|
|
272
|
+
{saved ? "Chosen — tap to unchoose" : "Choose this weaving"}
|
|
273
|
+
</Button>
|
|
274
|
+
<Button type="button" size="sm" variant="quiet" disabled={!ready || !can.edit(finishedNext).ok} onClick={finish}>
|
|
275
|
+
{weaving.finished ? "Keep weaving" : "Mark finished"}
|
|
276
|
+
</Button>
|
|
277
|
+
<Button type="button" size="sm" variant="quiet" tone="down" disabled={!ready || !can.delete(weaving).ok} onClick={remove}>
|
|
278
|
+
Delete weaving
|
|
279
|
+
</Button>
|
|
280
|
+
{ready && me && !editAllowed && <p role="status">The weaver who added this one can rename or finish it.</p>}
|
|
281
|
+
</article>;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
130
284
|
|
|
131
|
-
The
|
|
285
|
+
The control and its handler check the **same complete candidate** that `database.put` persists, including `_id` whenever a rule keys on it. Direct corrections and sentence corrections build the next record by spreading the saved one, so its `_id`, author and parent stay fixed, check `can.edit(next)`, and save that value. `can.edit` and `can.delete` evaluate the record they are given as both its old and new version: they say whether this person may write that record, and the server compares against the saved version to enforce the fixed fields, which the spread keeps true. This example's rules key every write on a signed-in handle, so a signed-out visitor reads what they can and the ask comes after it, offering a studio of their own; when an app's records work for a signed-out visitor, the ask waits until they have made something worth keeping (see `use-viewer.md`). Loading, signing in and a signed-in person's resolved refusal (`!ok && !pending`) each get one fitting line in the app's words (see `access.md`). A one-per-person choice reads its saved record back by its deterministic id: unchosen, the control checks `can.create` on the full candidate and puts it; chosen, it checks `can.delete` on the saved record and deletes it. The rule also admits an identical rewrite, since a second device can write the same id.
|
|
132
286
|
|
|
133
|
-
|
|
134
|
-
- **Stamp the writer on every user-authored doc.** Put the author's handle in the canonical field `authorHandle: me?.userHandle` (`me` from `useVibe(dbName)` — the client-side identity) on each doc a person creates, even when the UI never displays it. "Only the author can edit" keys on the author the doc records.
|
|
135
|
-
- **Carry the parent id on every child doc.** A comment, item, reaction, or member doc stores the id of the thing it belongs to (`listId`, `boardId`, `postId`, `rideId`, …), written at create and left alone afterwards. Queries need it, and the sharing rules route reads/writes off it.
|
|
136
|
-
- **Stamp the creator on the object itself.** The list, board, trip or space a person makes carries `creatorHandle: me.userHandle` — the child docs' `authorHandle` says who wrote each record, and this says whose object they are in.
|
|
137
|
-
- **List objects by their type.** A query on `type` returns every object the rules let the person read — the ones they made and the ones a membership lets them into — so the list is whole from the first build, and `creatorHandle` says who made each one.
|
|
138
|
-
- **Give each person a default object derived from their own handle.** `` `default-${me.userHandle}` `` is the id a child doc names when the person picked no object, so a brand-new visitor's first write lands with nothing for them to create first. It needs no document of its own, and an app one person keeps for themselves is simply this with one object in it. The person arrives inside that default object: the first screen is the app at work — the roster, the shelf, the board — with the object's name a setting that starts sensible ("My roster") and is theirs to change, and the form for a second object sits beside the list once there is one to list. On arrival the object's name is the app's name, so the header carries that name once: the title is the object's name until the person renames it or makes a second object, and only then does the object's name earn a line of its own.
|
|
139
|
-
- **Deterministic `_id`s for one-per-user-per-thing docs.** A single vote/RSVP/favorite per person per parent uses an `_id` that embeds the handle and parent — `favorite-<handle>-<parentId>` — so re-writes replace rather than duplicate, and one-per-user rules have something to key on.
|
|
140
|
-
- **Store sharing choices as doc fields, not client state.** Any "share with friends" / "make public" toggle the UI offers is persisted as a boolean on the doc (`isPublic`, `sharedWithFollowers`), never held only in React state — the access rules read the field, not the UI.
|
|
141
|
-
- **Keep secrets and contact details behind the rules.** A doc type holding a capability token, a phone number or other contact details is one this turn's `access.js` keeps to the people it belongs to.
|
|
287
|
+
Write this turn's `access.js` against precisely these shapes: a loom grants its creator the loom channel and its admin channel; content and choices route to their stored `loomId`; membership grants that same loom channel. Keep each record's type, author and parent fixed across edits. A bobbin's `_id` equals `bobbin-<authorHandle>-<weavingId>`; the client takes both parent fields from the displayed weaving, and the rule holds them fixed on subsequent writes. Follow `access.md`'s stable-default-object and membership pattern: read the fixed default id before creating it, preserve existing name/settings, and await that parent before the first content write or invitation. This is ordinary initialization through live access rules, independent of importing example records. The creator's membership survives deletion of their last content row.
|
|
142
288
|
|
|
143
|
-
|
|
289
|
+
- **Keep the opening view useful.** The default id is `default-<handle>` for each person. The person arrives inside that default object: the first screen is the app at work with a sensible name they can change, and a second-object form joins the object list when useful. On arrival the object's name is the app's name, so the header carries that name once; a separate object-name line becomes useful after a rename or a second object. Show the whole readable type-query result in the picker, including objects someone else made; `creatorHandle` identifies their makers.
|
|
290
|
+
- **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + loom._id + ":" + handle, type: "member", loomId: loom._id, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }`. Construct that value after the picker resolves the handle, gate it with `can.create`, await `ensureDefault(loom._id)`, then persist the same value. The `member` branch admits exactly that shape, with creator-admin authority and fixed subject/parent/writer. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When sharing was not asked for, offer inviting as a closing option.
|
|
291
|
+
- **Persist the sharing choice.** A public/follower toggle writes the field its rules read (`isPublic`, `sharedWithFollowers`), with the full edited candidate checked before saving. Its who-can-see line sits beside that control and describes the channel the rules actually share. Follow `access.md` for follower consent and channel granularity.
|
|
292
|
+
- **Protect supporting records.** A membership record containing contact details or a capability token belongs only to its intended readers; choose its private channel accordingly while its grant admits the recipient to the object's channel. Keep contact/token-bearing types distinct from ordinary content and out of content queries and Ask inputs.
|
|
293
|
+
- **Keep examples and the file protocol consistent.** The first build writes the files and privacy rules the surrounding prompt requires. `seed.json` uses these same types, field names and parent ids, with `"@owner"` in writer fields and literal owner-derived default ids from `<app-owner>` where needed. The access rules admit every example record, and the final `// Summary:` accurately describes those rules; private records begin private. Follow the existing attachment/file protocol for uploaded files and their visibility.
|
|
144
294
|
|
|
145
295
|
### The prompt's content is launch content — emit `seed.json`
|
|
146
296
|
|