@vibes.diy/prompts 14.3.124 → 14.3.126

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/access.md CHANGED
@@ -802,14 +802,15 @@ export default function App() {
802
802
 
803
803
  async function submitPost() {
804
804
  if (!draft.trim() || !viewer) return;
805
- await database.put({
805
+ setDraft("");
806
+ const { ok } = await database.put({
806
807
  type: "post",
807
808
  channel,
808
809
  body: draft.trim(),
809
810
  authorHandle: viewer.userHandle,
810
811
  createdAt: Date.now(),
811
812
  });
812
- setDraft("");
813
+ if (!ok) setDraft((now) => now || draft);
813
814
  }
814
815
 
815
816
  return (
@@ -1139,10 +1140,11 @@ function BoardWorkspace() {
1139
1140
  const startBoard = async (event) => {
1140
1141
  event.preventDefault();
1141
1142
  if (!ready || !newBoard.name || !can.create(newBoard).ok) return;
1142
- await database.put(newBoard);
1143
- setName("");
1144
- setPickedBoardId(newBoard._id);
1145
- setDraftId("board-" + crypto.randomUUID());
1143
+ setName(""); // ready for the next name at once
1144
+ setDraftId("board-" + crypto.randomUUID()); // and the next board gets an id of its own
1145
+ const { ok } = await database.put(newBoard);
1146
+ if (ok) setPickedBoardId(newBoard._id);
1147
+ else setName((now) => now || newBoard.name);
1146
1148
  };
1147
1149
  const board =
1148
1150
  boards.find((board) => board._id === pickedBoardId) ??
@@ -1334,8 +1336,9 @@ function Shop() {
1334
1336
  const addFlavor = async (event) => {
1335
1337
  event.preventDefault();
1336
1338
  if (!ready || !flavor.name || !can.create(flavor).ok) return;
1337
- await database.put(flavor); // the exact value the control checked
1338
- setName("");
1339
+ setName(""); // ready for the next flavor at once
1340
+ const { ok } = await database.put(flavor); // the exact value the control checked
1341
+ if (!ok) setName((now) => now || flavor.name);
1339
1342
  };
1340
1343
  const order = async (f) => {
1341
1344
  const next = orderFor(f);
package/llms/calendar.md CHANGED
@@ -70,9 +70,10 @@ export default function App() {
70
70
  };
71
71
  const save = async () => {
72
72
  if (!words.trim() || !day) return;
73
- await database.put({ type: "session", piece: words.trim(), day, createdAt: Date.now() });
74
- setWords("");
73
+ setWords(""); // ready for the next session at once
75
74
  setDay(today);
75
+ const { ok } = await database.put({ type: "session", piece: words.trim(), day, createdAt: Date.now() });
76
+ if (!ok) setWords((now) => now || words);
76
77
  };
77
78
  return (
78
79
  <Stack gap={3}>
package/llms/fireproof.md CHANGED
@@ -62,9 +62,11 @@ function AddItem() {
62
62
  const [text, setText] = useState("");
63
63
  const [note, setNote] = useState("");
64
64
  async function add() {
65
- const { ok, synced } = await database.put({ type: "item", text });
65
+ const typed = text;
66
+ setText(""); // the field is ready for the next item the moment they press Add
67
+ const { ok, synced } = await database.put({ type: "item", text: typed });
66
68
  setNote(synced.code === "draft-read-only" ? synced.message : "");
67
- if (ok) setText(""); // clear the field once the record is saved
69
+ if (!ok) setText((now) => now || typed); // hand their words back while the field is still empty
68
70
  }
69
71
  return (
70
72
  <Stack gap={2}>
@@ -78,6 +80,8 @@ function AddItem() {
78
80
  }
79
81
  ```
80
82
 
83
+ Clear the field as the person submits, before the put: capture what they typed, empty the field, then write the captured value. The field is ready for their next entry the moment they press Add, and anything they type while the write settles stays theirs for the next save. When the result comes back `ok: false`, hand the words back with an updater that keeps newer typing, `setText((now) => now || typed)`. `useDocument`'s `submit()` follows the same shape on its own: it hands back a fresh draft as the person submits, saves the submitted one, and restores it when that save does not land while the form is still untouched.
84
+
81
85
  ### Basic Example
82
86
 
83
87
  When the composer is on screen, wire these visible actions in the same build, using the app's own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
@@ -255,7 +259,7 @@ export default function App() {
255
259
 
256
260
  ### Editing Documents
257
261
 
258
- Address an existing record by its `_id`. `useDocument(task)` above holds the selected record's editable fields: `merge(updates)` changes the draft, `save()` writes it, and `reset()` restores its initial state. `submit(event)` combines prevent-default, save and reset. Choose the operation that matches the editor's lifecycle. For correction by sentence, pass the current editable fields and the person's correction to the same extraction schema, normalize the result and immediately update the existing document in that submit action with its trusted `_id` and identity fields intact. The callai skill's Text to records example includes that path and pasted lists.
262
+ Address an existing record by its `_id`. `useDocument(task)` above holds the selected record's editable fields: `merge(updates)` changes the draft, `save()` writes it, and `reset()` restores its initial state. `submit(event)` combines prevent-default, reset and save: the form resets as the person submits, the submitted draft is saved, and edits made while the save settles stay in the form. Choose the operation that matches the editor's lifecycle. For correction by sentence, pass the current editable fields and the person's correction to the same extraction schema, normalize the result and immediately update the existing document in that submit action with its trusted `_id` and identity fields intact. The callai skill's Text to records example includes that path and pasted lists.
259
263
 
260
264
  ## Required starting records
261
265
 
@@ -793,7 +797,7 @@ import { Button, Card, ErrorNote, Field, Figure, Grid, Heading, Input, Stack, Te
793
797
  export default function App() {
794
798
  const { useDocument, useLiveQuery } = useFireproof("imageUploads");
795
799
 
796
- const { doc, merge, save, reset } = useDocument({
800
+ const { doc, merge, submit } = useDocument({
797
801
  _files: {},
798
802
  caption: "",
799
803
  type: "upload",
@@ -814,8 +818,9 @@ export default function App() {
814
818
  setSaveError("");
815
819
  try {
816
820
  // The save uploads the photo first, so it can reject; show why.
817
- await save();
818
- reset();
821
+ // submit() hands back a fresh form at once and saves the submitted one; a failed upload
822
+ // puts the photo back in the form while it is still untouched.
823
+ await submit();
819
824
  } catch (err) {
820
825
  setSaveError(err.message);
821
826
  }
package/llms/motion.md CHANGED
@@ -481,9 +481,9 @@ export default function App() {
481
481
 
482
482
  async function send() {
483
483
  if (!canPost) return;
484
- await database.put({ ...draft, _id: crypto.randomUUID(), createdAt: Date.now() });
485
- setTo("");
484
+ setTo(""); // ready for the next kudo at once
486
485
  setText("");
486
+ await database.put({ ...draft, _id: crypto.randomUUID(), createdAt: Date.now() });
487
487
  }
488
488
 
489
489
  return (
package/llms/physics.md CHANGED
@@ -333,9 +333,10 @@ export default function App() {
333
333
  async function keep() {
334
334
  if (!canMake) return;
335
335
  const doc = { _id: crypto.randomUUID(), type: "level", name: name || "My run", pieces, seed: 7, creatorHandle: me.userHandle, updatedAt: Date.now() };
336
- await database.put(doc);
337
- setName("");
338
- setActiveId(doc._id);
336
+ setName(""); // ready for the next name at once
337
+ const { ok } = await database.put(doc);
338
+ if (ok) setActiveId(doc._id);
339
+ else setName((now) => now || name);
339
340
  }
340
341
 
341
342
  return (
package/llms/pouf-blog.md CHANGED
@@ -107,11 +107,11 @@ export default function App() {
107
107
  if (Object.keys(next).length > 0) return;
108
108
  setPublishing(true);
109
109
  try {
110
- await database.put({ type: "post", title: draft.title.trim(), category: draft.category, body: draft.body.trim(), authorHandle: me?.userHandle, createdAt: Date.now() });
111
- // Cleared only once the write lands; the filter resets so the new post is in view.
110
+ // The form is ready at once; the filter resets so the new post is in view.
112
111
  setDraft(EMPTY_DRAFT);
113
112
  setWriting(false);
114
113
  pick("all");
114
+ await database.put({ type: "post", title: draft.title.trim(), category: draft.category, body: draft.body.trim(), authorHandle: me?.userHandle, createdAt: Date.now() });
115
115
  toast.success("Post published");
116
116
  } finally {
117
117
  setPublishing(false);
@@ -144,6 +144,8 @@ export default function App() {
144
144
  if (Object.keys(next).length > 0) return;
145
145
  setSaving(true);
146
146
  try {
147
+ setDraft(EMPTY_DRAFT);
148
+ setAdding(false);
147
149
  await database.put({
148
150
  type: "event",
149
151
  date: selected,
@@ -153,9 +155,6 @@ export default function App() {
153
155
  authorHandle: me?.userHandle,
154
156
  createdAt: Date.now(),
155
157
  });
156
- // Cleared only once the write lands, so a failed save keeps what was typed.
157
- setDraft(EMPTY_DRAFT);
158
- setAdding(false);
159
158
  toast.success("Event added");
160
159
  } finally {
161
160
  setSaving(false);
package/llms/pouf-chat.md CHANGED
@@ -41,9 +41,10 @@ function Thread({ room, onBack }) {
41
41
  e.preventDefault();
42
42
  const text = draft.trim();
43
43
  if (text === "") return;
44
- await database.put({ type: "message", roomId: room._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
45
- // Cleared only once the write lands, so a local failure keeps the typed words.
44
+ // The field is ready for the next one at once; a save that does not land hands the words back.
46
45
  setDraft("");
46
+ const { ok } = await database.put({ type: "message", roomId: room._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
47
+ if (!ok) setDraft((now) => now || text);
47
48
  }
48
49
 
49
50
  return (
@@ -128,9 +129,10 @@ export default function App() {
128
129
  return;
129
130
  }
130
131
  setError(undefined);
131
- const { id } = await database.put({ type: "room", name: text, authorHandle: me?.userHandle, createdAt: Date.now() });
132
- setName("");
133
- setActiveId(id);
132
+ setName(""); // ready for the next room name at once
133
+ const { id, ok } = await database.put({ type: "room", name: text, authorHandle: me?.userHandle, createdAt: Date.now() });
134
+ if (ok) setActiveId(id);
135
+ else setName((now) => now || text);
134
136
  }
135
137
 
136
138
  function latest(roomId) {
@@ -63,7 +63,9 @@ export default function App() {
63
63
  }
64
64
  setSending(true);
65
65
  try {
66
- await database.put({
66
+ // The form is fresh for whoever writes next; the thank-you waits for the receipt.
67
+ setForm(BLANK);
68
+ const { ok } = await database.put({
67
69
  ...draft,
68
70
  name: form.name.trim(),
69
71
  email: form.email.trim(),
@@ -71,9 +73,8 @@ export default function App() {
71
73
  message: form.message.trim(),
72
74
  createdAt: Date.now(),
73
75
  });
74
- setSent({ name: form.name.trim(), email: form.email.trim() });
75
- // Cleared only once the write lands, so a local failure keeps every word.
76
- setForm(BLANK);
76
+ if (ok) setSent({ name: form.name.trim(), email: form.email.trim() });
77
+ else setForm((now) => (now === BLANK ? form : now));
77
78
  } finally {
78
79
  setSending(false);
79
80
  }
@@ -161,9 +161,9 @@ export default function App() {
161
161
  };
162
162
  setErrors(found);
163
163
  if (found.title || found.minutes) return;
164
- await database.put({ type: "lesson", title: form.title.trim(), kind: form.kind, minutes, authorHandle: me?.userHandle, createdAt: Date.now() });
165
164
  setForm({ title: "", kind: "Lesson", minutes: "10" });
166
165
  setAdding(false);
166
+ await database.put({ type: "lesson", title: form.title.trim(), kind: form.kind, minutes, authorHandle: me?.userHandle, createdAt: Date.now() });
167
167
  toast.success("Lesson added");
168
168
  }
169
169
 
package/llms/pouf-crm.md CHANGED
@@ -88,9 +88,10 @@ function CustomerView({ customer, notes, database, can, me, onBack }) {
88
88
  return;
89
89
  }
90
90
  setError(undefined);
91
- await database.put({ type: "note", customerId: customer._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
92
- // Cleared only once the write lands, so a local failure keeps the typed words.
91
+ // The field is ready for the next one at once; a save that does not land hands the words back.
93
92
  setDraft("");
93
+ const { ok } = await database.put({ type: "note", customerId: customer._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
94
+ if (!ok) setDraft((now) => now || text);
94
95
  }
95
96
 
96
97
  return (
@@ -201,6 +202,8 @@ export default function App() {
201
202
  };
202
203
  setErrors(found);
203
204
  if (found.name || found.value) return;
205
+ setForm({ name: "", company: "", email: "", value: "", stage: "lead" });
206
+ setAdding(false);
204
207
  await database.put({
205
208
  type: "customer",
206
209
  name: form.name.trim(),
@@ -211,8 +214,6 @@ export default function App() {
211
214
  authorHandle: me?.userHandle,
212
215
  createdAt: Date.now(),
213
216
  });
214
- setForm({ name: "", company: "", email: "", value: "", stage: "lead" });
215
- setAdding(false);
216
217
  toast.success("Customer added");
217
218
  }
218
219
 
@@ -113,13 +113,18 @@ export default function App() {
113
113
  setErrors(next);
114
114
  if (Object.keys(next).length > 0) return;
115
115
  setSaving(true);
116
+ // The fields are ready for the next sale at once; a save that does not land hands them back.
117
+ setItem("");
118
+ setAmount("");
119
+ setState("paid");
116
120
  try {
117
- await database.put({ type: "sale", item: item.trim(), amount: value, state, authorHandle: me?.userHandle, createdAt: Date.now() });
118
- // Cleared only once the write lands, so a local failure keeps what was typed.
119
- setItem("");
120
- setAmount("");
121
- setState("paid");
122
- toast.success("Sale recorded");
121
+ const { ok } = await database.put({ type: "sale", item: item.trim(), amount: value, state, authorHandle: me?.userHandle, createdAt: Date.now() });
122
+ if (ok) toast.success("Sale recorded");
123
+ else {
124
+ setItem((now) => now || item);
125
+ setAmount((now) => now || amount);
126
+ setState((now) => (now === "paid" ? state : now));
127
+ }
123
128
  } finally {
124
129
  setSaving(false);
125
130
  }
@@ -141,6 +141,9 @@ export default function App() {
141
141
  const found = { title: form.title.trim() === "" ? "Give the story a working title." : undefined };
142
142
  setErrors(found);
143
143
  if (found.title) return;
144
+ setForm({ title: "", section: "", due: "" });
145
+ setPitching(false);
146
+ setStage("draft");
144
147
  await database.put({
145
148
  type: "story",
146
149
  title: form.title.trim(),
@@ -150,9 +153,6 @@ export default function App() {
150
153
  authorHandle: me?.userHandle,
151
154
  createdAt: Date.now(),
152
155
  });
153
- setForm({ title: "", section: "", due: "" });
154
- setPitching(false);
155
- setStage("draft");
156
156
  toast.success("Story pitched");
157
157
  }
158
158
 
@@ -97,6 +97,8 @@ export default function App() {
97
97
  return;
98
98
  }
99
99
  setNameError(undefined);
100
+ setForm({ name: "", group: "", ticket: "General" });
101
+ setAdding(false);
100
102
  await database.put({
101
103
  type: "guest",
102
104
  name: form.name.trim(),
@@ -106,8 +108,6 @@ export default function App() {
106
108
  authorHandle: me?.userHandle,
107
109
  createdAt: Date.now(),
108
110
  });
109
- setForm({ name: "", group: "", ticket: "General" });
110
- setAdding(false);
111
111
  toast.success("Guest added");
112
112
  }
113
113
 
package/llms/pouf-feed.md CHANGED
@@ -42,8 +42,9 @@ function Post({ post, likes, comments, database, can, me }) {
42
42
  e.preventDefault();
43
43
  const text = reply.trim();
44
44
  if (text === "") return;
45
- await database.put({ type: "comment", postId: post._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
46
- setReply("");
45
+ setReply(""); // ready for the next reply at once
46
+ const { ok } = await database.put({ type: "comment", postId: post._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
47
+ if (!ok) setReply((now) => now || text);
47
48
  }
48
49
 
49
50
  async function remove() {
@@ -162,9 +163,10 @@ export default function App() {
162
163
  return;
163
164
  }
164
165
  setError(undefined);
165
- await database.put({ type: "post", text, authorHandle: me?.userHandle, createdAt: Date.now() });
166
- // Cleared only once the write lands, so a local failure keeps the typed words.
166
+ // The field is ready for the next one at once; a save that does not land hands the words back.
167
167
  setDraft("");
168
+ const { ok } = await database.put({ type: "post", text, authorHandle: me?.userHandle, createdAt: Date.now() });
169
+ if (!ok) setDraft((now) => now || text);
168
170
  }
169
171
 
170
172
  return (
@@ -54,9 +54,13 @@ function Reader({ note, replies, database, can, me, onBack }) {
54
54
  return;
55
55
  }
56
56
  setError(undefined);
57
- await database.put({ type: "reply", noteId: note._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
58
- // Cleared only once the write lands, so a local failure keeps the typed words.
57
+ // The field is ready for the next one at once; a save that does not land hands the words back.
59
58
  setDraft("");
59
+ const { ok } = await database.put({ type: "reply", noteId: note._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
60
+ if (!ok) {
61
+ setDraft((now) => now || text);
62
+ return;
63
+ }
60
64
  toast.success("Reply sent");
61
65
  }
62
66
 
@@ -194,6 +198,9 @@ export default function App() {
194
198
  };
195
199
  setErrors(found);
196
200
  if (found.subject || found.body) return;
201
+ setSubject("");
202
+ setBody("");
203
+ setComposing(false);
197
204
  await database.put({
198
205
  type: "note",
199
206
  subject: subject.trim(),
@@ -203,9 +210,6 @@ export default function App() {
203
210
  authorHandle: me?.userHandle,
204
211
  createdAt: Date.now(),
205
212
  });
206
- setSubject("");
207
- setBody("");
208
- setComposing(false);
209
213
  toast.success("Note sent");
210
214
  }
211
215
 
@@ -183,6 +183,11 @@ export default function App() {
183
183
  };
184
184
  setErrors(found);
185
185
  if (Object.values(found).some(Boolean)) return;
186
+ setForm(EMPTY_FORM);
187
+ setAdding(false);
188
+ // Clear the search and filter so the new item is on screen, then open it.
189
+ setQuery("");
190
+ setFilter("all");
186
191
  const { id } = await database.put({
187
192
  type: "item",
188
193
  name: form.name.trim(),
@@ -193,11 +198,6 @@ export default function App() {
193
198
  authorHandle: me?.userHandle,
194
199
  createdAt: Date.now(),
195
200
  });
196
- setForm(EMPTY_FORM);
197
- setAdding(false);
198
- // Clear the search and filter so the new item is on screen, then open it.
199
- setQuery("");
200
- setFilter("all");
201
201
  setSelectedId(id);
202
202
  toast.success("Item added");
203
203
  }
@@ -64,9 +64,10 @@ export default function App() {
64
64
  return;
65
65
  }
66
66
  setError(undefined);
67
- await database.put({ type: "card", title, column: "todo", authorHandle: me?.userHandle, createdAt: Date.now() });
68
- // Cleared only once the write lands, so a local failure keeps the typed words.
67
+ // The field is ready for the next one at once; a save that does not land hands the words back.
69
68
  setDraft("");
69
+ const { ok } = await database.put({ type: "card", title, column: "todo", authorHandle: me?.userHandle, createdAt: Date.now() });
70
+ if (!ok) setDraft((now) => now || title);
70
71
  }
71
72
 
72
73
  function moveCard(card, column) {
@@ -63,9 +63,9 @@ function SettingsForm({ saved, database, can, me }) {
63
63
  setSaving(true);
64
64
  try {
65
65
  const written = { ...saved, ...draft, ...form, name: form.name.trim(), updatedAt: Date.now() };
66
- await database.put(written);
67
- // Match the form to what was stored (trimmed), so it reads clean after saving.
66
+ // Match the form to what is stored (trimmed), so it reads clean the moment they save.
68
67
  setForm(pick(written));
68
+ await database.put(written);
69
69
  toast.success("Settings saved");
70
70
  } finally {
71
71
  setSaving(false);
@@ -76,12 +76,16 @@ function TicketView({ ticket, replies, database, can, me, onBack }) {
76
76
  return;
77
77
  }
78
78
  setError(undefined);
79
- await database.put({ type: "reply", ticketId: ticket._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
79
+ // The field is ready for the next one at once; a save that does not land hands the words back.
80
+ setDraft("");
81
+ const { ok } = await database.put({ type: "reply", ticketId: ticket._id, text, authorHandle: me?.userHandle, createdAt: Date.now() });
82
+ if (!ok) {
83
+ setDraft((now) => now || text);
84
+ return;
85
+ }
80
86
  // The asker replying puts the ball back with the desk; anyone else replying waits on the asker.
81
87
  const next = me?.userHandle && me.userHandle === ticket.authorHandle ? "open" : "waiting";
82
88
  if (canSetStatus && ticket.status !== next) await database.put({ ...ticket, status: next });
83
- // Cleared only once the write lands, so a local failure keeps the typed words.
84
- setDraft("");
85
89
  toast.success("Reply sent");
86
90
  }
87
91
 
@@ -203,6 +207,10 @@ export default function App() {
203
207
  };
204
208
  setErrors(found);
205
209
  if (found.subject || found.body) return;
210
+ setSubject("");
211
+ setBody("");
212
+ setUrgent(false);
213
+ setLogging(false);
206
214
  await database.put({
207
215
  type: "ticket",
208
216
  subject: subject.trim(),
@@ -212,10 +220,6 @@ export default function App() {
212
220
  authorHandle: me?.userHandle,
213
221
  createdAt: Date.now(),
214
222
  });
215
- setSubject("");
216
- setBody("");
217
- setUrgent(false);
218
- setLogging(false);
219
223
  toast.success("Ticket logged");
220
224
  }
221
225
 
package/llms/pouf-todo.md CHANGED
@@ -61,9 +61,10 @@ export default function App() {
61
61
  return;
62
62
  }
63
63
  setError(undefined);
64
- await database.put({ type: "task", text, done: false, createdAt: Date.now(), authorHandle: me?.userHandle });
65
- // Cleared only once the write lands, so a local failure keeps the typed words.
64
+ // The field is ready for the next one at once; a save that does not land hands the words back.
66
65
  setDraft("");
66
+ const { ok } = await database.put({ type: "task", text, done: false, createdAt: Date.now(), authorHandle: me?.userHandle });
67
+ if (!ok) setDraft((now) => now || text);
67
68
  }
68
69
 
69
70
  function setDone(task, checked) {
package/llms/strudel.md CHANGED
@@ -268,8 +268,9 @@ export default function App() {
268
268
  const verdict = can.create(draft);
269
269
 
270
270
  async function keep() {
271
- await database.put({ ...draft, createdAt: Date.now() });
272
- setName("");
271
+ setName(""); // ready for the next name at once
272
+ const { ok } = await database.put({ ...draft, createdAt: Date.now() });
273
+ if (!ok) setName((now) => now || name);
273
274
  }
274
275
 
275
276
  return (
package/llms/use-vibe.md CHANGED
@@ -62,8 +62,9 @@ function PromptBar({ database }) {
62
62
  <form
63
63
  onSubmit={async (e) => {
64
64
  e.preventDefault();
65
- await database.put({ type: "tile", authorHandle: me?.userHandle, text });
66
- setText("");
65
+ setText(""); // ready for the next tile at once
66
+ const { ok } = await database.put({ type: "tile", authorHandle: me?.userHandle, text });
67
+ if (!ok) setText((now) => now || text);
67
68
  }}
68
69
  >
69
70
  {/* no current-user pill — identity + sign-in live in the Vibes Switch (the logo) */}
@@ -200,12 +200,13 @@ Also add the `post` handler before `if (isViewerPending)`:
200
200
  =======
201
201
  async function post() {
202
202
  if (!body.trim()) return;
203
- await database.put({
203
+ setBody(""); // ready for the next post at once
204
+ const { ok } = await database.put({
204
205
  body: body.trim(),
205
206
  createdAt: Date.now(),
206
207
  authorHandle: me?.userHandle,
207
208
  });
208
- setBody("");
209
+ if (!ok) setBody((now) => now || body);
209
210
  }
210
211
 
211
212
  if (isViewerPending) return null;
package/llms/worlds.md CHANGED
@@ -451,8 +451,9 @@ export default function App() {
451
451
  async function keep() {
452
452
  const world = worldRef.current;
453
453
  if (!world || !canKeep) return;
454
- await database.put({ ...draft, _id: crypto.randomUUID(), tick: world.tick, createdAt: Date.now() });
455
- setName("");
454
+ setName(""); // ready for the next name at once
455
+ const { ok } = await database.put({ ...draft, _id: crypto.randomUUID(), tick: world.tick, createdAt: Date.now() });
456
+ if (!ok) setName((now) => now || name);
456
457
  }
457
458
 
458
459
  const dial = (key, label, max) => (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.124",
3
+ "version": "14.3.126",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -39,10 +39,10 @@
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
41
  "@adviser/cement": "~0.5.34",
42
- "@vibes.diy/api-types": "14.3.124",
43
- "@vibes.diy/call-ai-v2": "14.3.124",
44
- "@vibes.diy/identity": "14.3.124",
45
- "@vibes.diy/use-vibes-types": "14.3.124",
42
+ "@vibes.diy/api-types": "14.3.126",
43
+ "@vibes.diy/call-ai-v2": "14.3.126",
44
+ "@vibes.diy/identity": "14.3.126",
45
+ "@vibes.diy/use-vibes-types": "14.3.126",
46
46
  "arktype": "~2.2.6",
47
47
  "json-schema-faker": "~0.6.3"
48
48
  },
@@ -37,6 +37,7 @@ You are an AI assistant tasked with creating React components. You should create
37
37
  - Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) pass `loading={isLoading}` to the trigger `Button`, which shows the package's own spinner and blocks a second click, (2) 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); }`
38
38
  - Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
39
39
  - 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 against the local replica instantly — there is no visible in-flight window to spinner over — but the UI must still react the moment the user acts. Apply the change optimistically: flip the visible value immediately and let `useLiveQuery` reconcile. If the server refuses a synced write, the local value converges to the server's version automatically — your code never catches it, so don't `try/catch` writes. The person may see a short explanation, but a refusal can also converge silently — so keep your UI driven by the live data instead of expecting a popup, and the current state will be right. Transport and offline failures are handled by the built-in queue, so don't hand-roll retry either. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
40
+ - A form that adds one record after another empties its field as the person submits, before the write: capture what they typed, clear the field, then save the captured value — `const typed = name; setName(""); const { ok } = await database.put({ type: "habit", name: typed }); if (!ok) setName((now) => now || typed);`. The field is ready for the next entry the moment they press Add, and words they type while the save settles stay theirs.
40
41
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
41
42
  - 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)`); the verdict has three states — while it is `pending` (or `ready` is false) hold the surface in a quiet checking state, and on a resolved denial write the sign-in or join copy in your app's own words, letting `reason` inform the sentence you write. `can.*` asks the server's permission rules — the same rules 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 is system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill for the current user. But ASKING an anonymous visitor to sign in is the app's job: call `useViewer().requestLogin()` from your own button, gated on `!isViewerPending`, at the moment the visitor has made something worth keeping — never a sentence telling them to find the logo, and never a wall in front of an app that already works for them. Owner-only management UI is gated on `can.*` too (the runtime knows the owner). 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.
42
43
  - 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
@@ -158,10 +159,11 @@ function Studio() {
158
159
  const startLoom = async (event) => {
159
160
  event.preventDefault();
160
161
  if (!ready || !newLoom.name || !can.create(newLoom).ok) return;
161
- await database.put(newLoom); // the exact value the control checked
162
- setLoomName("");
163
- setPickedPlace(newLoom._id);
164
- setLoomId("loom-" + crypto.randomUUID());
162
+ setLoomName(""); // ready for the next name at once
163
+ setLoomId("loom-" + crypto.randomUUID()); // and the next loom gets an id of its own
164
+ const { ok } = await database.put(newLoom); // the exact value the control checked
165
+ if (ok) setPickedPlace(newLoom._id);
166
+ else setLoomName((now) => now || newLoom.name);
165
167
  };
166
168
  // Hooks stay above this hold. Identity resolves before sign-in copy or controls.
167
169
  if (!ready) return <p role="status">Checking studio access…</p>;
@@ -197,8 +199,9 @@ function PlaceContents({ place }) {
197
199
  const add = async (event) => {
198
200
  event.preventDefault();
199
201
  if (!ready || !draft.title || !can.create(draft).ok) return;
200
- await database.put(draft); // the exact value the control checked
201
- setTitle("");
202
+ setTitle(""); // ready for the next weaving at once
203
+ const { ok } = await database.put(draft); // the exact value the control checked
204
+ if (!ok) setTitle((now) => now || draft.title);
202
205
  };
203
206
  return <section>
204
207
  {place.loom ? <h1>{place.loom.name}</h1> : <StudioName studioHandle={place.key.studioHandle} />}
@@ -37,6 +37,7 @@ You are an AI assistant tasked with creating React components. You should create
37
37
  - Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) pass `loading={isLoading}` to the trigger `Button`, which shows the package's own spinner and blocks a second click, (2) 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); }`
38
38
  - Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
39
39
  - 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 against the local replica instantly — there is no visible in-flight window to spinner over — but the UI must still react the moment the user acts. Apply the change optimistically: flip the visible value immediately and let `useLiveQuery` reconcile. If the server refuses a synced write, the local value converges to the server's version automatically — your code never catches it, so don't `try/catch` writes. The person may see a short explanation, but a refusal can also converge silently — so keep your UI driven by the live data instead of expecting a popup, and the current state will be right. Transport and offline failures are handled by the built-in queue, so don't hand-roll retry either. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
40
+ - A form that adds one record after another empties its field as the person submits, before the write: capture what they typed, clear the field, then save the captured value — `const typed = name; setName(""); const { ok } = await database.put({ type: "habit", name: typed }); if (!ok) setName((now) => now || typed);`. The field is ready for the next entry the moment they press Add, and words they type while the save settles stay theirs.
40
41
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
41
42
  - 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)`); the verdict has three states — while it is `pending` (or `ready` is false) hold the surface in a quiet checking state, and on a resolved denial write the sign-in or join copy in your app's own words, letting `reason` inform the sentence you write. `can.*` asks the server's permission rules — the same rules 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 is system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill for the current user. But ASKING an anonymous visitor to sign in is the app's job: call `useViewer().requestLogin()` from your own button, gated on `!isViewerPending`, at the moment the visitor has made something worth keeping — never a sentence telling them to find the logo, and never a wall in front of an app that already works for them. Owner-only management UI is gated on `can.*` too (the runtime knows the owner). 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.
42
43
  - 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
@@ -162,10 +163,11 @@ function Studio() {
162
163
  const startLoom = async (event) => {
163
164
  event.preventDefault();
164
165
  if (!ready || !newLoom.name || !can.create(newLoom).ok) return;
165
- await database.put(newLoom); // the exact value the control checked
166
- setLoomName("");
167
- setPickedPlace(newLoom._id);
168
- setLoomId("loom-" + crypto.randomUUID());
166
+ setLoomName(""); // ready for the next name at once
167
+ setLoomId("loom-" + crypto.randomUUID()); // and the next loom gets an id of its own
168
+ const { ok } = await database.put(newLoom); // the exact value the control checked
169
+ if (ok) setPickedPlace(newLoom._id);
170
+ else setLoomName((now) => now || newLoom.name);
169
171
  };
170
172
  // Hooks stay above this hold. Identity resolves before sign-in copy or controls.
171
173
  if (!ready) return <p role="status">Checking studio access…</p>;
@@ -201,8 +203,9 @@ function PlaceContents({ place }) {
201
203
  const add = async (event) => {
202
204
  event.preventDefault();
203
205
  if (!ready || !draft.title || !can.create(draft).ok) return;
204
- await database.put(draft); // the exact value the control checked
205
- setTitle("");
206
+ setTitle(""); // ready for the next weaving at once
207
+ const { ok } = await database.put(draft); // the exact value the control checked
208
+ if (!ok) setTitle((now) => now || draft.title);
206
209
  };
207
210
  return <section>
208
211
  {place.loom ? <h1>{place.loom.name}</h1> : <StudioName studioHandle={place.key.studioHandle} />}
package/system-prompt.md CHANGED
@@ -37,6 +37,7 @@ You are an AI assistant tasked with creating React components. You should create
37
37
  - Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) pass `loading={isLoading}` to the trigger `Button`, which shows the package's own spinner and blocks a second click, (2) 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); }`
38
38
  - Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
39
39
  - 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 against the local replica instantly, so make the UI react the moment the person acts: flip the visible value immediately and let `useLiveQuery` reconcile. The server has the final word — a refused write converges to the server's version on its own, and the platform tells the person, in `access.js`'s own `forbidden("…")` words when it gives some — so write plainly, keep the UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state shows the truth, and gate write surfaces up front with `useVibe(dbName).can`. A resolved `put` means saved on this device; for feedback beside a control, follow its receipt, and every `useLiveQuery` row carries `doc._synced.status`: `const { synced } = await database.put({ ...task, done: !task.done }); synced.on((s) => setNote(s.status === "refused" && s.message ? s.message : ""));`. The built-in queue retries transport and offline failures for you. Every checkbox tap, toggle, and saved inline edit shows a visible response.
40
+ - A form that adds one record after another empties its field as the person submits, before the write: capture what they typed, clear the field, then save the captured value — `const typed = name; setName(""); const { ok } = await database.put({ type: "habit", name: typed }); if (!ok) setName((now) => now || typed);`. The field is ready for the next entry the moment they press Add, and words they type while the save settles stay theirs.
40
41
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
41
42
  - 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)`); the verdict has three states — while it is `pending` (or `ready` is false) hold the surface in a quiet checking state, and on a resolved denial write the sign-in or join copy in your app's own words, letting `reason` inform the sentence you write. `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 is system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill for the current user. But ASKING an anonymous visitor to sign in is the app's job: call `useViewer().requestLogin()` from your own button, gated on `!isViewerPending`, at the moment the visitor has made something worth keeping — never a sentence telling them to find the logo, and never a wall in front of an app that already works for them. 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. `can.*` is a hint: the server checks each change against the saved record, so a write `can.*` allowed can still be refused — it converges on its own and the platform says so. See use-vibe docs.
42
43
  - 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