@cosmicdrift/kumiko-samples 0.363.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 (140) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +7 -1
  3. package/packages/bundled-features/src/audit/changes.json +6 -0
  4. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  5. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  6. package/packages/bundled-features/src/auth-email-password/changes.json +37 -0
  7. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  8. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  9. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  10. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  11. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
  12. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  13. package/packages/bundled-features/src/billing-foundation/changes.json +36 -0
  14. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
  15. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  16. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
  17. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  18. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
  19. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  20. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  21. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
  22. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  24. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  25. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
  26. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  27. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +17 -5
  28. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  29. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  30. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  31. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  32. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  33. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  34. package/packages/bundled-features/src/channel-email/email-channel.ts +17 -4
  35. package/packages/bundled-features/src/config/changes.json +6 -0
  36. package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
  37. package/packages/bundled-features/src/config/resolver.ts +15 -12
  38. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  39. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  40. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  41. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  42. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  43. package/packages/bundled-features/src/delivery/attachment-ref.ts +21 -0
  44. package/packages/bundled-features/src/delivery/changes.json +18 -0
  45. package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
  46. package/packages/bundled-features/src/delivery/index.ts +2 -0
  47. package/packages/bundled-features/src/delivery/jobs.ts +51 -14
  48. package/packages/bundled-features/src/delivery/types.ts +7 -1
  49. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  50. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  51. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
  52. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  53. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  56. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  57. package/packages/bundled-features/src/jobs/changes.json +19 -0
  58. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  59. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  60. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  61. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  62. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  63. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  64. package/packages/bundled-features/src/ledger/changes.json +7 -0
  65. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  66. package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
  67. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  68. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  69. package/packages/bundled-features/src/secrets/changes.json +6 -0
  70. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  71. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  72. package/packages/bundled-features/src/shared/changes.json +6 -0
  73. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  74. package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
  75. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  76. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  77. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  78. package/packages/bundled-features/src/template-resolver/README.md +4 -2
  79. package/packages/bundled-features/src/template-resolver/api.ts +3 -2
  80. package/packages/bundled-features/src/template-resolver/changes.json +68 -0
  81. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +77 -13
  82. package/packages/bundled-features/src/template-resolver/constants.ts +12 -2
  83. package/packages/bundled-features/src/template-resolver/document-schema.ts +26 -4
  84. package/packages/bundled-features/src/template-resolver/feature.ts +4 -1
  85. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +31 -13
  86. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +5 -44
  87. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  88. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
  89. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
  90. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
  91. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
  92. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  93. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +90 -17
  94. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +1 -1
  95. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
  96. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
  97. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
  98. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
  99. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
  100. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  101. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
  102. package/packages/bundled-features/src/template-resolver/index.ts +12 -1
  103. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  104. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  105. package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
  106. package/packages/bundled-features/src/template-resolver/seeding.ts +25 -4
  107. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +2 -2
  108. package/packages/bundled-features/src/template-resolver/table.ts +13 -1
  109. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  110. package/packages/bundled-features/src/template-resolver/template-meta.ts +87 -24
  111. package/packages/bundled-features/src/template-resolver/template-preview.ts +159 -56
  112. package/packages/bundled-features/src/template-resolver/template-resources.ts +44 -4
  113. package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
  114. package/packages/bundled-features/src/template-resolver/template-validation.ts +1 -0
  115. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
  116. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +63 -10
  117. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +12 -3
  118. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +349 -134
  119. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +11 -0
  120. package/packages/bundled-features/src/template-resolver/web/i18n.ts +25 -4
  121. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  122. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +10 -3
  123. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  124. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  125. package/packages/bundled-features/src/user/changes.json +6 -0
  126. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  127. package/packages/bundled-features/src/user/seeding.ts +19 -5
  128. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  129. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  130. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  131. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  132. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  133. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  134. package/samples/apps/ui-walkthrough/package.json +9 -0
  135. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  136. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
  137. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
  138. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
  139. package/samples/apps/ui-walkthrough/src/run-config.ts +4 -0
  140. package/samples/apps/use-all-bundled/feature-manifest.json +5 -3
@@ -1,4 +1,8 @@
1
- import { parseQn, type Registry } from "@cosmicdrift/kumiko-framework/engine";
1
+ import {
2
+ type NotificationDefinition,
3
+ parseQn,
4
+ type Registry,
5
+ } from "@cosmicdrift/kumiko-framework/engine";
2
6
  import { DEFAULT_MAIL_LAYOUT_SLUG, type TemplateKind } from "./constants.js";
3
7
 
4
8
  type UsageRole = "template" | "layout";
@@ -11,27 +15,43 @@ export type TemplateUsageEntry = {
11
15
  readonly role: UsageRole;
12
16
  };
13
17
 
18
+ /** Layout slug of the effective stored mail template, by notification qualified
19
+ * name and locale. The renderer reads the layout from the stored row's meta,
20
+ * so it wins over the code-registered notification meta. */
21
+ export type EffectiveMailLayoutSlugs = ReadonlyMap<string, ReadonlyMap<string, string>>;
22
+
23
+ function layoutSlugsOf(
24
+ qualifiedName: string,
25
+ notification: NotificationDefinition,
26
+ effectiveLayouts: EffectiveMailLayoutSlugs,
27
+ ): string[] {
28
+ const stored = effectiveLayouts.get(qualifiedName);
29
+ const locales = new Set([
30
+ ...Object.keys(notification.template ?? {}),
31
+ ...Object.keys(notification.meta ?? {}),
32
+ ...(stored?.keys() ?? []),
33
+ ]);
34
+ return [...locales].map(
35
+ (locale) =>
36
+ stored?.get(locale) ?? notification.meta?.[locale]?.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG,
37
+ );
38
+ }
39
+
14
40
  export function collectTemplateUsage(
15
41
  registry: Registry,
16
42
  slug: string,
17
43
  kind: TemplateKind,
44
+ effectiveLayouts: EffectiveMailLayoutSlugs = new Map(),
18
45
  ): TemplateUsageEntry[] {
19
46
  const usedBy: TemplateUsageEntry[] = [];
20
47
  if (kind === "mail" || kind === "mail-layout") {
21
48
  for (const [qualifiedName, notification] of registry.getAllNotifications()) {
22
49
  const role: UsageRole = kind === "mail" ? "template" : "layout";
50
+ // Notifications without a code template are not rendered through the resolver.
23
51
  const isUsed =
24
52
  kind === "mail"
25
- ? qualifiedName === slug
26
- : [
27
- ...new Set([
28
- ...Object.keys(notification.template ?? {}),
29
- ...Object.keys(notification.meta ?? {}),
30
- ]),
31
- ].some(
32
- (locale) =>
33
- (notification.meta?.[locale]?.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG) === slug,
34
- );
53
+ ? qualifiedName === slug && notification.template !== undefined
54
+ : layoutSlugsOf(qualifiedName, notification, effectiveLayouts).includes(slug);
35
55
  if (!isUsed) continue;
36
56
  const parsed = parseQn(qualifiedName);
37
57
  usedBy.push({
@@ -47,6 +47,7 @@ const textLikeExample = z.string().optional();
47
47
  const scalarVariableSchema = z.discriminatedUnion("type", [
48
48
  z.object({ ...variableCommon, type: z.literal("text"), example: textLikeExample }),
49
49
  z.object({ ...variableCommon, type: z.literal("link"), example: textLikeExample }),
50
+ z.object({ ...variableCommon, type: z.literal("richText"), example: textLikeExample }),
50
51
  z.object({
51
52
  ...variableCommon,
52
53
  type: z.literal("money"),
@@ -40,7 +40,8 @@ import {
40
40
  TemplateResolverHandlers,
41
41
  TemplateResolverQueries,
42
42
  } from "../qualified-names.js";
43
- import { DocumentContentEditor } from "./document-editor.js";
43
+ import { DocumentBlocksContext, DocumentContentEditor } from "./document-editor.js";
44
+ import { documentBlocksForKind } from "./document-toolbar-blocks.js";
44
45
  import { defaultTranslations } from "./i18n.js";
45
46
 
46
47
  // Exported for the unit test — groupBlocksByFolder is a pure function.
@@ -311,6 +312,7 @@ function TextBlockEditor({
311
312
  const contentFormat = findContentFormat(features, collectionId);
312
313
  const variableExamples = findVariableExamples(features, collectionId);
313
314
  const variables = Object.keys(variableExamples);
315
+ const documentBlocks = documentBlocksForKind(findCollection(features, collectionId)?.kind);
314
316
  const ContentEditor = useContentEditor(contentFormat);
315
317
  const canWrite =
316
318
  user?.roles.includes("TenantAdmin") === true || user?.roles.includes("SystemAdmin") === true;
@@ -448,13 +450,15 @@ function TextBlockEditor({
448
450
  contentFormat={contentFormat}
449
451
  />
450
452
  ) : (
451
- <ContentEditor
452
- id={contentEditorId}
453
- value={content}
454
- onChange={setContent}
455
- variables={variables}
456
- readOnly={disabled}
457
- />
453
+ <DocumentBlocksContext.Provider value={documentBlocks}>
454
+ <ContentEditor
455
+ id={contentEditorId}
456
+ value={content}
457
+ onChange={setContent}
458
+ variables={variables}
459
+ readOnly={disabled}
460
+ />
461
+ </DocumentBlocksContext.Provider>
458
462
  )}
459
463
  </Field>
460
464
  {saveError !== null && <Banner variant="error">{saveError}</Banner>}
@@ -2,6 +2,7 @@
2
2
  import type { Extensions, NodeViewRenderer } from "@tiptap/core";
3
3
  import { ListKeymap } from "@tiptap/extension-list";
4
4
  import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
5
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
5
6
  import {
6
7
  DocumentButton,
7
8
  DocumentPageBreak,
@@ -11,11 +12,16 @@ import {
11
12
  } from "../document-schema.js";
12
13
  import type { DocumentResourcePreview } from "./document-toolbar-blocks.js";
13
14
 
15
+ const DOCUMENT_RESOURCE_SIZE_TOKEN = "{size}";
16
+ export const DOCUMENT_BLOCK_ATTRIBUTE = "data-document-block";
17
+
14
18
  export type DocumentEditorLabels = {
15
19
  readonly placeholder: string;
16
20
  readonly image: string;
17
21
  readonly pageBreak: string;
18
22
  readonly button?: string;
23
+ /** Canvas block label of an image, e.g. "Image · inline". */
24
+ readonly imageBlock?: string;
19
25
  readonly variableLabels?: Readonly<Record<string, string>>;
20
26
  readonly systemVariables?: readonly string[];
21
27
  readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
@@ -38,22 +44,36 @@ const CHIP_CLASS = "mx-0.5 inline-flex items-center gap-1 rounded border px-1.5
38
44
  const USER_CHIP_CLASS = `${CHIP_CLASS} border-primary/30 bg-primary/10 text-primary`;
39
45
  const SYSTEM_CHIP_CLASS = `${CHIP_CLASS} border-input bg-muted text-muted-foreground`;
40
46
 
47
+ // A chip inside a repeat row names an item field; its label is registered as `list.field`.
48
+ function repeatRowListAt(doc: ProseMirrorNode, position: number | undefined): string | null {
49
+ if (position === undefined) return null;
50
+ const resolved = doc.resolve(position);
51
+ for (let depth = resolved.depth; depth > 0; depth--) {
52
+ const each: unknown = resolved.node(depth).attrs["each"];
53
+ if (typeof each === "string" && each !== "") return each;
54
+ }
55
+ return null;
56
+ }
57
+
41
58
  // The chip is display only: the stored node carries just the name, so a
42
59
  // label never reaches the document.
43
60
  export function createVariableView(
44
61
  variableLabels: Readonly<Record<string, string>>,
45
62
  systemVariables: readonly string[],
63
+ labelOnly = false,
46
64
  ): NodeViewRenderer {
47
- return ({ node }) => {
65
+ return ({ node, editor, getPos }) => {
48
66
  const name = String(node.attrs["name"]);
49
- const label = Object.hasOwn(variableLabels, name) ? variableLabels[name] : undefined;
67
+ const listName = repeatRowListAt(editor.state.doc, getPos());
68
+ const labelKey = listName === null ? name : `${listName}.${name}`;
69
+ const label = Object.hasOwn(variableLabels, labelKey) ? variableLabels[labelKey] : undefined;
50
70
  const dom = createElementWithClass(
51
71
  "span",
52
72
  systemVariables.includes(name) ? SYSTEM_CHIP_CLASS : USER_CHIP_CLASS,
53
73
  );
54
74
  dom.contentEditable = "false";
55
75
  dom.append(document.createTextNode(label !== undefined && label !== "" ? label : name));
56
- if (label !== undefined && label !== "" && label !== name) {
76
+ if (!labelOnly && label !== undefined && label !== "" && label !== name) {
57
77
  dom.append(createElementWithClass("small", "font-mono text-[10px] opacity-75", name));
58
78
  }
59
79
  return { dom };
@@ -79,18 +99,46 @@ function createResourceView(labels: DocumentEditorLabels): NodeViewRenderer {
79
99
  const key = String(node.attrs["resource"]);
80
100
  const previews = labels.resourcePreviews ?? {};
81
101
  const preview = Object.hasOwn(previews, key) ? previews[key] : undefined;
102
+ const hasImage = preview !== undefined && PREVIEW_SRC.test(preview.src);
103
+ // A rendered image needs no frame until the node is selected (ProseMirror marks it).
82
104
  const dom = createElementWithClass(
83
105
  "div",
84
- "relative my-3 flex items-center gap-3 rounded border border-dashed border-input px-3 py-3 text-sm text-muted-foreground",
106
+ hasImage
107
+ ? "relative my-1 flex items-center gap-2 rounded border border-transparent px-2 py-1 text-sm text-muted-foreground [.ProseMirror-focused_&.ProseMirror-selectednode]:border-dashed [.ProseMirror-focused_&.ProseMirror-selectednode]:border-input"
108
+ : "relative my-3 flex items-center gap-3 rounded border border-dashed border-input px-3 py-3 text-sm text-muted-foreground",
85
109
  );
110
+ dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
86
111
  dom.contentEditable = "false";
87
- if (labels.blockLabels) dom.append(createBlockLabel(labels.image));
88
- if (preview !== undefined && PREVIEW_SRC.test(preview.src)) {
89
- const image = createElementWithClass("img", "max-h-16 max-w-48 rounded");
112
+ if (labels.blockLabels) {
113
+ const label = createBlockLabel(labels.imageBlock ?? labels.image);
114
+ if (hasImage)
115
+ label.classList.add("hidden", "[.ProseMirror-focused_.ProseMirror-selectednode_&]:block");
116
+ dom.append(label);
117
+ }
118
+ if (hasImage) {
119
+ const image = createElementWithClass("img", "max-w-[180px] rounded");
90
120
  image.src = preview.src;
91
121
  image.alt = typeof node.attrs["alt"] === "string" ? node.attrs["alt"] : key;
92
122
  dom.append(image);
93
- dom.append(createElementWithClass("span", "text-xs", preview.caption ?? key));
123
+ // An empty caption means "no caption", not the key.
124
+ const caption = preview.caption ?? key;
125
+ if (caption !== "") {
126
+ const captionElement = createElementWithClass(
127
+ "span",
128
+ "whitespace-nowrap text-[11px]",
129
+ caption.replace(` · ${DOCUMENT_RESOURCE_SIZE_TOKEN}`, ""),
130
+ );
131
+ // The size is only known once the browser decoded the image (possibly already, from cache).
132
+ const showSize = (): void => {
133
+ captionElement.textContent = caption.replace(
134
+ DOCUMENT_RESOURCE_SIZE_TOKEN,
135
+ `${image.naturalWidth} × ${image.naturalHeight} px`,
136
+ );
137
+ };
138
+ image.addEventListener("load", showSize);
139
+ if (image.complete && image.naturalWidth > 0) showSize();
140
+ dom.append(captionElement);
141
+ }
94
142
  } else {
95
143
  dom.append(
96
144
  createElementWithClass("span", "font-medium", labels.image),
@@ -107,6 +155,7 @@ function createButtonView(buttonLabel: string): NodeViewRenderer {
107
155
  "div",
108
156
  "relative my-3 rounded border border-dashed border-input p-3",
109
157
  );
158
+ dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
110
159
  const button = document.createElement("a");
111
160
  button.setAttribute("data-button", "");
112
161
  dom.append(createBlockLabel(buttonLabel), button);
@@ -118,7 +167,7 @@ function createPageBreakView(pageBreakLabel: string): NodeViewRenderer {
118
167
  return () => {
119
168
  const dom = createElementWithClass(
120
169
  "div",
121
- "my-3 border-t border-dashed border-input pt-1 text-center text-xs text-muted-foreground",
170
+ "my-3 border-t border-dashed border-input pt-1 text-center text-xs uppercase tracking-wide text-muted-foreground",
122
171
  pageBreakLabel,
123
172
  );
124
173
  dom.contentEditable = "false";
@@ -134,7 +183,11 @@ export function createDocumentEditorExtensions(labels: DocumentEditorLabels): Ex
134
183
  case DocumentVariable.name:
135
184
  return DocumentVariable.extend({
136
185
  addNodeView: () =>
137
- createVariableView(labels.variableLabels ?? {}, labels.systemVariables ?? []),
186
+ createVariableView(
187
+ labels.variableLabels ?? {},
188
+ labels.systemVariables ?? [],
189
+ labels.blockLabels === true,
190
+ ),
138
191
  });
139
192
  case DocumentResource.name:
140
193
  return DocumentResource.extend({ addNodeView: () => createResourceView(labels) });
@@ -4,12 +4,21 @@
4
4
  // Never import document-tiptap-editor or document-editor-extensions statically here.
5
5
 
6
6
  import { TextareaContentEditor } from "@cosmicdrift/kumiko-renderer";
7
- import { lazy, type ReactNode, Suspense } from "react";
8
- import type { DocumentEditorProps } from "./document-toolbar-blocks.js";
7
+ import { createContext, lazy, type ReactNode, Suspense, useContext } from "react";
8
+ import type { DocumentEditorProps, DocumentToolbarBlock } from "./document-toolbar-blocks.js";
9
9
 
10
10
  const LazyDocumentTiptapEditor = lazy(() => import("./document-tiptap-editor.js"));
11
11
 
12
+ // The editor registry only hands ContentEditorProps to an editor, so the
13
+ // collection's block set (mail vs. PDF) reaches it through this context. An
14
+ // explicit `blocks` prop still wins.
15
+ export const DocumentBlocksContext = createContext<readonly DocumentToolbarBlock[] | undefined>(
16
+ undefined,
17
+ );
18
+
12
19
  export function DocumentContentEditor(props: DocumentEditorProps): ReactNode {
20
+ const contextBlocks = useContext(DocumentBlocksContext);
21
+ const editorProps = props.blocks === undefined ? { ...props, blocks: contextBlocks } : props;
13
22
  return (
14
23
  <Suspense
15
24
  fallback={
@@ -22,7 +31,7 @@ export function DocumentContentEditor(props: DocumentEditorProps): ReactNode {
22
31
  />
23
32
  }
24
33
  >
25
- <LazyDocumentTiptapEditor {...props} />
34
+ <LazyDocumentTiptapEditor {...editorProps} />
26
35
  </Suspense>
27
36
  );
28
37
  }