@vibes.diy/prompts 14.3.64 → 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 (133) hide show
  1. package/llms/index.d.ts +29 -1
  2. package/llms/index.js +84 -0
  3. package/llms/index.js.map +1 -1
  4. package/llms/pouf-blog.d.ts +2 -0
  5. package/llms/pouf-blog.js +6 -0
  6. package/llms/pouf-blog.js.map +1 -0
  7. package/llms/pouf-blog.md +305 -0
  8. package/llms/pouf-booking.d.ts +2 -0
  9. package/llms/pouf-booking.js +6 -0
  10. package/llms/pouf-booking.js.map +1 -0
  11. package/llms/pouf-booking.md +397 -0
  12. package/llms/pouf-calendar.d.ts +2 -0
  13. package/llms/pouf-calendar.js +6 -0
  14. package/llms/pouf-calendar.js.map +1 -0
  15. package/llms/pouf-calendar.md +352 -0
  16. package/llms/pouf-chat.d.ts +2 -0
  17. package/llms/pouf-chat.js +6 -0
  18. package/llms/pouf-chat.js.map +1 -0
  19. package/llms/pouf-chat.md +220 -0
  20. package/llms/pouf-contact.d.ts +2 -0
  21. package/llms/pouf-contact.js +6 -0
  22. package/llms/pouf-contact.js.map +1 -0
  23. package/llms/pouf-contact.md +231 -0
  24. package/llms/pouf-course.d.ts +2 -0
  25. package/llms/pouf-course.js +6 -0
  26. package/llms/pouf-course.js.map +1 -0
  27. package/llms/pouf-course.md +299 -0
  28. package/llms/pouf-crm.d.ts +2 -0
  29. package/llms/pouf-crm.js +6 -0
  30. package/llms/pouf-crm.js.map +1 -0
  31. package/llms/pouf-crm.md +330 -0
  32. package/llms/pouf-dashboard.d.ts +2 -0
  33. package/llms/pouf-dashboard.js +6 -0
  34. package/llms/pouf-dashboard.js.map +1 -0
  35. package/llms/pouf-dashboard.md +269 -0
  36. package/llms/pouf-editorial.d.ts +2 -0
  37. package/llms/pouf-editorial.js +6 -0
  38. package/llms/pouf-editorial.js.map +1 -0
  39. package/llms/pouf-editorial.md +247 -0
  40. package/llms/pouf-event.d.ts +2 -0
  41. package/llms/pouf-event.js +6 -0
  42. package/llms/pouf-event.js.map +1 -0
  43. package/llms/pouf-event.md +237 -0
  44. package/llms/pouf-feed.d.ts +2 -0
  45. package/llms/pouf-feed.js +6 -0
  46. package/llms/pouf-feed.js.map +1 -0
  47. package/llms/pouf-feed.md +249 -0
  48. package/llms/pouf-game.d.ts +2 -0
  49. package/llms/pouf-game.js +6 -0
  50. package/llms/pouf-game.js.map +1 -0
  51. package/llms/pouf-game.md +273 -0
  52. package/llms/pouf-inbox.d.ts +2 -0
  53. package/llms/pouf-inbox.js +6 -0
  54. package/llms/pouf-inbox.js.map +1 -0
  55. package/llms/pouf-inbox.md +317 -0
  56. package/llms/pouf-inventory.d.ts +2 -0
  57. package/llms/pouf-inventory.js +6 -0
  58. package/llms/pouf-inventory.js.map +1 -0
  59. package/llms/pouf-inventory.md +345 -0
  60. package/llms/pouf-kanban.d.ts +2 -0
  61. package/llms/pouf-kanban.js +6 -0
  62. package/llms/pouf-kanban.js.map +1 -0
  63. package/llms/pouf-kanban.md +235 -0
  64. package/llms/pouf-landing.d.ts +2 -0
  65. package/llms/pouf-landing.js +6 -0
  66. package/llms/pouf-landing.js.map +1 -0
  67. package/llms/pouf-landing.md +222 -0
  68. package/llms/pouf-login.d.ts +2 -0
  69. package/llms/pouf-login.js +6 -0
  70. package/llms/pouf-login.js.map +1 -0
  71. package/llms/pouf-login.md +162 -0
  72. package/llms/pouf-music.d.ts +2 -0
  73. package/llms/pouf-music.js +6 -0
  74. package/llms/pouf-music.js.map +1 -0
  75. package/llms/pouf-music.md +190 -0
  76. package/llms/pouf-onboarding.d.ts +2 -0
  77. package/llms/pouf-onboarding.js +6 -0
  78. package/llms/pouf-onboarding.js.map +1 -0
  79. package/llms/pouf-onboarding.md +227 -0
  80. package/llms/pouf-pricing.d.ts +2 -0
  81. package/llms/pouf-pricing.js +6 -0
  82. package/llms/pouf-pricing.js.map +1 -0
  83. package/llms/pouf-pricing.md +193 -0
  84. package/llms/pouf-profile.d.ts +2 -0
  85. package/llms/pouf-profile.js +6 -0
  86. package/llms/pouf-profile.js.map +1 -0
  87. package/llms/pouf-profile.md +177 -0
  88. package/llms/pouf-quiz.d.ts +2 -0
  89. package/llms/pouf-quiz.js +6 -0
  90. package/llms/pouf-quiz.js.map +1 -0
  91. package/llms/pouf-quiz.md +267 -0
  92. package/llms/pouf-settings.d.ts +2 -0
  93. package/llms/pouf-settings.js +6 -0
  94. package/llms/pouf-settings.js.map +1 -0
  95. package/llms/pouf-settings.md +220 -0
  96. package/llms/pouf-storefront.d.ts +2 -0
  97. package/llms/pouf-storefront.js +6 -0
  98. package/llms/pouf-storefront.js.map +1 -0
  99. package/llms/pouf-storefront.md +293 -0
  100. package/llms/pouf-support.d.ts +2 -0
  101. package/llms/pouf-support.js +6 -0
  102. package/llms/pouf-support.js.map +1 -0
  103. package/llms/pouf-support.md +322 -0
  104. package/llms/pouf-testimonials.d.ts +2 -0
  105. package/llms/pouf-testimonials.js +6 -0
  106. package/llms/pouf-testimonials.js.map +1 -0
  107. package/llms/pouf-testimonials.md +156 -0
  108. package/llms/pouf-todo.d.ts +2 -0
  109. package/llms/pouf-todo.js +6 -0
  110. package/llms/pouf-todo.js.map +1 -0
  111. package/llms/pouf-todo.md +179 -0
  112. package/llms/pouf-weather.d.ts +2 -0
  113. package/llms/pouf-weather.js +6 -0
  114. package/llms/pouf-weather.js.map +1 -0
  115. package/llms/pouf-weather.md +186 -0
  116. package/package.json +4 -4
  117. package/prompts.js +2 -2
  118. package/prompts.js.map +1 -1
  119. package/themes/colorsets.d.ts +1 -1
  120. package/themes/colorsets.js +63 -1
  121. package/themes/colorsets.js.map +1 -1
  122. package/themes/index.d.ts +1 -0
  123. package/themes/index.js +1 -0
  124. package/themes/index.js.map +1 -1
  125. package/themes/looks/index.js +6 -2
  126. package/themes/looks/index.js.map +1 -1
  127. package/themes/looks/pouf/brief.md +144 -0
  128. package/themes/looks/pouf/catalog.generated.d.ts +16 -0
  129. package/themes/looks/pouf/catalog.generated.js +764 -0
  130. package/themes/looks/pouf/catalog.generated.js.map +1 -0
  131. package/themes/looks/pouf/variety.d.ts +4 -0
  132. package/themes/looks/pouf/variety.js +36 -0
  133. package/themes/looks/pouf/variety.js.map +1 -0
@@ -0,0 +1,247 @@
1
+ # Pouf Editorial
2
+
3
+ A story pipeline that walks each piece from idea to launch: a school newspaper's desk, a newsletter's post queue, a team blog's calendar, a zine's issue plan. Reach for it whenever the app's core is pieces of writing that each sit in one stage (draft, scheduled, published) and move forward one step at a time. Stories and their stage persist, so the desk is where the team left it on the next visit and for everyone working it.
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 views, one job each. The desk opens with the `Heading` and a muted count of stories, a `Pitch a story` button (the desk's one lead) that opens a `Dialog` with the pitch form, and a `Segmented` stage filter (`tone="info"`) beside a `Dot` and a muted count for that stage. Each story is a clickable `RowCard` with its section in muted `Text`, its title, the due date and its stage as a `Badge` toned by meaning (`idle` draft, `info` scheduled, `up` published); an `Empty` stands in for a stage with nothing in it. Opening a story swaps in the story view: a quiet `IconButton` to go back, the title as the `Heading`, a `Card` with the writer as a `ViewerTag` and `Metric`s, and the one step forward as that view's lead (`Schedule story` from draft, `Publish story` from scheduled), with a quiet `Back to drafts` beside it. Every write waits on its `can` verdict from `useVibe("editorial")`: the pitch form shows a `Skeleton` inside an `aria-busy` wrapper while the verdict resolves and a sentence in the app's own words when pitching is closed to this visitor, and the stage buttons follow the same three states on `can.edit(story)`.
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
+ Button,
19
+ Card,
20
+ Dot,
21
+ Empty,
22
+ Field,
23
+ Heading,
24
+ Icon,
25
+ IconButton,
26
+ Input,
27
+ Metric,
28
+ Row,
29
+ RowCard,
30
+ Segmented,
31
+ Skeleton,
32
+ Spacer,
33
+ Stack,
34
+ Text,
35
+ } from "@vibes.diy/pouf";
36
+ import { Dialog, Toaster, toast } from "@vibes.diy/pouf/interactive";
37
+
38
+ const STAGES = {
39
+ draft: { label: "Draft", plural: "Drafts", empty: "No drafts", tone: "idle" },
40
+ scheduled: { label: "Scheduled", plural: "Scheduled", empty: "Nothing scheduled", tone: "info" },
41
+ published: { label: "Published", plural: "Published", empty: "Nothing published yet", tone: "up" },
42
+ };
43
+ const FILTERS = Object.entries(STAGES).map(([value, s]) => ({ value, label: s.plural }));
44
+ const NEXT = { draft: { to: "scheduled", label: "Schedule story" }, scheduled: { to: "published", label: "Publish story" } };
45
+
46
+ // A due date is stored as the "YYYY-MM-DD" string a date input gives; read it at local midnight.
47
+ const due = (date) => (date ? new Date(`${date}T00:00`).toLocaleDateString([], { month: "short", day: "numeric" }) : "No date");
48
+
49
+ function Writer({ handle }) {
50
+ const { ViewerTag } = useViewer();
51
+ return handle ? <ViewerTag userHandle={handle} /> : <Text>A guest</Text>;
52
+ }
53
+
54
+ /** Three states for any write surface: resolving, closed to this visitor, open. */
55
+ function Gate({ verdict, closed, children }) {
56
+ if (verdict.pending)
57
+ return (
58
+ <div aria-busy="true">
59
+ <Skeleton variant="card" />
60
+ </div>
61
+ );
62
+ if (!verdict.ok) return <Text muted>{closed}</Text>;
63
+ return children;
64
+ }
65
+
66
+ function StageBadge({ stage }) {
67
+ return <Badge tone={STAGES[stage].tone}>{STAGES[stage].label}</Badge>;
68
+ }
69
+
70
+ function StoryView({ story, database, can, onBack, onMoved }) {
71
+ const next = NEXT[story.status];
72
+
73
+ async function moveTo(status) {
74
+ await database.put({ ...story, status });
75
+ onMoved(status);
76
+ toast.success(status === "draft" ? "Back in drafts" : `${STAGES[status].label}: ${story.title}`);
77
+ }
78
+
79
+ return (
80
+ <Stack gap={5}>
81
+ <Stack gap={2}>
82
+ <Row gap={2} wrap={false}>
83
+ <IconButton size="sm" onClick={onBack} label="Back to the desk" icon={<Icon name="prev" size="sm" />} />
84
+ <Heading level={1}>{story.title}</Heading>
85
+ </Row>
86
+ <Row gap={2}>
87
+ <Text muted>{story.section || "No section"}</Text>
88
+ <StageBadge stage={story.status} />
89
+ </Row>
90
+ </Stack>
91
+
92
+ <Card variant="tight">
93
+ <Stack gap={3}>
94
+ <Row gap={2} wrap={false}>
95
+ <Text size="sm" muted>
96
+ Written by
97
+ </Text>
98
+ <Writer handle={story.authorHandle} />
99
+ </Row>
100
+ <Row gap={4}>
101
+ <Metric label="Due" value={due(story.due)} num={false} />
102
+ <Metric label="Pitched" value={new Date(story.createdAt).toLocaleDateString([], { month: "short", day: "numeric" })} num={false} />
103
+ </Row>
104
+ </Stack>
105
+ </Card>
106
+
107
+ <Gate verdict={can.edit(story)} closed="Only the desk's editors move a story along.">
108
+ <Row gap={2} justify="end">
109
+ {story.status !== "draft" && (
110
+ <Button variant="quiet" onClick={() => moveTo("draft")}>
111
+ Back to drafts
112
+ </Button>
113
+ )}
114
+ {next && <Button onClick={() => moveTo(next.to)}>{next.label}</Button>}
115
+ </Row>
116
+ </Gate>
117
+ </Stack>
118
+ );
119
+ }
120
+
121
+ export default function App() {
122
+ const { database, useLiveQuery } = useFireproof("editorial");
123
+ const { can, me } = useVibe("editorial");
124
+ const { docs } = useLiveQuery("type", { key: "story" });
125
+ const stories = [...docs].sort((a, b) => (a.due || "9999").localeCompare(b.due || "9999"));
126
+
127
+ const [stage, setStage] = useState("draft");
128
+ const [openId, setOpenId] = useState(undefined);
129
+ const [pitching, setPitching] = useState(false);
130
+ const [form, setForm] = useState({ title: "", section: "", due: "" });
131
+ const [errors, setErrors] = useState({});
132
+ // The verdict for the doc pitch writes, carrying the same fields it stamps.
133
+ const canPitch = can.create({ type: "story", status: "draft", authorHandle: me?.userHandle });
134
+
135
+ const set = (key) => (value) => setForm((f) => ({ ...f, [key]: value }));
136
+ const visible = stories.filter((s) => s.status === stage);
137
+ const current = stories.find((s) => s._id === openId);
138
+
139
+ async function pitch(e) {
140
+ e.preventDefault();
141
+ const found = { title: form.title.trim() === "" ? "Give the story a working title." : undefined };
142
+ setErrors(found);
143
+ if (found.title) return;
144
+ await database.put({
145
+ type: "story",
146
+ title: form.title.trim(),
147
+ section: form.section.trim(),
148
+ due: form.due,
149
+ status: "draft",
150
+ authorHandle: me?.userHandle,
151
+ createdAt: Date.now(),
152
+ });
153
+ setForm({ title: "", section: "", due: "" });
154
+ setPitching(false);
155
+ setStage("draft");
156
+ toast.success("Story pitched");
157
+ }
158
+
159
+ return (
160
+ <main className="mx-auto max-w-2xl px-4 py-8">
161
+ {current ? (
162
+ <StoryView story={current} database={database} can={can} onBack={() => setOpenId(undefined)} onMoved={setStage} />
163
+ ) : (
164
+ <Stack gap={5}>
165
+ <Row justify="between" align="top">
166
+ <Stack gap={1}>
167
+ <Heading level={1}>Editorial desk</Heading>
168
+ <Text muted>{stories.length === 1 ? "1 story" : `${stories.length} stories`}</Text>
169
+ </Stack>
170
+ <Dialog title="Pitch a story" open={pitching} onOpenChange={setPitching} trigger={<Button>Pitch a story</Button>}>
171
+ <Gate verdict={canPitch} closed="Pitching opens up once you're on the writing team.">
172
+ <form onSubmit={pitch} noValidate>
173
+ <Stack gap={3}>
174
+ <Field label="Working title" error={errors.title}>
175
+ {(id, describedBy) => (
176
+ <Input id={id} describedBy={describedBy} value={form.title} onChange={set("title")} invalid={errors.title !== undefined} />
177
+ )}
178
+ </Field>
179
+ <Field label="Section" hint="Where it runs, like News or Sport.">
180
+ {(id, describedBy) => <Input id={id} describedBy={describedBy} value={form.section} onChange={set("section")} />}
181
+ </Field>
182
+ <Field label="Due">
183
+ {(id, describedBy) => <Input id={id} describedBy={describedBy} value={form.due} onChange={set("due")} type="date" />}
184
+ </Field>
185
+ <Row justify="end">
186
+ <Button type="submit">Add to drafts</Button>
187
+ </Row>
188
+ </Stack>
189
+ </form>
190
+ </Gate>
191
+ </Dialog>
192
+ </Row>
193
+
194
+ <Row justify="between">
195
+ <Segmented label="Stage" tone="info" value={stage} onChange={setStage} options={FILTERS} />
196
+ <Row gap={2} wrap={false}>
197
+ <Dot tone={STAGES[stage].tone} />
198
+ <Text size="sm" muted>
199
+ {visible.length === 1 ? "1 story here" : `${visible.length} stories here`}
200
+ </Text>
201
+ </Row>
202
+ </Row>
203
+
204
+ {visible.length === 0 ? (
205
+ <Empty icon="draft" title={STAGES[stage].empty}>
206
+ {stage === "draft" ? "Pitch one to get the desk moving." : "Stories land here as they move along."}
207
+ </Empty>
208
+ ) : (
209
+ <Stack gap={2}>
210
+ {visible.map((s) => (
211
+ <RowCard key={s._id} onClick={() => setOpenId(s._id)}>
212
+ <Stack gap={2}>
213
+ <Text size="sm" muted>
214
+ {s.section || "No section"}
215
+ </Text>
216
+ <Text>{s.title}</Text>
217
+ <Row gap={2} wrap={false}>
218
+ <Text size="sm" muted>
219
+ Due {due(s.due)}
220
+ </Text>
221
+ <Spacer />
222
+ <StageBadge stage={s.status} />
223
+ </Row>
224
+ </Stack>
225
+ </RowCard>
226
+ ))}
227
+ </Stack>
228
+ )}
229
+ </Stack>
230
+ )}
231
+ <div className="pouf-toasts">
232
+ <Toaster />
233
+ </div>
234
+ </main>
235
+ );
236
+ }
237
+ ```
238
+
239
+ ## Adapting
240
+
241
+ - Rename stories to the app's own noun (`post`, `article`, `episode`) in the `type` key, the headings and the copy; keep one document per story with its stage on it.
242
+ - A new stage (`in review`, `edited`) is one more entry in `STAGES` and `NEXT`; tone it by meaning, `warn` for one that needs an editor's attention soon.
243
+ - Assigning an editor is a `HandleInput` from `useViewer()` writing an `editorHandle`, shown on the story view as a `ViewerTag`.
244
+ - The draft itself can live on the story: a `Textarea` in a `Field` on the story view, saved with `can.edit(story)` and counted as words in a `Metric`.
245
+ - 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.
246
+
247
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/editorial.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufEventConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufEventConfig = {
2
+ name: "pouf-event",
3
+ label: "Pouf event block",
4
+ description: "Guest check-in desk with arrivals count, search and attendance filter",
5
+ };
6
+ //# sourceMappingURL=pouf-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-event.js","sourceRoot":"","sources":["../../jsr/llms/pouf-event.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,eAAe,GAAc;IACxC,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,kBAAkB;IACzB,WAAW,EAAE,uEAAuE;CACrF,CAAC"}
@@ -0,0 +1,237 @@
1
+ # Pouf Event
2
+
3
+ A check-in desk for the door of an event: a fundraiser's guest list, a wedding reception, a meetup's RSVPs, a workshop's sign-in sheet. Reach for it whenever the app's core is a list of people expected somewhere who get marked as arrived, with a running count of who is in and who is still on the way. Guests and their check-ins persist, so every volunteer at the door sees the same list on their own phone and it survives a reload.
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
+ One view with one job: get people through the door. The `Heading` and a muted line open the page beside an `Add guest` button (the view's one lead) that opens a `Dialog` with the guest form. Three `Stat`s count the room, each toned by meaning (`info` expected, `up` checked in, `idle` still to come). A search `Input` in a `Field` and a `Segmented` attendance filter (`tone="info"`) sit above the list, and each guest is a `RowCard` with their name, a muted line for their group and ticket, an arrival `Badge` (`up` checked in, `idle` not yet) and the row's action: an `up`-toned `Check in`, or a quiet `IconButton` to undo a check-in made by mistake. The check-in buttons wear the meaning tone `up`, not the lead; an `Empty` stands in when nothing matches. Every write waits on its `can` verdict from `useVibe("event")`: the guest form shows a `Skeleton` inside an `aria-busy` wrapper while the verdict resolves and a sentence in the app's own words when adding is closed to this visitor, and each row's check-in button is disabled unless `can.edit(guest).ok`, with one muted line above the list saying so in the app's words once that verdict resolves to a denial.
10
+
11
+ ### `App.jsx`
12
+ ```jsx
13
+ import React, { useState } from "react";
14
+ import { useFireproof } from "use-fireproof";
15
+ import { useVibe } from "use-vibes";
16
+ import {
17
+ Badge,
18
+ Button,
19
+ Empty,
20
+ Field,
21
+ Grid,
22
+ Heading,
23
+ Icon,
24
+ IconButton,
25
+ Input,
26
+ Row,
27
+ RowCard,
28
+ Segmented,
29
+ Skeleton,
30
+ Spacer,
31
+ Stack,
32
+ Stat,
33
+ Text,
34
+ } from "@vibes.diy/pouf";
35
+ import { Dialog, Select, Toaster, toast } from "@vibes.diy/pouf/interactive";
36
+
37
+ const NUMBER = new Intl.NumberFormat();
38
+
39
+ const FILTERS = [
40
+ { value: "all", label: "Everyone" },
41
+ { value: "waiting", label: "Not here yet" },
42
+ { value: "in", label: "Checked in" },
43
+ ];
44
+
45
+ const TICKETS = [
46
+ { value: "General", label: "General" },
47
+ { value: "Speaker", label: "Speaker" },
48
+ { value: "VIP", label: "VIP" },
49
+ ];
50
+
51
+ const time = (ms) => new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
52
+
53
+ /** Three states for any create form: resolving, closed to this visitor, open. */
54
+ function Gate({ verdict, closed, children }) {
55
+ if (verdict.pending)
56
+ return (
57
+ <div aria-busy="true">
58
+ <Skeleton variant="card" />
59
+ </div>
60
+ );
61
+ if (!verdict.ok) return <Text muted>{closed}</Text>;
62
+ return children;
63
+ }
64
+
65
+ export default function App() {
66
+ const { database, useLiveQuery } = useFireproof("event");
67
+ const { can, me } = useVibe("event");
68
+ const { docs } = useLiveQuery("type", { key: "guest" });
69
+ const guests = [...docs].sort((a, b) => a.name.localeCompare(b.name));
70
+
71
+ const [query, setQuery] = useState("");
72
+ const [filter, setFilter] = useState("all");
73
+ const [adding, setAdding] = useState(false);
74
+ const [form, setForm] = useState({ name: "", group: "", ticket: "General" });
75
+ const [nameError, setNameError] = useState(undefined);
76
+ // The verdict for the doc addGuest writes, carrying the same fields it stamps.
77
+ const canAdd = can.create({ type: "guest", checkedIn: false, authorHandle: me?.userHandle });
78
+
79
+ const set = (key) => (value) => setForm((f) => ({ ...f, [key]: value }));
80
+ const needle = query.trim().toLowerCase();
81
+ const visible = guests.filter(
82
+ (g) =>
83
+ (filter === "all" || (filter === "in" ? g.checkedIn : !g.checkedIn)) &&
84
+ (needle === "" || `${g.name} ${g.group} ${g.ticket}`.toLowerCase().includes(needle))
85
+ );
86
+ const inCount = guests.filter((g) => g.checkedIn).length;
87
+ // A resolved denial on any guest: say so once, above the list, rather than leaving buttons silently dead.
88
+ const checkInClosed = guests.some((g) => {
89
+ const v = can.edit(g);
90
+ return !v.pending && !v.ok;
91
+ });
92
+
93
+ async function addGuest(e) {
94
+ e.preventDefault();
95
+ if (form.name.trim() === "") {
96
+ setNameError("Add the guest's name.");
97
+ return;
98
+ }
99
+ setNameError(undefined);
100
+ await database.put({
101
+ type: "guest",
102
+ name: form.name.trim(),
103
+ group: form.group.trim(),
104
+ ticket: form.ticket,
105
+ checkedIn: false,
106
+ authorHandle: me?.userHandle,
107
+ createdAt: Date.now(),
108
+ });
109
+ setForm({ name: "", group: "", ticket: "General" });
110
+ setAdding(false);
111
+ toast.success("Guest added");
112
+ }
113
+
114
+ async function setArrived(guest, arrived) {
115
+ await database.put({ ...guest, checkedIn: arrived, checkedInAt: arrived ? Date.now() : null, checkedInBy: arrived ? me?.userHandle : null });
116
+ toast.success(arrived ? `${guest.name} is in` : `${guest.name} is back on the list`);
117
+ }
118
+
119
+ return (
120
+ <main className="mx-auto max-w-2xl px-4 py-8">
121
+ <Stack gap={5}>
122
+ <Row justify="between" align="top">
123
+ <Stack gap={1}>
124
+ <Heading level={1}>Guest check-in</Heading>
125
+ <Text muted>Find a name, tap check in, keep the line moving.</Text>
126
+ </Stack>
127
+ <Dialog
128
+ title="Add a guest"
129
+ open={adding}
130
+ onOpenChange={setAdding}
131
+ trigger={<Button size="sm">Add guest</Button>}
132
+ >
133
+ <Gate verdict={canAdd} closed="Adding guests opens up once you're helping run the door.">
134
+ <form onSubmit={addGuest} noValidate>
135
+ <Stack gap={3}>
136
+ <Field label="Name" error={nameError}>
137
+ {(id, describedBy) => (
138
+ <Input
139
+ id={id}
140
+ describedBy={describedBy}
141
+ value={form.name}
142
+ onChange={(value) => {
143
+ set("name")(value);
144
+ if (nameError) setNameError(undefined);
145
+ }}
146
+ invalid={nameError !== undefined}
147
+ />
148
+ )}
149
+ </Field>
150
+ <Field label="Group" hint="A company, family or table, if any.">
151
+ {(id, describedBy) => <Input id={id} describedBy={describedBy} value={form.group} onChange={set("group")} />}
152
+ </Field>
153
+ <Field label="Ticket">
154
+ {(id, describedBy) => <Select id={id} describedBy={describedBy} value={form.ticket} onChange={set("ticket")} options={TICKETS} />}
155
+ </Field>
156
+ <Row justify="end">
157
+ <Button type="submit">Add to the list</Button>
158
+ </Row>
159
+ </Stack>
160
+ </form>
161
+ </Gate>
162
+ </Dialog>
163
+ </Row>
164
+
165
+ <Grid cols={3} gap={3}>
166
+ <Stat label="Expected" value={NUMBER.format(guests.length)} icon="users" tone="info" />
167
+ <Stat label="Checked in" value={NUMBER.format(inCount)} icon="ok" tone="up" />
168
+ <Stat label="Still to come" value={NUMBER.format(guests.length - inCount)} icon="clock" tone="idle" />
169
+ </Grid>
170
+
171
+ <Stack gap={3}>
172
+ <Field label="Find a guest">
173
+ {(id, describedBy) => (
174
+ <Input id={id} describedBy={describedBy} value={query} onChange={setQuery} placeholder="Name, group or ticket" inputMode="search" />
175
+ )}
176
+ </Field>
177
+ <Segmented label="Attendance" tone="info" value={filter} onChange={setFilter} options={FILTERS} />
178
+ </Stack>
179
+
180
+ {checkInClosed && <Text muted>Checking guests in is for the people running the door. You can still look anyone up.</Text>}
181
+
182
+ {visible.length === 0 ? (
183
+ <Empty icon="search" title={guests.length === 0 ? "No guests yet" : "No guests found"}>
184
+ {guests.length === 0 ? "Add the guest list to start checking people in." : "Check the spelling or show everyone."}
185
+ </Empty>
186
+ ) : (
187
+ <Stack gap={2}>
188
+ {visible.map((g) => {
189
+ const editable = can.edit(g).ok;
190
+ return (
191
+ <RowCard key={g._id}>
192
+ <Row gap={3} wrap={false}>
193
+ <Stack gap={1}>
194
+ <Text truncate>{g.name}</Text>
195
+ <Text size="sm" muted truncate>
196
+ {[g.group, g.ticket].filter(Boolean).join(" · ")}
197
+ </Text>
198
+ </Stack>
199
+ <Spacer />
200
+ <Badge tone={g.checkedIn ? "up" : "idle"}>{g.checkedIn ? `In at ${time(g.checkedInAt)}` : "Not here yet"}</Badge>
201
+ {g.checkedIn ? (
202
+ <IconButton
203
+ size="sm"
204
+ label={`Undo check-in for ${g.name}`}
205
+ icon={<Icon name="rewind" size="sm" />}
206
+ disabled={!editable}
207
+ onClick={() => setArrived(g, false)}
208
+ />
209
+ ) : (
210
+ <Button size="sm" tone="up" disabled={!editable} onClick={() => setArrived(g, true)}>
211
+ Check in
212
+ </Button>
213
+ )}
214
+ </Row>
215
+ </RowCard>
216
+ );
217
+ })}
218
+ </Stack>
219
+ )}
220
+ </Stack>
221
+ <div className="pouf-toasts">
222
+ <Toaster />
223
+ </div>
224
+ </main>
225
+ );
226
+ }
227
+ ```
228
+
229
+ ## Adapting
230
+
231
+ - Rename guests to the app's own noun (`attendee`, `member`, `student`) in the `type` key, the headings and the copy; keep one document per guest with the check-in on it.
232
+ - Tone arrival by meaning: `up` for checked in, `idle` for not yet, `warn` for a guest flagged at the door (an unpaid ticket, a dietary note to pass on).
233
+ - Who checked a guest in is already stored as `checkedInBy`; show it with `useViewer()`'s `ViewerTag` beside the arrival `Badge`.
234
+ - Several sessions or days become a `Segmented` over a `session` property on each check-in, not a second database.
235
+ - 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.
236
+
237
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/event.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufFeedConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufFeedConfig = {
2
+ name: "pouf-feed",
3
+ label: "Pouf feed block",
4
+ description: "Social feed to post updates, like them and reply in comments",
5
+ };
6
+ //# sourceMappingURL=pouf-feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-feed.js","sourceRoot":"","sources":["../../jsr/llms/pouf-feed.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,8DAA8D;CAC5E,CAAC"}