@vibes.diy/prompts 14.3.59 → 14.3.65

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.
Files changed (160) hide show
  1. package/llms/access.md +4 -0
  2. package/llms/index.d.ts +29 -1
  3. package/llms/index.js +84 -0
  4. package/llms/index.js.map +1 -1
  5. package/llms/pouf-blog.d.ts +2 -0
  6. package/llms/pouf-blog.js +6 -0
  7. package/llms/pouf-blog.js.map +1 -0
  8. package/llms/pouf-blog.md +305 -0
  9. package/llms/pouf-booking.d.ts +2 -0
  10. package/llms/pouf-booking.js +6 -0
  11. package/llms/pouf-booking.js.map +1 -0
  12. package/llms/pouf-booking.md +397 -0
  13. package/llms/pouf-calendar.d.ts +2 -0
  14. package/llms/pouf-calendar.js +6 -0
  15. package/llms/pouf-calendar.js.map +1 -0
  16. package/llms/pouf-calendar.md +352 -0
  17. package/llms/pouf-chat.d.ts +2 -0
  18. package/llms/pouf-chat.js +6 -0
  19. package/llms/pouf-chat.js.map +1 -0
  20. package/llms/pouf-chat.md +220 -0
  21. package/llms/pouf-contact.d.ts +2 -0
  22. package/llms/pouf-contact.js +6 -0
  23. package/llms/pouf-contact.js.map +1 -0
  24. package/llms/pouf-contact.md +231 -0
  25. package/llms/pouf-course.d.ts +2 -0
  26. package/llms/pouf-course.js +6 -0
  27. package/llms/pouf-course.js.map +1 -0
  28. package/llms/pouf-course.md +299 -0
  29. package/llms/pouf-crm.d.ts +2 -0
  30. package/llms/pouf-crm.js +6 -0
  31. package/llms/pouf-crm.js.map +1 -0
  32. package/llms/pouf-crm.md +330 -0
  33. package/llms/pouf-dashboard.d.ts +2 -0
  34. package/llms/pouf-dashboard.js +6 -0
  35. package/llms/pouf-dashboard.js.map +1 -0
  36. package/llms/pouf-dashboard.md +269 -0
  37. package/llms/pouf-editorial.d.ts +2 -0
  38. package/llms/pouf-editorial.js +6 -0
  39. package/llms/pouf-editorial.js.map +1 -0
  40. package/llms/pouf-editorial.md +247 -0
  41. package/llms/pouf-event.d.ts +2 -0
  42. package/llms/pouf-event.js +6 -0
  43. package/llms/pouf-event.js.map +1 -0
  44. package/llms/pouf-event.md +237 -0
  45. package/llms/pouf-feed.d.ts +2 -0
  46. package/llms/pouf-feed.js +6 -0
  47. package/llms/pouf-feed.js.map +1 -0
  48. package/llms/pouf-feed.md +249 -0
  49. package/llms/pouf-game.d.ts +2 -0
  50. package/llms/pouf-game.js +6 -0
  51. package/llms/pouf-game.js.map +1 -0
  52. package/llms/pouf-game.md +273 -0
  53. package/llms/pouf-inbox.d.ts +2 -0
  54. package/llms/pouf-inbox.js +6 -0
  55. package/llms/pouf-inbox.js.map +1 -0
  56. package/llms/pouf-inbox.md +317 -0
  57. package/llms/pouf-inventory.d.ts +2 -0
  58. package/llms/pouf-inventory.js +6 -0
  59. package/llms/pouf-inventory.js.map +1 -0
  60. package/llms/pouf-inventory.md +345 -0
  61. package/llms/pouf-kanban.d.ts +2 -0
  62. package/llms/pouf-kanban.js +6 -0
  63. package/llms/pouf-kanban.js.map +1 -0
  64. package/llms/pouf-kanban.md +235 -0
  65. package/llms/pouf-landing.d.ts +2 -0
  66. package/llms/pouf-landing.js +6 -0
  67. package/llms/pouf-landing.js.map +1 -0
  68. package/llms/pouf-landing.md +222 -0
  69. package/llms/pouf-login.d.ts +2 -0
  70. package/llms/pouf-login.js +6 -0
  71. package/llms/pouf-login.js.map +1 -0
  72. package/llms/pouf-login.md +162 -0
  73. package/llms/pouf-music.d.ts +2 -0
  74. package/llms/pouf-music.js +6 -0
  75. package/llms/pouf-music.js.map +1 -0
  76. package/llms/pouf-music.md +190 -0
  77. package/llms/pouf-onboarding.d.ts +2 -0
  78. package/llms/pouf-onboarding.js +6 -0
  79. package/llms/pouf-onboarding.js.map +1 -0
  80. package/llms/pouf-onboarding.md +227 -0
  81. package/llms/pouf-pricing.d.ts +2 -0
  82. package/llms/pouf-pricing.js +6 -0
  83. package/llms/pouf-pricing.js.map +1 -0
  84. package/llms/pouf-pricing.md +193 -0
  85. package/llms/pouf-profile.d.ts +2 -0
  86. package/llms/pouf-profile.js +6 -0
  87. package/llms/pouf-profile.js.map +1 -0
  88. package/llms/pouf-profile.md +177 -0
  89. package/llms/pouf-quiz.d.ts +2 -0
  90. package/llms/pouf-quiz.js +6 -0
  91. package/llms/pouf-quiz.js.map +1 -0
  92. package/llms/pouf-quiz.md +267 -0
  93. package/llms/pouf-settings.d.ts +2 -0
  94. package/llms/pouf-settings.js +6 -0
  95. package/llms/pouf-settings.js.map +1 -0
  96. package/llms/pouf-settings.md +220 -0
  97. package/llms/pouf-storefront.d.ts +2 -0
  98. package/llms/pouf-storefront.js +6 -0
  99. package/llms/pouf-storefront.js.map +1 -0
  100. package/llms/pouf-storefront.md +293 -0
  101. package/llms/pouf-support.d.ts +2 -0
  102. package/llms/pouf-support.js +6 -0
  103. package/llms/pouf-support.js.map +1 -0
  104. package/llms/pouf-support.md +322 -0
  105. package/llms/pouf-testimonials.d.ts +2 -0
  106. package/llms/pouf-testimonials.js +6 -0
  107. package/llms/pouf-testimonials.js.map +1 -0
  108. package/llms/pouf-testimonials.md +156 -0
  109. package/llms/pouf-todo.d.ts +2 -0
  110. package/llms/pouf-todo.js +6 -0
  111. package/llms/pouf-todo.js.map +1 -0
  112. package/llms/pouf-todo.md +179 -0
  113. package/llms/pouf-weather.d.ts +2 -0
  114. package/llms/pouf-weather.js +6 -0
  115. package/llms/pouf-weather.js.map +1 -0
  116. package/llms/pouf-weather.md +186 -0
  117. package/llms/use-viewer.md +3 -0
  118. package/package.json +5 -4
  119. package/prompts.js +2 -2
  120. package/prompts.js.map +1 -1
  121. package/style-prompts.d.ts +1 -1
  122. package/style-prompts.js +2 -0
  123. package/style-prompts.js.map +1 -1
  124. package/themes/colors/aqua.yaml +79 -0
  125. package/themes/colors/brutalist.yaml +4 -4
  126. package/themes/colors/celery.yaml +79 -0
  127. package/themes/colors/home.yaml +79 -0
  128. package/themes/colors/house.yaml +2 -2
  129. package/themes/colors/lilac.yaml +79 -0
  130. package/themes/colors/mint.yaml +79 -0
  131. package/themes/colors/peach.yaml +79 -0
  132. package/themes/colors/periwinkle.yaml +79 -0
  133. package/themes/colors/rose.yaml +79 -0
  134. package/themes/colorsets-bundle.js +654 -6
  135. package/themes/colorsets-bundle.js.map +1 -1
  136. package/themes/colorsets.d.ts +3 -2
  137. package/themes/colorsets.js +84 -2
  138. package/themes/colorsets.js.map +1 -1
  139. package/themes/index.d.ts +1 -0
  140. package/themes/index.js +1 -0
  141. package/themes/index.js.map +1 -1
  142. package/themes/looks/index.d.ts +3 -1
  143. package/themes/looks/index.js +44 -7
  144. package/themes/looks/index.js.map +1 -1
  145. package/themes/looks/pouf/brief.md +144 -0
  146. package/themes/looks/pouf/catalog.generated.d.ts +16 -0
  147. package/themes/looks/pouf/catalog.generated.js +764 -0
  148. package/themes/looks/pouf/catalog.generated.js.map +1 -0
  149. package/themes/looks/pouf/variety.d.ts +4 -0
  150. package/themes/looks/pouf/variety.js +36 -0
  151. package/themes/looks/pouf/variety.js.map +1 -0
  152. package/themes/palette-gen.d.ts +5 -0
  153. package/themes/palette-gen.js +55 -1
  154. package/themes/palette-gen.js.map +1 -1
  155. package/themes/pouf-rooms.d.ts +47 -0
  156. package/themes/pouf-rooms.js +274 -0
  157. package/themes/pouf-rooms.js.map +1 -0
  158. package/themes/style-presets.d.ts +2 -0
  159. package/themes/style-presets.js +29 -1
  160. package/themes/style-presets.js.map +1 -1
@@ -0,0 +1,397 @@
1
+ # Pouf Booking
2
+
3
+ An appointment booker: pick a service, a day and a free time, add a name, review, confirm. It fits a massage practice's sessions, a tutor's lessons, a studio's portrait slots or a repair café's drop-in times. Reach for it whenever the app's core is people claiming time slots that only one person can hold. Every booking persists as its own document and availability is worked out from them, so a taken time disappears for everyone and the booker finds their sessions on every visit.
4
+
5
+ Built from @vibes.diy/pouf components; persisted state lives in Fireproof. Every write surface is gated on `useVibe(dbName).can`.
6
+
7
+ ## The block
8
+
9
+ Two cards beside each other in a `Grid cols="sidebar"`. The main side opens with an `Eyebrow`, the `Heading` and a muted promise, then the person's own bookings on a `Card`. Each booking shows a `Badge`: `up` when confirmed, `warn` when someone else got the time first. Each one has a quiet `Cancel` behind a `Confirm` wearing `tone="down"`, offered only where `can.delete(booking).ok`. The side holds the wizard, one step at a time under an `Eyebrow` and a `Progress` bar (`tone="info"`): each service is a clickable `RowCard` with a decorative `orange` `Blob`; the day is a `Segmented` (`tone="info"`) and the time a `RadioGroup` of only the free times, with a muted line when a day is full; the details are a name `Input` in a `Field` with its own error (nothing private, because every booking is readable so free times can be worked out); and the review ends in `Confirm booking`, that step's one lead, which shows `loading` while the write lands. Each booking is its own document, never a flag on a shared calendar document. Two people can confirm the same time at once, and sync keeps both documents, so availability is derived: a slot belongs to its earliest booking (by `createdAt`, then `_id`). A later one is shown to its booker as a clash, with a `Pick another time` button that removes it and keeps their details. Confirming waits on its `can` verdict from `useVibe("bookings")`: a `Skeleton` inside an `aria-busy` wrapper while it resolves, and a sentence in the app's own words when booking is closed. For a visitor who has not signed in, the lead is `Sign in to confirm`, wired to `useViewer().requestLogin()`, and their choices stay in place. Sign-in is required here, unlike a local-first note, because a signed-out write stays on the device until sign-in: a booking made that way would never reach the practice or hold the slot against anyone else. Between the write and the live query catching up, the wizard shows a confirming `Skeleton` rather than the review again, so the lead can't book twice; cancelling the booking just made resets the wizard.
10
+
11
+ ### `App.jsx`
12
+ ```jsx
13
+ import React, { useState } from "react";
14
+ import { useFireproof } from "use-fireproof";
15
+ import { useViewer, useVibe } from "use-vibes";
16
+ import {
17
+ Badge,
18
+ Blob,
19
+ Button,
20
+ Card,
21
+ Empty,
22
+ Eyebrow,
23
+ Field,
24
+ Grid,
25
+ Heading,
26
+ Input,
27
+ Row,
28
+ RowCard,
29
+ Segmented,
30
+ Skeleton,
31
+ Spacer,
32
+ Stack,
33
+ Text,
34
+ } from "@vibes.diy/pouf";
35
+ import { Confirm, Progress, RadioGroup, Toaster, toast } from "@vibes.diy/pouf/interactive";
36
+
37
+ const SERVICES = [
38
+ { id: "relax", name: "Relaxation massage", blurb: "A slow, full-body session to unwind.", minutes: 60, price: 80, icon: "sparkle" },
39
+ { id: "deep", name: "Deep tissue", blurb: "Focused work on tight shoulders and back.", minutes: 45, price: 70, icon: "target" },
40
+ { id: "short", name: "Neck and shoulders", blurb: "A quick reset between meetings.", minutes: 30, price: 45, icon: "smile" },
41
+ ];
42
+ const TIMES = ["09:30", "11:00", "14:00", "16:30"];
43
+ const STEPS = ["Service", "Time", "Your details", "Review"];
44
+ const MONEY = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD", maximumFractionDigits: 0 });
45
+ const DAY_LABEL = new Intl.DateTimeFormat(undefined, { weekday: "short", month: "short", day: "numeric" });
46
+
47
+ const isoDate = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
48
+ const dayLabel = (iso) => DAY_LABEL.format(new Date(`${iso}T12:00:00`));
49
+
50
+ /** The next `n` weekdays, starting tomorrow, as local YYYY-MM-DD strings. */
51
+ function upcomingDays(n) {
52
+ const out = [];
53
+ const d = new Date();
54
+ while (out.length < n) {
55
+ d.setDate(d.getDate() + 1);
56
+ if (d.getDay() !== 0 && d.getDay() !== 6) out.push(isoDate(d));
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /** Who holds each slot: its earliest booking. A later booking of the same slot is a clash. */
62
+ function holders(bookings) {
63
+ const first = {};
64
+ for (const b of bookings) {
65
+ const held = first[b.slot];
66
+ if (!held || b.createdAt < held.createdAt || (b.createdAt === held.createdAt && b._id < held._id)) first[b.slot] = b;
67
+ }
68
+ return first;
69
+ }
70
+
71
+ export default function App() {
72
+ const { database, useLiveQuery } = useFireproof("bookings");
73
+ const { docs: bookings } = useLiveQuery("type", { key: "booking" });
74
+ const { can, me } = useVibe("bookings");
75
+ const { requestLogin } = useViewer();
76
+ const days = upcomingDays(5);
77
+
78
+ const [step, setStep] = useState(0);
79
+ const [serviceId, setServiceId] = useState(SERVICES[0].id);
80
+ const [day, setDay] = useState(days[0]);
81
+ const [time, setTime] = useState("");
82
+ const [name, setName] = useState("");
83
+ const [nameError, setNameError] = useState(undefined);
84
+ const [saving, setSaving] = useState(false);
85
+ const [resultId, setResultId] = useState(undefined);
86
+
87
+ const held = holders(bookings);
88
+ const service = SERVICES.find((s) => s.id === serviceId);
89
+ const slot = `${day} ${time}`;
90
+ const freeTimes = TIMES.filter((t) => !held[`${day} ${t}`]);
91
+ const mine = me ? bookings.filter((b) => b.authorHandle === me.userHandle).sort((a, b) => a.slot.localeCompare(b.slot)) : [];
92
+ const result = resultId ? bookings.find((b) => b._id === resultId) : undefined;
93
+
94
+ // The verdict for the booking book() writes, carrying the same fields it stamps.
95
+ const draft = { type: "booking", authorHandle: me?.userHandle };
96
+ const canBook = can.create(draft);
97
+
98
+ function pickDay(value) {
99
+ setDay(value);
100
+ setTime("");
101
+ }
102
+
103
+ function saveDetails(e) {
104
+ e.preventDefault();
105
+ if (name.trim() === "") {
106
+ setNameError("Add the name we should call you.");
107
+ return;
108
+ }
109
+ setStep(3);
110
+ }
111
+
112
+ async function book() {
113
+ if (held[slot]) {
114
+ // Taken while they were reviewing: back to the times, details kept.
115
+ toast.error("That time was just booked. Pick another.");
116
+ setTime("");
117
+ setStep(1);
118
+ return;
119
+ }
120
+ setSaving(true);
121
+ try {
122
+ const { id } = await database.put({
123
+ ...draft,
124
+ serviceId,
125
+ serviceName: service.name,
126
+ day,
127
+ time,
128
+ slot,
129
+ name: name.trim(),
130
+ createdAt: Date.now(),
131
+ });
132
+ setResultId(id);
133
+ } finally {
134
+ setSaving(false);
135
+ }
136
+ }
137
+
138
+ async function pickAgain() {
139
+ if (result && can.delete(result).ok) await database.del(result._id);
140
+ setResultId(undefined);
141
+ setTime("");
142
+ setStep(1);
143
+ }
144
+
145
+ function bookAnother() {
146
+ setResultId(undefined);
147
+ setTime("");
148
+ setStep(0);
149
+ }
150
+
151
+ async function cancel(booking) {
152
+ await database.del(booking._id);
153
+ // Cancelling the booking the wizard just made starts the wizard over.
154
+ if (booking._id === resultId) bookAnother();
155
+ toast.success("Booking cancelled");
156
+ }
157
+
158
+ let confirmButton;
159
+ if (canBook.pending)
160
+ confirmButton = (
161
+ <div aria-busy="true">
162
+ <Skeleton variant="row" />
163
+ </div>
164
+ );
165
+ else if (!me)
166
+ // A booking is held for a person; their choices stay put while they sign in.
167
+ confirmButton = <Button onClick={requestLogin}>Sign in to confirm</Button>;
168
+ else if (!canBook.ok) confirmButton = <Text muted>Online booking is paused. Call us to book.</Text>;
169
+ else
170
+ confirmButton = (
171
+ <Button loading={saving} onClick={book}>
172
+ Confirm booking
173
+ </Button>
174
+ );
175
+
176
+ let wizard;
177
+ if (result && held[result.slot]?._id === result._id)
178
+ wizard = (
179
+ <Stack gap={4}>
180
+ <Empty icon="ok" title="You're booked">
181
+ {result.serviceName}, {dayLabel(result.day)} at {result.time}.
182
+ </Empty>
183
+ <Button block variant="quiet" onClick={bookAnother}>
184
+ Book another session
185
+ </Button>
186
+ </Stack>
187
+ );
188
+ else if (resultId && !result)
189
+ // Written, but the live query hasn't caught up: hold here so Confirm can't fire twice.
190
+ wizard = (
191
+ <div aria-busy="true">
192
+ <Stack gap={3}>
193
+ <Text muted>Confirming your booking…</Text>
194
+ <Skeleton variant="card" />
195
+ </Stack>
196
+ </div>
197
+ );
198
+ else if (result)
199
+ wizard = (
200
+ <Stack gap={4}>
201
+ <Badge tone="warn">Time taken</Badge>
202
+ <Text>Someone booked {result.time} a moment before you. Your details are kept; pick another time.</Text>
203
+ <Button block onClick={pickAgain}>
204
+ Pick another time
205
+ </Button>
206
+ </Stack>
207
+ );
208
+ else
209
+ wizard = (
210
+ <Stack gap={5}>
211
+ <Stack gap={2}>
212
+ <Eyebrow>
213
+ Step {step + 1} of {STEPS.length} · {STEPS[step]}
214
+ </Eyebrow>
215
+ <Progress value={step + 1} max={STEPS.length} tone="info" label="Booking progress" />
216
+ </Stack>
217
+
218
+ {step === 0 && (
219
+ <Stack gap={3}>
220
+ <Heading level={2}>Choose a service</Heading>
221
+ {SERVICES.map((s) => (
222
+ <RowCard
223
+ key={s.id}
224
+ onClick={() => {
225
+ setServiceId(s.id);
226
+ setStep(1);
227
+ }}
228
+ >
229
+ <Row gap={3} wrap={false} align="top">
230
+ <Blob icon={s.icon} tone="orange" size="sm" />
231
+ <Stack gap={1}>
232
+ <Text>{s.name}</Text>
233
+ <Text size="sm" muted>
234
+ {s.blurb}
235
+ </Text>
236
+ </Stack>
237
+ <Spacer />
238
+ <Stack gap={1}>
239
+ <Text num>{MONEY.format(s.price)}</Text>
240
+ <Text size="sm" muted>
241
+ {s.minutes} min
242
+ </Text>
243
+ </Stack>
244
+ </Row>
245
+ </RowCard>
246
+ ))}
247
+ </Stack>
248
+ )}
249
+
250
+ {step === 1 && (
251
+ <Stack gap={4}>
252
+ <Heading level={2}>Choose a time</Heading>
253
+ <Segmented label="Day" tone="info" value={day} onChange={pickDay} options={days.map((d) => ({ value: d, label: dayLabel(d) }))} />
254
+ {freeTimes.length === 0 ? (
255
+ <Text muted>{dayLabel(day)} is fully booked. Try another day.</Text>
256
+ ) : (
257
+ <RadioGroup label="Time" value={time} onChange={setTime} options={freeTimes.map((t) => ({ value: t, label: t }))} />
258
+ )}
259
+ <Row justify="between">
260
+ <Button variant="quiet" onClick={() => setStep(0)}>
261
+ Back
262
+ </Button>
263
+ <Button disabled={time === "" || !!held[slot]} onClick={() => setStep(2)}>
264
+ Add your details
265
+ </Button>
266
+ </Row>
267
+ </Stack>
268
+ )}
269
+
270
+ {step === 2 && (
271
+ <form onSubmit={saveDetails} noValidate>
272
+ <Stack gap={4}>
273
+ <Heading level={2}>Your details</Heading>
274
+ <Field label="Name" error={nameError}>
275
+ {(id, describedBy) => (
276
+ <Input
277
+ id={id}
278
+ describedBy={describedBy}
279
+ value={name}
280
+ onChange={(value) => {
281
+ setName(value);
282
+ if (nameError) setNameError(undefined);
283
+ }}
284
+ autoComplete="name"
285
+ invalid={nameError !== undefined}
286
+ />
287
+ )}
288
+ </Field>
289
+ <Row justify="between">
290
+ <Button variant="quiet" onClick={() => setStep(1)}>
291
+ Back
292
+ </Button>
293
+ <Button type="submit">Review booking</Button>
294
+ </Row>
295
+ </Stack>
296
+ </form>
297
+ )}
298
+
299
+ {step === 3 && (
300
+ <Stack gap={4}>
301
+ <Heading level={2}>Review your booking</Heading>
302
+ <Card variant="tight">
303
+ <Stack gap={2}>
304
+ <Row justify="between">
305
+ <Text muted>Service</Text>
306
+ <Text>{service.name}</Text>
307
+ </Row>
308
+ <Row justify="between">
309
+ <Text muted>When</Text>
310
+ <Text>
311
+ {dayLabel(day)} at {time}
312
+ </Text>
313
+ </Row>
314
+ <Row justify="between">
315
+ <Text muted>Price</Text>
316
+ <Text num>{MONEY.format(service.price)}</Text>
317
+ </Row>
318
+ </Stack>
319
+ </Card>
320
+ <Row justify="between">
321
+ <Button variant="quiet" onClick={() => setStep(2)}>
322
+ Edit details
323
+ </Button>
324
+ {confirmButton}
325
+ </Row>
326
+ </Stack>
327
+ )}
328
+ </Stack>
329
+ );
330
+
331
+ return (
332
+ <main className="mx-auto max-w-6xl px-4 py-8">
333
+ <Grid cols="sidebar" gap={6}>
334
+ <Stack gap={5}>
335
+ <Stack gap={2}>
336
+ <Eyebrow>Book a session</Eyebrow>
337
+ <Heading level={1}>Make time to feel better.</Heading>
338
+ <Text muted>Pick a service and a free time. Nothing is charged until you arrive.</Text>
339
+ </Stack>
340
+ <Card>
341
+ <Stack gap={3}>
342
+ <Heading level={2}>Your bookings</Heading>
343
+ {mine.length === 0 ? (
344
+ <Text muted>{me ? "Nothing booked yet." : "Sign in when you confirm and your bookings show here."}</Text>
345
+ ) : (
346
+ mine.map((b) => {
347
+ const ok = held[b.slot]?._id === b._id;
348
+ return (
349
+ <Row key={b._id} gap={2} wrap={false}>
350
+ <Stack gap={1}>
351
+ <Text>{b.serviceName}</Text>
352
+ <Text size="sm" muted>
353
+ {dayLabel(b.day)} at {b.time}
354
+ </Text>
355
+ </Stack>
356
+ <Spacer />
357
+ <Badge tone={ok ? "up" : "warn"}>{ok ? "Confirmed" : "Time taken"}</Badge>
358
+ {can.delete(b).ok && (
359
+ <Confirm
360
+ title="Cancel this booking?"
361
+ body={`${b.serviceName} on ${dayLabel(b.day)} at ${b.time} is released for someone else.`}
362
+ confirmLabel="Cancel booking"
363
+ cancelLabel="Keep it"
364
+ tone="down"
365
+ onConfirm={() => cancel(b)}
366
+ >
367
+ <Button size="sm" variant="quiet">
368
+ Cancel
369
+ </Button>
370
+ </Confirm>
371
+ )}
372
+ </Row>
373
+ );
374
+ })
375
+ )}
376
+ </Stack>
377
+ </Card>
378
+ </Stack>
379
+ <Card>{wizard}</Card>
380
+ </Grid>
381
+ <div className="pouf-toasts">
382
+ <Toaster />
383
+ </div>
384
+ </main>
385
+ );
386
+ }
387
+ ```
388
+
389
+ ## Adapting
390
+
391
+ - Swap `SERVICES` and `TIMES` for the practice's own; several practitioners make the slot `${person} ${day} ${time}`, so each one's calendar fills on its own.
392
+ - Keep one document per booking and derive who holds a slot from them; a shared "calendar" document with a taken flag per slot loses one of two bookings made at the same moment. Moving a booking is cancelling and booking again; never edit the `slot` in place, or the clash rule loses track of who was first.
393
+ - Bookings are readable so everyone can see which times are free, so keep private details (a phone number, health notes) out of the booking document, which is why the example asks only for a name; `access.js` can hold those in a separate document only the practice and the booker read.
394
+ - Whoever reads the bookings (the practice's own day view included) applies the same earliest-`createdAt` rule, so everyone agrees who holds a slot; `access.js` can reject a booking whose `createdAt` is far from now, so a wrong clock can't jump the queue.
395
+ - A new write surface gets its own verdict: `can.create(draft)` with the fields the write stamps, `can.edit(doc)` or `can.delete(doc)` on the document it changes, always on `useVibe` of the database it writes.
396
+
397
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/booking.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufCalendarConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufCalendarConfig = {
2
+ name: "pouf-calendar",
3
+ label: "Pouf calendar block",
4
+ description: "Calendar agenda/month view UI: month grid, day timeline, saved events",
5
+ };
6
+ //# sourceMappingURL=pouf-calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-calendar.js","sourceRoot":"","sources":["../../jsr/llms/pouf-calendar.ts"],"names":[],"mappings":"AAQA,MAAM,CAAC,MAAM,kBAAkB,GAAc;IAC3C,IAAI,EAAE,eAAe;IACrB,KAAK,EAAE,qBAAqB;IAC5B,WAAW,EAAE,uEAAuE;CACrF,CAAC"}