@cosmicdrift/kumiko-samples 0.373.0 → 0.377.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 (207) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +6 -4
  3. package/packages/bundled-features/src/account-security/changes.json +6 -0
  4. package/packages/bundled-features/src/account-security/constants.ts +1 -0
  5. package/packages/bundled-features/src/account-security/index.ts +1 -0
  6. package/packages/bundled-features/src/admin-shell/web/client-plugin.tsx +1 -1
  7. package/packages/bundled-features/src/audit/web/client-plugin.tsx +1 -1
  8. package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
  9. package/packages/bundled-features/src/auth-email-password/i18n.ts +1 -0
  10. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +1 -1
  11. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +1 -1
  12. package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +1 -1
  13. package/packages/bundled-features/src/auth-email-password/web/client-actions.ts +36 -0
  14. package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +7 -4
  15. package/packages/bundled-features/src/auth-email-password/web/default-topbar-actions.tsx +1 -1
  16. package/packages/bundled-features/src/auth-email-password/web/index.ts +1 -1
  17. package/packages/bundled-features/src/auth-email-password/web/session.tsx +1 -1
  18. package/packages/bundled-features/src/auth-email-password/web/tenant-menu-items.tsx +1 -1
  19. package/packages/bundled-features/src/auth-email-password/web/tenant-switcher.tsx +1 -1
  20. package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
  21. package/packages/bundled-features/src/auth-foundation/changes.json +6 -0
  22. package/packages/bundled-features/src/auth-foundation/env.ts +27 -0
  23. package/packages/bundled-features/src/auth-foundation/index.ts +1 -0
  24. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  25. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
  26. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -1
  27. package/packages/bundled-features/src/billing-foundation/contact-card.ts +14 -0
  28. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +1 -1
  29. package/packages/bundled-features/src/billing-foundation/index.ts +1 -0
  30. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
  31. package/packages/bundled-features/src/billing-foundation/types.ts +12 -0
  32. package/packages/bundled-features/src/billing-foundation/validate-options.ts +16 -0
  33. package/packages/bundled-features/src/billing-foundation/web/billing-contact-plan.tsx +32 -0
  34. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +5 -15
  35. package/packages/bundled-features/src/billing-foundation/web/cancel-contract-dialog.tsx +2 -2
  36. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +1 -1
  37. package/packages/bundled-features/src/billing-foundation/web/client-plugin.tsx +1 -1
  38. package/packages/bundled-features/src/billing-foundation/web/localized-params.ts +14 -0
  39. package/packages/bundled-features/src/cap-overview/changes.json +7 -0
  40. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +15 -76
  41. package/packages/bundled-features/src/cap-overview/i18n.ts +0 -2
  42. package/packages/bundled-features/src/cap-overview/web/cap-cards-panel.tsx +3 -3
  43. package/packages/bundled-features/src/cap-overview/web/client-plugin.tsx +1 -1
  44. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  45. package/packages/bundled-features/src/channel-email/email-channel.ts +2 -1
  46. package/packages/bundled-features/src/compliance-profiles/web/client-plugin.tsx +1 -1
  47. package/packages/bundled-features/src/config/feature.ts +2 -2
  48. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  49. package/packages/bundled-features/src/custom-fields/web/client-plugin.tsx +1 -1
  50. package/packages/bundled-features/src/delivery/unsubscribe-page.ts +1 -1
  51. package/packages/bundled-features/src/delivery/web/client-plugin.tsx +1 -1
  52. package/packages/bundled-features/src/delivery/web/delivery-status-cell.tsx +1 -1
  53. package/packages/bundled-features/src/feature-toggles/web/client-plugin.tsx +1 -1
  54. package/packages/bundled-features/src/file-provider-s3/changes.json +6 -0
  55. package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +45 -0
  56. package/packages/bundled-features/src/file-provider-s3/env-schema.ts +20 -13
  57. package/packages/bundled-features/src/file-provider-s3/index.ts +4 -0
  58. package/packages/bundled-features/src/folders/web/client-plugin.tsx +1 -1
  59. package/packages/bundled-features/src/folders/web/folder-section.tsx +3 -4
  60. package/packages/bundled-features/src/host-routing/README.md +7 -0
  61. package/packages/bundled-features/src/host-routing/changes.json +6 -0
  62. package/packages/bundled-features/src/host-routing/constants.ts +4 -0
  63. package/packages/bundled-features/src/host-routing/dispatch.ts +34 -26
  64. package/packages/bundled-features/src/host-routing/feature.ts +122 -41
  65. package/packages/bundled-features/src/host-routing/host.ts +28 -1
  66. package/packages/bundled-features/src/host-routing/index.ts +3 -0
  67. package/packages/bundled-features/src/host-routing/options.ts +45 -2
  68. package/packages/bundled-features/src/host-routing/redirects.ts +33 -0
  69. package/packages/bundled-features/src/host-routing/tenant-lookup.ts +65 -0
  70. package/packages/bundled-features/src/host-routing/validate-options.ts +48 -1
  71. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
  72. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +8 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +42 -0
  74. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +4 -0
  75. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +100 -0
  76. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +2 -2
  77. package/packages/bundled-features/src/jobs/web/client-plugin.tsx +1 -1
  78. package/packages/bundled-features/src/legal-pages/changes.json +6 -0
  79. package/packages/bundled-features/src/legal-pages/feature.ts +14 -1
  80. package/packages/bundled-features/src/legal-pages/layout.ts +6 -6
  81. package/packages/bundled-features/src/legal-pages/web/client-plugin.ts +1 -1
  82. package/packages/bundled-features/src/mail-foundation/feature.ts +21 -22
  83. package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +1 -1
  84. package/packages/bundled-features/src/notes-history/web/client-plugin.tsx +1 -1
  85. package/packages/bundled-features/src/page-render/branding.ts +1 -1
  86. package/packages/bundled-features/src/page-render/layout.ts +2 -2
  87. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  88. package/packages/bundled-features/src/personal-access-tokens/i18n.ts +1 -1
  89. package/packages/bundled-features/src/personal-access-tokens/screens.ts +1 -1
  90. package/packages/bundled-features/src/secrets/feature.ts +4 -4
  91. package/packages/bundled-features/src/seo/sitemap.ts +1 -1
  92. package/packages/bundled-features/src/shared/mail-template-body.ts +1 -1
  93. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +127 -2
  94. package/packages/bundled-features/src/tags/web/client-plugin.tsx +1 -1
  95. package/packages/bundled-features/src/template-resolver/changes.json +18 -0
  96. package/packages/bundled-features/src/template-resolver/document-blocks.ts +34 -0
  97. package/packages/bundled-features/src/template-resolver/document-ops.ts +515 -0
  98. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +9 -0
  99. package/packages/bundled-features/src/template-resolver/feature.ts +4 -0
  100. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +1 -0
  101. package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +86 -0
  102. package/packages/bundled-features/src/template-resolver/handlers/document-blocks.query.ts +48 -0
  103. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +11 -3
  104. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +125 -0
  105. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +2 -1
  106. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +2 -1
  107. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +22 -1
  108. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +24 -9
  109. package/packages/bundled-features/src/template-resolver/{web/placeholder-text.ts → placeholder-text.ts} +1 -1
  110. package/packages/bundled-features/src/template-resolver/qualified-names.ts +2 -0
  111. package/packages/bundled-features/src/template-resolver/seeding.ts +3 -1
  112. package/packages/bundled-features/src/template-resolver/template-bundle.ts +6 -1
  113. package/packages/bundled-features/src/template-resolver/template-preview.ts +24 -8
  114. package/packages/bundled-features/src/template-resolver/template-validation.ts +1 -1
  115. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +3 -3
  116. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1 -1
  117. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +7 -33
  118. package/packages/bundled-features/src/template-resolver/web/i18n.ts +24 -0
  119. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +2 -2
  120. package/packages/bundled-features/src/tenant/web/client-plugin.tsx +1 -1
  121. package/packages/bundled-features/src/tenant/web/member-status-cell.tsx +1 -1
  122. package/packages/bundled-features/src/tier-engine/web/client-plugin.tsx +1 -1
  123. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  124. package/packages/bundled-features/src/user-data-rights/env.ts +15 -0
  125. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -2
  126. package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
  127. package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
  128. package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
  129. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +1 -1
  130. package/packages/bundled-features/src/user-data-rights/web/client-plugin.tsx +1 -1
  131. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -2
  132. package/packages/bundled-features/src/waitlist/changes.json +6 -0
  133. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +51 -2
  134. package/samples/apps/admin-console/package.json +0 -3
  135. package/samples/apps/admin-console/src/app/client.tsx +1 -1
  136. package/samples/apps/admin-console/src/app/shell.tsx +1 -1
  137. package/samples/apps/config-demo/package.json +1 -3
  138. package/samples/apps/config-demo/src/app/client.tsx +1 -1
  139. package/samples/apps/config-demo/src/app/shell.tsx +2 -2
  140. package/samples/apps/config-demo/src/features/demo/web/index.ts +1 -1
  141. package/samples/apps/config-demo/tsconfig.json +0 -12
  142. package/samples/apps/marketing-demo/package.json +1 -3
  143. package/samples/apps/marketing-demo/src/app/client.tsx +1 -1
  144. package/samples/apps/marketing-demo/src/app/shell.tsx +1 -1
  145. package/samples/apps/marketing-demo/src/features/assets/web/index.ts +1 -1
  146. package/samples/apps/marketing-demo/src/features/helpdesk/web/index.ts +1 -1
  147. package/samples/apps/showcase/package.json +1 -3
  148. package/samples/apps/showcase/src/app/client.tsx +1 -1
  149. package/samples/apps/showcase/src/app/shell.tsx +1 -1
  150. package/samples/apps/showcase/src/features/demos/web/index.ts +1 -1
  151. package/samples/apps/showcase/src/features/demos/web/pages/sidebar-panel.tsx +1 -1
  152. package/samples/apps/showcase/src/features/demos/web/pages/toast.tsx +1 -1
  153. package/samples/apps/showcase/src/features/items/web/index.ts +1 -1
  154. package/samples/apps/styleguide/package.json +1 -3
  155. package/samples/apps/styleguide/src/app/client.tsx +1 -1
  156. package/samples/apps/styleguide/src/app/shell.tsx +2 -2
  157. package/samples/apps/styleguide/src/features/ai-settings/web.ts +1 -1
  158. package/samples/apps/styleguide/src/features/content/web.tsx +2 -2
  159. package/samples/apps/styleguide/src/features/demo/web.ts +1 -1
  160. package/samples/apps/styleguide/src/features/examples/web.ts +1 -1
  161. package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +1 -1
  162. package/samples/apps/styleguide/src/features/gallery/web/index.ts +1 -1
  163. package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +1 -1
  164. package/samples/apps/styleguide/src/features/widgets/feature.ts +17 -0
  165. package/samples/apps/styleguide/src/features/widgets/i18n.ts +8 -0
  166. package/samples/apps/styleguide/src/features/widgets/lib/format.ts +1 -1
  167. package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +1 -1
  168. package/samples/apps/styleguide/src/features/widgets/web/FormExamples.tsx +1 -1
  169. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +1 -1
  170. package/samples/apps/styleguide/src/features/widgets/web/index.ts +1 -1
  171. package/samples/apps/styleguide/tsconfig.json +0 -12
  172. package/samples/apps/ui-walkthrough/package.json +1 -3
  173. package/samples/apps/ui-walkthrough/src/app/client.tsx +1 -1
  174. package/samples/apps/ui-walkthrough/src/app/shell.tsx +1 -1
  175. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +1 -1
  176. package/samples/apps/ui-walkthrough/src/features/rental/web/index.ts +1 -1
  177. package/samples/apps/ui-walkthrough/src/features/tasks/web/index.ts +1 -1
  178. package/samples/apps/ui-walkthrough/src/features/vehicles/web/index.ts +1 -1
  179. package/samples/apps/use-all-bundled/bin/main.ts +3 -2
  180. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +12 -0
  181. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +1 -1
  182. package/samples/apps/use-all-bundled/feature-manifest.json +1 -0
  183. package/samples/apps/use-all-bundled/package.json +1 -3
  184. package/samples/apps/use-all-bundled/src/app/client.tsx +1 -1
  185. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +5 -4
  186. package/samples/apps/use-all-bundled/src/app/shell.tsx +1 -1
  187. package/samples/apps/use-all-bundled/src/run-config.ts +10 -1
  188. package/samples/apps/workspaces/package.json +1 -3
  189. package/samples/apps/workspaces/src/app/client.tsx +1 -1
  190. package/samples/apps/workspaces/src/app/shell.tsx +1 -1
  191. package/samples/apps/workspaces/src/features/demo/web/index.ts +1 -1
  192. package/samples/apps/workspaces/src/features/demo-driver/web/index.ts +1 -1
  193. package/samples/recipes/apex-landing/package.json +1 -1
  194. package/samples/recipes/apex-landing/src/feature.ts +1 -1
  195. package/samples/recipes/apex-surface-auth/README.md +1 -1
  196. package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
  197. package/samples/recipes/compose-stacks/README.md +7 -4
  198. package/samples/recipes/embedded-entity-form/package.json +0 -1
  199. package/samples/recipes/embedded-entity-form/src/web/accept-suggestion-drawer.tsx +1 -1
  200. package/samples/recipes/notes-history-basic/README.md +1 -1
  201. package/samples/recipes/record-detail-layout/package.json +0 -1
  202. package/samples/recipes/tags-basic/README.md +1 -1
  203. package/samples/recipes/user-profile/README.md +1 -1
  204. package/samples/recipes/wizard-form/package.json +0 -1
  205. package/samples/recipes/wizard-form/src/web/index.ts +2 -2
  206. package/samples/recipes/wizard-form/src/web/listing-review-section.tsx +2 -2
  207. package/samples/recipes/writeform-section/package.json +0 -1
@@ -0,0 +1,48 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import * as z from "zod";
4
+ import { DOCUMENT_HTML_KINDS } from "../constants.ts";
5
+ import { documentBlocksForKind } from "../document-blocks.ts";
6
+ import { documentBlocksOf } from "../document-ops.ts";
7
+ import { parseDocumentJson } from "../document-sanitizer.ts";
8
+ import { resolveTemplateVariables } from "../template-validation.ts";
9
+ import { loadDocumentBase } from "./document-base.ts";
10
+ import { localeSchema, templateSlugSchema } from "./shared.ts";
11
+ import { knownResourceKeysFor } from "./validate.query.ts";
12
+
13
+ export const documentBlocksQuery = defineQueryHandler({
14
+ name: "document-blocks",
15
+ schema: z.object({
16
+ slug: templateSlugSchema,
17
+ kind: z.enum(DOCUMENT_HTML_KINDS),
18
+ locale: localeSchema,
19
+ }),
20
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
21
+ description:
22
+ "Reads a mail or PDF document template as a list of blocks with stable ids (b0, b1, ...) for structured editing: each block has its type (paragraph, heading, list, table, repeat, image, button, divider, pageBreak, steps), its text with {{name}} placeholders and its settings, plus the meta, the stored and effective variables (variablesDeclaredByFeature is true when a notification declares them, so they cannot be changed), the block types allowed for the kind and the uploaded image keys. It reads the caller's own version (the open draft if there is one), else the served system default; base carries the scope and version that patch-draft expects back.",
23
+ handler: async (query, ctx) => {
24
+ const { slug, kind, locale } = query.payload;
25
+ const loaded = await loadDocumentBase(ctx, query.user, { slug, kind, locale });
26
+ if (loaded === null) throw new NotFoundError("template-resource", slug);
27
+ return {
28
+ slug,
29
+ kind,
30
+ locale,
31
+ base: loaded.base,
32
+ status: loaded.status,
33
+ blocks: documentBlocksOf(parseDocumentJson(loaded.content)),
34
+ meta: loaded.meta,
35
+ variableSchema: loaded.variableSchema,
36
+ variables: resolveTemplateVariables(ctx.registry, slug, loaded.variableSchema),
37
+ variablesDeclaredByFeature:
38
+ ctx.registry.getAllNotifications().get(slug)?.variables !== undefined,
39
+ allowedBlocks: documentBlocksForKind(kind),
40
+ resourceKeys: await knownResourceKeysFor(ctx, query.user, {
41
+ slug,
42
+ kind,
43
+ scope: "tenant",
44
+ locale,
45
+ }),
46
+ };
47
+ },
48
+ });
@@ -66,6 +66,15 @@ function assertLocalesShareFormat(rows: readonly TemplateResourceRow[]): void {
66
66
  });
67
67
  }
68
68
 
69
+ function localeEntryOf(row: TemplateResourceRow): { body: string; meta?: unknown; title?: string } {
70
+ const meta = parseStoredJson(row.meta);
71
+ return {
72
+ body: row.content ?? "",
73
+ ...(meta === undefined ? {} : { meta }),
74
+ ...(row.title === null ? {} : { title: row.title }),
75
+ };
76
+ }
77
+
69
78
  export const exportBundleQuery = defineQueryHandler({
70
79
  name: "export-bundle",
71
80
  schema: z.object({
@@ -92,11 +101,10 @@ export const exportBundleQuery = defineQueryHandler({
92
101
  if (!first) throw new NotFoundError("template-resource", slug);
93
102
  assertLocalesShareFormat(rows);
94
103
 
95
- const locales: Record<string, { body: string; meta?: unknown }> = {};
104
+ const locales: Record<string, { body: string; meta?: unknown; title?: string }> = {};
96
105
  const entryByKey = new Map<string, LinkedResourceEntry>();
97
106
  for (const row of rows) {
98
- const meta = parseStoredJson(row.meta);
99
- locales[row.locale] = { body: row.content ?? "", ...(meta === undefined ? {} : { meta }) };
107
+ locales[row.locale] = localeEntryOf(row);
100
108
  for (const [key, entry] of Object.entries(parseLinkedResourceEntries(row.linkedResources))) {
101
109
  if (!entryByKey.has(key)) entryByKey.set(key, entry);
102
110
  }
@@ -0,0 +1,125 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import {
3
+ type FieldIssue,
4
+ NotFoundError,
5
+ ValidationError,
6
+ VersionConflictError,
7
+ writeFailure,
8
+ } from "@cosmicdrift/kumiko-framework/errors";
9
+ import * as z from "zod";
10
+ import { DOCUMENT_HTML_KINDS } from "../constants.ts";
11
+ import { applyDocumentOps, documentOpsSchema } from "../document-ops.ts";
12
+ import { parseDocumentJson, serializeDocumentJson } from "../document-sanitizer.ts";
13
+ import { resolveTemplateVariables } from "../template-validation.ts";
14
+ import { loadDocumentBase } from "./document-base.ts";
15
+ import { localeSchema, templateSlugSchema } from "./shared.ts";
16
+ import { writeTemplateRow } from "./template-write.ts";
17
+ import { validateTemplateDraft } from "./validate.query.ts";
18
+
19
+ function validationFailure(issue: FieldIssue) {
20
+ return writeFailure(new ValidationError({ fields: [issue] }));
21
+ }
22
+
23
+ export const patchDraftWrite = defineWriteHandler({
24
+ name: "patch-draft",
25
+ schema: z.object({
26
+ slug: templateSlugSchema,
27
+ kind: z.enum(DOCUMENT_HTML_KINDS),
28
+ locale: localeSchema,
29
+ base: z.object({
30
+ scope: z.enum(["tenant", "system"]),
31
+ version: z.number().int().nonnegative(),
32
+ }),
33
+ ops: documentOpsSchema,
34
+ }),
35
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
36
+ description:
37
+ "Changes a mail or PDF document template with structured operations instead of a full rewrite and saves the result as the caller's tenant draft (never published, an active version keeps being served until publish): insertBlock after a block id (null = at the start; paragraph, heading, image, button, divider, pageBreak), replaceText inside one block (the text must occur exactly once), removeBlock, setResource on an image block, setMeta (flat merge, null removes a key) and setVariable (adds, replaces or with null removes a variable definition; refused when a notification declares the variables). Block ids and base come from document-blocks and refer to the state that was read; if the template changed since, the call fails with version_conflict and nothing is written. A patch that removes a required placeholder is refused. Returns the written row.",
38
+ handler: async (event, ctx) => {
39
+ const { slug, kind, locale, base, ops } = event.payload;
40
+ const loaded = await loadDocumentBase(ctx, event.user, { slug, kind, locale });
41
+ if (loaded === null) return writeFailure(new NotFoundError("template-resource", slug));
42
+ if (loaded.base.scope !== base.scope || loaded.base.version !== base.version) {
43
+ return writeFailure(
44
+ new VersionConflictError({
45
+ entityId: loaded.rowId,
46
+ expectedVersion: base.version,
47
+ currentVersion: loaded.base.version,
48
+ }),
49
+ );
50
+ }
51
+
52
+ const variableOpIndex = ops.findIndex((op) => op.op === "setVariable");
53
+ if (
54
+ variableOpIndex !== -1 &&
55
+ ctx.registry.getAllNotifications().get(slug)?.variables !== undefined
56
+ ) {
57
+ return validationFailure({
58
+ path: `ops.${variableOpIndex}`,
59
+ code: "variables_declared_by_feature",
60
+ i18nKey: "templateResolver.errors.patch.variablesDeclaredByFeature",
61
+ params: { slug },
62
+ });
63
+ }
64
+
65
+ const applied = applyDocumentOps({
66
+ doc: parseDocumentJson(loaded.content),
67
+ meta: loaded.meta,
68
+ variableSchema: loaded.variableSchema,
69
+ kind,
70
+ ops,
71
+ });
72
+ if (!applied.ok) return writeFailure(new ValidationError({ fields: applied.issues }));
73
+ const content = serializeDocumentJson(applied.doc);
74
+
75
+ const draftOf = (
76
+ draftContent: string,
77
+ meta: unknown,
78
+ variableSchema: Record<string, unknown>,
79
+ ) =>
80
+ validateTemplateDraft(ctx, event.user, {
81
+ slug,
82
+ kind,
83
+ contentFormat: "html",
84
+ content: draftContent,
85
+ meta,
86
+ variableSchema,
87
+ locale,
88
+ scope: "tenant",
89
+ });
90
+ const before = await draftOf(loaded.content, loaded.meta, loaded.variableSchema);
91
+ const after = await draftOf(content, applied.meta, applied.variableSchema);
92
+ const alreadyMissing = new Set(before.response.missingRequired);
93
+ const removed = after.response.missingRequired.filter((name) => !alreadyMissing.has(name));
94
+ if (removed.length > 0) {
95
+ return validationFailure({
96
+ path: "ops",
97
+ code: "required_placeholder_removed",
98
+ i18nKey: "templateResolver.errors.patch.requiredPlaceholderRemoved",
99
+ params: { names: removed },
100
+ });
101
+ }
102
+
103
+ return writeTemplateRow({
104
+ ctx,
105
+ user: event.user,
106
+ tenantId: event.user.tenantId,
107
+ scope: "tenant",
108
+ status: "draft",
109
+ variables: resolveTemplateVariables(ctx.registry, slug, applied.variableSchema),
110
+ payload: {
111
+ slug,
112
+ kind,
113
+ locale,
114
+ content,
115
+ contentFormat: "html",
116
+ variableSchema: applied.variableSchema,
117
+ meta: applied.meta,
118
+ title: loaded.title,
119
+ folder: loaded.folder,
120
+ parentTemplateId: loaded.parentTemplateId,
121
+ },
122
+ expectedVersion: loaded.base.scope === "tenant" ? loaded.base.version : null,
123
+ });
124
+ },
125
+ });
@@ -13,10 +13,11 @@ export const previewQuery = defineQueryHandler({
13
13
  content: z.string().max(200_000).optional(),
14
14
  meta: z.unknown().optional(),
15
15
  variableSchema: z.record(z.string(), z.unknown()).default({}),
16
+ variables: z.record(z.string(), z.unknown()).optional(),
16
17
  }),
17
18
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
18
19
  description:
19
- "Renders a template draft, or the stored template when no content is given, with sample values taken from the declared variable examples, without saving or sending anything: returns the mail subject, html and plain text with its inline images (a PDF template returns its body and layout regions instead), the sample values used, and checks for unresolved images, missing plain text, mail size and unused required placeholders; a draft that fails the write rules is rejected with the same problems as a save.",
20
+ "Renders a template draft, or the stored template when no content is given, with sample values taken from the declared variable examples (values passed in variables replace them and are checked against the variable schema), without saving or sending anything: returns the mail subject, html and plain text with its inline images (a PDF template returns its body and layout regions instead), the sample values used, and checks for unresolved images, missing plain text, mail size and unused required placeholders; a draft that fails the write rules is rejected with the same problems as a save.",
20
21
  handler: async (query, ctx) => {
21
22
  const outcome = await renderTemplatePreview(ctx, query.user, query.payload);
22
23
  if (!outcome.ok) throw outcome.error;
@@ -42,13 +42,14 @@ export const sendTestWrite = defineWriteHandler({
42
42
  content: z.string().max(200_000).optional(),
43
43
  meta: z.unknown().optional(),
44
44
  variableSchema: z.record(z.string(), z.unknown()).default({}),
45
+ variables: z.record(z.string(), z.unknown()).optional(),
45
46
  includeAttachments: z.boolean().default(true),
46
47
  prefixSubject: z.boolean().default(true),
47
48
  }),
48
49
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
49
50
  rateLimit: { per: "user+handler", ...TEST_MAIL_RATE_LIMIT },
50
51
  description:
51
- "Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST] unless prefixSubject is false, carrying the inline images and, unless includeAttachments is false, the files attached to the template; there is no recipient input, the sample values come from the declared variable examples, and a draft that fails the write rules is rejected without sending.",
52
+ "Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST] unless prefixSubject is false, carrying the inline images and, unless includeAttachments is false, the files attached to the template; there is no recipient input, the sample values come from the declared variable examples unless variables replaces them (checked against the variable schema), and a draft that fails the write rules is rejected without sending.",
52
53
  handler: async (event, ctx) => {
53
54
  const { includeAttachments, prefixSubject, ...previewPayload } = event.payload;
54
55
  const outcome = await renderTemplatePreview(ctx, event.user, {
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  AccessDeniedError,
13
13
  ValidationError,
14
+ VersionConflictError,
14
15
  type WriteFailure,
15
16
  writeFailure,
16
17
  } from "@cosmicdrift/kumiko-framework/errors";
@@ -57,6 +58,8 @@ export type TemplateRowWrite = {
57
58
  readonly status: "draft" | "active";
58
59
  readonly variables: TemplateVariables;
59
60
  readonly payload: TemplateRowPayload;
61
+ /** The row version the edit was made against; null means no own row may exist yet. */
62
+ readonly expectedVersion?: number | null;
60
63
  };
61
64
 
62
65
  // The tenant-scope-under-system-tenant ban lives here so no write path can
@@ -151,6 +154,21 @@ function updateChanges(
151
154
  : { draft };
152
155
  }
153
156
 
157
+ function versionConflictFor(
158
+ expectedVersion: number | null | undefined,
159
+ existing: TemplateResourceRow | undefined,
160
+ slug: string,
161
+ ): WriteFailure | null {
162
+ if (expectedVersion === undefined || (existing?.version ?? null) === expectedVersion) return null;
163
+ return writeFailure(
164
+ new VersionConflictError({
165
+ entityId: existing?.id ?? slug,
166
+ expectedVersion: expectedVersion ?? 0,
167
+ currentVersion: existing?.version ?? 0,
168
+ }),
169
+ );
170
+ }
171
+
154
172
  // Shared by every template write: validate through the one pipeline, then
155
173
  // create or update the (tenant, slug, kind, locale) row via the event-store
156
174
  // executor. linkedResources is never part of an update; resources are attached
@@ -201,11 +219,14 @@ export async function writeTemplateRow(
201
219
  status,
202
220
  };
203
221
 
222
+ const conflict = versionConflictFor(args.expectedVersion, existing, payload.slug);
223
+ if (conflict !== null) return conflict;
224
+
204
225
  if (existing) {
205
226
  const result = await executor.update(
206
227
  {
207
228
  id: existing.id,
208
- version: existing.version,
229
+ version: args.expectedVersion ?? existing.version,
209
230
  changes: updateChanges(existing, status, fields),
210
231
  },
211
232
  user,
@@ -47,17 +47,19 @@ type TemplateDraftInput = {
47
47
  readonly scope: TemplateWriteScope;
48
48
  };
49
49
 
50
- // The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
51
- export async function validateTemplateDraft(
50
+ // Image keys the editor may reference: the system rows plus, for a tenant write, the caller's own.
51
+ export async function knownResourceKeysFor(
52
52
  ctx: HandlerContext,
53
53
  user: SessionUser,
54
- payload: TemplateDraftInput,
55
- ) {
56
- const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
57
- const { slug, kind, locale } = payload;
58
- // The dry run must not promise what the caller's write handlers would refuse.
59
- const scope: TemplateWriteScope = isSystemAdmin(user) ? payload.scope : "tenant";
60
- const known = [
54
+ where: {
55
+ readonly slug: string;
56
+ readonly kind: string;
57
+ readonly scope: TemplateWriteScope;
58
+ readonly locale: string | undefined;
59
+ },
60
+ ): Promise<string[]> {
61
+ const { slug, kind, scope, locale } = where;
62
+ return [
61
63
  ...(await knownResourceKeys(ctx.db, {
62
64
  tenantId: SYSTEM_TENANT_ID,
63
65
  slug,
@@ -69,6 +71,19 @@ export async function validateTemplateDraft(
69
71
  ? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
70
72
  : []),
71
73
  ];
74
+ }
75
+
76
+ // The dry run of the write pipeline; preview feeds on the evaluation, validate returns the response.
77
+ export async function validateTemplateDraft(
78
+ ctx: HandlerContext,
79
+ user: SessionUser,
80
+ payload: TemplateDraftInput,
81
+ ) {
82
+ const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
83
+ const { slug, kind, locale } = payload;
84
+ // The dry run must not promise what the caller's write handlers would refuse.
85
+ const scope: TemplateWriteScope = isSystemAdmin(user) ? payload.scope : "tenant";
86
+ const known = await knownResourceKeysFor(ctx, user, { slug, kind, scope, locale });
72
87
  const evaluation = evaluateTemplateWrite({
73
88
  kind,
74
89
  scope,
@@ -1,6 +1,6 @@
1
1
  // @runtime client
2
2
  import type { JSONContent } from "@tiptap/core";
3
- import { isDocumentReferenceName } from "../document-schema.ts";
3
+ import { isDocumentReferenceName } from "./document-schema.ts";
4
4
 
5
5
  const PLACEHOLDER_TOKEN = /\{\{([^{}]*)\}\}/g;
6
6
 
@@ -20,6 +20,7 @@ export const TemplateResolverHandlers = {
20
20
  resourceRemove: "template-resolver:write:resource-remove",
21
21
  resourceLinkPdf: "template-resolver:write:resource-link-pdf",
22
22
  sendTest: "template-resolver:write:send-test",
23
+ patchDraft: "template-resolver:write:patch-draft",
23
24
  } as const;
24
25
 
25
26
  export const TemplateResolverQueries = {
@@ -32,6 +33,7 @@ export const TemplateResolverQueries = {
32
33
  diffToSystem: "template-resolver:query:diff-to-system",
33
34
  validate: "template-resolver:query:validate",
34
35
  preview: "template-resolver:query:preview",
36
+ documentBlocks: "template-resolver:query:document-blocks",
35
37
  exportBundle: "template-resolver:query:export-bundle",
36
38
  // Public pair — anonymous-reachable, kind pinned to text-block.
37
39
  bySlug: "template-resolver:query:by-slug",
@@ -78,6 +78,7 @@ export type SeedSystemTemplateOptions = {
78
78
  * system admin uploaded through resource-upload. */
79
79
  readonly linkedResources?: Record<string, string>;
80
80
  readonly meta?: unknown;
81
+ readonly title?: string;
81
82
  readonly parentTemplateId?: string | null;
82
83
  readonly by?: SessionUser;
83
84
  readonly ifExists?: SeedIfExists;
@@ -138,7 +139,7 @@ export async function seedSystemTemplate(
138
139
  variableSchema,
139
140
  ...(linkedResources !== undefined && { linkedResources }),
140
141
  meta,
141
- title: null,
142
+ title: opts.title ?? null,
142
143
  folder: null,
143
144
  scope: "system" as const,
144
145
  parentTemplateId,
@@ -261,6 +262,7 @@ export async function seedSystemTemplateBundle(
261
262
  contentFormat: bundle.contentFormat,
262
263
  variableSchema: bundle.variableSchema ?? {},
263
264
  meta: entry.meta,
265
+ ...(entry.title !== undefined ? { title: entry.title } : {}),
264
266
  ...(opts.by !== undefined ? { by: opts.by } : {}),
265
267
  ...(opts.ifExists !== undefined ? { ifExists: opts.ifExists } : {}),
266
268
  });
@@ -40,7 +40,12 @@ const bundleResourceSchema = z
40
40
  const pdfAttachmentLinkBodySchema = templatePdfAttachmentLinkSchema.omit({ kind: true });
41
41
 
42
42
  const bundleLocaleEntrySchema = z
43
- .object({ body: z.string().max(200_000), meta: z.unknown().optional() })
43
+ .object({
44
+ body: z.string().max(200_000),
45
+ meta: z.unknown().optional(),
46
+ // Shown in the template overview instead of the slug; one per locale, so it is translated.
47
+ title: z.string().min(1).max(200).optional(),
48
+ })
44
49
  .strict();
45
50
 
46
51
  export const templateBundleSchema = z
@@ -1,20 +1,23 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
- import type {
3
- HandlerContext,
4
- SessionUser,
5
- TemplateListVariableDef,
6
- TemplateScalarVariableDef,
7
- TemplateVariables,
2
+ import {
3
+ type HandlerContext,
4
+ type SessionUser,
5
+ type TemplateListVariableDef,
6
+ type TemplateScalarVariableDef,
7
+ type TemplateVariables,
8
+ templateVariablesToZod,
8
9
  } from "@cosmicdrift/kumiko-framework/engine";
9
10
  import {
10
11
  type KumikoError,
11
12
  NotFoundError,
12
13
  UnprocessableError,
13
14
  ValidationError,
15
+ validationErrorFromZod,
14
16
  } from "@cosmicdrift/kumiko-framework/errors";
15
17
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
16
18
  import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
17
- import { escapeHtml } from "@cosmicdrift/kumiko-headless";
19
+ import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
20
+ import * as z from "zod";
18
21
  import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.ts";
19
22
  import { fileContextForTenant } from "../file-foundation/index.ts";
20
23
  import {
@@ -177,6 +180,8 @@ export type TemplatePreviewInput = {
177
180
  readonly content?: string | undefined;
178
181
  readonly meta?: unknown;
179
182
  readonly variableSchema: Readonly<Record<string, unknown>>;
183
+ /** Replaces the declared example values for the named variables. */
184
+ readonly variables?: Readonly<Record<string, unknown>> | undefined;
180
185
  };
181
186
 
182
187
  type ValidationResponse = Awaited<ReturnType<typeof validateTemplateDraft>>["response"];
@@ -197,6 +202,7 @@ export type TemplatePreview = {
197
202
  readonly scope: TemplateScope;
198
203
  readonly version: number;
199
204
  } | null;
205
+ /** The values the preview was actually rendered with: examples plus any overrides. */
200
206
  readonly sampleValues: Readonly<Record<string, unknown>>;
201
207
  readonly checks: readonly PreviewCheck[];
202
208
  readonly validation: ValidationResponse;
@@ -689,7 +695,17 @@ export async function renderTemplatePreview(
689
695
 
690
696
  const content = evaluation.content ?? "";
691
697
  const meta = parseTemplateMeta(input.kind, parseStoredJson(evaluation.metaJson));
692
- const sampleValues = sampleValuesFor(validation.variables, labelTextFor(ctx, input.locale));
698
+ const exampleValues = sampleValuesFor(validation.variables, labelTextFor(ctx, input.locale));
699
+ let sampleValues = exampleValues;
700
+ if (input.variables !== undefined) {
701
+ const overrides = z.object({
702
+ variables: templateVariablesToZod(validation.variables).partial().strict(),
703
+ });
704
+ const checked = overrides.safeParse({ variables: input.variables });
705
+ if (!checked.success) return failed(validationErrorFromZod(checked.error));
706
+ // Original values, not the parse result: parsing would strip keys inside list rows.
707
+ sampleValues = { ...exampleValues, ...input.variables };
708
+ }
693
709
 
694
710
  if (input.kind === "document-pdf") {
695
711
  return renderDocumentPreview(ctx, user, {
@@ -76,7 +76,7 @@ const variableDefSchema = z.union([
76
76
  // A stored variableSchema value is either a bare string (legacy "this is a text
77
77
  // variable") or a TemplateVariableDef object; anything else is not declared and
78
78
  // therefore shows up as an unknown placeholder instead of being guessed at.
79
- function toVariableDef(raw: unknown): TemplateVariableDef | null {
79
+ export function toVariableDef(raw: unknown): TemplateVariableDef | null {
80
80
  if (typeof raw === "string") return { type: "text" };
81
81
  const parsed = variableDefSchema.safeParse(raw);
82
82
  return parsed.success ? parsed.data : null;
@@ -1,4 +1,4 @@
1
- import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
1
+ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-renderer/headless";
2
2
  // @runtime client
3
3
  // Client feature factory for the text-block content tree. Apps mount it via
4
4
  // createKumikoApp({ clientFeatures: [textBlocksClient()] }); it loads the
@@ -12,7 +12,6 @@ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
12
12
  // (a hint that the slug exists but is empty).
13
13
 
14
14
  import { useShellUser } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
15
- import { CSRF_HEADER_NAME, readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
16
15
  import type {
17
16
  TargetRef,
18
17
  TreeChildrenSubscribe,
@@ -28,11 +27,12 @@ import {
28
27
  useQuery,
29
28
  useTranslation,
30
29
  } from "@cosmicdrift/kumiko-renderer";
30
+ import { CSRF_HEADER_NAME, readCsrfToken } from "@cosmicdrift/kumiko-renderer/live";
31
31
  import {
32
32
  type ClientFeatureDefinition,
33
33
  PlainContentEditor,
34
34
  RichContentEditor,
35
- } from "@cosmicdrift/kumiko-renderer-web";
35
+ } from "@cosmicdrift/kumiko-renderer/web";
36
36
  import type { ContentEditorFormat } from "@cosmicdrift/kumiko-types/nav";
37
37
  import { type FormEvent, type ReactNode, useEffect, useId, useState } from "react";
38
38
  import {
@@ -12,7 +12,7 @@ import {
12
12
  useTranslation,
13
13
  VariableChips,
14
14
  } from "@cosmicdrift/kumiko-renderer";
15
- import { cn } from "@cosmicdrift/kumiko-renderer-web";
15
+ import { cn } from "@cosmicdrift/kumiko-renderer/web";
16
16
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
17
17
  import type { Editor, JSONContent } from "@tiptap/core";
18
18
  import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
@@ -1,16 +1,7 @@
1
1
  // @runtime client
2
2
  import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
3
3
  import type { ReactNode } from "react";
4
-
5
- export type DocumentToolbarBlock =
6
- | "image"
7
- | "table"
8
- | "button"
9
- | "divider"
10
- | "repeat"
11
- | "pageBreak"
12
- | "align"
13
- | "steps";
4
+ import type { DocumentToolbarBlock } from "../document-blocks.ts";
14
5
 
15
6
  /** Imperative access for an outside panel (placeholder list, resource list). */
16
7
  export type DocumentEditorHandle = {
@@ -50,26 +41,9 @@ export type DocumentEditorProps = ContentEditorProps & {
50
41
  readonly onEditorReady?: (handle: DocumentEditorHandle | null) => void;
51
42
  };
52
43
 
53
- export const MAIL_DOCUMENT_BLOCKS = [
54
- "image",
55
- "table",
56
- "button",
57
- "divider",
58
- "repeat",
59
- "steps",
60
- ] as const satisfies readonly DocumentToolbarBlock[];
61
-
62
- export const PDF_DOCUMENT_BLOCKS = [
63
- "align",
64
- "image",
65
- "table",
66
- "divider",
67
- "repeat",
68
- "steps",
69
- "pageBreak",
70
- ] as const satisfies readonly DocumentToolbarBlock[];
71
-
72
- /** Block set for a template-resource kind; every kind except "document-pdf" gets the mail set. */
73
- export function documentBlocksForKind(kind: string | undefined): readonly DocumentToolbarBlock[] {
74
- return kind === "document-pdf" ? PDF_DOCUMENT_BLOCKS : MAIL_DOCUMENT_BLOCKS;
75
- }
44
+ export {
45
+ type DocumentToolbarBlock,
46
+ documentBlocksForKind,
47
+ MAIL_DOCUMENT_BLOCKS,
48
+ PDF_DOCUMENT_BLOCKS,
49
+ } from "../document-blocks.ts";
@@ -80,6 +80,30 @@ export const defaultTranslations: TranslationsByLocale = {
80
80
  "templateResolver.checks.sizeUnderCap": "The mail is within the 10 MiB size limit",
81
81
  "templateResolver.checks.requiredPlaceholders": "All required placeholders are used",
82
82
  "templateResolver.errors.preview.renderFailed": "The preview could not be rendered.",
83
+ "templateResolver.errors.patch.unknownBlock":
84
+ "The block {blockId} does not exist in the template.",
85
+ "templateResolver.errors.patch.blockRemoved":
86
+ "The block {blockId} was already removed by an earlier step.",
87
+ "templateResolver.errors.patch.textNotFound": "The text {find} was not found in the block.",
88
+ "templateResolver.errors.patch.textAmbiguous":
89
+ "The text {find} occurs {count} times in the block; use a longer text.",
90
+ "templateResolver.errors.patch.blockNotAllowed":
91
+ "The block type {type} is not available in {kind} templates.",
92
+ "templateResolver.errors.patch.blockTypeMismatch":
93
+ "This step needs a {expected} block, but the block is a {actual}.",
94
+ "templateResolver.errors.patch.invalidResourceKey": "The image key {resource} is not valid.",
95
+ "templateResolver.errors.patch.invalidButtonHref":
96
+ "A button target must be a placeholder in double curly braces.",
97
+ "templateResolver.errors.patch.invalidVariableName": "The variable name {name} is not valid.",
98
+ "templateResolver.errors.patch.invalidVariableDefinition":
99
+ "The definition of the variable {name} is not valid.",
100
+ "templateResolver.errors.patch.invalidDocument":
101
+ "The change would produce an invalid document.",
102
+ "templateResolver.errors.patch.invalidMetaKey": "The meta key {key} is not allowed.",
103
+ "templateResolver.errors.patch.variablesDeclaredByFeature":
104
+ "The variables of {slug} are fixed by the application and cannot be changed.",
105
+ "templateResolver.errors.patch.requiredPlaceholderRemoved":
106
+ "The change removes required placeholders: {names}",
83
107
  "templateResolver.errors.testMail.noChannel":
84
108
  "No email channel is set up, so no test mail can be sent.",
85
109
  "templateResolver.errors.testMail.noAddress":
@@ -4,17 +4,17 @@
4
4
  // (subject, preheader). The stored value stays plain text; placeholders show
5
5
  // as chips while editing. Lives in the same lazy chunk as the document editor.
6
6
 
7
- import { cn } from "@cosmicdrift/kumiko-renderer-web";
7
+ import { cn } from "@cosmicdrift/kumiko-renderer/web";
8
8
  import { Extension, Node } from "@tiptap/core";
9
9
  import { Placeholder, UndoRedo } from "@tiptap/extensions";
10
10
  import { EditorContent, useEditor } from "@tiptap/react";
11
11
  import StarterKit from "@tiptap/starter-kit";
12
12
  import { type ReactNode, useMemo, useState } from "react";
13
13
  import { DocumentVariable } from "../document-schema.ts";
14
+ import { contentToLine, lineToContent, singleLine } from "../placeholder-text.ts";
14
15
  import { createVariableView } from "./document-editor-extensions.ts";
15
16
  import type { DocumentEditorHandle } from "./document-toolbar-blocks.ts";
16
17
  import { createEditorHandle } from "./editor-handle.ts";
17
- import { contentToLine, lineToContent, singleLine } from "./placeholder-text.ts";
18
18
  import { useStableByKey } from "./use-stable-by-key.ts";
19
19
 
20
20
  export type PlaceholderLineEditorProps = {
@@ -1,6 +1,6 @@
1
1
  // @runtime client
2
2
  import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
3
- import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
3
+ import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
4
4
  import {
5
5
  MEMBER_ROLES_CELL_COMPONENT,
6
6
  MEMBER_STATUS_CELL_COMPONENT,
@@ -5,7 +5,7 @@
5
5
  // projectionList renderer — only this one cell stays TSX.
6
6
 
7
7
  import { type ColumnRendererProps, useTranslation } from "@cosmicdrift/kumiko-renderer";
8
- import { StatusBadge, type StatusTone } from "@cosmicdrift/kumiko-renderer-web";
8
+ import { StatusBadge, type StatusTone } from "@cosmicdrift/kumiko-renderer/web";
9
9
  import type { ReactNode } from "react";
10
10
  import { translateOrRaw } from "../../shared/web/translate-or-raw.ts";
11
11