@vibes.diy/prompts 3.0.4 → 4.0.3

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/README.md CHANGED
@@ -1,23 +1,23 @@
1
1
  [![Join our
2
2
  Discord](https://discord-badge.selem.workers.dev/i/vnpWycj4Ta.svg)](https://discord.gg/vnpWycj4Ta)
3
3
 
4
- # ✨ Vibes DIY — make apps with your friends
4
+ # ✨ Vibes DIY — make apps with your friends, run the front counter
5
5
 
6
6
  Software is getting weird again. Describe what you want and get a real, live app — fun, done, and alive. [Try it now](https://vibes.diy/) or [fork on GitHub](https://github.com/VibesDIY/vibes.diy).
7
7
 
8
- Make apps with your friends, so easy even AI can do it. Describe your app in plain words, it builds instantly, and you keep changing it just by talking to it. Share the link and your friends join right away no setup, no "wait, let me send you the invite."
8
+ Make apps with your friends or run the front counter. So easy even AI can do it. Describe your app in plain words, it builds instantly, and you keep changing it just by talking to it. Share the link and your friends — or your customers — join right away. No setup, no "wait, let me send you the invite."
9
9
 
10
10
  ## Why it's different
11
11
 
12
- - **Fun** — build for any reason, or no reason at all.
13
- - **Done** — it's a real app at its own live link, not a prototype. State is saved automatically.
12
+ - **Fun** — build for any reason, or no reason at all. Or because the spreadsheet is killing you.
13
+ - **Done** — it's a real app at its own live link, not a prototype. It can take an order — add a payment flow when you wire one up. State is saved automatically.
14
14
  - **Alive** — keep talking to it and it keeps changing. So can anyone you share it with.
15
15
 
16
16
  Seeing a vibe makes you want to change it. Start from someone else's, tweak it to fit you, share it, and the people you share with remix their own version. You're not a user anymore — you're a maker.
17
17
 
18
18
  ## What people make
19
19
 
20
- A character bot that answers in your voice. A neighborhood resource tracker you share at the next town meeting. A flashcard app remixed into collaborative trivia your students change live. A workout tracker a whole gym swaps routines on. Every one is the same move: start, tweak, share, remix.
20
+ From the group chat to the front counter. A character bot that answers in your voice. A neighborhood resource tracker you share at the next town meeting. A daily-specials board that takes the order. The shared spreadsheet your team fights over, turned into a real app with a form and a dashboard. A booking-and-intake app your clients fill out themselves. Every one is the same move: start, tweak, share, remix.
21
21
 
22
22
  ## Yours to control
23
23
 
package/llms/fireproof.md CHANGED
@@ -367,6 +367,22 @@ Each capability (`read`, `write`, `delete`) is independent. Omitting one falls b
367
367
 
368
368
  Other `acl` variants: `useFireproof("drafts", { acl: { read: ["editors"], write: ["editors"], delete: ["editors"] } })` for editors-only space, or omit `acl` entirely to fall back to app-level role gates (existing behavior, always safe).
369
369
 
370
+ ## Anonymous local writes (`anonymousLocal`)
371
+
372
+ For "let a logged-out visitor try it and save a little state before signing in," pass `{ anonymousLocal: true }`. While logged out, `put`/`del`/`useLiveQuery`/`useDocument` run against a local (localStorage) store with the identical API — no auth branching in your code. On first sign-in the local docs migrate into the cloud database, then local storage clears.
373
+
374
+ ```js
375
+ const { useLiveQuery, database } = useFireproof("favorites", {
376
+ anonymousLocal: true,
377
+ // Optional: reshape/stamp each local doc for its new owner; return falsy to drop it.
378
+ // Preserve `_id` (spread `...doc`) — migration retries are only idempotent when
379
+ // the id is kept, otherwise a retry after a partial failure creates duplicates.
380
+ migrate: (doc, userHandle) => ({ ...doc, owner: userHandle }),
381
+ });
382
+ ```
383
+
384
+ Notes: use it only where anonymous-first state makes sense (favorites, drafts, a scratch list). A returning signed-out visitor is automatically steered to sign in rather than starting a fresh throwaway local session. Migration keeps local data intact if it fails, so nothing is lost.
385
+
370
386
  ## Reading Resolved Grants (`access`)
371
387
 
372
388
  `useFireproof()` returns an `access` property — the viewer's resolved roles and channels for that database, computed server-side from the access function's `members` and `grant` declarations. Use `access.roles` (ReadonlySet), `access.channels` (ReadonlySet), `access.hasRole(name)`, and `access.hasChannel(name)`. Use these to reflect roles/channels in the UI; gate writes with `useVibe(dbName).can`.
@@ -434,7 +450,7 @@ This example shows the full round-trip — access.js declares channels and grant
434
450
  - **Channel identity:** Channel docs use `_id: "ch:" + name` so names are unique. The `_id` is the channel identifier everywhere — in `channels`, `grant`, and `ctx.requireAccess()`.
435
451
  - **Channel grant:** A channel document grants the creator (`grant.users`), adds `grant.public` so all members can read, and `grant.roles` so posters can write.
436
452
  - **Write surfaces** are gated with `useVibe(dbName).can.create/edit/delete` — it runs this same access function, so the UI verdict matches the server. Render `.reason` when denied. (See use-vibe docs.)
437
- - **`ViewerTag`** takes `userHandle` when rendering another user.
453
+ - **`ViewerTag`** takes `userHandle` to render another user (authors, rosters). The current viewer's own pill and sign-in button are system chrome in the Vibes Switch (the logo) — don't add one to the app's UI, except a guarded no-prop `{viewer && <ViewerTag />}` when you want inline avatar self-edit for any signed-in member (see use-viewer docs).
438
454
 
439
455
  access.js
440
456
 
@@ -519,7 +535,8 @@ export default function App() {
519
535
 
520
536
  return (
521
537
  <div>
522
- <ViewerTag />
538
+ {/* No current-user pill / sign-in button here — that's system chrome in the
539
+ Vibes Switch (the panel the logo opens). ViewerTag below renders post authors. */}
523
540
 
524
541
  {/* gate the write surface on useVibe().can — it runs the access function */}
525
542
  {canPost.ok ? (
package/llms/use-vibe.md CHANGED
@@ -16,20 +16,19 @@ Pass the Fireproof database name you are writing to. You get:
16
16
 
17
17
  ## The rule
18
18
 
19
- Gate every write affordance on `can.*`. Render `reason` when denied. Never branch write permission on `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields — those drift from what `access.js` actually does. Identity display (avatars, "signed in as") comes from `useViewer()`'s `ViewerTag`, not from `useVibe`.
19
+ Gate every write affordance on `can.*`. Render `reason` when denied. Never branch write permission on `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields — those drift from what `access.js` actually does. Rendering **other** users (authors, rosters) is `useViewer()`'s `<ViewerTag userHandle={...} />`, not `useVibe`. The current viewer's own pill and the "signed in as" / sign-in button are system chrome in the Vibes Switch (the logo) — don't build them into the app. (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.)
20
20
 
21
21
  ```jsx
22
- import { useVibe, useViewer } from "use-vibes";
22
+ import { useVibe } from "use-vibes";
23
23
 
24
24
  function PromptBar({ database }) {
25
25
  const { can, ready, me } = useVibe("aestheticBoard");
26
- const { ViewerTag } = useViewer();
27
26
  if (!ready) return <div className="skeleton" />;
28
27
  const v = can.create({ type: "tile", authorHandle: me?.userHandle });
29
28
  if (!v.ok) return <p className="muted">{v.reason}</p>; // e.g. "authentication required"
30
29
  return (
31
30
  <form onSubmit={/* … */}>
32
- <ViewerTag />
31
+ {/* no current-user pill — identity + sign-in live in the Vibes Switch (the logo) */}
33
32
  <input placeholder="Add a tile…" />
34
33
  <button type="submit">Post</button>
35
34
  </form>
@@ -1,12 +1,14 @@
1
1
  # useViewer Hook
2
2
 
3
- `useViewer()` is a **read-only window** into viewer identity. The platform owns the rules — who's the owner, who has been granted read or write — and `useViewer()` lets your app see who's signed in and render their identity. You cannot grant or revoke access from code; you can only reflect the runtime's verdict in your UI.
3
+ `useViewer()` is a **read-only window** into viewer identity. The platform owns the rules — who's the owner, who has been granted read or write — and `useViewer()` lets your app see who's signed in and render identity. You cannot grant or revoke access from code; you can only reflect the runtime's verdict in your UI.
4
4
 
5
- Use `useViewer()` for identity and display only`ViewerTag`, avatars, and showing who's signed in. **Write surfaces are gated with `useVibe(dbName).can`** (see use-vibe docs), not with `viewer`/`access.*`.
5
+ **The signed-in viewer's own pill and the Sign in button are system chrome — you don't build them.** The platform shows the current user (and a "Sign in" button when anonymous) inside the **Vibes Switch**, the panel that opens when you click the logo. A visitor always has a way to see who they are and sign in from there, so your app does **not** need to add a header pill or a login button for the current viewer don't render a no-prop `<ViewerTag />` just to show "who's signed in" or to offer sign-in. To find the login button, click the logo.
6
+
7
+ 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`/`access.*`.
6
8
 
7
9
  ## Basic Usage
8
10
 
9
- Start with a minimal component that shows the viewer identity:
11
+ Start with a minimal component that reads the viewer identity. You don't render a sign-in button or a current-user pill — those live in the Vibes Switch (click the logo). Just branch on `viewer` for any welcome/empty copy your app needs:
10
12
 
11
13
  App.jsx
12
14
 
@@ -16,17 +18,15 @@ import { useFireproof } from "use-fireproof";
16
18
  import { useViewer } from "use-vibes";
17
19
 
18
20
  export default function App() {
19
- const { viewer, isViewerPending, ViewerTag } = useViewer();
21
+ const { viewer, isViewerPending } = useViewer();
20
22
 
21
23
  if (isViewerPending) return null;
22
24
 
23
25
  return (
24
26
  <div>
25
- <header>
26
- <ViewerTag />
27
- </header>
28
- {!viewer && <p>Sign in.</p>}
29
- {viewer && <p>Welcome back!</p>}
27
+ {/* No sign-in button here — the logo opens the Vibes Switch, which shows
28
+ the current user and a "Sign in" button as system chrome. */}
29
+ {viewer ? <p>Welcome back, {viewer.displayName ?? viewer.userHandle}!</p> : <p>Sign in from the logo menu to get started.</p>}
30
30
  </div>
31
31
  );
32
32
  }
@@ -41,7 +41,7 @@ export default function App() {
41
41
 
42
42
  ## Gating UI
43
43
 
44
- Add a "commenting as" label and a write-gated form. Use `useVibe("comments").can` to gate the form; `ViewerTag` handles sign-in/identity display:
44
+ 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 its `reason` when denied:
45
45
 
46
46
  App.jsx
47
47
 
@@ -57,9 +57,9 @@ App.jsx
57
57
 
58
58
  ```jsx
59
59
  <<<<<<< SEARCH
60
- const { viewer, isViewerPending, ViewerTag } = useViewer();
60
+ const { viewer, isViewerPending } = useViewer();
61
61
  =======
62
- const { viewer, isViewerPending, ViewerTag } = useViewer();
62
+ const { viewer, isViewerPending } = useViewer();
63
63
  const { can, ready, me } = useVibe("comments");
64
64
  >>>>>>> REPLACE
65
65
  ```
@@ -68,15 +68,8 @@ App.jsx
68
68
 
69
69
  ```jsx
70
70
  <<<<<<< SEARCH
71
- {!viewer && <p>Sign in.</p>}
72
- {viewer && <p>Welcome back!</p>}
71
+ {viewer ? <p>Welcome back, {viewer.displayName ?? viewer.userHandle}!</p> : <p>Sign in from the logo menu to get started.</p>}
73
72
  =======
74
- {/* identity display */}
75
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
76
- {viewer && <span style={{ fontSize: 13, color: "var(--muted, #888)" }}>commenting as</span>}
77
- <ViewerTag />
78
- </div>
79
-
80
73
  {/* write gate: useVibe().can, not viewer */}
81
74
  {!ready ? null : (() => {
82
75
  const v = can.create({ type: "comment", authorHandle: me?.userHandle });
@@ -111,7 +104,7 @@ import { useFireproof } from "use-fireproof";
111
104
 
112
105
  ```jsx
113
106
  <<<<<<< SEARCH
114
- const { viewer, isViewerPending, ViewerTag } = useViewer();
107
+ const { viewer, isViewerPending } = useViewer();
115
108
  const { can, ready, me } = useVibe("comments");
116
109
  =======
117
110
  const { viewer, isViewerPending, ViewerTag } = useViewer();
@@ -137,6 +130,7 @@ import { useFireproof } from "use-fireproof";
137
130
  );
138
131
  })()}
139
132
  =======
133
+ {/* render OTHER users with userHandle — that's what ViewerTag is for here */}
140
134
  <ul>
141
135
  {comments.map((c) => (
142
136
  <li key={c._id}>
@@ -146,12 +140,6 @@ import { useFireproof } from "use-fireproof";
146
140
  ))}
147
141
  </ul>
148
142
 
149
- {/* identity display */}
150
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
151
- {viewer && <span style={{ fontSize: 13, color: "var(--muted, #888)" }}>commenting as</span>}
152
- <ViewerTag />
153
- </div>
154
-
155
143
  {/* write gate: useVibe().can, not viewer */}
156
144
  {!ready ? null : (() => {
157
145
  const v = can.create({ type: "comment", authorHandle: me?.userHandle });
@@ -201,36 +189,23 @@ Key points:
201
189
 
202
190
  ## ViewerTag
203
191
 
204
- `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.
205
-
206
- Show the current viewer (edit ring appears — they can tap to change their avatar):
207
-
208
- App.jsx
192
+ `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`:
209
193
 
210
194
  ```jsx
211
- <<<<<<< SEARCH
212
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
213
- {viewer && <span style={{ fontSize: 13, color: "var(--muted, #888)" }}>commenting as</span>}
214
- <ViewerTag />
215
- </div>
216
- =======
217
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
218
- {viewer && <span style={{ fontSize: 13, color: "var(--muted, #888)" }}>commenting as</span>}
219
- <ViewerTag />
220
- {/* Show another user read-only (no edit affordance): */}
221
- <ViewerTag userHandle={comments[0]?.authorHandle} />
222
- {/* Style override: */}
223
- <ViewerTag style={{ borderRadius: 8, fontSize: 12 }} />
224
- </div>
225
- >>>>>>> REPLACE
195
+ {/* Show another user read-only: */}
196
+ <ViewerTag userHandle={comment.authorHandle} />
197
+ {/* Style override: */}
198
+ <ViewerTag userHandle={member.userHandle} style={{ borderRadius: 8, fontSize: 12 }} />
226
199
  ```
227
200
 
228
- **Self-detection is automatic.** When `ViewerTag` renders the current viewer it shows a dashed indigo ring and pencil overlay on the avatar. Clicking it opens a file picker; the upload and profile save happen internally.
201
+ **The current viewer's identity and sign-in are system chrome, not app UI.** The platform already shows the current user and offers a "Sign in" button to anonymous viewers inside the Vibes Switch that opens from the logo. So don't add a no-prop `<ViewerTag />` as a header pill or login button just to show who's signed in; reach for `userHandle` to render someone else instead.
202
+
203
+ **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.
229
204
 
230
205
  **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 granting photo-edit access to an arbitrary pill.
231
206
 
232
- **Anonymous safety.** `ViewerTag` is always safe to call regardless of login state — it never throws. When the viewer is anonymous and no `userHandle` prop is given, it renders a "Sign in" button that opens the platform login UI when clicked. Wrap it in a `{viewer && <ViewerTag />}` guard if you want to suppress it entirely for anonymous users.
207
+ **Anonymous & always-safe.** `ViewerTag` never throws regardless of login state. A no-prop `<ViewerTag />` would render a "Sign in" button for anonymous viewers, but you don't need to place one for sign-in the logo's Vibes Switch already offers it.
233
208
 
234
209
  **Theming.** `ViewerTag` reads `--accent`, `--accent-text`, `--card-bg`, `--border`, `--text`, and `--muted` from the app's CSS variables with sensible fallbacks. If your app defines these on `:root` (which most generated themes do), `ViewerTag` inherits the theme automatically with no extra props.
235
210
 
236
- Use `<ViewerTag />` (no props) for the current user and `<ViewerTag userHandle={...} />` for others. That's the whole API.
211
+ Pass `<ViewerTag userHandle={...} />` to render other people. The no-prop `<ViewerTag />` (current user) covers identity and sign-in too, but the logo's Vibes Switch already does that — reach for it only when you specifically want inline avatar self-edit for any signed-in member.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "3.0.4",
3
+ "version": "4.0.3",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -24,13 +24,9 @@
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
26
  "@adviser/cement": "~0.5.34",
27
- "@fireproof/core": "~0.24.19",
28
- "@fireproof/core-types-base": "~0.24.19",
29
- "@fireproof/core-types-protocols-cloud": "~0.24.19",
30
- "@fireproof/use-fireproof": "~0.24.19",
31
- "@vibes.diy/call-ai-v2": "^3.0.4",
32
- "@vibes.diy/identity": "^3.0.4",
33
- "@vibes.diy/use-vibes-types": "^3.0.4",
27
+ "@vibes.diy/call-ai-v2": "^4.0.3",
28
+ "@vibes.diy/identity": "^4.0.3",
29
+ "@vibes.diy/use-vibes-types": "^4.0.3",
34
30
  "arktype": "~2.2.1",
35
31
  "json-schema-faker": "~0.6.2"
36
32
  },
@@ -17,7 +17,7 @@ You are an AI assistant tasked with creating React components. You should create
17
17
  - Always show loading states during any async operation (callAI, fetch, database queries): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
18
18
  - Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves fast but isn't instant, so the UI must react the moment the user acts. Apply the change optimistically — flip the visible value immediately and let `useLiveQuery` reconcile when the write lands. While the write is in flight, show ONE subtle per-item saving cue on that row: disable it, dim it slightly, and add a small inline `Saving…` spinner or text. Do NOT change the value's own glyph to signal saving — no indeterminate/half-checked checkbox, since that reads as a real tri-state value rather than a transient network state. Track pending state in a keyed collection (e.g. a `Set` of saving ids in `useState` — add the id before the write, delete it in `.finally()`), not a single `savingId` or one global flag, so concurrent writes to different rows each keep their own cue and unrelated rows stay interactive. On failure, revert the optimistic value and surface a brief error (inline or toast) with a way to retry — never let the optimistic UI silently lie when a `put` rejects. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
19
19
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
20
- - Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` for avatars and showing who's signed in. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
20
+ - Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill and the sign-in button are system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill or login button for the current user. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
21
21
  - Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
22
22
  - Never use emojis in the UI. Use inline SVG icons instead — simple, single-color, stroke-based SVGs (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round). Build icons directly in JSX, do not import icon libraries.
23
23
  - List data items on the main page of your app so users don't have to hunt for them
@@ -41,7 +41,7 @@ Every code block must be preceded by the file name on its own line — `App.jsx`
41
41
  - The `<header>` with the real brand title and any always-visible chrome.
42
42
  - One stub function component per feature with a heading — these are the anchors for later edits.
43
43
  - A default-exported `App` function composing them inside `<main id="app">` with `<header id="app-header">`.
44
- - When a write surface needs gating, destructure `useVibe` for the database it writes to — `const { can, ready } = useVibe("<dbName>");` and `useViewer` for identity — `const { ViewerTag } = useViewer();`
44
+ - When a write surface needs gating, destructure `useVibe` for the database it writes to — `const { can, ready } = useVibe("<dbName>");`. Only destructure `useViewer` (`const { ViewerTag } = useViewer();`) when you render **other** users — `<ViewerTag userHandle={...} />` for authors/rosters. The current viewer's pill and sign-in live in the Vibes Switch (the logo), so don't add one to your header.
45
45
  - **Be creative with the layout, but respect mobile idioms.** Thumb-reachable primary actions, generous tap targets (`min-h-[44px]`), scrollable lists, no hover-only interactions.
46
46
  - NO hooks beyond `useVibe`/`useViewer`, NO data wiring — those land in the feature edits.
47
47
 
package/system-prompt.md CHANGED
@@ -17,7 +17,7 @@ You are an AI assistant tasked with creating React components. You should create
17
17
  - Always show loading states during any async operation (callAI, fetch, database queries): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
18
18
  - Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves fast but isn't instant, so the UI must react the moment the user acts. Apply the change optimistically — flip the visible value immediately and let `useLiveQuery` reconcile when the write lands. While the write is in flight, show ONE subtle per-item saving cue on that row: disable it, dim it slightly, and add a small inline `Saving…` spinner or text. Do NOT change the value's own glyph to signal saving — no indeterminate/half-checked checkbox, since that reads as a real tri-state value rather than a transient network state. Track pending state in a keyed collection (e.g. a `Set` of saving ids in `useState` — add the id before the write, delete it in `.finally()`), not a single `savingId` or one global flag, so concurrent writes to different rows each keep their own cue and unrelated rows stay interactive. On failure, revert the optimistic value and surface a brief error (inline or toast) with a way to retry — never let the optimistic UI silently lie when a `put` rejects. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
19
19
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
20
- - Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` for avatars and showing who's signed in. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
20
+ - Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill and the sign-in button are system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill or login button for the current user. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
21
21
  - Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
22
22
  - Never use emojis in the UI. Use inline SVG icons instead — simple, single-color, stroke-based SVGs (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round). Build icons directly in JSX, do not import icon libraries.
23
23
  - Consider and potentially reuse/extend code from previous responses if relevant
@@ -49,7 +49,7 @@ Every code block must be preceded by the file name on its own line — `App.jsx`
49
49
  - one stub function component per feature with a heading — these are the anchors for later edits
50
50
  - a default-exported `App` function composing them inside `<main id="app">` with `<header id="app-header">`
51
51
  - name the section ids and components after the features (e.g. `id="board"`, `id="compose"`), not literal `feature-one`
52
- - When a write surface needs gating, destructure `useVibe` for the database it writes to — `const { can, ready } = useVibe("<dbName>");` and `useViewer` for identity — `const { ViewerTag } = useViewer();`
52
+ - When a write surface needs gating, destructure `useVibe` for the database it writes to — `const { can, ready } = useVibe("<dbName>");`. Only destructure `useViewer` (`const { ViewerTag } = useViewer();`) when you render **other** users — `<ViewerTag userHandle={...} />` for authors/rosters. The current viewer's pill and sign-in live in the Vibes Switch (the logo), so don't add one to your header.
53
53
  - NO hooks beyond `useVibe`/`useViewer`, NO data wiring — those land in the feature edits
54
54
  - **Be creative with the layout, but respect mobile idioms.** Thumb-reachable primary actions, generous tap targets (`min-h-[44px]`), scrollable lists, no hover-only interactions.
55
55
 
@@ -331,7 +331,7 @@ function FeatureThree() {
331
331
  }
332
332
 
333
333
  export default function App() {
334
- const { ViewerTag } = useViewer();
334
+ // No sign-in/current-user pill here — that's system chrome in the Vibes Switch (the logo).
335
335
  return (
336
336
  <main id="app" className={classNames.page}>
337
337
  <header id="app-header" className={classNames.header}>
@@ -345,7 +345,7 @@ export default function App() {
345
345
  }
346
346
  ````
347
347
 
348
- Keep `useViewer` (for `ViewerTag`) on `App`'s first line, and add `useVibe(dbName)` in the feature components that gate writes — `can`/`ready` are read where the write surface lives, not hoisted to `App`.
348
+ Don't put a current-user `<ViewerTag />` or login button in the header — the logo's Vibes Switch already shows who's signed in and offers sign-in. Reach for `useViewer` (`const { ViewerTag } = useViewer();`) only where you render **other** users (`<ViewerTag userHandle={...} />`) — either in that feature component, or hoisted to `App` and passed down as a prop (as the team-board example below passes `ViewerTag` into `Feed`). Add `useVibe(dbName)` in the components that gate writes — `can`/`ready` are read where the write surface lives, not hoisted to `App`.
349
349
 
350
350
  **If the app needs an `access.js`, emit it right after the scaffold — before any feature edits.** Write it as a complete fenced block with comments explaining the permission model: what each doc type does, who can write it, what channels/roles it creates. This commits to the permission design early so every subsequent App.jsx edit can gate its write surfaces on `useVibe(dbName).can` — the same rules the access function enforces. If later feature edits introduce new doc types, emit a follow-up `access.js` block with the additions.
351
351
 
@@ -403,7 +403,7 @@ Example streamed output for a team board app:
403
403
  > <div className={c.page}>
404
404
  > <header id="app-header" className={c.header}>
405
405
  > <h1>Crew Board</h1>
406
- > <ViewerTag />
406
+ > {/* no current-user pill — the logo's Vibes Switch carries identity + sign-in */}
407
407
  > </header>
408
408
  > <main id="app">
409
409
  > <Channels />