@vibes.diy/prompts 14.3.120 → 14.3.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms/fireproof.md CHANGED
@@ -259,110 +259,91 @@ Address an existing record by its `_id`. `useDocument(task)` above holds the sel
259
259
 
260
260
  ## Required starting records
261
261
 
262
- When a request names records the app must already have — the three projects in a
263
- studio, a template's starter rows, the rooms in a planner — those records are
264
- created by the app's own first render through the ordinary write path. That is
265
- the mechanism, and it is the same one described below for starter content.
266
- `seed.json` is a separate thing: an optional import a person runs by hand from
267
- the Data tab. Its sample records also populate the temporary, view-only preview
268
- while saving is being prepared; they become real saved records only through an
269
- explicit import. In that file, give each item a stable `key`; its derived identity
270
- is `seed:<key>`. References to another item use that derived identity. An `_id`
271
- that repeats the item's own derived identity is equivalent to leaving it out.
272
- Explicit singleton identities such as `config:board` remain available.
273
-
274
- Four rules make it hold:
262
+ A person's own action — a tap, a submit, a pick — is what starts a write; nothing in the app writes on load or on mount. Starting state comes in three kinds, and none of them is written by the app's own code:
263
+
264
+ - **A person's own starting state needs no record.** Their records live on the channel named by their handle (see `access.md`), so the app opens on their tree with nothing to create first, and the view renders its defaults — a name, a setting, an empty list's invitation — from constants until the person's own action saves one.
265
+ - **App-wide named containers or participants the request requires** — the pottery studio and its named apprentice, the three projects in a studio, the rooms in a planner — are written by the agent building the app, with `put_doc` as the owner, once the build whose rules admit them is saved, then read back. That save is the owner's own action. The app's opening view shows them when they are there and an inviting add flow until they are.
266
+ - **Example content stays the optional `seed.json` import** — believable entries the screenshot renders and the owner adds with one ask. A name present only in `seed.json` describes an offered import, not an existing record or grant. In that file, give each item a stable `key`; its derived identity is `seed:<key>`. References to another item use that derived identity. An `_id` that repeats the item's own derived identity is equivalent to leaving it out. Explicit singleton identities such as `config:board` remain available. The file also populates the temporary, view-only preview while saving is being prepared.
267
+
268
+ Four rules make the required records hold:
275
269
 
276
270
  - **Each required record gets a fixed `_id` you choose** (`"project:atlas"`), so
277
- a second write of it lands on the same document instead of minting another.
278
- - **Create them after `ready` and after `can.create` says yes**, so a write that
279
- the enforced access rules do not admit yet is skipped quietly and retried on a
280
- later run.
281
- - **Restrict their creation to the owner in `access.js`** when the records
282
- describe the app itself rather than a visitor's own work.
283
- - **Write parents and membership grants before the records that depend on
284
- them**, so a dependent document never lands against a container or a grant
285
- that does not exist yet.
286
-
287
- Fixed ids are also what makes two tabs safe. Two browsers opening the app for the
288
- first time at the same moment both see an empty query and both write; with fixed
289
- ids those are two writes of the same document, and last-writer-wins converges on
290
- one copy. Reading the query first and then deciding whether to write is the shape
291
- that races — the read is empty in both tabs before either write lands.
271
+ a second save of it lands on the same document instead of minting another.
272
+ - **Gate their creation on a role in `access.js`** — `ctx.requireRole("owner")`,
273
+ or a role the owner hands out with a role record — when the records describe the
274
+ app itself rather than a visitor's own work.
275
+ - **List parents and membership or role grants before the records that depend on
276
+ them**, so a dependent document never lands against a container or a grant that
277
+ does not exist yet.
278
+ - **Leave the app's render paths write-free.** A write in an effect that fires when
279
+ a query reads empty runs on every fresh load, on every device and for every
280
+ visitor, because the live query renders empty before data arrives.
281
+
282
+ Fixed ids are also what makes a repeated save safe: two devices saving the same
283
+ record write the same document, and last-writer-wins converges on one copy.
292
284
 
293
285
  ### Match sample records to the owner's opening query
294
286
 
295
- The initial build's `<app-owner>` context supplies the app owner's handle.
296
- Use it to evaluate the opening query's group ID when writing `seed.json`.
297
- For an owner named `mira`, the runtime expression below selects `default-mira`,
298
- so every sample card in this opening board carries that exact `boardId`:
287
+ The initial build's `<app-owner>` context supplies the app owner's handle. A
288
+ sample record in the owner's own tree names that tree with `"@owner"` as the whole
289
+ field value, which the platform fills in for whoever applies the seed. The opening
290
+ view reads the signed-in person's own tree by their handle, so every sample card
291
+ carries `"ownerHandle": "@owner"`:
299
292
 
300
293
  ```jsx
301
- const boardId = pickedBoardId ?? `default-${me?.userHandle}`;
302
- const { docs: cards } = useLiveQuery("boardId", { key: boardId });
294
+ const { docs: cards } = useLiveQuery("ownerHandle", { key: me?.userHandle });
303
295
  ```
304
296
 
305
297
  ```json
306
298
  {
307
299
  "boards": [
308
- { "key": "tent", "type": "card", "boardId": "default-mira", "text": "Blue tent, sleeps four" },
309
- { "key": "stove", "type": "card", "boardId": "default-mira", "text": "Two-burner camp stove" },
310
- { "key": "lantern", "type": "card", "boardId": "default-mira", "text": "Two rechargeable lanterns" },
311
- { "key": "cooler", "type": "card", "boardId": "default-mira", "text": "Wheeled cooler" }
300
+ { "key": "tent", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Blue tent, sleeps four" },
301
+ { "key": "stove", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Two-burner camp stove" },
302
+ { "key": "lantern", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Two rechargeable lanterns" },
303
+ { "key": "cooler", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Wheeled cooler" }
312
304
  ]
313
305
  }
314
306
  ```
315
307
 
316
- Use the supplied owner handle in these parent IDs; keep the app's query
317
- and real writes driven by the current viewer and selected object. The database
318
- name, record type, parent references and query filters agree across the sample
319
- file and opening view. Later access rules preserve those parent references.
320
- The platform labels the temporary preview as sample data; those records reach
321
- persistent storage only through the person's explicit import.
308
+ An id that only contains the handle (`settings-mira`) is not a whole value, so
309
+ write the supplied owner handle into it literally; keep the app's query and real
310
+ writes driven by the current viewer and selected object. The database name,
311
+ record type, parent references and query filters agree across the sample file and
312
+ opening view. Later access rules preserve those parent references. The platform
313
+ labels the temporary preview as sample data; those records reach persistent
314
+ storage only through the person's explicit import.
322
315
 
323
316
  ### Seeding starter data — give each seed a deterministic `_id`
324
317
 
325
- When an app ships with default content — starter slides, template rows, example cards — and writes it into the database on first load, give **each seed document a deterministic `_id`** (`"seed:intro"`, `"seed:" + key`). This makes the seeding write **idempotent**: if it ever runs again, it overwrites the same documents instead of creating fresh copies.
318
+ When an app ships with default content — starter slides, template rows, example cards — the content is a set of `seed.json` items, each with a **deterministic identity** (a stable `key`, or `_id: "seed:intro"`). This makes adding it **idempotent**: a second add overwrites the same documents instead of creating fresh copies.
326
319
 
327
- The trap is the live-query empty state. `useLiveQuery` renders empty before data arrives, so `docs.length === 0` is briefly true on **every** fresh load — and a `useRef`/`useState` "already seeded" flag only guards the current session, never the next reload, a second device, or another collaborator. With **auto-generated** `_id`s, each of those re-seeds the full set and duplicate copies pile up without bound. Deterministic `_id`s collapse every re-seed back onto the same N documents.
320
+ The app itself writes no copy. `useLiveQuery` renders empty before data arrives, so `docs.length === 0` is briefly true on **every** fresh load, and a "seed when empty" effect re-runs on every reload, every device and for every visitor. The empty view instead invites the person's own first action, and that action writes the record it checked:
328
321
 
329
322
  ### `App.jsx`
330
- ```jsx
331
- import React from "react";
332
- import { useFireproof } from "use-fireproof";
333
- import { useVibe } from "use-vibes";
334
-
335
- // Stable _id per seed, and no Date.now()/random fields — keep the payload
336
- // constant so a redundant seed is a true no-op, not a churning rewrite.
337
- const SEED = [
338
- { _id: "seed:intro", type: "slide", title: "Welcome", position: 1000 },
339
- { _id: "seed:problem", type: "slide", title: "The problem", position: 2000 },
340
- { _id: "seed:ask", type: "slide", title: "The ask", position: 3000 },
341
- ];
342
-
343
- export default function App() {
323
+ ```jsx partial
324
+ function Deck() {
344
325
  const { useLiveQuery, database } = useFireproof("deck");
345
326
  const { ready, can } = useVibe("deck");
346
327
  const { docs: slides } = useLiveQuery("position");
347
-
348
- React.useEffect(() => {
349
- if (slides.length > 0) return; // already has content — don't seed
350
- // Ask the access rules before writing. A doc type the currently-enforced
351
- // access rules don't allow yet (e.g. one this very edit just introduced —
352
- // the update binds only after the whole turn lands) would be rejected:
353
- // skip quietly now and seed on a later run instead of spamming errors.
354
- if (!ready || !can.create(SEED[0]).ok) return;
355
- // Deterministic _id → idempotent. Even if this runs again (another load or
356
- // device before the query resolves), it overwrites the same docs instead of
357
- // minting new ones, so seed slides never duplicate.
358
- SEED.forEach((s) => database.put(s).catch((e) => console.error(e)));
359
- }, [slides.length, ready, can, database]);
360
-
361
- return <Deck slides={slides} />;
328
+ // The person's own tap writes the first slide: the control checks this exact record.
329
+ const first = { type: "slide", title: "Welcome", position: 1000 };
330
+ const allowed = ready && can.create(first).ok;
331
+
332
+ if (slides.length === 0) {
333
+ return (
334
+ <Stack gap={2}>
335
+ <Text>Your deck is ready for its first slide.</Text>
336
+ <Button type="button" disabled={!allowed} onClick={() => allowed && database.put(first)}>
337
+ Add the first slide
338
+ </Button>
339
+ </Stack>
340
+ );
341
+ }
342
+ return <Slides slides={slides} />;
362
343
  }
363
344
  ```
364
345
 
365
- Don't stamp `Date.now()` or other changing values into seed documents — that makes each re-seed rewrite the doc with new content, churning revisions even though the `_id` is stable. Resource-like docs — channels, profiles, **seeds** — get deterministic `_id`s; event/content docs let `_id` auto-generate.
346
+ Don't stamp `Date.now()` or other changing values into seed documents — that makes each re-add rewrite the doc with new content, churning revisions even though the `_id` is stable. Resource-like docs — channels, profiles, **seeds** — get deterministic `_id`s; event/content docs let `_id` auto-generate.
366
347
 
367
348
  ### Updating Documents in Event Handlers
368
349
 
@@ -454,7 +435,7 @@ A drag that touches many documents (painting pixels, drawing on a shared board)
454
435
 
455
436
  Keep each cell its own document with a deterministic `_id` (`pixel-<x>-<y>`). Never fold the canvas into one document every visitor saves: the server keeps whichever save arrives last, so one visitor's save erases everyone else's strokes. An unsaved draft drops out for peers about 12 seconds after it stops updating.
456
437
 
457
- A draft reaches only the peers already subscribed to its channel, and a channel's grant comes from a saved document. Create a fixed-id room record on that channel (see Required starting records) so even the first stroke on an empty canvas is seen live.
438
+ A draft reaches only the peers already subscribed to its channel, and a channel's grant comes from a saved document. The painter's own pointer-down saves a fixed-id room record on that channel before the first stroke is drawn, so the stroke is seen live; nothing writes on load or on mount.
458
439
 
459
440
  ### `App.jsx`
460
441
  ```jsx
@@ -485,12 +466,6 @@ function Canvas({ me }) {
485
466
  const cellsRef = useRef([]);
486
467
  const canvasRef = useRef(null);
487
468
 
488
- useEffect(() => {
489
- // The room record grants the channel, so peers subscribe before the first stroke.
490
- const room = { _id: "canvas:main", type: "canvas" };
491
- if (ready && can.create(room).ok) database.put(room);
492
- }, [ready, can, database]);
493
-
494
469
  useEffect(() => {
495
470
  const ctx = canvasRef.current.getContext("2d");
496
471
  ctx.fillStyle = "#ffffff";
@@ -504,6 +479,13 @@ function Canvas({ me }) {
504
479
  for (const [x, y] of stroke.cells) fill(x, y, stroke.color);
505
480
  }, [pixels, strokes, stroke, me]);
506
481
 
482
+ function startStroke(e) {
483
+ // The painter's own pointer-down saves the room record that grants the channel; the
484
+ // fixed id makes every later save the same document.
485
+ const room = { _id: "canvas:main", type: "canvas" };
486
+ if (ready && can.create(room).ok) database.put(room);
487
+ extendStroke(e);
488
+ }
507
489
  function extendStroke(e) {
508
490
  if (!canPaint) return;
509
491
  const r = canvasRef.current.getBoundingClientRect();
@@ -532,7 +514,7 @@ function Canvas({ me }) {
532
514
  className="w-full max-w-lg aspect-square touch-none"
533
515
  onPointerDown={(e) => {
534
516
  e.currentTarget.setPointerCapture(e.pointerId);
535
- extendStroke(e);
517
+ startStroke(e);
536
518
  }}
537
519
  onPointerMove={(e) => e.buttons && extendStroke(e)}
538
520
  onPointerUp={endStroke}
package/llms/richtext.js CHANGED
@@ -6,7 +6,8 @@ export const richtextConfig = {
6
6
  "edits with the last editor named, meeting notes with action items, a recipe book with real pages, " +
7
7
  "a journal with one entry per day. Covers every Tiptap package pinned to one version by its esm.sh " +
8
8
  "URL (one ProseMirror, no second React), an editor module with createEditor, load, flush, run and " +
9
- "destroy, a saved note as its JSON plus plain text and a derived title, saving after typing pauses " +
9
+ "destroy, a saved note as its JSON plus plain text and a derived title, a title field beside the " +
10
+ "page saved with the body (after its own pause and on close, so drafts keep their titles), saving after typing pauses " +
10
11
  "with flush on blur and switch, loading a document without a save echo (setContent with emitUpdate " +
11
12
  "false), destroy in the effect's cleanup, the page styled from the host with arbitrary variants, " +
12
13
  "toolbar commands and active states, and images as _files. " +
@@ -1 +1 @@
1
- {"version":3,"file":"richtext.js","sourceRoot":"","sources":["../../jsr/llms/richtext.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,oBAAoB;IAC3B,WAAW,EACT,oGAAoG;QACpG,gGAAgG;QAChG,oGAAoG;QACpG,oGAAoG;QACpG,mGAAmG;QACnG,oGAAoG;QACpG,oGAAoG;QACpG,kGAAkG;QAClG,4DAA4D;QAC5D,sGAAsG;QACtG,2FAA2F;IAC7F,IAAI,EAAE;QACJ,UAAU;QACV,WAAW;QACX,aAAa;QACb,aAAa;QACb,YAAY;QACZ,SAAS;QACT,OAAO;QACP,MAAM;QACN,iBAAiB;QACjB,YAAY;QACZ,WAAW;QACX,WAAW;QACX,aAAa;QACb,SAAS;QACT,aAAa;QACb,eAAe;QACf,iBAAiB;QACjB,eAAe;QACf,gBAAgB;QAChB,oBAAoB;QACpB,iBAAiB;QACjB,oBAAoB;QACpB,cAAc;QACd,QAAQ;QACR,aAAa;QACb,OAAO;QACP,SAAS;QACT,cAAc;QACd,WAAW;KACZ;CACF,CAAC"}
1
+ {"version":3,"file":"richtext.js","sourceRoot":"","sources":["../../jsr/llms/richtext.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,oBAAoB;IAC3B,WAAW,EACT,oGAAoG;QACpG,gGAAgG;QAChG,oGAAoG;QACpG,oGAAoG;QACpG,mGAAmG;QACnG,kGAAkG;QAClG,uHAAuH;QACvH,oGAAoG;QACpG,kGAAkG;QAClG,4DAA4D;QAC5D,sGAAsG;QACtG,2FAA2F;IAC7F,IAAI,EAAE;QACJ,UAAU;QACV,WAAW;QACX,aAAa;QACb,aAAa;QACb,YAAY;QACZ,SAAS;QACT,OAAO;QACP,MAAM;QACN,iBAAiB;QACjB,YAAY;QACZ,WAAW;QACX,WAAW;QACX,aAAa;QACb,SAAS;QACT,aAAa;QACb,eAAe;QACf,iBAAiB;QACjB,eAAe;QACf,gBAAgB;QAChB,oBAAoB;QACpB,iBAAiB;QACjB,oBAAoB;QACpB,cAAc;QACd,QAAQ;QACR,aAAa;QACb,OAAO;QACP,SAAS;QACT,cAAc;QACd,WAAW;KACZ;CACF,CAAC"}
package/llms/richtext.md CHANGED
@@ -357,6 +357,55 @@ nothing twice. The save callback reads the open note through a ref rather
357
357
  than a closure, so the editor is created once and switching notes costs
358
358
  nothing.
359
359
 
360
+ ## A title field saves with the body
361
+
362
+ **A title field beside the editor saves through the same write as the body:
363
+ one `write` helper merges every change into the open document and puts it,
364
+ one put at a time, so the title saves after its own pause and closing the
365
+ document waits for the page and the title to land together.** A blog with
366
+ drafts, a journal with named entries or a recipe book with a name field
367
+ keeps the title in an `Input` above the page, and the writer expects both to
368
+ be there when they step back to the list. Publishing then changes `status`
369
+ and nothing else, so a private draft keeps every word its writer typed,
370
+ title included.
371
+
372
+ ```jsx
373
+ const [title, setTitle] = useState(post.title);
374
+ const docRef = useRef(post); // the open document as last written
375
+ const queue = useRef(Promise.resolve()); // puts to one _id run in turn
376
+
377
+ // The page's onChange and the title both write here: the change merges into
378
+ // the open document at once, and its put waits for the one before it.
379
+ function write(changes) {
380
+ docRef.current = { ...docRef.current, ...changes, updatedAt: Date.now() };
381
+ const next = docRef.current;
382
+ queue.current = queue.current.then(() => database.put(next));
383
+ return queue.current;
384
+ }
385
+
386
+ // The title waits for its own pause, like the page.
387
+ useEffect(() => {
388
+ if (title === docRef.current.title) return;
389
+ const timer = setTimeout(() => write({ title }), 600);
390
+ return () => clearTimeout(timer);
391
+ }, [title]);
392
+
393
+ // Back to the list: flush the page, add the title, and wait for both.
394
+ async function close() {
395
+ surfaceRef.current?.flush();
396
+ if (title !== docRef.current.title) write({ title });
397
+ await queue.current;
398
+ onClose();
399
+ }
400
+ ```
401
+
402
+ The page's `onChange` calls the same helper, `write({ body: json, text })`.
403
+ Because each change merges into `docRef` before its put is queued, the last
404
+ put carries the page and the title together, and puts to one `_id` never
405
+ overlap (overlapping puts collide on the document's sequence, as in the
406
+ saving section above). The `Input` stays `onChange={setTitle}`; the effect
407
+ does the saving.
408
+
360
409
  ## Loading a document lands without a save
361
410
 
362
411
  **A document arriving from the database is loaded with `setContent` and
@@ -604,6 +653,10 @@ warning.
604
653
  derived. `titleOf(json)` on every save.
605
654
  - **Typing is lost when switching notes** — the debounce had not fired.
606
655
  `flush()` before changing the open note.
656
+ - **Every draft comes back as "Untitled draft"** — the title field lives in
657
+ React state and only Publish writes it. Save the title after its own
658
+ pause and in the close step, through the same queued `write` as the page,
659
+ and let the close step wait for it.
607
660
  - **Two editors after a hot reload** — the effect's cleanup did not destroy
608
661
  the editor.
609
662
  - **The page has no styles** — Tiptap ships none, and a multi-file app
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.120",
3
+ "version": "14.3.122",
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.120",
43
- "@vibes.diy/call-ai-v2": "14.3.120",
44
- "@vibes.diy/identity": "14.3.120",
45
- "@vibes.diy/use-vibes-types": "14.3.120",
42
+ "@vibes.diy/api-types": "14.3.122",
43
+ "@vibes.diy/call-ai-v2": "14.3.122",
44
+ "@vibes.diy/identity": "14.3.122",
45
+ "@vibes.diy/use-vibes-types": "14.3.122",
46
46
  "arktype": "~2.2.6",
47
47
  "json-schema-faker": "~0.6.3"
48
48
  },
@@ -97,6 +97,7 @@ export default function App() {
97
97
  - **The named interaction works end to end**: the person's sentence names it, and the app builds that and only that, with realistic data. Sharing, invitations, roles, scheduled jobs, emails and outside services come in a later pass, and so does the line about who can see the data, which sits beside the share control that pass adds; the title, subtitle and hero say what the app is for.
98
98
  - **Seed content** is realistic and specific to the app's world, with a `key` per item and the exact fields the app reads.
99
99
  - **Layout comes from the components** (`Stack`, `Row`, `Grid`, `Card`) and their `tone`, `size` and `variant` props; plain spacing like `p-6` sits on your own wrapping elements only. The look brief below says how the page is composed.
100
+ - **A bar fixed to the bottom of the screen — a tab bar, a nav, a save bar — rides above the platform's controls**, so every tab and button on it stays tappable: `BottomNav` sits there by itself, and a bar of your own puts `bottom: 'var(--vibes-controls-inset, 0px)'` in its `style` instead of using `bottom-0`.
100
101
  - **Tapping a person opens their profile**: `<ViewerTag userHandle={m.userHandle} onClick={() => setProfile(m.userHandle)} />` binds the handle the tag shows, and a hero header is `<ViewerTag userHandle={profile} size={120} avatarOnly />` — the tag itself is the button, drawn natively. Opening a profile follows nobody.
101
102
 
102
103
  # Fireproof on vibes.diy — the short version