@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 +5 -5
- package/llms/fireproof.md +19 -2
- package/llms/use-vibe.md +3 -4
- package/llms/use-viewer.md +25 -50
- package/package.json +4 -8
- package/system-prompt-initial.md +2 -2
- package/system-prompt.md +5 -5
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
[](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
|
|
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
|
|
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`
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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>
|
package/llms/use-viewer.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
21
|
+
const { viewer, isViewerPending } = useViewer();
|
|
20
22
|
|
|
21
23
|
if (isViewerPending) return null;
|
|
22
24
|
|
|
23
25
|
return (
|
|
24
26
|
<div>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
60
|
+
const { viewer, isViewerPending } = useViewer();
|
|
61
61
|
=======
|
|
62
|
-
const { viewer, isViewerPending
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
+
"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
|
-
"@
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
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
|
},
|
package/system-prompt-initial.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
|
|
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>")
|
|
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
|
|
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>")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
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 />
|