@odla-ai/chapter 0.59.1 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1622,13 +1622,44 @@ These bite silently — a smoke test won't catch them:
1622
1622
  | `meetUrl`, `htmlLink` | `prepEmail` | the booked meeting's Google Meet and calendar links (blank when the provider returned none) |
1623
1623
  | `meetingTime`, `meetingLink` | `prepEmail` | aliases of `startAt` and `meetUrl` (falling back to `htmlLink`) for copy written before those existed |
1624
1624
  | `adminUrl`, `membersUrl` | `adminNotification`, `submitConfirmation`, `onboardingInvite` | `<origin>/admin/` and `<origin>/members/` from the request that fired the send |
1625
+ | `namedSeat`, `namedSeatRecipientName` | `adminNotification`, `submitConfirmation`, `paymentConfirmation`, `prepEmail`, `onboardingInvite` | `paid` or `included` when the frozen checkout carries a gift seat for another person, else blank (so `{{#namedSeat}}` hides); the recipient's name as entered at checkout. A host event reads the same values through `checkoutVariables` |
1625
1626
  | `refundPolicyText`, `commitmentText`, `normsText` | all | the group row's policy copy |
1626
1627
 
1628
+ **Sections and an HTML body.** `{{#name}} … {{/name}}` keeps its contents
1629
+ only when `name` has a value, so a line such as
1630
+ `{{#namedSeat}}Your gift for {{namedSeatRecipientName}} is paid.{{/namedSeat}}`
1631
+ appears for gift buyers alone; a section on its own line disappears with
1632
+ its line. The event must supply a section's name like any variable, and an
1633
+ unclosed section blocks the send. A template may also carry an `html`
1634
+ body (Settings → Email, under the plain body) with the same placeholders
1635
+ and sections: values are escaped, a line break inside a value becomes a
1636
+ `<br>`, the text body is still sent as the plain alternative and recorded
1637
+ in the log, and an unknown placeholder in the HTML blocks the send like
1638
+ one in the text. Without an `html` body the text is rendered to HTML by
1639
+ `textToHtml`.
1640
+
1627
1641
  **From display name.** `emails.fromName` (seeded to the group row, editable
1628
1642
  in Settings → Email) turns the From header into `{ name, email }` —
1629
1643
  `Tori Horton <tori@…>` in an inbox — while the address stays the Worker's
1630
1644
  `EMAIL_FROM`. A group with no name sends the bare address exactly as before.
1631
1645
 
1646
+ **Deliverability.** Every send carries the text part verbatim and an HTML
1647
+ alternative rendered from it by `textToHtml` (paragraphs, line breaks, and
1648
+ each https address as a link labelled with itself) — multipart like mail from
1649
+ any mainstream sender, with nothing the text lacks. Recipient-facing events
1650
+ (every audience except the administrator's) also carry a List-Unsubscribe
1651
+ header pointing at the group's reply-to as a mailto link, so a reader who
1652
+ wants out writes to you instead of reporting spam. Set the notify deps'
1653
+ render hook to `false` for text-only mail, or pass your own renderer. The
1654
+ Email page lists spam-filter signals beside each template — shouting or
1655
+ over-punctuated subjects, trigger phrases, insecure or development-host
1656
+ links, shorteners, link-only bodies — and beside the sender (a no-reply
1657
+ From, a Reply-To on another domain); `emailSpamSignals` and
1658
+ `emailSenderWarnings` are the same checks for a script, and the save route
1659
+ returns them. Warnings never block a send. Authentication (SPF, DKIM, DMARC)
1660
+ belongs to Cloudflare Email Service: onboard the sending domain in the
1661
+ Cloudflare dashboard and it adds the records.
1662
+
1632
1663
  **Re-check this on every chapter upgrade.** Wiring a send changes a site's
1633
1664
  outbound mail with no local diff — release notes call out send changes
1634
1665
  explicitly for that reason.
@@ -4,7 +4,7 @@ import {
4
4
  DEFAULT_CHAPTER_COPY,
5
5
  formatChapterCopy,
6
6
  useChapterCopy
7
- } from "./chunk-GADLTNJF.js";
7
+ } from "./chunk-PKVHXKGJ.js";
8
8
 
9
9
  // src/ui/admin.tsx
10
10
  import { useEffect as useEffect12, useMemo as useMemo3, useRef as useRef3, useState as useState27 } from "preact/hooks";
@@ -2739,6 +2739,11 @@ function EmailBody({ ctx }) {
2739
2739
  " ready"
2740
2740
  ] }),
2741
2741
  data.validation.issues.length ? /* @__PURE__ */ jsx29("ul", { role: "alert", children: data.validation.issues.map((issue, i) => /* @__PURE__ */ jsx29("li", { children: issue.message }, i)) }) : null,
2742
+ data.validation.warnings?.length ? /* @__PURE__ */ jsx29("ul", { children: data.validation.warnings.map((warning, i) => /* @__PURE__ */ jsxs27("li", { children: [
2743
+ copy.spamRisk,
2744
+ ": ",
2745
+ warning.message
2746
+ ] }, i)) }) : null,
2742
2747
  draft ? /* @__PURE__ */ jsx29("p", { role: "status", children: "Save your changes to refresh these checks." }) : /* @__PURE__ */ jsxs27("p", { className: "muted", children: [
2743
2748
  "Checked ",
2744
2749
  new Date(data.validation.checkedAt).toLocaleString()
@@ -2773,9 +2778,15 @@ function EmailBody({ ctx }) {
2773
2778
  "."
2774
2779
  ] }),
2775
2780
  event.issues.length ? /* @__PURE__ */ jsx29("ul", { children: event.issues.map((issue, i) => /* @__PURE__ */ jsx29("li", { children: issue.message }, i)) }) : null,
2781
+ event.warnings?.length ? /* @__PURE__ */ jsx29("ul", { className: "muted", children: event.warnings.map((warning, i) => /* @__PURE__ */ jsxs27("li", { children: [
2782
+ copy.spamRisk,
2783
+ ": ",
2784
+ warning.message
2785
+ ] }, i)) }) : null,
2776
2786
  /* @__PURE__ */ jsx29(Checkbox2, { label: copy.enabled, checked: t.enabled === true, onChange: (e) => editTemplate(key, { enabled: e.target.checked }) }),
2777
2787
  /* @__PURE__ */ jsx29(Field13, { label: copy.subject, children: /* @__PURE__ */ jsx29(Input6, { value: t.subject, onInput: (e) => editTemplate(key, { subject: e.target.value }) }) }),
2778
2788
  /* @__PURE__ */ jsx29(Field13, { label: copy.body, children: /* @__PURE__ */ jsx29(Textarea5, { prose: true, rows: 5, value: t.text, onInput: (e) => editTemplate(key, { text: e.target.value }) }) }),
2789
+ /* @__PURE__ */ jsx29(Field13, { label: copy.htmlBody, hint: copy.htmlBodyHint, children: /* @__PURE__ */ jsx29(Textarea5, { rows: 6, value: t.html ?? "", onInput: (e) => editTemplate(key, { html: e.target.value }) }) }),
2779
2790
  /* @__PURE__ */ jsxs27("p", { className: "muted", children: [
2780
2791
  "Available values: ",
2781
2792
  event.variables.map((name) => `{{${name}}}`).join(", ")
@@ -3875,4 +3886,4 @@ export {
3875
3886
  MembershipDisputes,
3876
3887
  MembershipPolicyVersions
3877
3888
  };
3878
- //# sourceMappingURL=chunk-P6X3W2XP.js.map
3889
+ //# sourceMappingURL=chunk-24L7FANC.js.map