@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,220 @@
1
+ # Pouf Settings
2
+
3
+ A settings screen for each person's own preferences: the display name in a habit tracker, reminder emails for a reading log, a compact or comfortable layout, how loud an alert should be. Reach for it whenever the app lets someone tune how it treats them and expects those choices to stick. Each person's settings persist as their own document, so a choice made on the phone is there on the laptop.
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: change your settings and save them. An `Eyebrow`, the `Heading` and a status `Badge` open it (`warn` while there are unsaved changes, `up` once saved). A `Card` holds `Tabs` (`tone="info"`) with two panels: Profile, with a display name `Input`, a bio `Textarea` and a `RadioGroup` for density, each in a `Field`; and Notifications, with two `Switch`es separated by a `Separator` and a `Slider` for alert volume. A save bar closes the card: a quiet `Cancel` that puts back the last saved values and `Save changes`, the view's one lead, which shows `loading` while the write lands. Both buttons wake only when something changed, the edit is checked in plain state (each error on its own `Field`, its control `invalid`), and a `beforeunload` guard warns before an unsaved change is lost. A person's settings are one document with a deterministic `_id` (`settings:<handle>`), written in place because only its owner ever writes it. The save waits on its `can` verdict from `useVibe("settings")`: `can.create` of the draft before the first save, `can.edit` of the saved document after; a `Skeleton` inside an `aria-busy` wrapper stands in while the verdict resolves, a sentence in the app's own words when saving is closed, and a visitor who has not signed in is asked to with a button wired to `useViewer().requestLogin()`, because settings belong to a person.
10
+
11
+ ### `App.jsx`
12
+ ```jsx
13
+ import React, { useEffect, useState } from "react";
14
+ import { useFireproof } from "use-fireproof";
15
+ import { useViewer, useVibe } from "use-vibes";
16
+ import { Badge, Button, Card, Eyebrow, Field, Heading, Input, Row, Skeleton, Stack, Text, Textarea } from "@vibes.diy/pouf";
17
+ import { RadioGroup, Separator, Slider, Switch, Tabs, Toaster, toast } from "@vibes.diy/pouf/interactive";
18
+
19
+ const DEFAULTS = { name: "", bio: "", density: "comfortable", emailNotif: true, pushNotif: false, volume: 60 };
20
+ const KEYS = Object.keys(DEFAULTS);
21
+ const DENSITY = [
22
+ { value: "comfortable", label: "Comfortable" },
23
+ { value: "compact", label: "Compact" },
24
+ ];
25
+
26
+ const pick = (doc) => Object.fromEntries(KEYS.map((k) => [k, doc?.[k] ?? DEFAULTS[k]]));
27
+ const same = (a, b) => KEYS.every((k) => a[k] === b[k]);
28
+
29
+ function SettingsForm({ saved, database, can, me }) {
30
+ const base = pick(saved);
31
+ const [form, setForm] = useState(base);
32
+ const [tab, setTab] = useState("profile");
33
+ const [errors, setErrors] = useState({});
34
+ const [saving, setSaving] = useState(false);
35
+ const dirty = !same(form, base);
36
+ // The document this save writes: one per person, so every save lands on the same one.
37
+ const draft = { _id: `settings:${me.userHandle}`, type: "settings", authorHandle: me.userHandle };
38
+ const verdict = saved ? can.edit(saved) : can.create(draft);
39
+
40
+ useEffect(() => {
41
+ if (!dirty) return;
42
+ const warn = (e) => e.preventDefault();
43
+ window.addEventListener("beforeunload", warn);
44
+ return () => window.removeEventListener("beforeunload", warn);
45
+ }, [dirty]);
46
+
47
+ const set = (key) => (value) => {
48
+ setForm((f) => ({ ...f, [key]: value }));
49
+ if (errors[key]) setErrors((e) => ({ ...e, [key]: undefined }));
50
+ };
51
+
52
+ async function save(e) {
53
+ e.preventDefault();
54
+ const found = {
55
+ name: form.name.trim() === "" ? "Add the name you'd like to show." : undefined,
56
+ bio: form.bio.length > 160 ? "Keep the bio to 160 characters." : undefined,
57
+ };
58
+ setErrors(found);
59
+ if (found.name || found.bio) {
60
+ setTab("profile");
61
+ return;
62
+ }
63
+ setSaving(true);
64
+ try {
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.
68
+ setForm(pick(written));
69
+ toast.success("Settings saved");
70
+ } finally {
71
+ setSaving(false);
72
+ }
73
+ }
74
+
75
+ if (verdict.pending)
76
+ return (
77
+ <div aria-busy="true">
78
+ <Skeleton variant="card" />
79
+ </div>
80
+ );
81
+ if (!verdict.ok) return <Text muted>Settings here are kept by the people this app is shared with.</Text>;
82
+
83
+ const profile = (
84
+ <Stack gap={4}>
85
+ <Field label="Display name" error={errors.name}>
86
+ {(id, describedBy) => (
87
+ <Input id={id} describedBy={describedBy} value={form.name} onChange={set("name")} autoComplete="nickname" invalid={errors.name !== undefined} />
88
+ )}
89
+ </Field>
90
+ <Field label="Bio" hint="A sentence or two." error={errors.bio}>
91
+ {(id, describedBy) => (
92
+ <Textarea id={id} describedBy={describedBy} value={form.bio} onChange={set("bio")} rows={3} invalid={errors.bio !== undefined} />
93
+ )}
94
+ </Field>
95
+ <RadioGroup label="Density" value={form.density} onChange={set("density")} options={DENSITY} />
96
+ </Stack>
97
+ );
98
+
99
+ const notifications = (
100
+ <Stack gap={4}>
101
+ <Row justify="between" wrap={false}>
102
+ <Stack gap={1}>
103
+ <Text>Email reminders</Text>
104
+ <Text size="sm" muted>
105
+ A nudge when something is due.
106
+ </Text>
107
+ </Stack>
108
+ <Switch label="Email reminders" checked={form.emailNotif} onChange={set("emailNotif")} />
109
+ </Row>
110
+ <Separator />
111
+ <Row justify="between" wrap={false}>
112
+ <Stack gap={1}>
113
+ <Text>Push notifications</Text>
114
+ <Text size="sm" muted>
115
+ Sent to your devices right away.
116
+ </Text>
117
+ </Stack>
118
+ <Switch label="Push notifications" checked={form.pushNotif} onChange={set("pushNotif")} />
119
+ </Row>
120
+ <Separator />
121
+ <Stack gap={2}>
122
+ <Row justify="between">
123
+ <Text>Alert volume</Text>
124
+ <Text size="sm" muted num>
125
+ {form.volume}
126
+ </Text>
127
+ </Row>
128
+ <Slider label="Alert volume" value={[form.volume]} onChange={(value) => set("volume")(value[0] ?? 0)} />
129
+ </Stack>
130
+ </Stack>
131
+ );
132
+
133
+ return (
134
+ <Stack gap={5}>
135
+ <Row justify="between" align="top">
136
+ <Stack gap={1}>
137
+ <Eyebrow>Account</Eyebrow>
138
+ <Heading level={1}>Settings</Heading>
139
+ </Stack>
140
+ {dirty ? <Badge tone="warn">Unsaved changes</Badge> : saved ? <Badge tone="up">Saved</Badge> : null}
141
+ </Row>
142
+ <Card>
143
+ <form onSubmit={save} noValidate>
144
+ <Stack gap={5}>
145
+ <Tabs
146
+ tone="info"
147
+ value={tab}
148
+ onChange={setTab}
149
+ tabs={[
150
+ { value: "profile", label: "Profile", content: profile },
151
+ { value: "notifications", label: "Notifications", content: notifications },
152
+ ]}
153
+ />
154
+ <Separator />
155
+ <Row gap={2} justify="end">
156
+ <Button
157
+ variant="quiet"
158
+ disabled={!dirty || saving}
159
+ onClick={() => {
160
+ setForm(base);
161
+ setErrors({});
162
+ }}
163
+ >
164
+ Cancel
165
+ </Button>
166
+ <Button type="submit" disabled={!dirty} loading={saving}>
167
+ Save changes
168
+ </Button>
169
+ </Row>
170
+ </Stack>
171
+ </form>
172
+ </Card>
173
+ </Stack>
174
+ );
175
+ }
176
+
177
+ export default function App() {
178
+ const { database, useLiveQuery } = useFireproof("settings");
179
+ const { docs } = useLiveQuery("type", { key: "settings" });
180
+ const { can, me, ready } = useVibe("settings");
181
+ const { requestLogin } = useViewer();
182
+ const saved = me ? docs.find((d) => d._id === `settings:${me.userHandle}`) : undefined;
183
+
184
+ return (
185
+ <main className="mx-auto max-w-2xl px-4 py-8">
186
+ {!ready ? (
187
+ <div aria-busy="true">
188
+ <Skeleton variant="card" />
189
+ </div>
190
+ ) : !me ? (
191
+ <Card>
192
+ <Stack gap={3}>
193
+ <Heading level={1}>Settings</Heading>
194
+ <Text muted>Your settings follow you once you're signed in, on every device.</Text>
195
+ <Row justify="end">
196
+ <Button onClick={requestLogin}>Sign in to keep your settings</Button>
197
+ </Row>
198
+ </Stack>
199
+ </Card>
200
+ ) : (
201
+ // Re-keyed when the saved copy first arrives, so the form starts from it.
202
+ <SettingsForm key={saved ? "saved" : "new"} saved={saved} database={database} can={can} me={me} />
203
+ )}
204
+ <div className="pouf-toasts">
205
+ <Toaster />
206
+ </div>
207
+ </main>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ## Adapting
213
+
214
+ - Add a setting by adding its key and default to `DEFAULTS` and its control to a panel; the dirty check, the quiet Cancel and the `Save changes` lead all follow from that one object.
215
+ - A yes/no that applies on its own is a `Switch`; a choice of a few is a `RadioGroup` or `Select`; a number in a range is a `Slider` with `min`/`max`.
216
+ - Settings the whole group shares (a club's name, a team's currency) are one owner-kept document gated on its own `can.edit`, not a field on each person's settings.
217
+ - Save writes the whole form, so a change made on another device while this form is open is overwritten by it; for settings several devices edit at once, reload the form from the saved document when it changes and the form is clean.
218
+ - 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.
219
+
220
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/settings.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufStorefrontConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufStorefrontConfig = {
2
+ name: "pouf-storefront",
3
+ label: "Pouf storefront block",
4
+ description: "Product grid with filters, a shopping bag and checkout, no payments",
5
+ };
6
+ //# sourceMappingURL=pouf-storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-storefront.js","sourceRoot":"","sources":["../../jsr/llms/pouf-storefront.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,oBAAoB,GAAc;IAC7C,IAAI,EAAE,iBAAiB;IACvB,KAAK,EAAE,uBAAuB;IAC9B,WAAW,EAAE,qEAAqE;CACnF,CAAC"}
@@ -0,0 +1,293 @@
1
+ # Pouf Storefront
2
+
3
+ A small shop: product cards to browse, a category filter and a search, a bag that adds things up, and a checkout that places the order. It fits a potter's ceramics, a bakery's preorders, a club's merch table or a school fair's stall. Reach for it whenever the app's core is a short catalogue that people pick from and then order. The catalogue stays inline in the code, and the bag lives in the page while someone shops. Each placed order persists as its own document, so the shop can see what to make and the buyer can see what they ordered. It records orders only; taking payment is a separate integration this block does not pretend to do.
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: fill the bag and place the order. A `Navbar` carries the brand and a quiet bag button with a live count, and the `Heading` puts an `orange` `Highlight` on one word beside a muted line. A `Segmented` category filter (`tone="info"`) and a search `Input` in a `Field` sit above a `Grid cols="sidebar"`: on the main side a two-column grid of product `Card`s, each with a decorative `orange` `Blob`, an `info` `Badge` for "New" or "Bestseller", the price in numeric `Text` and a quiet `Add to bag` (an `Empty` stands in when nothing matches); on the side the bag's `Card`, one line per product with its total and `IconButton`s to take one away or add one, then the subtotal, shipping and total. `Place order` is the view's one lead; it shows `loading` while the write lands, and the bag empties only after it does. An order is one document holding a snapshot of each line's name, price and quantity, the totals and the buyer's handle, so a later price change never rewrites an order already placed; the person's own orders list under the bag, each with a `Badge` for its status (`info` received, `warn` ready to collect, `up` collected). Checkout waits on its `can` verdict from `useVibe("shop")`: a `Skeleton` inside an `aria-busy` wrapper while it resolves, a sentence in the app's own words when ordering is closed, and for a visitor who has not signed in a `Sign in to place your order` lead wired to `useViewer().requestLogin()`, with the bag they filled kept in place. Sign-in is required here, unlike a local-first note, because a signed-out write stays on the device until sign-in: an order placed that way would never reach the shop.
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
+ Highlight,
27
+ Icon,
28
+ IconButton,
29
+ Input,
30
+ Navbar,
31
+ Row,
32
+ Segmented,
33
+ Skeleton,
34
+ Spacer,
35
+ Stack,
36
+ Text,
37
+ } from "@vibes.diy/pouf";
38
+ import { Separator, Toaster, toast } from "@vibes.diy/pouf/interactive";
39
+
40
+ const PRODUCTS = [
41
+ { id: "mug", name: "Speckled mug", blurb: "Holds a generous coffee, glazed oatmeal.", price: 28, category: "table", icon: "drop", badge: "Bestseller" },
42
+ { id: "bowl", name: "Nesting bowls", blurb: "A set of three for salads and snacks.", price: 64, category: "table", icon: "home" },
43
+ { id: "vase", name: "Bud vase", blurb: "A small home for a few stems.", price: 36, category: "home", icon: "sparkle", badge: "New" },
44
+ { id: "planter", name: "Hanging planter", blurb: "Thrown stoneware with a jute cord.", price: 48, category: "home", icon: "sun" },
45
+ { id: "dish", name: "Ring dish", blurb: "A shallow dish for keys and rings.", price: 18, category: "gifts", icon: "heart" },
46
+ { id: "card", name: "Gift card", blurb: "Let them pick their own favourite.", price: 50, category: "gifts", icon: "tag" },
47
+ ];
48
+ const CATEGORIES = [
49
+ { value: "all", label: "All" },
50
+ { value: "table", label: "Table" },
51
+ { value: "home", label: "Home" },
52
+ { value: "gifts", label: "Gifts" },
53
+ ];
54
+ const MONEY = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD" });
55
+ const FREE_SHIPPING = 100;
56
+ const ORDER_STATUS = {
57
+ received: { label: "Received", tone: "info" },
58
+ ready: { label: "Ready to collect", tone: "warn" },
59
+ collected: { label: "Collected", tone: "up" },
60
+ };
61
+ const plural = (n, one, many) => (n === 1 ? `1 ${one}` : `${n} ${many}`);
62
+
63
+ export default function App() {
64
+ const { database, useLiveQuery } = useFireproof("shop");
65
+ const { docs: orders } = useLiveQuery("type", { key: "order" });
66
+ const { can, me } = useVibe("shop");
67
+ const { requestLogin } = useViewer();
68
+
69
+ const [category, setCategory] = useState("all");
70
+ const [query, setQuery] = useState("");
71
+ // The bag is a draft: it lives in the page until the order is placed.
72
+ const [bag, setBag] = useState({});
73
+ const [placing, setPlacing] = useState(false);
74
+
75
+ const needle = query.trim().toLowerCase();
76
+ const visible = PRODUCTS.filter(
77
+ (p) => (category === "all" || p.category === category) && (needle === "" || `${p.name} ${p.blurb}`.toLowerCase().includes(needle))
78
+ );
79
+ const lines = PRODUCTS.filter((p) => (bag[p.id] ?? 0) > 0).map((p) => ({ productId: p.id, name: p.name, price: p.price, qty: bag[p.id] }));
80
+ const count = lines.reduce((n, l) => n + l.qty, 0);
81
+ const subtotal = lines.reduce((n, l) => n + l.price * l.qty, 0);
82
+ const shipping = subtotal === 0 || subtotal >= FREE_SHIPPING ? 0 : 8;
83
+ const mine = me ? orders.filter((o) => o.authorHandle === me.userHandle).sort((a, b) => b.createdAt - a.createdAt) : [];
84
+
85
+ // The verdict for the order placeOrder writes, carrying the same fields it stamps.
86
+ const orderDraft = { type: "order", status: "received", authorHandle: me?.userHandle };
87
+ const canOrder = can.create(orderDraft);
88
+
89
+ function change(id, delta) {
90
+ setBag((b) => {
91
+ const qty = Math.max(0, (b[id] ?? 0) + delta);
92
+ const { [id]: _gone, ...rest } = b;
93
+ return qty === 0 ? rest : { ...rest, [id]: qty };
94
+ });
95
+ }
96
+
97
+ async function placeOrder() {
98
+ setPlacing(true);
99
+ try {
100
+ await database.put({ ...orderDraft, lines, subtotal, shipping, total: subtotal + shipping, createdAt: Date.now() });
101
+ // Emptied only once the order lands, so a local failure keeps the bag.
102
+ setBag({});
103
+ toast.success("Order placed. We'll be in touch about collection.");
104
+ } finally {
105
+ setPlacing(false);
106
+ }
107
+ }
108
+
109
+ let checkout;
110
+ if (canOrder.pending)
111
+ checkout = (
112
+ <div aria-busy="true">
113
+ <Skeleton variant="row" />
114
+ </div>
115
+ );
116
+ else if (!me)
117
+ checkout = (
118
+ <Button block disabled={count === 0} onClick={requestLogin}>
119
+ Sign in to place your order
120
+ </Button>
121
+ );
122
+ else if (!canOrder.ok) checkout = <Text muted>The shop isn't taking orders right now. Check back soon.</Text>;
123
+ else
124
+ checkout = (
125
+ <Button block disabled={count === 0} loading={placing} onClick={placeOrder}>
126
+ Place order
127
+ </Button>
128
+ );
129
+
130
+ return (
131
+ <main className="mx-auto max-w-6xl px-4 py-6">
132
+ <Stack gap={6}>
133
+ <Navbar
134
+ brand={
135
+ <Row gap={2} wrap={false}>
136
+ <Blob icon="sparkle" tone="orange" size="sm" />
137
+ <Text>Kiln &amp; Co.</Text>
138
+ </Row>
139
+ }
140
+ actions={
141
+ <Button size="sm" variant="quiet" onClick={() => document.getElementById("bag")?.scrollIntoView({ block: "nearest" })}>
142
+ <Icon name="cart" size="sm" />
143
+ Bag ({count})
144
+ </Button>
145
+ }
146
+ />
147
+
148
+ <Stack gap={2}>
149
+ <Eyebrow>Handmade in small batches</Eyebrow>
150
+ <Heading level={1}>
151
+ Pots for <Highlight tone="orange">everyday</Highlight> things.
152
+ </Heading>
153
+ <Text muted>Free shipping over {MONEY.format(FREE_SHIPPING)}. Every piece is thrown and glazed by hand.</Text>
154
+ </Stack>
155
+
156
+ <Row justify="between" align="top">
157
+ <Segmented label="Category" tone="info" value={category} onChange={setCategory} options={CATEGORIES} />
158
+ <Field label="Search the shop">
159
+ {(id, describedBy) => <Input id={id} describedBy={describedBy} value={query} onChange={setQuery} inputMode="search" placeholder="Mug, vase…" />}
160
+ </Field>
161
+ </Row>
162
+
163
+ <Grid cols="sidebar" gap={5}>
164
+ {visible.length === 0 ? (
165
+ <Empty icon="search" title="Nothing matches">
166
+ Try another word or a different category.
167
+ </Empty>
168
+ ) : (
169
+ <Grid cols={2} gap={4}>
170
+ {visible.map((p) => (
171
+ <Card key={p.id}>
172
+ <Stack gap={3}>
173
+ <Row justify="between" align="top">
174
+ <Blob icon={p.icon} tone="orange" />
175
+ {p.badge && <Badge tone="info">{p.badge}</Badge>}
176
+ </Row>
177
+ <Stack gap={1}>
178
+ <Heading level={3}>{p.name}</Heading>
179
+ <Text size="sm" muted>
180
+ {p.blurb}
181
+ </Text>
182
+ </Stack>
183
+ <Row justify="between">
184
+ <Text num>{MONEY.format(p.price)}</Text>
185
+ <Button size="sm" variant="quiet" onClick={() => change(p.id, 1)}>
186
+ Add to bag
187
+ </Button>
188
+ </Row>
189
+ </Stack>
190
+ </Card>
191
+ ))}
192
+ </Grid>
193
+ )}
194
+
195
+ <div id="bag">
196
+ <Stack gap={4}>
197
+ <Card>
198
+ <Stack gap={4}>
199
+ <Row justify="between">
200
+ <Heading level={2}>Your bag</Heading>
201
+ <Text size="sm" muted>
202
+ {plural(count, "item", "items")}
203
+ </Text>
204
+ </Row>
205
+ {lines.length === 0 ? (
206
+ <Empty icon="cart" title="Your bag is empty">
207
+ Add something you love and it waits here.
208
+ </Empty>
209
+ ) : (
210
+ <Stack gap={3}>
211
+ {lines.map((l) => (
212
+ <Row key={l.productId} gap={2} wrap={false}>
213
+ <Stack gap={1}>
214
+ <Text truncate>{l.name}</Text>
215
+ <Text size="sm" muted num>
216
+ {MONEY.format(l.price * l.qty)}
217
+ </Text>
218
+ </Stack>
219
+ <Spacer />
220
+ <IconButton size="sm" variant="quiet" icon={<Icon name="remove" size="sm" />} label={`One fewer ${l.name}`} onClick={() => change(l.productId, -1)} />
221
+ <Text num>{l.qty}</Text>
222
+ <IconButton size="sm" variant="quiet" icon={<Icon name="add" size="sm" />} label={`One more ${l.name}`} onClick={() => change(l.productId, 1)} />
223
+ </Row>
224
+ ))}
225
+ <Separator />
226
+ <Row justify="between">
227
+ <Text size="sm" muted>
228
+ Subtotal
229
+ </Text>
230
+ <Text size="sm" num>
231
+ {MONEY.format(subtotal)}
232
+ </Text>
233
+ </Row>
234
+ <Row justify="between">
235
+ <Text size="sm" muted>
236
+ Shipping
237
+ </Text>
238
+ <Text size="sm" num>
239
+ {shipping === 0 ? "Free" : MONEY.format(shipping)}
240
+ </Text>
241
+ </Row>
242
+ <Row justify="between">
243
+ <Text>Total</Text>
244
+ <Text num>{MONEY.format(subtotal + shipping)}</Text>
245
+ </Row>
246
+ </Stack>
247
+ )}
248
+ {checkout}
249
+ <Text size="sm" muted>
250
+ No payment is taken here. We confirm your order and arrange payment when you collect.
251
+ </Text>
252
+ </Stack>
253
+ </Card>
254
+
255
+ {mine.length > 0 && (
256
+ <Card variant="tight">
257
+ <Stack gap={3}>
258
+ <Heading level={3}>Your orders</Heading>
259
+ {mine.map((o) => (
260
+ <Row key={o._id} justify="between" wrap={false}>
261
+ <Stack gap={1}>
262
+ <Text num>{MONEY.format(o.total)}</Text>
263
+ <Text size="sm" muted>
264
+ {new Date(o.createdAt).toLocaleDateString()} · {plural(o.lines.reduce((n, l) => n + l.qty, 0), "item", "items")}
265
+ </Text>
266
+ </Stack>
267
+ <Badge tone={ORDER_STATUS[o.status]?.tone ?? "info"}>{ORDER_STATUS[o.status]?.label ?? "Received"}</Badge>
268
+ </Row>
269
+ ))}
270
+ </Stack>
271
+ </Card>
272
+ )}
273
+ </Stack>
274
+ </div>
275
+ </Grid>
276
+ </Stack>
277
+ <div className="pouf-toasts">
278
+ <Toaster />
279
+ </div>
280
+ </main>
281
+ );
282
+ }
283
+ ```
284
+
285
+ ## Adapting
286
+
287
+ - Swap `PRODUCTS` for the shop's own catalogue; a catalogue the owner edits in the app becomes `product` documents behind an owner-only `can.create`, read with `useLiveQuery("type", { key: "product" })`.
288
+ - Keep the order a snapshot of names, prices and quantities: an order is a record of what was agreed, so a later price change must not touch it.
289
+ - Orders hold who bought what, so `access.js` and the app's sharing should keep each order between its buyer and the shop: a buyer creates and reads their own, the shop reads them all and moves each through `ORDER_STATUS` behind `can.edit(order)`.
290
+ - Real payment is a separate integration with its own provider; until there is one, keep the "no payment is taken here" line truthful.
291
+ - 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.
292
+
293
+ Adapted from 1st-Pouf (MIT) https://1st-pouf.worksonmy.dev/r/storefront.json
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const poufSupportConfig: LlmConfig;
@@ -0,0 +1,6 @@
1
+ export const poufSupportConfig = {
2
+ name: "pouf-support",
3
+ label: "Pouf support block",
4
+ description: "Support desk to log tickets, filter the queue, reply, resolve",
5
+ };
6
+ //# sourceMappingURL=pouf-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pouf-support.js","sourceRoot":"","sources":["../../jsr/llms/pouf-support.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,iBAAiB,GAAc;IAC1C,IAAI,EAAE,cAAc;IACpB,KAAK,EAAE,oBAAoB;IAC3B,WAAW,EAAE,+DAA+D;CAC7E,CAAC"}