@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 @@
1
+ {"version":3,"file":"pouf-profile.js","sourceRoot":"","sources":["../../jsr/llms/pouf-profile.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,iBAAiB,GAAc;IAC1C,IAAI,EAAE,cAAc;IACpB,KAAK,EAAE,oBAAoB;IAC3B,WAAW,EAAE,gEAAgE;CAC9E,CAAC"}
@@ -0,0 +1,177 @@
1
+ # Pouf Profile
2
+
3
+ A public page about one maker, coach or group that visitors can follow: an illustrator's page, a running coach's profile, a band's home, a club captain's corner. Reach for it whenever the app's core is "who is this, what have they made, and how do I keep up". The profile's own words and posts stay inline in the code; each follow is its own document, so the follower count is real, it survives the next visit, and two people following at once both count.
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 to know the person, then follow. A `Card` holds the header: an `Avatar` on the decorative `orange` tone, the name as the `Heading`, the handle as an `Eyebrow` and a muted line on where they are, with `Follow` as the view's one lead (it turns into a quiet `Following` once followed and shows `loading` while the write lands). Under it a `Row` of `Metric`s counts posts and followers, the follower figure computed from the follow documents. `Tabs` (`tone="info"`) switch between the posts (each a `tight` `Card` with its words and a muted date), an about panel (a paragraph and `info` `Badge`s for what they do) and the followers, one `RowCard` each with the follower as a `ViewerTag` and a muted date, with an `Empty` before the first. Following waits on its `can` verdict from `useVibe("profile")`, the database the follows live in: a `Skeleton` inside an `aria-busy` wrapper while it resolves, a `Sign in to follow` lead wired to `useViewer().requestLogin()` for a visitor who has not signed in, and a sentence in the app's own words when following is closed; unfollowing waits on `can.delete` of the visitor's own follow.
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 { Badge, Button, Card, Empty, Eyebrow, Heading, Metric, Row, RowCard, Skeleton, Stack, Text } from "@vibes.diy/pouf";
17
+ import { Avatar, Tabs } from "@vibes.diy/pouf/interactive";
18
+
19
+ const PROFILE = {
20
+ name: "Mira Okafor",
21
+ initials: "MO",
22
+ handle: "@mira.draws",
23
+ where: "Illustrator in Lisbon",
24
+ about: "I draw small, warm pictures of everyday things: kitchens, corner shops, the cat on the stairs. Commissions open twice a year.",
25
+ skills: ["Picture books", "Editorial", "Riso prints"],
26
+ };
27
+ const POSTS = [
28
+ { id: "p3", text: "New riso print of the fish market is up, two colours, fifty copies.", date: "2026-09-28" },
29
+ { id: "p2", text: "Sketchbook tour: a month of drawing the same café table every morning.", date: "2026-09-14" },
30
+ { id: "p1", text: "Picture book cover finished. Six months of drawing one very stubborn goat.", date: "2026-08-30" },
31
+ ];
32
+ const plural = (n, one, many) => (n === 1 ? `1 ${one}` : `${n} ${many}`);
33
+
34
+ function Follower({ handle }) {
35
+ const { ViewerTag } = useViewer();
36
+ return <ViewerTag userHandle={handle} />;
37
+ }
38
+
39
+ export default function App() {
40
+ const { database, useLiveQuery } = useFireproof("profile");
41
+ const { docs: follows } = useLiveQuery("type", { key: "follow" });
42
+ const { can, me } = useVibe("profile");
43
+ const { requestLogin } = useViewer();
44
+
45
+ const [tab, setTab] = useState("posts");
46
+ const [busy, setBusy] = useState(false);
47
+
48
+ const mine = me ? follows.find((f) => f.authorHandle === me.userHandle) : undefined;
49
+ // The follow this visitor would write: a deterministic id, so following twice lands on one document.
50
+ const followDraft = { _id: `follow:${me?.userHandle}`, type: "follow", authorHandle: me?.userHandle };
51
+ const verdict = mine ? can.delete(mine) : can.create(followDraft);
52
+
53
+ async function toggleFollow() {
54
+ setBusy(true);
55
+ try {
56
+ if (mine) await database.del(mine._id);
57
+ else await database.put({ ...followDraft, createdAt: Date.now() });
58
+ } finally {
59
+ setBusy(false);
60
+ }
61
+ }
62
+
63
+ let follow;
64
+ if (verdict.pending)
65
+ follow = (
66
+ <div aria-busy="true">
67
+ <Skeleton variant="text" />
68
+ </div>
69
+ );
70
+ else if (!me) follow = <Button onClick={requestLogin}>Sign in to follow</Button>;
71
+ else if (!verdict.ok) follow = <Text muted>Following opens up soon.</Text>;
72
+ else
73
+ follow = (
74
+ <Button variant={mine ? "quiet" : undefined} loading={busy} onClick={toggleFollow} aria-pressed={Boolean(mine)}>
75
+ {mine ? "Following" : "Follow"}
76
+ </Button>
77
+ );
78
+
79
+ const posts = (
80
+ <Stack gap={3}>
81
+ {POSTS.map((p) => (
82
+ <Card key={p.id} variant="tight">
83
+ <Stack gap={2}>
84
+ <Text>{p.text}</Text>
85
+ <Text size="sm" muted>
86
+ {new Date(`${p.date}T12:00:00`).toLocaleDateString(undefined, { month: "short", day: "numeric" })}
87
+ </Text>
88
+ </Stack>
89
+ </Card>
90
+ ))}
91
+ </Stack>
92
+ );
93
+
94
+ const about = (
95
+ <Stack gap={3}>
96
+ <Text>{PROFILE.about}</Text>
97
+ <Row gap={2}>
98
+ {PROFILE.skills.map((s) => (
99
+ <Badge key={s} tone="info">
100
+ {s}
101
+ </Badge>
102
+ ))}
103
+ </Row>
104
+ </Stack>
105
+ );
106
+
107
+ const followers =
108
+ follows.length === 0 ? (
109
+ <Empty icon="users" title="No followers yet">
110
+ Be the first to follow along.
111
+ </Empty>
112
+ ) : (
113
+ <Stack gap={2}>
114
+ {[...follows]
115
+ .sort((a, b) => b.createdAt - a.createdAt)
116
+ .map((f) => (
117
+ <RowCard key={f._id}>
118
+ <Row justify="between" wrap={false}>
119
+ <Follower handle={f.authorHandle} />
120
+ <Text size="sm" muted>
121
+ Since {new Date(f.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}
122
+ </Text>
123
+ </Row>
124
+ </RowCard>
125
+ ))}
126
+ </Stack>
127
+ );
128
+
129
+ return (
130
+ <main className="mx-auto max-w-2xl px-4 py-8">
131
+ <Card>
132
+ <Stack gap={5}>
133
+ <Row justify="between" align="top">
134
+ <Row gap={3} wrap={false} align="top">
135
+ <Avatar fallback={PROFILE.initials} size="lg" tone="orange" />
136
+ <Stack gap={1}>
137
+ <Heading level={1}>{PROFILE.name}</Heading>
138
+ <Eyebrow>{PROFILE.handle}</Eyebrow>
139
+ <Text size="sm" muted>
140
+ {PROFILE.where}
141
+ </Text>
142
+ </Stack>
143
+ </Row>
144
+ {follow}
145
+ </Row>
146
+
147
+ <Row gap={4}>
148
+ <Metric label="Posts" value={String(POSTS.length)} />
149
+ <Metric label="Followers" value={String(follows.length)} />
150
+ </Row>
151
+
152
+ <Tabs
153
+ tone="info"
154
+ value={tab}
155
+ onChange={setTab}
156
+ tabs={[
157
+ { value: "posts", label: "Posts", content: posts },
158
+ { value: "about", label: "About", content: about },
159
+ { value: "followers", label: "Followers", content: followers },
160
+ ]}
161
+ />
162
+ </Stack>
163
+ </Card>
164
+ </main>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ## Adapting
170
+
171
+ - Swap `PROFILE` and `POSTS` for the person's own words; posts the owner writes in the app become `post` documents behind an owner-only `can.create`, read with `useLiveQuery("type", { key: "post" })`.
172
+ - Keep one follow document per follower with a deterministic `_id`; a follower counter on the profile loses follows when two people tap at once, and the count is always `follows.length`.
173
+ - When the profile belongs to a member of the app rather than the app's owner, key the follows by both handles (`follow:<theirs>:<yours>`), show the member with `<ViewerTag userHandle={...} />` instead of the `Avatar` and typed name, and list profiles from the members' own documents.
174
+ - `Avatar` takes the decorative tone here because the page is about one person, not a list of people; every follower stays a `ViewerTag`.
175
+ - 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.
176
+
177
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/profile.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufQuizConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufQuizConfig = {
2
+ name: "pouf-quiz",
3
+ label: "Pouf quiz block",
4
+ description: "Multiple-choice quiz with instant feedback, a scored result and saved attempts",
5
+ };
6
+ //# sourceMappingURL=pouf-quiz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-quiz.js","sourceRoot":"","sources":["../../jsr/llms/pouf-quiz.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,iBAAiB;IACxB,WAAW,EAAE,gFAAgF;CAC9F,CAAC"}
@@ -0,0 +1,267 @@
1
+ # Pouf Quiz
2
+
3
+ A short multiple-choice quiz that keeps score: a book club's chapter check, a practice round before a test, a pub quiz warm-up, a training check for new staff. Reach for it when the app asks a handful of questions one at a time, says right or wrong after each answer, ends on a score and lets people come back to beat it. Each finished attempt someone saves is its own document, so their past scores are there on the next visit and on any device.
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: answer the questions, then keep the score. A `Heading` and a muted line open the page. The quiz sits on a `Card`: an `Eyebrow` with the question number beside an `info` `Badge` counting correct answers so far (it steps aside on the result, which states the score once), and a `Progress` (`info`) across the top. Each question is a `Heading level={2}` over a `RadioGroup`; `Check answer` is the view's one lead, then the answer shows as a `Badge` (`up` right, `down` wrong) with the right option in muted `Text`, and the lead becomes `Next question`. The result swaps in a `Blob` (`up` for a pass, `warn` otherwise), the score and a recap of every question with its own `up` or `down` `Badge`. `Save score` is then the lead, with `loading` while the write lands, beside a quiet `Play again`. A signed-in person's earlier attempts sit below as `RowCard`s, newest first, each with a `Badge` toned by how it went. Saving waits on its `can` verdict from `useVibe("quiz")`, the database the attempts live in: a `Skeleton` inside an `aria-busy` wrapper while it resolves, a `Sign in to save your score` lead wired to `useViewer().requestLogin()` for a visitor who has not signed in, and a sentence in the app's own words when saving is closed. Anyone can take the quiz without signing in; the attempt lives only in the page until it is saved, and the copy says so.
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 { Badge, Blob, Button, Card, Eyebrow, Heading, Row, RowCard, Skeleton, Stack, Text } from "@vibes.diy/pouf";
17
+ import { Progress, RadioGroup } from "@vibes.diy/pouf/interactive";
18
+
19
+ const QUIZ_ID = "houseplants";
20
+ const QUESTIONS = [
21
+ {
22
+ q: "What usually kills a houseplant first?",
23
+ options: [
24
+ { value: "a", label: "Too little water" },
25
+ { value: "b", label: "Too much water" },
26
+ { value: "c", label: "Too much light" },
27
+ ],
28
+ answer: "b",
29
+ },
30
+ {
31
+ q: "How can you tell a pot plant needs water?",
32
+ options: [
33
+ { value: "a", label: "The top few centimetres of soil are dry" },
34
+ { value: "b", label: "It is Monday" },
35
+ { value: "c", label: "The leaves are shiny" },
36
+ ],
37
+ answer: "a",
38
+ },
39
+ {
40
+ q: "Which plant copes best with a dim corner?",
41
+ options: [
42
+ { value: "a", label: "A cactus" },
43
+ { value: "b", label: "A snake plant" },
44
+ { value: "c", label: "A basil plant" },
45
+ ],
46
+ answer: "b",
47
+ },
48
+ {
49
+ q: "Why does a pot need a drainage hole?",
50
+ options: [
51
+ { value: "a", label: "So roots can grow out of it" },
52
+ { value: "b", label: "So extra water can run out instead of rotting the roots" },
53
+ { value: "c", label: "It makes the pot lighter" },
54
+ ],
55
+ answer: "b",
56
+ },
57
+ {
58
+ q: "When is the best time to repot most houseplants?",
59
+ options: [
60
+ { value: "a", label: "Spring, as they start growing" },
61
+ { value: "b", label: "Midwinter" },
62
+ { value: "c", label: "Whenever a leaf turns yellow" },
63
+ ],
64
+ answer: "a",
65
+ },
66
+ ];
67
+ const TOTAL = QUESTIONS.length;
68
+ const PASS = 3;
69
+ const answerLabel = (question) => question.options.find((o) => o.value === question.answer)?.label;
70
+ const when = (ms) => new Date(ms).toLocaleDateString(undefined, { month: "short", day: "numeric" });
71
+
72
+ export default function App() {
73
+ const { database, useLiveQuery } = useFireproof("quiz");
74
+ const { docs } = useLiveQuery("type", { key: "attempt" });
75
+ const { can, me } = useVibe("quiz");
76
+ const { requestLogin } = useViewer();
77
+
78
+ // The attempt is a draft: it lives in the page until it ends and someone saves it.
79
+ const [step, setStep] = useState(0);
80
+ const [choice, setChoice] = useState("");
81
+ const [revealed, setRevealed] = useState(false);
82
+ const [results, setResults] = useState([]);
83
+ const [saving, setSaving] = useState(false);
84
+ const [savedResults, setSavedResults] = useState(undefined);
85
+
86
+ const question = QUESTIONS[step];
87
+ const score = results.filter(Boolean).length;
88
+ const done = results.length === TOTAL;
89
+ const lastRight = results[results.length - 1];
90
+
91
+ // One document per saved attempt; this person's, newest first.
92
+ const mine = me
93
+ ? docs
94
+ .filter((d) => d.quiz === QUIZ_ID && d.authorHandle === me.userHandle)
95
+ .sort((a, b) => b.createdAt - a.createdAt)
96
+ .slice(0, 5)
97
+ : [];
98
+
99
+ // The verdict for the doc saveScore writes, carrying the same fields it stamps.
100
+ const canSave = can.create({ type: "attempt", quiz: QUIZ_ID, authorHandle: me?.userHandle });
101
+
102
+ function check() {
103
+ setRevealed(true);
104
+ setResults((r) => [...r, choice === question.answer]);
105
+ }
106
+
107
+ function next() {
108
+ setStep((s) => s + 1);
109
+ setChoice("");
110
+ setRevealed(false);
111
+ }
112
+
113
+ function playAgain() {
114
+ setStep(0);
115
+ setChoice("");
116
+ setRevealed(false);
117
+ setResults([]);
118
+ setSavedResults(undefined);
119
+ }
120
+
121
+ async function saveScore() {
122
+ setSaving(true);
123
+ try {
124
+ await database.put({ type: "attempt", quiz: QUIZ_ID, correct: score, total: TOTAL, authorHandle: me?.userHandle, createdAt: Date.now() });
125
+ // Marks this exact attempt as saved, so the button can't post it twice.
126
+ setSavedResults(results);
127
+ } finally {
128
+ setSaving(false);
129
+ }
130
+ }
131
+
132
+ let ending;
133
+ if (savedResults === results) ending = <Text muted>Saved. It is in your attempts below.</Text>;
134
+ else if (canSave.pending)
135
+ ending = (
136
+ <div aria-busy="true">
137
+ <Skeleton variant="row" />
138
+ </div>
139
+ );
140
+ else if (!me)
141
+ ending = (
142
+ <Stack gap={2}>
143
+ <Text size="sm" muted>
144
+ Sign in to keep this score. Signing in may start the quiz over.
145
+ </Text>
146
+ <Button onClick={requestLogin}>Sign in to save your score</Button>
147
+ </Stack>
148
+ );
149
+ else if (!canSave.ok) ending = <Text muted>Scores aren't being kept right now. Play on for practice.</Text>;
150
+ else
151
+ ending = (
152
+ <Button loading={saving} onClick={saveScore}>
153
+ Save score
154
+ </Button>
155
+ );
156
+
157
+ const passed = score >= PASS;
158
+
159
+ return (
160
+ <main className="mx-auto max-w-xl px-4 py-8">
161
+ <Stack gap={5}>
162
+ <Stack gap={1}>
163
+ <Heading level={1}>Houseplant basics</Heading>
164
+ <Text muted>{TOTAL} questions. Pick an answer, check it, move on.</Text>
165
+ </Stack>
166
+
167
+ <Card>
168
+ <Stack gap={5}>
169
+ <Stack gap={2}>
170
+ <Row justify="between">
171
+ <Eyebrow>{done ? "Finished" : `Question ${step + 1} of ${TOTAL}`}</Eyebrow>
172
+ {!done && <Badge tone="info">{score} correct</Badge>}
173
+ </Row>
174
+ <Progress value={results.length} max={TOTAL} tone="info" label="Quiz progress" />
175
+ </Stack>
176
+
177
+ {done ? (
178
+ <Stack gap={4}>
179
+ <Row gap={3} wrap={false}>
180
+ <Blob icon={passed ? "trophy" : "flame"} tone={passed ? "up" : "warn"} />
181
+ <Stack gap={1}>
182
+ <Heading level={2}>{score === TOTAL ? "Perfect score!" : passed ? "Nice work" : "Keep practising"}</Heading>
183
+ <Text muted>
184
+ You got {score} of {TOTAL} right.
185
+ </Text>
186
+ </Stack>
187
+ </Row>
188
+ <Stack gap={2}>
189
+ {QUESTIONS.map((item, i) => (
190
+ <Row key={item.q} gap={2} wrap={false}>
191
+ <Badge tone={results[i] ? "up" : "down"}>{results[i] ? "Right" : "Missed"}</Badge>
192
+ <Text size="sm" muted truncate>
193
+ {item.q}
194
+ </Text>
195
+ </Row>
196
+ ))}
197
+ </Stack>
198
+ <Row gap={2}>
199
+ {ending}
200
+ <Button variant="quiet" onClick={playAgain}>
201
+ Play again
202
+ </Button>
203
+ </Row>
204
+ </Stack>
205
+ ) : (
206
+ <Stack gap={4}>
207
+ <Heading level={2}>{question.q}</Heading>
208
+ <RadioGroup label="Your answer" value={choice} onChange={setChoice} options={question.options} disabled={revealed} />
209
+ {revealed && (
210
+ <Row gap={2} wrap={false}>
211
+ <Badge tone={lastRight ? "up" : "down"}>{lastRight ? "Correct" : "Not quite"}</Badge>
212
+ {!lastRight && <Text muted>The answer is “{answerLabel(question)}”.</Text>}
213
+ </Row>
214
+ )}
215
+ <Row justify="end">
216
+ {revealed ? (
217
+ <Button onClick={next}>Next question</Button>
218
+ ) : (
219
+ <Button disabled={!choice} onClick={check}>
220
+ Check answer
221
+ </Button>
222
+ )}
223
+ </Row>
224
+ </Stack>
225
+ )}
226
+ </Stack>
227
+ </Card>
228
+
229
+ {me && mine.length > 0 && (
230
+ <Stack gap={3}>
231
+ <Heading level={2}>Your attempts</Heading>
232
+ <Stack gap={2}>
233
+ {mine.map((a) => (
234
+ <RowCard key={a._id}>
235
+ <Row justify="between" wrap={false}>
236
+ <Text>
237
+ {a.correct} of {a.total} right
238
+ </Text>
239
+ <Row gap={2} wrap={false}>
240
+ <Text size="sm" muted>
241
+ {when(a.createdAt)}
242
+ </Text>
243
+ <Badge tone={a.correct === a.total ? "up" : a.correct >= PASS ? "info" : "warn"}>
244
+ {a.correct === a.total ? "Perfect" : a.correct >= PASS ? "Passed" : "Practise"}
245
+ </Badge>
246
+ </Row>
247
+ </Row>
248
+ </RowCard>
249
+ ))}
250
+ </Stack>
251
+ </Stack>
252
+ )}
253
+ </Stack>
254
+ </main>
255
+ );
256
+ }
257
+ ```
258
+
259
+ ## Adapting
260
+
261
+ - Change the questions, not the flow: each one is `{ q, options: [{ value, label }], answer }`, the attempt is a list of right-or-wrong results in page state, and the database is touched only when a finished attempt is saved.
262
+ - Keep one attempt document per saved run and compute everything else (best score, average, a class board) from them at read time; a single "best score" document that each attempt rewrites loses scores when two people finish at once.
263
+ - Store what a later view might group or sort by on the attempt (`quiz` for which quiz, `correct`, `total`, a `level`), and give each quiz its own `QUIZ_ID` so several quizzes can share one database.
264
+ - A board of everyone's scores names each player with `useViewer()`'s `ViewerTag` from the stored `authorHandle`. Scores people would rather keep to themselves (a staff assessment, a medical exam drill) need an access function that limits who can read them before they are shown to anyone else.
265
+ - 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.
266
+
267
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/quiz.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufSettingsConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufSettingsConfig = {
2
+ name: "pouf-settings",
3
+ label: "Pouf settings block",
4
+ description: "Per-person settings with tabs, switches and a save bar",
5
+ };
6
+ //# sourceMappingURL=pouf-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-settings.js","sourceRoot":"","sources":["../../jsr/llms/pouf-settings.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,kBAAkB,GAAc;IAC3C,IAAI,EAAE,eAAe;IACrB,KAAK,EAAE,qBAAqB;IAC5B,WAAW,EAAE,wDAAwD;CACtE,CAAC"}