@vibes.diy/prompts 14.1.21 → 14.1.23

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.js CHANGED
@@ -2,6 +2,7 @@ export const fireproofConfig = {
2
2
  name: "fireproof",
3
3
  label: "useFireproof",
4
4
  description: "cloud-backed document database with live sync",
5
+ initialVariant: true,
5
6
  importModule: "use-fireproof",
6
7
  importName: "useFireproof",
7
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"fireproof.js","sourceRoot":"","sources":["../../jsr/llms/fireproof.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,cAAc;IACrB,WAAW,EAAE,+CAA+C;IAC5D,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE,cAAc;CAC3B,CAAC"}
1
+ {"version":3,"file":"fireproof.js","sourceRoot":"","sources":["../../jsr/llms/fireproof.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,cAAc;IACrB,WAAW,EAAE,+CAA+C;IAC5D,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE,cAAc;CAC3B,CAAC"}
@@ -0,0 +1,155 @@
1
+ # ImgGen Component
2
+
3
+ Generate and edit images from a text prompt. Each generated image lands as a file ref on the doc — display reads the platform-minted URL via `_files`.
4
+
5
+ **When to use it:** reach for `<ImgGen>` when a picture adds real value — items whose identity includes an image (cards, creatures, products, covers), user-personalized art (avatars, portraits from a photo), or apps whose whole point is imagery. Most text/data apps (lists, trackers, notes, forms) need **no** images — leave the import unused rather than decorating with gratuitous pictures.
6
+
7
+ ## Basic Usage
8
+
9
+ Start with a minimal image generation component:
10
+
11
+ App.jsx
12
+
13
+ ```jsx
14
+ import React from "react";
15
+ import { useFireproof } from "use-fireproof";
16
+ import { ImgGen } from "use-vibes";
17
+
18
+ export default function App() {
19
+ return (
20
+ <div>
21
+ <h2>Image Generator</h2>
22
+ <ImgGen prompt="A sunset over mountains" />
23
+ </div>
24
+ );
25
+ }
26
+ ```
27
+
28
+ `<ImgGen>` stores generated versions in its component-owned media database, associated
29
+ with the host database name and `_id`. Read a saved image through
30
+ `useImgGen({ _id, database })` from `use-vibes`: its `document` composes current media
31
+ versions and legacy host versions. From that composed document, select
32
+ `document.versions?.[document.currentVersion ?? 0]`, then
33
+ `document._files?.[version.id]`. The returned file ref supplies both `.url` for display
34
+ and `.file()` for a transformation input, so a ref you have in hand is ready to pass
35
+ straight to `images`. Ordinary host records hold the app's own data; the image hook
36
+ supplies the associated generated output.
37
+
38
+ For example, refining a saved pottery illustration keeps its original ID and uses its
39
+ actual output as the input to a separate result ID:
40
+
41
+ ```jsx
42
+ import { ImgGen, useImgGen } from "use-vibes";
43
+
44
+ function GlazingPreview({ database }) {
45
+ const { document: original, pending, error } = useImgGen({ _id: "vessel-original", database });
46
+ const version = original?.versions?.[original.currentVersion ?? 0];
47
+ const source = version?.id ? original?._files?.[version.id] : undefined;
48
+ const sourceReady = source !== undefined;
49
+ return <section>
50
+ <ImgGen _id="vessel-original" database={database} showControls={false} />
51
+ {sourceReady ? <ImgGen _id="vessel-glazed" database={database}
52
+ prompt="Add a blue glaze while preserving this vessel's shape and composition"
53
+ images={[source]} /> : <p>{error ? error.message : pending ? "Loading saved source…" : "Source image unavailable"}</p>}
54
+ </section>;
55
+ }
56
+ ```
57
+
58
+ This is the same `_files`-shape contract documented in `fireproof.md`'s "Working with Files" section — read it first if you have not seen the platform's file/URL story.
59
+
60
+ ## Editing an Uploaded Image
61
+
62
+ Pass a `File` object via `images` to run img2img. Adding a file picker that feeds into ImgGen:
63
+
64
+ App.jsx
65
+
66
+ ```jsx
67
+ <<<<<<< SEARCH
68
+ export default function App() {
69
+ return (
70
+ <div>
71
+ <h2>Image Generator</h2>
72
+ =======
73
+ export default function App() {
74
+ const [file, setFile] = React.useState(null);
75
+
76
+ return (
77
+ <div>
78
+ <h2>Image Generator</h2>
79
+ <input type="file" accept="image/*" onChange={(e) => setFile(e.target.files[0])} />
80
+ {file && <ImgGen prompt="Make it look like a watercolor painting" images={[file]} />}
81
+ >>>>>>> REPLACE
82
+ ```
83
+
84
+ The input image is automatically resized (max 1024px) and compressed as JPEG before sending.
85
+
86
+ `images` accepts **either** a raw `File` (fresh from an `<input type="file">`) **or** a stored Fireproof file ref — anything shaped like `{ file: () => Promise<File> }` passes straight through. So if the app has already saved the upload onto a doc, feed it back directly:
87
+
88
+ ```jsx
89
+ // doc was saved earlier with: database.put({ ..., _files: { photo: file } })
90
+ <ImgGen prompt="Turn this pet into a cute cartoon character" images={[doc._files.photo]} _id={doc._id} database={database} />
91
+ ```
92
+
93
+ Do **not** invent wrappers like `images={x?.file ? something : undefined}` — pass the `_files` entry itself. If the stored ref might be absent, gate the whole `<ImgGen>` mount on it instead: `{doc._files?.photo && <ImgGen images={[doc._files.photo]} ... />}`.
94
+
95
+ **Displaying an img2img result later** (gallery, detail view, reload): render by `_id` and do NOT re-pass `images` — `<ImgGen _id={doc._id} database={database} showControls={false} />` shows the stored version. ImgGen records which input produced each version, so re-passing that SAME input (the doc's own stored source, e.g. `images={[doc._files.photo]}` on every render) — or `_id` alone — just displays the saved result and will not force a fresh, billed generation. Passing a genuinely DIFFERENT input to the same `_id` DOES generate a new version — that's a requested img2img edit, not a reload: a new upload, a different stored ref, or a PRIOR OUTPUT fed back in to refine it. To force a new version from the CURRENT input, use the regenerate control (or set `generationId`). Generate once with `images`, display forever with `_id` alone.
96
+
97
+ Do **not** set `model` for img2img: when an input image is present the platform automatically selects its image-**edit** default, which is tuned to produce edits faithful to the source. An explicit `model` override bypasses that routing — only use one when the app has a specific, stated reason.
98
+
99
+ ## Loading a Specific Doc
100
+
101
+ Load a previously generated image by `_id` — if the doc has a `prompt` but no `_files` yet, the component generates one: `<ImgGen _id="my-image-id" database={database} />`
102
+
103
+ ## Illustrations That Are Part of an Item's Identity
104
+
105
+ When an item's picture is intrinsic to what it is — a card in a deck, a creature in a bestiary, a product in a catalog, a character in a cast — render `<ImgGen>` **by default** for every such item that lacks an image, so the illustration appears as soon as the item does. Because ImgGen auto-generates when the target doc has a `prompt` but no `_files` yet, mounting it directly on the item is all it takes:
106
+
107
+ ```jsx
108
+ <ImgGen
109
+ _id={card._id}
110
+ database={database}
111
+ prompt={`Illustration of ${card.title}. ${card.description ?? ""}`}
112
+ />
113
+ ```
114
+
115
+ Do **not** hide first generation behind an "Illustrate" / "Generate image" button — a button leaves every item blank until tapped, which reads as "the app has no pictures." Reserve buttons and `showControls` for *regeneration* of an image that already exists. (For incidental or user-optional images — an avatar upload, a one-off hero graphic — an explicit action is still fine; this rule is about items whose identity includes a picture.)
116
+
117
+ ## Gallery Pattern
118
+
119
+ Keep gallery entries as app-owned records with the saved image's host `_id`. Query those
120
+ records with `useLiveQuery`, then let each `<ImgGen>` read its associated media. This also
121
+ keeps the app's title, caption and other editable fields separate from image versions:
122
+
123
+ ```jsx
124
+ function Gallery() {
125
+ const { database, useLiveQuery } = useFireproof("artwork");
126
+ const { docs } = useLiveQuery("type", { key: "artwork" });
127
+ return <div>{docs.map(doc => <figure key={doc._id}>
128
+ <ImgGen _id={doc._id} database={database} showControls={false} />
129
+ <figcaption>{doc.caption}</figcaption>
130
+ </figure>)}</div>;
131
+ }
132
+ ```
133
+
134
+ ## Caching and Versions
135
+
136
+ - Same prompt produces a deterministic `_id` (hash-based), so results are cached across reloads.
137
+ - Each image has a regenerate button that appends a new version (writes a new `_files.v<N>` entry).
138
+ - Prev / next controls navigate between stored versions.
139
+ - Set `showControls={false}` to hide regenerate and version navigation.
140
+
141
+ ## Choosing a Model
142
+
143
+ Override the model per component: `<ImgGen prompt="An astronaut riding a horse" model="openai/gpt-5-image-mini" />`
144
+
145
+ Model ids follow the `provider/model-name` form from the platform's model catalog. Unknown ids surface as an error in the component's error UI.
146
+
147
+ #### Props
148
+
149
+ - `prompt`: text prompt (required unless `_id` is provided)
150
+ - `images`: array of `File` objects **or stored `_files` refs** for img2img (uses first image; only pass while generating — display stored results via `_id`)
151
+ - `_id`: load a specific doc instead of generating
152
+ - `database`: Fireproof db name or instance (default `"ImgGen"`)
153
+ - `className`, `alt`, `style`: standard image styling
154
+ - `showControls`: toggle regenerate + version nav (default `true`)
155
+ - `model`: override the image-gen model for this component
package/llms/image-gen.js CHANGED
@@ -2,6 +2,7 @@ export const imageGenConfig = {
2
2
  name: "image-gen",
3
3
  label: "Image Generation",
4
4
  description: "Generate and edit images",
5
+ initialVariant: true,
5
6
  cues: [
6
7
  "image",
7
8
  "images",
@@ -1 +1 @@
1
- {"version":3,"file":"image-gen.js","sourceRoot":"","sources":["../../jsr/llms/image-gen.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,kBAAkB;IACzB,WAAW,EAAE,0BAA0B;IACvC,IAAI,EAAE;QACJ,OAAO;QACP,QAAQ;QACR,KAAK;QACL,OAAO;QACP,QAAQ;QACR,SAAS;QACT,UAAU;QACV,QAAQ;QACR,cAAc;QACd,MAAM;QACN,cAAc;QACd,gBAAgB;KACjB;IACD,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
1
+ {"version":3,"file":"image-gen.js","sourceRoot":"","sources":["../../jsr/llms/image-gen.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,kBAAkB;IACzB,WAAW,EAAE,0BAA0B;IACvC,cAAc,EAAE,IAAI;IACpB,IAAI,EAAE;QACJ,OAAO;QACP,QAAQ;QACR,KAAK;QACL,OAAO;QACP,QAAQ;QACR,SAAS;QACT,UAAU;QACV,QAAQ;QACR,cAAc;QACd,MAAM;QACN,cAAc;QACd,gBAAgB;KACjB;IACD,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
package/llms/types.d.ts CHANGED
@@ -3,6 +3,7 @@ export interface LlmConfig {
3
3
  label: string;
4
4
  description: string;
5
5
  cues?: string[];
6
+ initialVariant?: boolean;
6
7
  importModule?: string;
7
8
  importName?: string;
8
9
  importType?: "named" | "namespace" | "default";
@@ -0,0 +1,58 @@
1
+ # useVibe Hook — write gating
2
+
3
+ `useVibe(dbName)` is how you gate write surfaces. It asks the platform whether a candidate document would be accepted, so the UI's enabled/disabled state matches what the server will actually allow. You never re-implement the answer in the app; you ask, and you render the verdict.
4
+
5
+ ```jsx
6
+ const { me, can, ready } = useVibe("comments");
7
+ ```
8
+
9
+ Pass the Fireproof database name you are writing to. You get:
10
+
11
+ - `can.create(draft)` / `can.edit(doc)` / `can.delete(doc)` → `{ ok: boolean, pending?: true, reason?: string }`. **The verdict has three states, not two** — allowed, still resolving, and denied — so branch on `pending` before `ok`.
12
+ - `ready` — `false` until identity and the verdict have resolved. It is the same moment `pending` marks, so either one can hold the surface.
13
+ - `me` — `{ userHandle, displayName? } | null` (null = anonymous). For display only.
14
+
15
+ ## The three-way branch
16
+
17
+ While `pending` is set, show a quiet checking state — never a sign-in prompt, never the raw `reason` token, and never a blank space where the write surface was. A pending verdict means "we don't know yet", not "you may not"; it resolves on its own within a moment, so anything that reads as a refusal is wrong for that moment, and hiding the surface with nothing in its place makes the app look broken.
18
+
19
+ On a real denial (`!ok` and no `pending`), explain it in your app's own words — `reason` is a machine token, not display copy. Let it *inform* the sentence you write ("Sign in to post here"), and never render it directly: a token like `pending` printed into the page is internal vocabulary leaking at a user.
20
+
21
+ Never stamp author fields from `me` while the verdict is pending — `me` is null until identity resolves, and a write that skips the gate lands authorless.
22
+
23
+ **Build the candidate from the doc you'll actually write.** `can.create(draft)` is evaluated against `draft`, so `draft` must carry the fields the write carries — `authorHandle`, the id of the object the record belongs to, and so on. A bare `can.create({ type: "post" })` can be denied (e.g. `"not author"`) and hide the form even from users who could post. Stamp the same fields you will `put`: `can.create({ type: "card", boardId, authorHandle: me?.userHandle })`. And gate the **same database** you write to — `useVibe(dbName)` answers per `dbName`, so a gate on a different db won't reflect what the server does.
24
+
25
+ ## The rule
26
+
27
+ Gate every write affordance on `can.*`. Hold it while `pending`, and write your own copy when denied. Never branch write permission on `viewer` or on document fields — those drift from the platform's own answer. Rendering **other** users (authors, rosters) is `useViewer()`'s `<ViewerTag userHandle={...} />`, not `useVibe`. The current viewer's own pill and the "signed in as" label are system chrome in the Vibes Switch (the logo) — don't build them into the app. Asking an anonymous visitor to sign in is a different thing and it IS yours: `useViewer().requestLogin()`, at the moment their work becomes worth keeping (see use-viewer docs). A resolved denial is one such moment — write the sign-in copy in your app's own words and wire it to that callable, never to a sentence about the logo. (The one exception is inline avatar self-edit: a guarded no-prop `{viewer && <ViewerTag />}` lets any signed-in member change their own photo in place — see use-viewer docs.)
28
+
29
+ ```jsx
30
+ import { useVibe } from "use-vibes";
31
+
32
+ function PromptBar({ database, boardId }) {
33
+ const { can, me } = useVibe("aestheticBoard");
34
+ // The candidate carries the same keys the write will: the board it belongs
35
+ // to, and who is writing it.
36
+ const v = can.create({ type: "tile", boardId, authorHandle: me?.userHandle });
37
+ // 1. still resolving — a quiet placeholder, no refusal wording, no empty hole
38
+ if (v.pending) return <div className="skeleton" aria-busy="true">Checking…</div>;
39
+ // 2. resolved denial — your words, informed by v.reason, never v.reason itself
40
+ if (!v.ok) return <p className="muted">Sign in to add a tile.</p>;
41
+ // 3. allowed
42
+ return (
43
+ <form onSubmit={/* … */}>
44
+ {/* no current-user pill — identity + sign-in live in the Vibes Switch (the logo) */}
45
+ <input placeholder="Add a tile…" />
46
+ <button type="submit">Post</button>
47
+ </form>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Per-row affordances
53
+
54
+ Per-row edit/delete affordances read the same way: `{can.edit(doc).ok && <EditButton doc={doc} />}`. By default every signed-in visitor is a first-class participant who creates and edits their own data, so on a fresh app these verdicts come back `ok` — which is the point. The gate is already in the right place when the app later grows a narrower answer, and nothing about the surface has to be rewritten to pick that answer up.
55
+
56
+ ## The server is still the authority
57
+
58
+ `can.*` is a fast, faithful preview, not the final word. A write can still be rejected server-side (the source may be stale, async, or unevaluable — in which case `can.*` optimistically returns `ok` and defers to the server). Keep the optimistic-write + rollback pattern: apply the change immediately, revert and surface an error if the `put` rejects.
package/llms/use-vibe.js CHANGED
@@ -2,6 +2,7 @@ export const useVibeConfig = {
2
2
  name: "use-vibe",
3
3
  label: "Vibe Write Gating",
4
4
  description: "Gate write surfaces on the app's own access.js via useVibe().can",
5
+ initialVariant: true,
5
6
  importModule: "use-vibes",
6
7
  importName: "useVibe",
7
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-vibe.js","sourceRoot":"","sources":["../../jsr/llms/use-vibe.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EAAE,kEAAkE;IAC/E,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,SAAS;CACtB,CAAC"}
1
+ {"version":3,"file":"use-vibe.js","sourceRoot":"","sources":["../../jsr/llms/use-vibe.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EAAE,kEAAkE;IAC/E,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,SAAS;CACtB,CAAC"}
@@ -0,0 +1,303 @@
1
+ # useViewer Hook
2
+
3
+ `useViewer()` is a **read-only window** into viewer identity. The platform owns who is signed in, and `useViewer()` lets your app see that and render it. Your app reflects the runtime's answer in its UI; it never invents one of its own.
4
+
5
+ **The current viewer's pill is system chrome — you don't build it.** The platform shows the current user inside the **Vibes Switch**, the panel that opens when you click the logo. So your app does **not** need a header pill for whoever is looking — don't render a no-prop `<ViewerTag />` just to show "who's signed in".
6
+
7
+ **The sign-in _ask_ is yours.** `requestLogin()` from `useViewer()` opens the platform sign-in UI from anywhere in your app, so you can word, place and time the ask yourself. Reach for it at the moment your app becomes worth keeping. The Switch is chrome outside your app's frame, and pointing at the logo is not a call to action — it is a scavenger hunt. See **Asking a visitor to sign in** below.
8
+
9
+ Use `useViewer()` to render **other** people's identity — comment authors, rosters, "added by" labels — with `<ViewerTag userHandle={...} />`, and to read `viewer`/`can` when your UI needs to branch on who's looking. **Write surfaces are gated with `useVibe(dbName).can`** (see use-vibe docs), not with `viewer`.
10
+
11
+ ## Basic Usage
12
+
13
+ Start with a minimal component that reads the viewer identity. You don't render a current-user pill — that lives in the Vibes Switch (click the logo). Branch on `viewer` for any welcome/empty copy. Note there is **no sign-in button here**: the ask belongs at the moment the visitor has made something worth keeping, not on arrival — see **Asking a visitor to sign in** below.
14
+
15
+ App.jsx
16
+
17
+ ```jsx
18
+ import React from "react";
19
+ import { useFireproof } from "use-fireproof";
20
+ import { useViewer } from "use-vibes";
21
+
22
+ export default function App() {
23
+ const { viewer, isViewerPending } = useViewer();
24
+
25
+ if (isViewerPending) return null;
26
+
27
+ return (
28
+ <div>
29
+ {/* No current-user pill here — that's the Vibes Switch's job. */}
30
+ {viewer ? (
31
+ <p>Welcome back, {viewer.displayName ?? viewer.userHandle}!</p>
32
+ ) : (
33
+ <p>Everything you make here is saved on this device.</p>
34
+ )}
35
+ </div>
36
+ );
37
+ }
38
+ ```
39
+
40
+ ## What you get
41
+
42
+ - `viewer` — `{ userHandle, displayName? }` or `null` for anonymous visitors. Avatars are not on the payload — render them with `<ViewerTag userHandle={...} />`, which resolves the avatar from the handle. Don't build avatar URLs yourself.
43
+ - `isViewerPending` — `true` while the platform is still resolving the viewer identity (e.g. on first render before the parent shell has pushed the identity update). **Gate any auth-dependent UI on `!isViewerPending`** to avoid flashing the wrong state. Once it becomes `false`, `viewer` is either populated or definitively `null`.
44
+ - `can(action)` — membership boolean for `"read"`/`"write"`/`"delete"`: is the viewer through the door? It answers about the viewer, not about a particular document, so prefer `useVibe(dbName).can.create/edit/delete` for write gating — that one is asked about the doc you are writing and comes back with a `reason`.
45
+ - `viewerLapsed` — `true` when `viewer` is the identity this device *remembers* and the platform's answer came back anonymous: the sign-in lapsed (an expired session, a cold boot before auth restores), rather than the person signing out. Keep rendering their stuff — the local data is still theirs, and writes queue locally and sync once they sign back in — and offer a quiet "Sign back in to sync" nudge wired to `requestLogin()`. Never fall back to the anonymous copy ("Sign in to get started") while `viewerLapsed` is true; that reads as if their work is gone. `can()` and every write gate stay closed while lapsed, so gate *writes* on `can`, not on this flag, and `isViewerPending` is `false` (lapsed is a settled state, not a loading one).
46
+ - `requestLogin()` — opens the platform sign-in UI. Fire-and-forget: there is nothing to await and no result to branch on, because the new identity arrives on its own through `viewer`. This is how your app asks — see **Asking a visitor to sign in** below.
47
+ - `ViewerTag` — ready-made user pill; see the ViewerTag section below.
48
+
49
+ ## Gating UI
50
+
51
+ Gate the comment form on `useVibe("comments").can` — not on `viewer`. The current viewer never needs a pill here (that's in the Vibes Switch); just render the gated form, and on a denial a sentence in your app's own words — `reason` is a machine token that informs that sentence and is never printed:
52
+
53
+ App.jsx
54
+
55
+ ```jsx
56
+ <<<<<<< SEARCH
57
+ import { useViewer } from "use-vibes";
58
+ =======
59
+ import { useViewer, useVibe } from "use-vibes";
60
+ >>>>>>> REPLACE
61
+ ```
62
+
63
+ App.jsx
64
+
65
+ ```jsx
66
+ <<<<<<< SEARCH
67
+ const { viewer, isViewerPending } = useViewer();
68
+ =======
69
+ const { viewer, isViewerPending } = useViewer();
70
+ const { can, ready, me } = useVibe("comments");
71
+ >>>>>>> REPLACE
72
+ ```
73
+
74
+ App.jsx
75
+
76
+ ```jsx
77
+ <<<<<<< SEARCH
78
+ {viewer ? (
79
+ <p>Welcome back, {viewer.displayName ?? viewer.userHandle}!</p>
80
+ ) : (
81
+ <p>Everything you make here is saved on this device.</p>
82
+ )}
83
+ =======
84
+ {/* write gate: useVibe().can, not viewer */}
85
+ {!ready ? null : (() => {
86
+ const v = can.create({ type: "comment", postId, authorHandle: me?.userHandle });
87
+ return v.ok ? (
88
+ <form>
89
+ <input placeholder="Add a comment..." />
90
+ <button type="submit">Post</button>
91
+ </form>
92
+ ) : (
93
+ // Your own words, informed by v.reason — never the token itself.
94
+ <p>Sign in to add a comment.</p>
95
+ );
96
+ })()}
97
+ >>>>>>> REPLACE
98
+ ```
99
+
100
+ ## Tagging content with the viewer (write/render pattern)
101
+
102
+ When one user writes content others will see (comments, posts, messages), **stamp `authorHandle` on the doc at write time**, and stamp the id of the thing it belongs to beside it (`postId`, `boardId`, `listId`). Render the author with `<ViewerTag userHandle={doc.authorHandle} />` which resolves display name and avatar automatically. Do not stamp `displayName` or `avatarUrl` on docs — ViewerTag handles that from the handle alone.
103
+
104
+ Wire up a full comment thread with Fireproof and viewer attribution:
105
+
106
+ App.jsx
107
+
108
+ ```jsx
109
+ <<<<<<< SEARCH
110
+ import { useViewer, useVibe } from "use-vibes";
111
+ =======
112
+ import { useViewer, useVibe } from "use-vibes";
113
+ >>>>>>> REPLACE
114
+ ```
115
+
116
+ ```jsx
117
+ <<<<<<< SEARCH
118
+ const { viewer, isViewerPending } = useViewer();
119
+ const { can, ready, me } = useVibe("comments");
120
+ =======
121
+ const { viewer, isViewerPending, ViewerTag } = useViewer();
122
+ const { can, ready, me } = useVibe("comments");
123
+ const { useLiveQuery, database } = useFireproof("comments");
124
+ // Every comment names the post it belongs to. A thread shows one post, so
125
+ // its id is fixed here. An app where each person has a space of their own
126
+ // derives the id from the viewer's handle instead (`default-${me.userHandle}`)
127
+ // once `ready` says the handle is known, so the first write lands in a space
128
+ // that is theirs and stays theirs.
129
+ const postId = "post:welcome";
130
+ const { docs: comments } = useLiveQuery("postId", { key: postId });
131
+ const [body, setBody] = React.useState("");
132
+ >>>>>>> REPLACE
133
+ ```
134
+
135
+ ```jsx
136
+ <<<<<<< SEARCH
137
+ {/* write gate: useVibe().can, not viewer */}
138
+ {!ready ? null : (() => {
139
+ const v = can.create({ type: "comment", authorHandle: me?.userHandle });
140
+ return v.ok ? (
141
+ <form>
142
+ <input placeholder="Add a comment..." />
143
+ <button type="submit">Post</button>
144
+ </form>
145
+ ) : (
146
+ // Your own words, informed by v.reason — never the token itself.
147
+ <p>Sign in to add a comment.</p>
148
+ );
149
+ })()}
150
+ =======
151
+ {/* render OTHER users with userHandle — that's what ViewerTag is for here */}
152
+ <ul>
153
+ {comments.map((c) => (
154
+ <li key={c._id}>
155
+ <ViewerTag userHandle={c.authorHandle} />
156
+ <p>{c.body}</p>
157
+ </li>
158
+ ))}
159
+ </ul>
160
+
161
+ {/* write gate: useVibe().can, not viewer */}
162
+ {!ready ? null : (() => {
163
+ const v = can.create({ type: "comment", postId, authorHandle: me?.userHandle });
164
+ return v.ok ? (
165
+ <form onSubmit={(e) => { e.preventDefault(); post(); }}>
166
+ <input value={body} onChange={(e) => setBody(e.target.value)} placeholder="Add a comment..." />
167
+ <button type="submit">Post</button>
168
+ </form>
169
+ ) : (
170
+ // Your own words, informed by v.reason — never the token itself.
171
+ <p>Sign in to add a comment.</p>
172
+ );
173
+ })()}
174
+ >>>>>>> REPLACE
175
+ ```
176
+
177
+ Also add the `post` handler before `if (isViewerPending)`:
178
+
179
+ ```jsx
180
+ <<<<<<< SEARCH
181
+ if (isViewerPending) return null;
182
+ =======
183
+ async function post() {
184
+ if (!body.trim()) return;
185
+ await database.put({
186
+ type: "comment",
187
+ postId, // the id of the thing this record belongs to, on the record itself
188
+ body: body.trim(),
189
+ createdAt: Date.now(),
190
+ authorHandle: me?.userHandle,
191
+ });
192
+ setBody("");
193
+ }
194
+
195
+ if (isViewerPending) return null;
196
+ >>>>>>> REPLACE
197
+ ```
198
+
199
+ Key points:
200
+
201
+ - **Stamp `authorHandle` at write time** — persist the author's handle on the doc. Render with `<ViewerTag userHandle={authorHandle} />` which resolves display name and avatar automatically.
202
+ - **Name the thing it belongs to** — every record carries the id of its parent object (`postId`, `boardId`, `listId`), written at create and left alone afterwards. Queries read off it, and so does everything the app grows later.
203
+ - **Give each person a default object of their own** — `` `default-${me.userHandle}` `` — so someone who has just arrived types once and it lands, with no object for them to create first. An object someone does make carries `creatorHandle: me.userHandle`.
204
+ - **Avatars are stable** — ViewerTag resolves the avatar from the handle; if the author changes their avatar, the URL stays the same and the bytes update. ViewerTag handles this for you.
205
+ - **One source of identity** — persist `authorHandle` on the doc. ViewerTag does the rest.
206
+
207
+ ## Asking a visitor to sign in
208
+
209
+ An anonymous visitor is having a legitimately good time. Under local-first their work is already saved on their device and your app should keep working for them — the account buys **sync, followers and durability**, not permission. So the ask is an **offer to keep what they already have — never a wall in front of it**. A visitor who never signs in should still get a working app; one who does should feel like they kept something.
210
+
211
+ Three rules separate a CTA that converts from the two shapes that don't.
212
+
213
+ **Ask at the moment of value.** The ask lands when the person has just made something they'd hate to lose — posted their first comment, starred their first pick, saved their first note. A prompt on arrival asks a stranger to commit before they know what the app does; a prompt after the first save offers to keep something they can already see.
214
+
215
+ **Wire the ask to `requestLogin()`, never to a sentence about the logo.** Copy like "sign in via the logo" points at platform chrome outside your app's frame and sends the reader hunting for it. It is not a call to action and it does not convert. Render a real control: `<button onClick={requestLogin}>Keep these</button>`.
216
+
217
+ **Only ask once identity has settled.** `isViewerPending` is `true` while the platform is still resolving who's looking, and a CTA rendered in that window shows signed-in people a SIGN IN button on every load. Compute the settled-signed-out state once and gate on it — `const settledSignedOut = !isViewerPending && !viewer;`.
218
+
219
+ Layer it onto the comment app: the visitor can already read and post, and once they've posted, we offer to keep it.
220
+
221
+ ```jsx
222
+ <<<<<<< SEARCH
223
+ const { viewer, isViewerPending, ViewerTag } = useViewer();
224
+ =======
225
+ const { viewer, isViewerPending, ViewerTag, requestLogin } = useViewer();
226
+ >>>>>>> REPLACE
227
+ ```
228
+
229
+ ```jsx
230
+ <<<<<<< SEARCH
231
+ const [body, setBody] = React.useState("");
232
+ =======
233
+ const [body, setBody] = React.useState("");
234
+
235
+ // Settled AND anonymous — never merely anonymous, or signed-in viewers flash
236
+ // this on every load while their identity is still resolving (#3639).
237
+ const settledSignedOut = !isViewerPending && !viewer;
238
+ // The moment of value: a comment THIS visitor wrote. An anonymous write
239
+ // stamps `authorHandle: me?.userHandle` with `me` still null, so an undefined
240
+ // author is our own unclaimed work — which is exactly what signing in keeps.
241
+ // Reading `comments.length` instead would nag people who only ever read.
242
+ const hasSomethingToKeep = comments.some((c) => c.authorHandle === undefined);
243
+ >>>>>>> REPLACE
244
+ ```
245
+
246
+ ```jsx
247
+ <<<<<<< SEARCH
248
+ {/* write gate: useVibe().can, not viewer */}
249
+ =======
250
+ {settledSignedOut && hasSomethingToKeep && (
251
+ <div>
252
+ {/* Says what they GET. Never "you must sign in to continue" — they
253
+ can continue, and their comment above is already saved locally.
254
+ And never "post it under your name": signing in syncs the comment
255
+ they already wrote, it does not rewrite the author stamped on it.
256
+ Promise continuity, which is true — not attribution, which isn't. */}
257
+ <p>Your comment is saved on this device.</p>
258
+ <button onClick={requestLogin}>Sign in to keep it on all your devices</button>
259
+ </div>
260
+ )}
261
+
262
+ {/* write gate: useVibe().can, not viewer */}
263
+ >>>>>>> REPLACE
264
+ ```
265
+
266
+ Copy that works, and copy that doesn't:
267
+
268
+ | Write this | Not this |
269
+ | --- | --- |
270
+ | "Sign in to sync your picks everywhere" | "Sign in to continue" |
271
+ | "Keep these on all your devices" | "You must sign in to save" |
272
+ | "Saved on this device — sign in to sync" | "Sign in via the Vibes DIY logo" |
273
+
274
+ And never disable the app's real controls on `!viewer`. If favoriting works signed out, keep it working signed out — the platform refuses the *cloud* write on its own, calmly, while the local write still succeeds. Turning a working app into a signup wall is worse than never asking at all.
275
+
276
+ ## Notes
277
+
278
+ - Never use Clerk user IDs. Only `userHandle` crosses into vibe code.
279
+ - Avatar URLs are stable indirection URLs — when a user changes their avatar, the URL stays the same and the bytes update. Treat them as opaque strings.
280
+ - To gate a write surface, use `useVibe(dbName).can` — asked about the document you are about to write. The server is the authority either way; `can` is the app's faithful preview of its answer.
281
+
282
+ ## ViewerTag
283
+
284
+ `ViewerTag` is a ready-made inline user pill returned alongside `viewer` from `useViewer()`. It is not a separate import — you get it from the hook. **Use it to render _other_ people** — comment authors, roster rows, "added by" labels — by passing `userHandle`:
285
+
286
+ ```jsx
287
+ {/* Show another user read-only: */}
288
+ <ViewerTag userHandle={comment.authorHandle} />
289
+ {/* Style override: */}
290
+ <ViewerTag userHandle={member.userHandle} style={{ borderRadius: 8, fontSize: 12 }} />
291
+ ```
292
+
293
+ **The current viewer's pill is system chrome, not app UI.** The platform already shows the current user inside the Vibes Switch that opens from the logo, so don't add a no-prop `<ViewerTag />` as a header pill just to show who's signed in; reach for `userHandle` to render someone else instead. To *ask* an anonymous visitor to sign in, use `requestLogin()` and your own copy (see **Asking a visitor to sign in**) — that ask belongs at the moment of value, which is a place only your app knows.
294
+
295
+ **The one in-app reason to render a no-prop `<ViewerTag />` is inline avatar self-edit.** A no-prop tag shows the signed-in viewer a dashed edit ring that lets them change their _own_ avatar in place, and that works for **every** signed-in viewer — the Switch's avatar editing is owner-only. So if your app wants any member (not just the owner) to update their photo without leaving the app, render a guarded `{viewer && <ViewerTag />}` for that — otherwise leave the current-viewer pill to the Switch.
296
+
297
+ **Undefined safety.** If `userHandle` is present in props but falsy (e.g. a missing field from a loop lookup), `ViewerTag` renders a dim italic placeholder instead of the edit ring. This prevents a broken data source from accidentally turning an arbitrary pill into a photo-edit control.
298
+
299
+ **Anonymous & always-safe.** `ViewerTag` never throws regardless of login state. A no-prop `<ViewerTag />` renders a "Sign in" button for anonymous viewers — a fine fallback, but it is an identity chip in whatever spot you put it, not a CTA you can word or time. When the goal is conversion, write the prompt yourself and wire it to `requestLogin()`.
300
+
301
+ **Theming.** `ViewerTag` reads the canonical palette variables — `--surface`, `--text-primary`, `--border`, and `--accent` (plus optional `--accent-text` to override the on-accent text color) — from the app's CSS with sensible fallbacks. If your app defines these on `:root` (which every generated theme does), `ViewerTag` inherits the theme automatically with no extra props. The one exception is the signed-out **Sign in** CTA, which deliberately stays the Vibes brand blue: it is a platform affordance matched to the Vibes Switch shown to the same visitor, so it is not theme-inherited.
302
+
303
+ Pass `<ViewerTag userHandle={...} />` to render other people. Reach for the no-prop `<ViewerTag />` when you specifically want inline avatar self-edit for any signed-in member; for the current-user pill the Switch already covers it, and for a sign-in ask use `requestLogin()`.
@@ -2,6 +2,7 @@ export const useViewerConfig = {
2
2
  name: "use-viewer",
3
3
  label: "Viewer Identity",
4
4
  description: "Get the current viewer's identity and capability gates",
5
+ initialVariant: true,
5
6
  importModule: "use-vibes",
6
7
  importName: "useViewer",
7
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-viewer.js","sourceRoot":"","sources":["../../jsr/llms/use-viewer.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,wDAAwD;IACrE,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,WAAW;CACxB,CAAC"}
1
+ {"version":3,"file":"use-viewer.js","sourceRoot":"","sources":["../../jsr/llms/use-viewer.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,wDAAwD;IACrE,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,WAAW;IACzB,UAAU,EAAE,WAAW;CACxB,CAAC"}
@@ -322,7 +322,7 @@ async function assign(handle) {
322
322
 
323
323
  ### Adding a member to your own object — the canonical use
324
324
 
325
- Whenever the app is for a group — a shared list, a board, a trip, a club's page — each signed-in person has their own object and lets a friend into it by picking them here. The write is one small membership doc, and `access.js` turns that doc into the grant that gives the named person the object's channel (see access.md's per-object sharing examples). Keys on the picked handle and on the writer's own — the app owner has no special place in it.
325
+ Almost every app works this way — a shared list, a board, a trip, a club's page, a tracker one person starts and later shares with a coach: each signed-in person has their own object and lets a friend into it by picking them here. The write is one small membership doc, and `access.js` turns that doc into the grant that gives the named person the object's channel (see access.md's per-object sharing examples). Keys on the picked handle and on the writer's own — the app owner has no special place in it.
326
326
 
327
327
  ```jsx
328
328
  const { HandleInput } = useViewer();
@@ -338,4 +338,4 @@ async function addMember(handle) {
338
338
  {canInvite && <HandleInput onChange={addMember} placeholder="Add a friend…" />}
339
339
  ```
340
340
 
341
- Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app one person keeps for themselves needs none of this — the picker belongs where the prompt asks for other people.
341
+ 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", "no access control" — needs none of this; everywhere else the picker belongs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.1.21",
3
+ "version": "14.1.23",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -34,9 +34,9 @@
34
34
  "license": "Apache-2.0",
35
35
  "dependencies": {
36
36
  "@adviser/cement": "~0.5.34",
37
- "@vibes.diy/call-ai-v2": "14.1.21",
38
- "@vibes.diy/identity": "14.1.21",
39
- "@vibes.diy/use-vibes-types": "14.1.21",
37
+ "@vibes.diy/call-ai-v2": "14.1.23",
38
+ "@vibes.diy/identity": "14.1.23",
39
+ "@vibes.diy/use-vibes-types": "14.1.23",
40
40
  "arktype": "~2.2.3",
41
41
  "json-schema-faker": "~0.6.3"
42
42
  },