@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -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/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -0,0 +1,1065 @@
1
+ // @runtime client
2
+ //
3
+ // tiptap-backed implementation behind DocumentContentEditor's lazy() boundary —
4
+ // never import this file statically, tiptap must only load when a "document"
5
+ // collection is rendered. Variables are inserted as `variable` nodes only,
6
+ // never as `{{name}}` text, so the sanitizer's schema keeps them.
7
+
8
+ import {
9
+ type ActionMenuItemSpec,
10
+ TextareaContentEditor,
11
+ usePrimitives,
12
+ useTranslation,
13
+ VariableChips,
14
+ } from "@cosmicdrift/kumiko-renderer";
15
+ import { cn } from "@cosmicdrift/kumiko-renderer-web";
16
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
17
+ import type { Editor, JSONContent } from "@tiptap/core";
18
+ import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
19
+ import { BubbleMenu } from "@tiptap/react/menus";
20
+ import {
21
+ AlignCenter,
22
+ AlignJustify,
23
+ AlignLeft,
24
+ AlignRight,
25
+ Bold as BoldIcon,
26
+ Braces,
27
+ ChevronDown,
28
+ Image as ImageIcon,
29
+ Italic as ItalicIcon,
30
+ Link as LinkIcon,
31
+ List,
32
+ ListOrdered,
33
+ Minus,
34
+ MousePointerClick,
35
+ Repeat,
36
+ Scissors,
37
+ Table as TableIcon,
38
+ Underline as UnderlineIcon,
39
+ } from "lucide-react";
40
+ import { Fragment, type ReactNode, useEffect, useMemo, useState } from "react";
41
+ import { createPortal } from "react-dom";
42
+ import {
43
+ DOCUMENT_TEXT_ALIGNMENTS,
44
+ type DocumentTextAlignment,
45
+ isAllowedDocumentLinkHref,
46
+ isDocumentPlaceholderHref,
47
+ isDocumentReferenceName,
48
+ isDocumentResourceKey,
49
+ } from "../document-schema.js";
50
+ import {
51
+ createDocumentEditorExtensions,
52
+ DOCUMENT_BLOCK_ATTRIBUTE,
53
+ } from "./document-editor-extensions.js";
54
+ import {
55
+ type DocumentEditorProps,
56
+ type DocumentResourcePreview,
57
+ type DocumentToolbarBlock,
58
+ MAIL_DOCUMENT_BLOCKS,
59
+ } from "./document-toolbar-blocks.js";
60
+ import { createEditorHandle } from "./editor-handle.js";
61
+ import { useStableByKey } from "./use-stable-by-key.js";
62
+
63
+ const NO_LABELS: Readonly<Record<string, string>> = {};
64
+ const NO_NAMES: readonly string[] = [];
65
+ const NO_PREVIEWS: Readonly<Record<string, DocumentResourcePreview>> = {};
66
+
67
+ function previewsKey(previews: Readonly<Record<string, DocumentResourcePreview>>): string {
68
+ return Object.entries(previews)
69
+ .map(([key, { src, caption }]) => `${key}|${src.length}|${src.slice(-24)}|${caption ?? ""}`)
70
+ .join("\n");
71
+ }
72
+
73
+ type PanelKind = "link" | "placeholder" | "image" | "button" | "repeat" | "align";
74
+
75
+ // Blocks that stay in the canvas toolbar; the rest moves into the overflow menu.
76
+ const CANVAS_INLINE_BLOCKS: readonly DocumentToolbarBlock[] = ["image", "table"];
77
+
78
+ const BLOCK_PANEL: Partial<Record<DocumentToolbarBlock, PanelKind>> = {
79
+ image: "image",
80
+ button: "button",
81
+ repeat: "repeat",
82
+ align: "align",
83
+ };
84
+
85
+ const ALIGNMENT_ICONS: Record<DocumentTextAlignment, ReactNode> = {
86
+ left: <AlignLeft size={16} />,
87
+ center: <AlignCenter size={16} />,
88
+ right: <AlignRight size={16} />,
89
+ justify: <AlignJustify size={16} />,
90
+ };
91
+
92
+ const ALIGNMENT_LABEL_KEYS: Record<DocumentTextAlignment, string> = {
93
+ left: "template-resolver.documentEditor.alignLeft",
94
+ center: "template-resolver.documentEditor.alignCenter",
95
+ right: "template-resolver.documentEditor.alignRight",
96
+ justify: "template-resolver.documentEditor.alignJustify",
97
+ };
98
+
99
+ type RepeatTableState = {
100
+ readonly lists: readonly string[];
101
+ readonly headerRepeats: boolean;
102
+ readonly firstRowPos: number;
103
+ };
104
+
105
+ // print-document moves the leading rows flagged repeatHeader into thead, so the
106
+ // toggle reads and writes the table's first row.
107
+ function findRepeatTable(editor: Editor): RepeatTableState | null {
108
+ const { $from } = editor.state.selection;
109
+ for (let depth = $from.depth; depth > 0; depth--) {
110
+ const node = $from.node(depth);
111
+ if (node.type.name !== "table") continue;
112
+ const lists: string[] = [];
113
+ node.forEach((row) => {
114
+ const each: unknown = row.attrs["each"];
115
+ if (typeof each === "string" && each !== "" && !lists.includes(each)) lists.push(each);
116
+ });
117
+ if (lists.length === 0) return null;
118
+ return {
119
+ lists,
120
+ headerRepeats: node.firstChild?.attrs["repeatHeader"] === true,
121
+ firstRowPos: $from.start(depth),
122
+ };
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function toggleRepeatHeader(editor: Editor): void {
128
+ const found = findRepeatTable(editor);
129
+ if (found === null) return;
130
+ editor
131
+ .chain()
132
+ .focus()
133
+ .command(({ tr }) => {
134
+ const row = tr.doc.nodeAt(found.firstRowPos);
135
+ if (row === null) return false;
136
+ tr.setNodeMarkup(found.firstRowPos, undefined, {
137
+ ...row.attrs,
138
+ repeatHeader: !found.headerRepeats,
139
+ });
140
+ return true;
141
+ })
142
+ .run();
143
+ }
144
+
145
+ const PARAGRAPH_STYLES = ["paragraph", "1", "2", "3"] as const;
146
+ type ParagraphStyle = (typeof PARAGRAPH_STYLES)[number];
147
+
148
+ const HEADING_LEVELS = { "1": 1, "2": 2, "3": 3 } as const;
149
+
150
+ const PLACEHOLDER_HREF = /^\{\{([^{}]+)\}\}$/;
151
+
152
+ function buttonTargetLabel(href: string, labels: Readonly<Record<string, string>>): string {
153
+ const name = PLACEHOLDER_HREF.exec(href)?.[1];
154
+ if (name === undefined) return href;
155
+ return Object.hasOwn(labels, name) ? (labels[name] ?? name) : name;
156
+ }
157
+
158
+ // The bubble menu registers its plugin once per mount, so a rebuilt editor needs a fresh mount.
159
+ const editorMountKeys = new WeakMap<Editor, number>();
160
+ let lastMountKey = 0;
161
+ function mountKeyOf(editor: Editor): number {
162
+ const known = editorMountKeys.get(editor);
163
+ if (known !== undefined) return known;
164
+ lastMountKey += 1;
165
+ const created = lastMountKey;
166
+ editorMountKeys.set(editor, created);
167
+ return created;
168
+ }
169
+
170
+ // Board .hb: a small bar that straddles the block's top frame line on the right; above a table it sits fully
171
+ // outside so the header row stays readable.
172
+ const BUBBLE_BAR_CLASS =
173
+ "flex w-max items-center gap-1 rounded border border-input bg-popover p-px text-xs [&_button]:h-5 [&_button]:px-1.5 [&_button]:text-[10px]";
174
+
175
+ function bubbleOffset(editor: Editor): (args: { rects: { floating: { height: number } } }) => {
176
+ mainAxis: number;
177
+ crossAxis: number;
178
+ } {
179
+ return ({ rects }) => ({
180
+ mainAxis: findRepeatTable(editor) !== null ? 2 : -rects.floating.height / 2,
181
+ crossAxis: -8,
182
+ });
183
+ }
184
+
185
+ const BUBBLE_MENU_PLUGIN_KEY = "documentBlockOptions";
186
+
187
+ // The options bar belongs to the block frame, not to the caret inside it.
188
+ function selectedBlockReference(editor: Editor): {
189
+ getBoundingClientRect: () => DOMRect;
190
+ getClientRects: () => DOMRect[];
191
+ contextElement: Element;
192
+ } | null {
193
+ const { node } = editor.view.domAtPos(editor.state.selection.from);
194
+ const start = node instanceof Element ? node : node.parentElement;
195
+ const block = start?.closest(`[${DOCUMENT_BLOCK_ATTRIBUTE}], table`);
196
+ if (block === null || block === undefined) return null;
197
+ return {
198
+ getBoundingClientRect: () => block.getBoundingClientRect(),
199
+ getClientRects: () => [block.getBoundingClientRect()],
200
+ contextElement: block,
201
+ };
202
+ }
203
+
204
+ // The bubble is first measured before React has filled it, so it sits at the wrong place until a second pass.
205
+ function repositionBubble(editor: Editor): void {
206
+ if (editor.isDestroyed) return;
207
+ editor.view.dispatch(editor.state.tr.setMeta(BUBBLE_MENU_PLUGIN_KEY, "updatePosition"));
208
+ }
209
+
210
+ function isParagraphStyle(value: string): value is ParagraphStyle {
211
+ return isOneOf(PARAGRAPH_STYLES, value);
212
+ }
213
+
214
+ function emptyParagraph(): JSONContent {
215
+ return { type: "paragraph" };
216
+ }
217
+
218
+ function repeatTableContent(listName: string): JSONContent {
219
+ const headerRow: JSONContent = {
220
+ type: "tableRow",
221
+ content: [
222
+ { type: "tableHeader", content: [emptyParagraph()] },
223
+ { type: "tableHeader", content: [emptyParagraph()] },
224
+ ],
225
+ };
226
+ const repeatRow: JSONContent = {
227
+ type: "tableRow",
228
+ attrs: { each: listName },
229
+ content: [
230
+ { type: "tableCell", content: [emptyParagraph()] },
231
+ { type: "tableCell", content: [emptyParagraph()] },
232
+ ],
233
+ };
234
+ return { type: "table", content: [headerRow, repeatRow] };
235
+ }
236
+
237
+ function Separator({ compact = false }: { readonly compact?: boolean }): ReactNode {
238
+ return (
239
+ <span aria-hidden="true" className={cn("h-5 w-px bg-border", compact ? "mx-0.5" : "mx-2")} />
240
+ );
241
+ }
242
+
243
+ function PanelActions({
244
+ onInsert,
245
+ onRemoveLink,
246
+ onCancel,
247
+ }: {
248
+ readonly onInsert?: () => void;
249
+ readonly onRemoveLink?: () => void;
250
+ readonly onCancel: () => void;
251
+ }): ReactNode {
252
+ const { Button } = usePrimitives();
253
+ const t = useTranslation();
254
+ return (
255
+ <div className="flex gap-2">
256
+ {onInsert && (
257
+ <Button type="button" size="sm" onClick={onInsert}>
258
+ {t("template-resolver.documentEditor.insert")}
259
+ </Button>
260
+ )}
261
+ {onRemoveLink && (
262
+ <Button type="button" size="sm" variant="danger-ghost" onClick={onRemoveLink}>
263
+ {t("template-resolver.documentEditor.removeLink")}
264
+ </Button>
265
+ )}
266
+ <Button type="button" size="sm" variant="secondary" onClick={onCancel}>
267
+ {t("template-resolver.documentEditor.cancel")}
268
+ </Button>
269
+ </div>
270
+ );
271
+ }
272
+
273
+ type OptionSelectProps = {
274
+ readonly id: string;
275
+ readonly label: string;
276
+ readonly names: readonly string[];
277
+ readonly value: string;
278
+ readonly onChange: (name: string) => void;
279
+ };
280
+
281
+ function NameSelect({ id, label, names, value, onChange }: OptionSelectProps): ReactNode {
282
+ const { Field, Input } = usePrimitives();
283
+ return (
284
+ <Field id={id} label={label}>
285
+ <Input
286
+ kind="select"
287
+ id={id}
288
+ name={id}
289
+ value={value}
290
+ onChange={onChange}
291
+ display="dropdown"
292
+ options={names.map((name) => ({ value: name, label: name }))}
293
+ />
294
+ </Field>
295
+ );
296
+ }
297
+
298
+ type PanelProps = {
299
+ readonly editor: Editor;
300
+ readonly editorId: string;
301
+ readonly kind: PanelKind;
302
+ readonly variables: readonly string[];
303
+ readonly onClose: () => void;
304
+ };
305
+
306
+ function InsertPanel({ editor, editorId, kind, variables, onClose }: PanelProps): ReactNode {
307
+ const { Button, Field, Input } = usePrimitives();
308
+ const t = useTranslation();
309
+ const [href, setHref] = useState(() => {
310
+ const current = editor.getAttributes("link")["href"];
311
+ return typeof current === "string" ? current : "";
312
+ });
313
+ const [resource, setResource] = useState("");
314
+ const [alt, setAlt] = useState("");
315
+ const [buttonText, setButtonText] = useState(() =>
316
+ t("template-resolver.documentEditor.buttonDefaultText"),
317
+ );
318
+ const [chosenName, setChosenName] = useState("");
319
+ const [error, setError] = useState<string | null>(null);
320
+
321
+ const referenceNames = variables.filter(isDocumentReferenceName);
322
+ const buttonTargets = referenceNames.filter((name) => isDocumentPlaceholderHref(`{{${name}}}`));
323
+ const selectable = kind === "button" ? buttonTargets : referenceNames;
324
+ const selectedName = selectable.includes(chosenName) ? chosenName : (selectable[0] ?? "");
325
+
326
+ const insertAndClose = (content: JSONContent): void => {
327
+ editor.chain().focus().insertContent(content).run();
328
+ onClose();
329
+ };
330
+
331
+ const submitLink = (): void => {
332
+ if (!isAllowedDocumentLinkHref(href)) {
333
+ setError(t("template-resolver.documentEditor.invalidLink"));
334
+ return;
335
+ }
336
+ const chain = editor.chain().focus();
337
+ if (!editor.state.selection.empty) chain.setLink({ href }).run();
338
+ else if (editor.isActive("link")) chain.extendMarkRange("link").setLink({ href }).run();
339
+ else if (isDocumentPlaceholderHref(href))
340
+ // Placeholders are never inserted as {{name}} text; the link shows the variable's value.
341
+ chain
342
+ .insertContent({
343
+ type: "variable",
344
+ attrs: { name: href.slice(2, -2) },
345
+ marks: [{ type: "link", attrs: { href } }],
346
+ })
347
+ .run();
348
+ else
349
+ chain
350
+ .insertContent({ type: "text", text: href, marks: [{ type: "link", attrs: { href } }] })
351
+ .run();
352
+ onClose();
353
+ };
354
+
355
+ const submitImage = (): void => {
356
+ if (!isDocumentResourceKey(resource)) {
357
+ setError(t("template-resolver.documentEditor.invalidResourceKey"));
358
+ return;
359
+ }
360
+ if (/[<>]/.test(alt)) {
361
+ setError(t("template-resolver.documentEditor.invalidImageAlt"));
362
+ return;
363
+ }
364
+ insertAndClose({ type: "resource", attrs: { resource, alt: alt || null } });
365
+ };
366
+
367
+ const needsVariables = kind === "button" || kind === "repeat" || kind === "placeholder";
368
+ const noVariables = needsVariables && selectable.length === 0;
369
+ const idPrefix = `${editorId}-panel`;
370
+
371
+ return (
372
+ <div className="flex flex-col gap-2 border-b border-input bg-muted/40 p-2">
373
+ {noVariables && (
374
+ <p className="text-sm text-muted-foreground">
375
+ {t("template-resolver.documentEditor.noVariables")}
376
+ </p>
377
+ )}
378
+ {kind === "link" && (
379
+ <Field id={`${idPrefix}-href`} label={t("template-resolver.documentEditor.linkHref")}>
380
+ <Input
381
+ kind="text"
382
+ id={`${idPrefix}-href`}
383
+ name="href"
384
+ value={href}
385
+ onChange={setHref}
386
+ placeholder={t("template-resolver.documentEditor.linkHrefPlaceholder")}
387
+ />
388
+ </Field>
389
+ )}
390
+ {kind === "placeholder" && !noVariables && (
391
+ <div className="flex flex-wrap gap-1">
392
+ <VariableChips
393
+ variables={selectable}
394
+ onInsert={(name) => insertAndClose({ type: "variable", attrs: { name } })}
395
+ />
396
+ </div>
397
+ )}
398
+ {kind === "align" && (
399
+ <div className="flex flex-wrap gap-1">
400
+ {DOCUMENT_TEXT_ALIGNMENTS.map((alignment) => (
401
+ <Button
402
+ key={alignment}
403
+ type="button"
404
+ variant="secondary"
405
+ size="sm"
406
+ onClick={() => {
407
+ editor
408
+ .chain()
409
+ .focus()
410
+ .updateAttributes("paragraph", { textAlign: alignment })
411
+ .updateAttributes("heading", { textAlign: alignment })
412
+ .run();
413
+ onClose();
414
+ }}
415
+ >
416
+ {ALIGNMENT_ICONS[alignment]}
417
+ {t(ALIGNMENT_LABEL_KEYS[alignment])}
418
+ </Button>
419
+ ))}
420
+ </div>
421
+ )}
422
+ {kind === "image" && (
423
+ <>
424
+ <Field
425
+ id={`${idPrefix}-resource`}
426
+ label={t("template-resolver.documentEditor.resourceKey")}
427
+ >
428
+ <Input
429
+ kind="text"
430
+ id={`${idPrefix}-resource`}
431
+ name="resource"
432
+ value={resource}
433
+ onChange={setResource}
434
+ />
435
+ </Field>
436
+ <Field id={`${idPrefix}-alt`} label={t("template-resolver.documentEditor.imageAlt")}>
437
+ <Input kind="text" id={`${idPrefix}-alt`} name="alt" value={alt} onChange={setAlt} />
438
+ </Field>
439
+ </>
440
+ )}
441
+ {kind === "button" && !noVariables && (
442
+ <>
443
+ <NameSelect
444
+ id={`${idPrefix}-target`}
445
+ label={t("template-resolver.documentEditor.buttonTarget")}
446
+ names={selectable}
447
+ value={selectedName}
448
+ onChange={setChosenName}
449
+ />
450
+ <Field id={`${idPrefix}-text`} label={t("template-resolver.documentEditor.buttonText")}>
451
+ <Input
452
+ kind="text"
453
+ id={`${idPrefix}-text`}
454
+ name="text"
455
+ value={buttonText}
456
+ onChange={setButtonText}
457
+ />
458
+ </Field>
459
+ </>
460
+ )}
461
+ {kind === "repeat" && !noVariables && (
462
+ <NameSelect
463
+ id={`${idPrefix}-list`}
464
+ label={t("template-resolver.documentEditor.repeatList")}
465
+ names={selectable}
466
+ value={selectedName}
467
+ onChange={setChosenName}
468
+ />
469
+ )}
470
+ {error !== null && (
471
+ <p role="alert" className="text-sm text-destructive">
472
+ {error}
473
+ </p>
474
+ )}
475
+ <div className="flex flex-wrap gap-2">
476
+ <PanelActions
477
+ onCancel={onClose}
478
+ {...(kind === "link" && { onInsert: submitLink })}
479
+ {...(kind === "link" &&
480
+ editor.isActive("link") && {
481
+ onRemoveLink: () => {
482
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
483
+ onClose();
484
+ },
485
+ })}
486
+ {...(kind === "image" && { onInsert: submitImage })}
487
+ {...(kind === "button" &&
488
+ !noVariables && {
489
+ onInsert: () =>
490
+ insertAndClose({
491
+ type: "button",
492
+ attrs: { href: `{{${selectedName}}}` },
493
+ content: buttonText ? [{ type: "text", text: buttonText }] : [],
494
+ }),
495
+ })}
496
+ {...(kind === "repeat" &&
497
+ !noVariables && { onInsert: () => insertAndClose(repeatTableContent(selectedName)) })}
498
+ />
499
+ </div>
500
+ </div>
501
+ );
502
+ }
503
+
504
+ export default function DocumentTiptapEditor({
505
+ id,
506
+ value,
507
+ onChange,
508
+ variables,
509
+ readOnly,
510
+ blocks = MAIL_DOCUMENT_BLOCKS,
511
+ variableLabels = NO_LABELS,
512
+ systemVariables = NO_NAMES,
513
+ resourcePreviews = NO_PREVIEWS,
514
+ chrome = "default",
515
+ toolbar = "full",
516
+ renderPaper,
517
+ toolbarHost,
518
+ onEditorReady,
519
+ }: DocumentEditorProps): ReactNode {
520
+ const t = useTranslation();
521
+ const { ActionOverflowMenu, Button, Field, Input } = usePrimitives();
522
+ const [openPanel, setOpenPanel] = useState<PanelKind | null>(null);
523
+ const [canvasScroller, setCanvasScroller] = useState<HTMLElement | null>(null);
524
+ const [showHtml, setShowHtml] = useState(false);
525
+ const canvas = chrome === "canvas";
526
+ const compact = toolbar === "compact" && !canvas;
527
+ const hosted = toolbarHost !== undefined;
528
+
529
+ const placeholderLabel = t("template-resolver.documentEditor.placeholder");
530
+ const imageLabel = t("template-resolver.documentEditor.image");
531
+ const imageBlockLabel = t("template-resolver.documentEditor.imageBlock");
532
+ const pageBreakLabel = t("template-resolver.documentEditor.pageBreak");
533
+ const buttonLabel = t("template-resolver.documentEditor.button");
534
+ const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
535
+ const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
536
+ const stablePreviews = useStableByKey(resourcePreviews, previewsKey(resourcePreviews));
537
+ const extensions = useMemo(
538
+ () =>
539
+ createDocumentEditorExtensions({
540
+ placeholder: placeholderLabel,
541
+ image: imageLabel,
542
+ pageBreak: pageBreakLabel,
543
+ button: buttonLabel,
544
+ imageBlock: imageBlockLabel,
545
+ variableLabels: stableLabels,
546
+ systemVariables: stableSystem,
547
+ resourcePreviews: stablePreviews,
548
+ blockLabels: canvas,
549
+ }),
550
+ [
551
+ placeholderLabel,
552
+ imageLabel,
553
+ imageBlockLabel,
554
+ pageBreakLabel,
555
+ buttonLabel,
556
+ stableLabels,
557
+ stableSystem,
558
+ stablePreviews,
559
+ canvas,
560
+ ],
561
+ );
562
+
563
+ // The node views are built from labels and previews, so a changed set
564
+ // rebuilds the editor; its content is re-read from `value`.
565
+ const editor = useEditor(
566
+ {
567
+ extensions,
568
+ content: value,
569
+ editable: !readOnly,
570
+ // Suspense (DocumentContentEditor's lazy boundary) speculatively mounts
571
+ // this component before committing it; immediate render would create an
572
+ // editor instance during that throwaway pass.
573
+ immediatelyRender: false,
574
+ editorProps: { attributes: { id } },
575
+ onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
576
+ onUpdate: ({ editor: e }) => onChange(e.getHTML()),
577
+ },
578
+ [extensions],
579
+ );
580
+
581
+ // @tiptap/react v3 does not re-render on selection-only transactions, so
582
+ // active states must come from a subscription.
583
+ const activeState = useEditorState({
584
+ editor,
585
+ selector: ({ editor: e }) =>
586
+ e === null
587
+ ? null
588
+ : {
589
+ blockStyle: e.isActive("heading", { level: 1 })
590
+ ? "1"
591
+ : e.isActive("heading", { level: 2 })
592
+ ? "2"
593
+ : e.isActive("heading", { level: 3 })
594
+ ? "3"
595
+ : "paragraph",
596
+ bold: e.isActive("bold"),
597
+ italic: e.isActive("italic"),
598
+ underline: e.isActive("underline"),
599
+ selectedBlock: e.isActive("resource")
600
+ ? {
601
+ type: "resource" as const,
602
+ detail: String(e.getAttributes("resource")["resource"]),
603
+ }
604
+ : e.isActive("button")
605
+ ? { type: "button" as const, detail: String(e.getAttributes("button")["href"]) }
606
+ : null,
607
+ html: showHtml ? e.getHTML() : "",
608
+ bulletList: e.isActive("bulletList"),
609
+ orderedList: e.isActive("orderedList"),
610
+ link: e.isActive("link"),
611
+ repeatTable: findRepeatTable(e),
612
+ },
613
+ });
614
+
615
+ // The entry loads asynchronously, so `value` arrives after useEditor read
616
+ // its initial content; the guard keeps in-flight typing from being clobbered.
617
+ // kumiko-lint-ignore no-raw-hooks DOM integration: syncs the controlled value into the imperative tiptap editor
618
+ useEffect(() => {
619
+ if (editor && value !== editor.getHTML()) {
620
+ editor.commands.setContent(value, { emitUpdate: false });
621
+ }
622
+ }, [value, editor]);
623
+
624
+ // kumiko-lint-ignore no-raw-hooks DOM integration: tiptap exposes editability only imperatively via setEditable
625
+ useEffect(() => {
626
+ editor?.setEditable(!readOnly);
627
+ }, [readOnly, editor]);
628
+
629
+ if (!editor)
630
+ return (
631
+ <TextareaContentEditor
632
+ id={id}
633
+ value={value}
634
+ onChange={onChange}
635
+ variables={variables}
636
+ readOnly={readOnly}
637
+ />
638
+ );
639
+
640
+ const togglePanel = (kind: PanelKind): void =>
641
+ setOpenPanel((current) => (current === kind ? null : kind));
642
+
643
+ const setBlockStyle = (style: string): void => {
644
+ if (!isParagraphStyle(style)) return;
645
+ const chain = editor.chain().focus();
646
+ if (style === "paragraph") chain.setParagraph().run();
647
+ else chain.setHeading({ level: HEADING_LEVELS[style] }).run();
648
+ };
649
+
650
+ const runBlock = (block: DocumentToolbarBlock): void => {
651
+ const panel = BLOCK_PANEL[block];
652
+ if (panel) {
653
+ togglePanel(panel);
654
+ return;
655
+ }
656
+ const chain = editor.chain().focus();
657
+ if (block === "table") chain.insertTable({ rows: 3, cols: 2, withHeaderRow: true }).run();
658
+ else if (block === "divider") chain.insertContent({ type: "divider" }).run();
659
+ else if (block === "pageBreak") chain.insertContent({ type: "pageBreak" }).run();
660
+ };
661
+
662
+ const blockButtons: Record<DocumentToolbarBlock, { label: string; icon: ReactNode }> = {
663
+ image: { label: imageLabel, icon: <ImageIcon size={16} /> },
664
+ table: { label: t("template-resolver.documentEditor.table"), icon: <TableIcon size={16} /> },
665
+ button: {
666
+ label: t("template-resolver.documentEditor.button"),
667
+ icon: <MousePointerClick size={16} />,
668
+ },
669
+ divider: { label: t("template-resolver.documentEditor.divider"), icon: <Minus size={16} /> },
670
+ repeat: { label: t("template-resolver.documentEditor.repeat"), icon: <Repeat size={16} /> },
671
+ pageBreak: { label: pageBreakLabel, icon: <Scissors size={16} /> },
672
+ align: { label: t("template-resolver.documentEditor.align"), icon: <AlignLeft size={16} /> },
673
+ };
674
+
675
+ const repeatTable = activeState?.repeatTable ?? null;
676
+ const repeatListLabel =
677
+ repeatTable !== null && repeatTable.lists.length === 1
678
+ ? (variableLabels[repeatTable.lists[0] ?? ""] ?? repeatTable.lists[0])
679
+ : undefined;
680
+
681
+ const repeatBarLabel =
682
+ repeatListLabel === undefined
683
+ ? t("template-resolver.documentEditor.repeat")
684
+ : t("template-resolver.documentEditor.repeatBarList", { list: repeatListLabel });
685
+
686
+ const paragraphStyleLabel = t("template-resolver.documentEditor.paragraphStyle");
687
+ const styleOptions = [
688
+ { value: "paragraph", label: t("template-resolver.documentEditor.paragraph") },
689
+ { value: "1", label: t("kumiko.contentEditor.heading1") },
690
+ { value: "2", label: t("kumiko.contentEditor.heading2") },
691
+ { value: "3", label: t("template-resolver.documentEditor.heading3") },
692
+ ];
693
+
694
+ // Canvas chrome is one flat row: unframed ghost buttons, glyph plus text for the longer names.
695
+ const iconButton = (
696
+ label: string,
697
+ icon: ReactNode,
698
+ isActive: boolean,
699
+ onClick: () => void,
700
+ text?: string,
701
+ ): ReactNode =>
702
+ canvas ? (
703
+ <Button
704
+ type="button"
705
+ variant="ghost"
706
+ size="sm"
707
+ pressed={isActive}
708
+ disabled={readOnly}
709
+ onClick={onClick}
710
+ ariaLabel={label}
711
+ title={label}
712
+ >
713
+ {icon}
714
+ {text}
715
+ </Button>
716
+ ) : (
717
+ <Button
718
+ type="button"
719
+ variant={isActive ? "primary" : "secondary"}
720
+ size="icon"
721
+ pressed={isActive}
722
+ disabled={readOnly}
723
+ onClick={onClick}
724
+ ariaLabel={label}
725
+ title={label}
726
+ >
727
+ {icon}
728
+ </Button>
729
+ );
730
+
731
+ const markContent = (letter: string, icon: ReactNode, className: string): ReactNode =>
732
+ canvas ? <span className={className}>{letter}</span> : icon;
733
+ const selectedBlock = activeState?.selectedBlock ?? null;
734
+
735
+ const flatVariant = canvas ? "ghost" : "secondary";
736
+
737
+ const labelledButton = (spec: {
738
+ readonly key?: string;
739
+ readonly label: string;
740
+ readonly icon: ReactNode;
741
+ readonly showIcon: boolean;
742
+ readonly trailingIcon?: ReactNode;
743
+ readonly expanded?: boolean;
744
+ readonly onClick: () => void;
745
+ }): ReactNode => {
746
+ const shared = {
747
+ type: "button",
748
+ variant: flatVariant,
749
+ disabled: readOnly,
750
+ onClick: spec.onClick,
751
+ ...(spec.expanded !== undefined && { expanded: spec.expanded }),
752
+ } as const;
753
+ return compact ? (
754
+ <Button key={spec.key} {...shared} size="icon" ariaLabel={spec.label} title={spec.label}>
755
+ {spec.icon}
756
+ </Button>
757
+ ) : (
758
+ <Button key={spec.key} {...shared} size="sm">
759
+ {spec.showIcon && spec.icon}
760
+ {spec.label}
761
+ {spec.trailingIcon}
762
+ </Button>
763
+ );
764
+ };
765
+
766
+ const editorContent = (
767
+ <EditorContent
768
+ editor={editor}
769
+ className={cn(
770
+ !canvas && "min-h-40 px-3 py-2 text-base outline-none md:text-sm",
771
+ canvas && "text-sm",
772
+ "[&_.ProseMirror-focused_.ProseMirror-selectednode]:ring-2 [&_.ProseMirror-focused_.ProseMirror-selectednode]:ring-primary",
773
+ hosted
774
+ ? "[&_.ProseMirror]:outline-none"
775
+ : "[&_.ProseMirror]:min-h-40 [&_.ProseMirror]:outline-none",
776
+ "[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-6 [&_ol]:pl-6",
777
+ "[&_h1]:text-xl [&_h1]:font-bold [&_h2]:text-lg [&_h2]:font-bold [&_h3]:text-base [&_h3]:font-bold",
778
+ "[&_a]:underline [&_a]:text-primary",
779
+ "[&_strong]:font-bold [&_em]:italic [&_u]:underline",
780
+ canvas
781
+ ? "[&_table]:mb-2 [&_table]:mt-6 [&_.ProseMirror>:first-child_table]:mt-0 [&_.ProseMirror>table:first-child]:mt-0 [&_td:last-child>p[data-size=small][data-align=right]]:whitespace-nowrap"
782
+ : "[&_table]:my-2",
783
+ "[&_table]:w-full [&_table]:border-collapse",
784
+ "[&_td]:border-b [&_td]:border-border [&_td]:px-2 [&_td]:py-1.5 [&_td>p]:m-0 [&_th>p]:m-0",
785
+ "[&_th]:border-b [&_th]:border-border [&_th]:px-2 [&_th]:py-1.5 [&_th]:text-left [&_th]:text-xs [&_th]:font-medium [&_th]:text-muted-foreground",
786
+ "[&_tr[data-each]>td]:bg-muted/50",
787
+ "[&_[data-align=left]]:text-left [&_[data-align=center]]:text-center [&_[data-align=right]]:text-right [&_[data-align=justify]]:text-justify",
788
+ "[&_p[data-size=small]]:text-xs [&_p[data-size=small]]:text-muted-foreground",
789
+ "[&_a[data-button]]:inline-block [&_a[data-button]]:rounded-full [&_a[data-button]]:bg-primary [&_a[data-button]]:px-4 [&_a[data-button]]:py-1.5 [&_a[data-button]]:text-primary-foreground [&_a[data-button]]:no-underline",
790
+ "[&_hr]:my-3 [&_hr]:border-input",
791
+ "[&_.is-editor-empty]:before:pointer-events-none [&_.is-editor-empty]:before:float-left [&_.is-editor-empty]:before:h-0 [&_.is-editor-empty]:before:text-muted-foreground [&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
792
+ )}
793
+ />
794
+ );
795
+
796
+ // The canvas toolbar never scrolls sideways: the rarer blocks and the HTML view sit in the menu.
797
+ const menuBlocks =
798
+ canvas && ActionOverflowMenu !== undefined
799
+ ? blocks.filter((block) => !CANVAS_INLINE_BLOCKS.includes(block))
800
+ : [];
801
+ const inlineBlocks = blocks.filter((block) => !menuBlocks.includes(block));
802
+ const overflowItems: readonly ActionMenuItemSpec[] = [
803
+ ...menuBlocks.map((block) => ({
804
+ id: block,
805
+ label: blockButtons[block].label,
806
+ disabled: readOnly,
807
+ onSelect: () => runBlock(block),
808
+ })),
809
+ ...(canvas && !hosted
810
+ ? [
811
+ {
812
+ id: "html",
813
+ label: t("template-resolver.documentEditor.showHtml"),
814
+ onSelect: () => setShowHtml((current) => !current),
815
+ },
816
+ ]
817
+ : []),
818
+ ];
819
+
820
+ const toolbarBar = (
821
+ <div
822
+ className={cn(
823
+ "flex items-center border-b border-input",
824
+ canvas
825
+ ? "min-h-9 flex-nowrap gap-0 overflow-hidden bg-background px-1.5 [&>*]:shrink-0 [&_button]:h-7 [&_button]:gap-1 [&_button]:px-1 [&_button]:text-xs"
826
+ : "flex-wrap gap-1 p-1",
827
+ )}
828
+ >
829
+ <div
830
+ // kumiko-lint-ignore raw-classname the toolbar paragraph select sits flush in the editor toolbar; ComboboxInput has no flat trigger variant
831
+ className={cn(
832
+ !canvas && (compact ? "w-24" : "w-40"),
833
+ canvas &&
834
+ "w-auto shrink-0 [&_button[aria-haspopup=listbox]]:border-transparent [&_button[aria-haspopup=listbox]]:bg-transparent [&_button[aria-haspopup=listbox]]:shadow-none",
835
+ )}
836
+ >
837
+ <Field id={`${id}-paragraph-style`} label={paragraphStyleLabel} hideLabel>
838
+ <Input
839
+ kind="select"
840
+ id={`${id}-paragraph-style`}
841
+ name="paragraph-style"
842
+ value={activeState?.blockStyle ?? "paragraph"}
843
+ onChange={setBlockStyle}
844
+ options={styleOptions}
845
+ display="dropdown"
846
+ disabled={readOnly}
847
+ />
848
+ </Field>
849
+ </div>
850
+ <Separator compact={compact || canvas} />
851
+ {iconButton(
852
+ t("kumiko.contentEditor.bold"),
853
+ markContent("B", <BoldIcon size={16} />, "font-bold"),
854
+ activeState?.bold ?? false,
855
+ () => editor.chain().focus().toggleBold().run(),
856
+ )}
857
+ {iconButton(
858
+ t("kumiko.contentEditor.italic"),
859
+ markContent("I", <ItalicIcon size={16} />, "italic"),
860
+ activeState?.italic ?? false,
861
+ () => editor.chain().focus().toggleItalic().run(),
862
+ )}
863
+ {iconButton(
864
+ t("template-resolver.documentEditor.underline"),
865
+ markContent("U", <UnderlineIcon size={16} />, "underline"),
866
+ activeState?.underline ?? false,
867
+ () => editor.chain().focus().toggleUnderline().run(),
868
+ )}
869
+ <Separator compact={compact || canvas} />
870
+ {iconButton(
871
+ t("kumiko.contentEditor.bulletList"),
872
+ <List size={16} />,
873
+ activeState?.bulletList ?? false,
874
+ () => editor.chain().focus().toggleBulletList().run(),
875
+ canvas ? undefined : t("template-resolver.documentEditor.list"),
876
+ )}
877
+ {iconButton(
878
+ t("kumiko.contentEditor.orderedList"),
879
+ <ListOrdered size={16} />,
880
+ activeState?.orderedList ?? false,
881
+ () => editor.chain().focus().toggleOrderedList().run(),
882
+ )}
883
+ {iconButton(
884
+ t("template-resolver.documentEditor.link"),
885
+ <LinkIcon size={16} />,
886
+ activeState?.link ?? false,
887
+ () => togglePanel("link"),
888
+ )}
889
+ <Separator compact={compact || canvas} />
890
+ {labelledButton({
891
+ label: t("template-resolver.documentEditor.insertPlaceholder"),
892
+ icon: <Braces size={16} />,
893
+ showIcon: true,
894
+ trailingIcon: canvas ? <ChevronDown size={12} /> : undefined,
895
+ expanded: openPanel === "placeholder",
896
+ onClick: () => togglePanel("placeholder"),
897
+ })}
898
+ {inlineBlocks.length > 0 && <Separator compact={compact || canvas} />}
899
+ {inlineBlocks.map((block) =>
900
+ canvas ? (
901
+ <Fragment key={block}>
902
+ {iconButton(blockButtons[block].label, blockButtons[block].icon, false, () =>
903
+ runBlock(block),
904
+ )}
905
+ </Fragment>
906
+ ) : (
907
+ labelledButton({
908
+ key: block,
909
+ label: blockButtons[block].label,
910
+ icon: blockButtons[block].icon,
911
+ showIcon: true,
912
+ onClick: () => runBlock(block),
913
+ })
914
+ ),
915
+ )}
916
+ {overflowItems.length > 0 && ActionOverflowMenu !== undefined && (
917
+ <span className="ml-auto shrink-0">
918
+ <ActionOverflowMenu
919
+ items={overflowItems}
920
+ label={t("template-resolver.documentEditor.more")}
921
+ testId={`${id}-toolbar-more`}
922
+ />
923
+ </span>
924
+ )}
925
+ </div>
926
+ );
927
+
928
+ const bubbleMenu = (
929
+ <BubbleMenu
930
+ key={mountKeyOf(editor)}
931
+ pluginKey={BUBBLE_MENU_PLUGIN_KEY}
932
+ editor={editor}
933
+ options={{
934
+ strategy: "fixed",
935
+ placement: "top-end",
936
+ offset: bubbleOffset(editor),
937
+ scrollTarget: canvasScroller ?? window,
938
+ onShow: () => requestAnimationFrame(() => repositionBubble(editor)),
939
+ }}
940
+ getReferencedVirtualElement={() => selectedBlockReference(editor)}
941
+ shouldShow={({ editor: e, element }) =>
942
+ // A leading image is selected by default; the bubble belongs to a deliberate selection.
943
+ (e.isFocused || element.contains(document.activeElement)) &&
944
+ (e.isActive("resource") || e.isActive("button") || findRepeatTable(e) !== null)
945
+ }
946
+ >
947
+ {selectedBlock !== null ? (
948
+ <div className={BUBBLE_BAR_CLASS} data-testid="document-block-options">
949
+ <span className="px-1.5 text-[10px] text-muted-foreground">
950
+ {selectedBlock.type === "resource"
951
+ ? t("template-resolver.documentEditor.resourceKey")
952
+ : t("template-resolver.documentEditor.buttonTarget")}
953
+ {": "}
954
+ {selectedBlock.type === "button" ? (
955
+ <strong>{buttonTargetLabel(selectedBlock.detail, variableLabels)}</strong>
956
+ ) : (
957
+ <code>{selectedBlock.detail}</code>
958
+ )}
959
+ </span>
960
+ <Button
961
+ type="button"
962
+ size="sm"
963
+ variant="danger-ghost"
964
+ onClick={() => {
965
+ editor.chain().focus().deleteNode(selectedBlock.type).run();
966
+ }}
967
+ >
968
+ {t("template-resolver.documentEditor.removeBlock")}
969
+ </Button>
970
+ </div>
971
+ ) : (
972
+ repeatTable !== null && (
973
+ <div data-testid="document-repeat-bar" className={BUBBLE_BAR_CLASS}>
974
+ <span className="px-1.5 text-[10px] uppercase tracking-wide text-muted-foreground">
975
+ {repeatBarLabel}
976
+ </span>
977
+ <Button
978
+ type="button"
979
+ variant="secondary"
980
+ size="sm"
981
+ pressed={repeatTable.headerRepeats}
982
+ onClick={() => toggleRepeatHeader(editor)}
983
+ >
984
+ {t("template-resolver.documentEditor.repeatHeader")}
985
+ </Button>
986
+ </div>
987
+ )
988
+ )}
989
+ </BubbleMenu>
990
+ );
991
+
992
+ const insertPanel = openPanel !== null && !readOnly && (
993
+ <InsertPanel
994
+ key={openPanel}
995
+ editor={editor}
996
+ editorId={id}
997
+ kind={openPanel}
998
+ variables={variables}
999
+ onClose={() => setOpenPanel(null)}
1000
+ />
1001
+ );
1002
+
1003
+ if (hosted) {
1004
+ return (
1005
+ <>
1006
+ {toolbarHost !== null &&
1007
+ createPortal(
1008
+ <>
1009
+ {toolbarBar}
1010
+ {insertPanel}
1011
+ </>,
1012
+ toolbarHost,
1013
+ )}
1014
+ {!readOnly && bubbleMenu}
1015
+ {editorContent}
1016
+ </>
1017
+ );
1018
+ }
1019
+
1020
+ return (
1021
+ <div
1022
+ className={cn(
1023
+ canvas ? "flex min-h-0 flex-1 flex-col" : "rounded-md border border-input bg-transparent",
1024
+ )}
1025
+ >
1026
+ {!hosted && toolbarBar}
1027
+ {canvas && !readOnly && bubbleMenu}
1028
+ {canvas && !hosted && showHtml && (
1029
+ <section aria-label={t("template-resolver.documentEditor.htmlSource")}>
1030
+ <pre className="h-40 overflow-auto whitespace-pre-wrap break-all border-b border-input bg-muted p-2 font-mono text-xs">
1031
+ {activeState?.html ?? ""}
1032
+ </pre>
1033
+ </section>
1034
+ )}
1035
+ {!hosted && insertPanel}
1036
+ {repeatTable !== null && !readOnly && !canvas && (
1037
+ <div
1038
+ data-testid="document-repeat-bar"
1039
+ className="flex flex-wrap items-center gap-3 border-b border-input bg-muted/40 px-2 py-1 text-xs"
1040
+ >
1041
+ <span className="font-medium">
1042
+ {repeatListLabel === undefined
1043
+ ? t("template-resolver.documentEditor.repeat")
1044
+ : t("template-resolver.documentEditor.repeatBarList", { list: repeatListLabel })}
1045
+ </span>
1046
+ <Button
1047
+ type="button"
1048
+ variant="secondary"
1049
+ size="sm"
1050
+ pressed={repeatTable.headerRepeats}
1051
+ onClick={() => toggleRepeatHeader(editor)}
1052
+ >
1053
+ {t("template-resolver.documentEditor.repeatHeader")}
1054
+ </Button>
1055
+ </div>
1056
+ )}
1057
+ <div
1058
+ ref={setCanvasScroller}
1059
+ className={cn(canvas && "min-h-0 flex-1 overflow-auto bg-muted px-8 py-6")}
1060
+ >
1061
+ {canvas && renderPaper !== undefined ? renderPaper(editorContent) : editorContent}
1062
+ </div>
1063
+ </div>
1064
+ );
1065
+ }