@cosmicdrift/kumiko-samples 0.362.0 → 0.363.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.
Files changed (252) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -0,0 +1,229 @@
1
+ import type {
2
+ Registry,
3
+ TemplateScalarVariableDef,
4
+ TemplateVariableDef,
5
+ TemplateVariables,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import type { Element as ScanElement } from "happy-dom";
8
+ import * as z from "zod";
9
+ import { withParsedDocument } from "./document-sanitizer.js";
10
+ import { isDocumentPlaceholderHref, isDocumentReferenceName } from "./document-schema.js";
11
+ import type { TemplateMeta } from "./template-meta.js";
12
+
13
+ export const SYSTEM_PLACEHOLDERS: ReadonlySet<string> = new Set([
14
+ "tenantName",
15
+ "signature",
16
+ "imprint",
17
+ "unsubscribeUrl",
18
+ ]);
19
+
20
+ const PAGE_PLACEHOLDERS = ["page", "pages"] as const;
21
+ const PAGED_KINDS: ReadonlySet<string> = new Set(["document-pdf", "pdf-layout"]);
22
+
23
+ export function systemPlaceholdersFor(kind: string): ReadonlySet<string> {
24
+ return PAGED_KINDS.has(kind)
25
+ ? new Set([...SYSTEM_PLACEHOLDERS, ...PAGE_PLACEHOLDERS])
26
+ : SYSTEM_PLACEHOLDERS;
27
+ }
28
+
29
+ const BASE_RESOURCE_KEYS = ["logo"] as const;
30
+
31
+ export type TemplateValidation = {
32
+ readonly unknown: readonly string[];
33
+ readonly missingRequired: readonly string[];
34
+ readonly used: readonly string[];
35
+ /** data-resource keys with no uploaded file; a warning, never a blocker. */
36
+ readonly unknownResources: readonly string[];
37
+ };
38
+
39
+ // Mirrors TemplateVariableDef: a stored or draft schema keeps its label and example, which the
40
+ // preview uses as sample values.
41
+ const variableCommon = {
42
+ label: z.string().optional(),
43
+ required: z.boolean().optional(),
44
+ };
45
+ const textLikeExample = z.string().optional();
46
+
47
+ const scalarVariableSchema = z.discriminatedUnion("type", [
48
+ z.object({ ...variableCommon, type: z.literal("text"), example: textLikeExample }),
49
+ z.object({ ...variableCommon, type: z.literal("link"), example: textLikeExample }),
50
+ z.object({
51
+ ...variableCommon,
52
+ type: z.literal("money"),
53
+ example: z
54
+ .union([z.number(), z.object({ amount: z.number(), currency: z.string() })])
55
+ .optional(),
56
+ }),
57
+ z.object({ ...variableCommon, type: z.literal("date"), example: textLikeExample }),
58
+ ]);
59
+
60
+ const variableDefSchema = z.union([
61
+ scalarVariableSchema,
62
+ z.object({
63
+ ...variableCommon,
64
+ type: z.literal("list"),
65
+ item: z.record(z.string(), scalarVariableSchema),
66
+ example: z.array(z.record(z.string(), z.unknown())).optional(),
67
+ }),
68
+ ]);
69
+
70
+ // A stored variableSchema value is either a bare string (legacy "this is a text
71
+ // variable") or a TemplateVariableDef object; anything else is not declared and
72
+ // therefore shows up as an unknown placeholder instead of being guessed at.
73
+ function toVariableDef(raw: unknown): TemplateVariableDef | null {
74
+ if (typeof raw === "string") return { type: "text" };
75
+ const parsed = variableDefSchema.safeParse(raw);
76
+ return parsed.success ? parsed.data : null;
77
+ }
78
+
79
+ export function resolveTemplateVariables(
80
+ registry: Registry,
81
+ slug: string,
82
+ variableSchema: Readonly<Record<string, unknown>>,
83
+ ): TemplateVariables {
84
+ const declared = registry.getAllNotifications().get(slug)?.variables;
85
+ return declared ?? variablesFromSchema(variableSchema);
86
+ }
87
+
88
+ export function variablesFromSchema(
89
+ variableSchema: Readonly<Record<string, unknown>>,
90
+ ): TemplateVariables {
91
+ const variables: Record<string, TemplateVariableDef> = {};
92
+ for (const [name, raw] of Object.entries(variableSchema)) {
93
+ const def = toVariableDef(raw);
94
+ if (def !== null) variables[name] = def;
95
+ }
96
+ return variables;
97
+ }
98
+
99
+ type Resolution = { readonly known: boolean; readonly used: string };
100
+
101
+ function rootName(name: string): string {
102
+ return name.split(".")[0] ?? name;
103
+ }
104
+
105
+ type PlaceholderScope = {
106
+ readonly variables: TemplateVariables;
107
+ readonly systemPlaceholders: ReadonlySet<string>;
108
+ readonly resourceKeys: ReadonlySet<string>;
109
+ readonly used: Set<string>;
110
+ readonly unknown: Set<string>;
111
+ readonly unknownResources: Set<string>;
112
+ };
113
+
114
+ function listItemFields(
115
+ variables: TemplateVariables,
116
+ listName: string | null,
117
+ ): Readonly<Record<string, TemplateScalarVariableDef>> {
118
+ const def =
119
+ listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
120
+ return def?.type === "list" ? def.item : {};
121
+ }
122
+
123
+ function isKnownName(
124
+ name: string,
125
+ scope: PlaceholderScope,
126
+ itemFields: Readonly<Record<string, unknown>>,
127
+ ): Resolution {
128
+ const { variables } = scope;
129
+ const root = rootName(name);
130
+ if (Object.hasOwn(itemFields, name) || Object.hasOwn(itemFields, root)) {
131
+ return { known: true, used: "" };
132
+ }
133
+ if (scope.systemPlaceholders.has(root)) return { known: true, used: "" };
134
+ if (Object.hasOwn(variables, name)) return { known: true, used: name };
135
+ if (Object.hasOwn(variables, root)) return { known: true, used: root };
136
+ return { known: false, used: "" };
137
+ }
138
+
139
+ function recordName(
140
+ name: string,
141
+ itemFields: Readonly<Record<string, unknown>>,
142
+ scope: PlaceholderScope,
143
+ ) {
144
+ const resolution = isKnownName(name, scope, itemFields);
145
+ if (!resolution.known) scope.unknown.add(name);
146
+ else if (resolution.used !== "") scope.used.add(resolution.used);
147
+ }
148
+
149
+ function enclosingEachName(element: ScanElement): string | null {
150
+ return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
151
+ }
152
+
153
+ function scanBodyPlaceholders(body: string, scope: PlaceholderScope): void {
154
+ withParsedDocument(body, (root) => {
155
+ for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
156
+ const listName = element.getAttribute("data-each") ?? "";
157
+ const def = Object.hasOwn(scope.variables, listName) ? scope.variables[listName] : undefined;
158
+ if (def?.type === "list") scope.used.add(listName);
159
+ else scope.unknown.add(listName);
160
+ }
161
+ for (const element of Array.from(
162
+ root.querySelectorAll("span[data-variable], a[href], img[data-resource]"),
163
+ )) {
164
+ const itemFields = listItemFields(scope.variables, enclosingEachName(element));
165
+ const tag = element.tagName.toLowerCase();
166
+ if (tag === "span") {
167
+ recordName(element.getAttribute("data-variable") ?? "", itemFields, scope);
168
+ } else if (tag === "a") {
169
+ const href = element.getAttribute("href") ?? "";
170
+ if (isDocumentPlaceholderHref(href)) recordName(href.slice(2, -2), itemFields, scope);
171
+ } else {
172
+ const key = element.getAttribute("data-resource") ?? "";
173
+ if (!scope.resourceKeys.has(key)) scope.unknownResources.add(key);
174
+ }
175
+ }
176
+ });
177
+ }
178
+
179
+ const HANDLEBARS_EXPRESSION = /\{\{([^{}]*)\}\}/g;
180
+
181
+ // Meta text supports only plain `{{name}}` / `{{name.field}}`; every other
182
+ // Handlebars construct is reported as unknown so it can never reach the compiler.
183
+ function scanMetaPlaceholders(text: string, scope: PlaceholderScope): void {
184
+ const leftover = text.replace(HANDLEBARS_EXPRESSION, (_match, inner: string) => {
185
+ const name = inner.trim();
186
+ if (isDocumentReferenceName(name)) recordName(name, {}, scope);
187
+ else scope.unknown.add(`{{${inner}}}`);
188
+ return "";
189
+ });
190
+ if (/[{}]/.test(leftover)) scope.unknown.add("{");
191
+ }
192
+
193
+ function metaTexts(meta: TemplateMeta | null): readonly string[] {
194
+ if (meta === null || !("subject" in meta)) return [];
195
+ return [meta.subject, ...(meta.previewText === undefined ? [] : [meta.previewText])];
196
+ }
197
+
198
+ export function validateTemplatePlaceholders(input: {
199
+ readonly kind: string;
200
+ readonly body: string;
201
+ readonly meta: TemplateMeta | null;
202
+ readonly variables: TemplateVariables;
203
+ /** data-resource keys with an uploaded file besides the built-in `logo`. */
204
+ readonly resourceKeys?: readonly string[];
205
+ }): TemplateValidation {
206
+ const scope: PlaceholderScope = {
207
+ variables: input.variables,
208
+ systemPlaceholders: systemPlaceholdersFor(input.kind),
209
+ resourceKeys: new Set([...BASE_RESOURCE_KEYS, ...(input.resourceKeys ?? [])]),
210
+ used: new Set(),
211
+ unknown: new Set(),
212
+ unknownResources: new Set(),
213
+ };
214
+ scanBodyPlaceholders(input.body, scope);
215
+ for (const text of metaTexts(input.meta)) scanMetaPlaceholders(text, scope);
216
+ const missingRequired = Object.entries(input.variables)
217
+ .filter(([name, def]) => def.required === true && !scope.used.has(name))
218
+ .map(([name]) => name);
219
+ return {
220
+ unknown: [...scope.unknown].sort(),
221
+ missingRequired: missingRequired.sort(),
222
+ used: [...scope.used].sort(),
223
+ unknownResources: [...scope.unknownResources].sort(),
224
+ };
225
+ }
226
+
227
+ export function hasPlaceholderProblems(validation: TemplateValidation): boolean {
228
+ return validation.unknown.length > 0 || validation.missingRequired.length > 0;
229
+ }
@@ -79,7 +79,7 @@ export async function assertConsumerHandlesNotFound(
79
79
  await consumer.resolve({
80
80
  tenantId,
81
81
  slug,
82
- kind: "mail-html",
82
+ kind: "mail",
83
83
  locale: "de",
84
84
  });
85
85
  } catch (e) {
@@ -105,7 +105,7 @@ export async function assertConsumerRespectsLocaleFallback(
105
105
  await seedTemplate(db, {
106
106
  tenantId: SYSTEM_TENANT_ID,
107
107
  slug,
108
- kind: "notification",
108
+ kind: "document-pdf",
109
109
  locale: FALLBACK_LOCALE,
110
110
  scope: "system",
111
111
  content: "conformance-fallback-content",
@@ -113,7 +113,7 @@ export async function assertConsumerRespectsLocaleFallback(
113
113
  const result = await consumer.resolve({
114
114
  tenantId,
115
115
  slug,
116
- kind: "notification",
116
+ kind: "document-pdf",
117
117
  locale: "tr",
118
118
  });
119
119
  if (result.locale !== FALLBACK_LOCALE) {
@@ -143,7 +143,7 @@ export async function assertConsumerHandlesMissingResourceKeys(
143
143
  await seedTemplate(db, {
144
144
  tenantId: SYSTEM_TENANT_ID,
145
145
  slug,
146
- kind: "mail-html",
146
+ kind: "mail",
147
147
  locale: "de",
148
148
  scope: "system",
149
149
  linkedResources: { logo: "file_missing" },
@@ -151,7 +151,7 @@ export async function assertConsumerHandlesMissingResourceKeys(
151
151
  const template = await consumer.resolve({
152
152
  tenantId,
153
153
  slug,
154
- kind: "mail-html",
154
+ kind: "mail",
155
155
  locale: "de",
156
156
  });
157
157
 
@@ -40,6 +40,7 @@ import {
40
40
  TemplateResolverHandlers,
41
41
  TemplateResolverQueries,
42
42
  } from "../qualified-names.js";
43
+ import { DocumentContentEditor } from "./document-editor.js";
43
44
  import { defaultTranslations } from "./i18n.js";
44
45
 
45
46
  // Exported for the unit test — groupBlocksByFolder is a pure function.
@@ -256,21 +257,22 @@ type TextBlock = {
256
257
  readonly modifiedAt: string | null;
257
258
  };
258
259
 
259
- // The collection's declared contentFormat ("plain" | "rich" | "markdown",
260
- // see ContentCollectionDefinition.contentFormat) uses a different vocabulary
260
+ // The collection's declared contentFormat ("plain" | "rich" | "markdown" |
261
+ // "document", see ContentCollectionDefinition.contentFormat) uses a different vocabulary
261
262
  // than the stored entry's contentFormat ("plain" | "html" | "markdown" |
262
- // "mjml", see CONTENT_FORMATS) — "rich" always stores HTML. Without this,
263
+ // "mjml", see CONTENT_FORMATS) — "rich" and "document" always store HTML. Without this,
263
264
  // handleSave omitting contentFormat let the write schema's
264
265
  // contentFormatSchema.default("markdown") silently stamp every save as
265
266
  // markdown, clobbering e.g. a "plain" entry. Undefined (no collection, or
266
267
  // the collection didn't declare a format) keeps whatever was already saved
267
268
  // instead of guessing.
268
- function resolveWriteContentFormat(
269
+ // Exported for the unit test — resolveWriteContentFormat is a pure function.
270
+ export function resolveWriteContentFormat(
269
271
  collectionContentFormat: string | undefined,
270
272
  loadedContentFormat: string | undefined,
271
273
  ): string | undefined {
272
274
  if (collectionContentFormat === "plain") return "plain";
273
- if (collectionContentFormat === "rich") return "html";
275
+ if (collectionContentFormat === "rich" || collectionContentFormat === "document") return "html";
274
276
  if (collectionContentFormat === "markdown") return "markdown";
275
277
  return loadedContentFormat;
276
278
  }
@@ -499,12 +501,13 @@ export function textBlocksClient(opts?: {
499
501
  resolvers: {
500
502
  "template-resolver:edit": TextBlockEditor,
501
503
  },
502
- // The default for every "plain" collection (ai-prompt, mail-html without
504
+ // The default for every "plain" collection (ai-prompt, mail without
503
505
  // an explicit rich override) — an app-registered "plain" editor on
504
506
  // another clientFeature still wins, last-wins same as columnRenderers.
505
507
  contentEditors: {
506
508
  plain: PlainContentEditor,
507
509
  rich: RichContentEditor,
510
+ document: DocumentContentEditor,
508
511
  // ponytail: same textarea+chips as "plain" — markdown is stored as
509
512
  // text either way. Shares PlainContentEditor's fixed-DOM-id ceiling
510
513
  // (see its own file comment); swap for a live-preview widget or a
@@ -0,0 +1,162 @@
1
+ // @runtime client
2
+ import type { Extensions, NodeViewRenderer } from "@tiptap/core";
3
+ import { ListKeymap } from "@tiptap/extension-list";
4
+ import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
5
+ import {
6
+ DocumentButton,
7
+ DocumentPageBreak,
8
+ DocumentResource,
9
+ DocumentVariable,
10
+ documentExtensions,
11
+ } from "../document-schema.js";
12
+ import type { DocumentResourcePreview } from "./document-toolbar-blocks.js";
13
+
14
+ export type DocumentEditorLabels = {
15
+ readonly placeholder: string;
16
+ readonly image: string;
17
+ readonly pageBreak: string;
18
+ readonly button?: string;
19
+ readonly variableLabels?: Readonly<Record<string, string>>;
20
+ readonly systemVariables?: readonly string[];
21
+ readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
22
+ /** Canvas chrome labels every block with its type. */
23
+ readonly blockLabels?: boolean;
24
+ };
25
+
26
+ function createElementWithClass<K extends keyof HTMLElementTagNameMap>(
27
+ tag: K,
28
+ className: string,
29
+ text?: string,
30
+ ): HTMLElementTagNameMap[K] {
31
+ const element = document.createElement(tag);
32
+ element.className = className;
33
+ if (text !== undefined) element.textContent = text;
34
+ return element;
35
+ }
36
+
37
+ const CHIP_CLASS = "mx-0.5 inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-xs";
38
+ const USER_CHIP_CLASS = `${CHIP_CLASS} border-primary/30 bg-primary/10 text-primary`;
39
+ const SYSTEM_CHIP_CLASS = `${CHIP_CLASS} border-input bg-muted text-muted-foreground`;
40
+
41
+ // The chip is display only: the stored node carries just the name, so a
42
+ // label never reaches the document.
43
+ export function createVariableView(
44
+ variableLabels: Readonly<Record<string, string>>,
45
+ systemVariables: readonly string[],
46
+ ): NodeViewRenderer {
47
+ return ({ node }) => {
48
+ const name = String(node.attrs["name"]);
49
+ const label = Object.hasOwn(variableLabels, name) ? variableLabels[name] : undefined;
50
+ const dom = createElementWithClass(
51
+ "span",
52
+ systemVariables.includes(name) ? SYSTEM_CHIP_CLASS : USER_CHIP_CLASS,
53
+ );
54
+ dom.contentEditable = "false";
55
+ dom.append(document.createTextNode(label !== undefined && label !== "" ? label : name));
56
+ if (label !== undefined && label !== "" && label !== name) {
57
+ dom.append(createElementWithClass("small", "font-mono text-[10px] opacity-75", name));
58
+ }
59
+ return { dom };
60
+ };
61
+ }
62
+
63
+ // Same rule the sanitizer's image handling follows: previews never carry a
64
+ // scriptable scheme.
65
+ const PREVIEW_SRC = /^(data:image\/(png|jpeg|gif|webp);|blob:|https:\/\/|\/)/;
66
+
67
+ function createBlockLabel(text: string): HTMLSpanElement {
68
+ const label = createElementWithClass(
69
+ "span",
70
+ "absolute -top-2 left-2 bg-card px-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground",
71
+ text,
72
+ );
73
+ label.contentEditable = "false";
74
+ return label;
75
+ }
76
+
77
+ function createResourceView(labels: DocumentEditorLabels): NodeViewRenderer {
78
+ return ({ node }) => {
79
+ const key = String(node.attrs["resource"]);
80
+ const previews = labels.resourcePreviews ?? {};
81
+ const preview = Object.hasOwn(previews, key) ? previews[key] : undefined;
82
+ const dom = createElementWithClass(
83
+ "div",
84
+ "relative my-3 flex items-center gap-3 rounded border border-dashed border-input px-3 py-3 text-sm text-muted-foreground",
85
+ );
86
+ dom.contentEditable = "false";
87
+ if (labels.blockLabels) dom.append(createBlockLabel(labels.image));
88
+ if (preview !== undefined && PREVIEW_SRC.test(preview.src)) {
89
+ const image = createElementWithClass("img", "max-h-16 max-w-48 rounded");
90
+ image.src = preview.src;
91
+ image.alt = typeof node.attrs["alt"] === "string" ? node.attrs["alt"] : key;
92
+ dom.append(image);
93
+ dom.append(createElementWithClass("span", "text-xs", preview.caption ?? key));
94
+ } else {
95
+ dom.append(
96
+ createElementWithClass("span", "font-medium", labels.image),
97
+ createElementWithClass("code", "text-xs", key),
98
+ );
99
+ }
100
+ return { dom };
101
+ };
102
+ }
103
+
104
+ function createButtonView(buttonLabel: string): NodeViewRenderer {
105
+ return () => {
106
+ const dom = createElementWithClass(
107
+ "div",
108
+ "relative my-3 rounded border border-dashed border-input p-3",
109
+ );
110
+ const button = document.createElement("a");
111
+ button.setAttribute("data-button", "");
112
+ dom.append(createBlockLabel(buttonLabel), button);
113
+ return { dom, contentDOM: button };
114
+ };
115
+ }
116
+
117
+ function createPageBreakView(pageBreakLabel: string): NodeViewRenderer {
118
+ return () => {
119
+ const dom = createElementWithClass(
120
+ "div",
121
+ "my-3 border-t border-dashed border-input pt-1 text-center text-xs text-muted-foreground",
122
+ pageBreakLabel,
123
+ );
124
+ dom.contentEditable = "false";
125
+ return { dom };
126
+ };
127
+ }
128
+
129
+ // Replaced by name, not appended: the schema must keep exactly one node per
130
+ // name, and only the editor view differs from the shared document schema.
131
+ export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Extensions {
132
+ const base = documentExtensions.map((extension) => {
133
+ switch (extension.name) {
134
+ case DocumentVariable.name:
135
+ return DocumentVariable.extend({
136
+ addNodeView: () =>
137
+ createVariableView(labels.variableLabels ?? {}, labels.systemVariables ?? []),
138
+ });
139
+ case DocumentResource.name:
140
+ return DocumentResource.extend({ addNodeView: () => createResourceView(labels) });
141
+ case DocumentButton.name:
142
+ return labels.blockLabels
143
+ ? DocumentButton.extend({ addNodeView: () => createButtonView(labels.button ?? "") })
144
+ : extension;
145
+ case DocumentPageBreak.name:
146
+ return DocumentPageBreak.extend({
147
+ addNodeView: () => createPageBreakView(labels.pageBreak),
148
+ });
149
+ default:
150
+ return extension;
151
+ }
152
+ });
153
+ // No TrailingNode: it appends an empty paragraph that breaks the HTML roundtrip.
154
+ return [
155
+ ...base,
156
+ UndoRedo,
157
+ Gapcursor,
158
+ Dropcursor,
159
+ ListKeymap,
160
+ Placeholder.configure({ placeholder: labels.placeholder }),
161
+ ];
162
+ }
@@ -0,0 +1,28 @@
1
+ // @runtime client
2
+ //
3
+ // Lazy boundary: tiptap only loads when a "document" collection is rendered.
4
+ // Never import document-tiptap-editor or document-editor-extensions statically here.
5
+
6
+ import { TextareaContentEditor } from "@cosmicdrift/kumiko-renderer";
7
+ import { lazy, type ReactNode, Suspense } from "react";
8
+ import type { DocumentEditorProps } from "./document-toolbar-blocks.js";
9
+
10
+ const LazyDocumentTiptapEditor = lazy(() => import("./document-tiptap-editor.js"));
11
+
12
+ export function DocumentContentEditor(props: DocumentEditorProps): ReactNode {
13
+ return (
14
+ <Suspense
15
+ fallback={
16
+ <TextareaContentEditor
17
+ id={props.id}
18
+ value={props.value}
19
+ onChange={props.onChange}
20
+ variables={props.variables}
21
+ readOnly={props.readOnly}
22
+ />
23
+ }
24
+ >
25
+ <LazyDocumentTiptapEditor {...props} />
26
+ </Suspense>
27
+ );
28
+ }