@cosmicdrift/kumiko-bundled-features 0.377.0 → 0.378.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 (76) hide show
  1. package/dist/cap-counter/stock-cap-guard.js +8 -0
  2. package/dist/config/web/i18n.js +1 -0
  3. package/dist/inbound-mail-foundation/handlers/list-accounts.query.d.ts +1 -1
  4. package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +11 -3
  5. package/dist/mail-foundation/i18n.d.ts +5 -2
  6. package/dist/mail-foundation/i18n.js +3 -2
  7. package/dist/mail-transport-smtp/i18n.d.ts +1 -1
  8. package/dist/mail-transport-smtp/i18n.js +1 -1
  9. package/dist/template-resolver/compile-document-body.d.ts +3 -0
  10. package/dist/template-resolver/compile-document-body.js +50 -3
  11. package/dist/template-resolver/constants.d.ts +3 -0
  12. package/dist/template-resolver/constants.js +2 -0
  13. package/dist/template-resolver/document-blocks.d.ts +2 -2
  14. package/dist/template-resolver/document-blocks.js +1 -0
  15. package/dist/template-resolver/document-images.d.ts +2 -0
  16. package/dist/template-resolver/document-images.js +28 -0
  17. package/dist/template-resolver/document-sanitizer.d.ts +1 -1
  18. package/dist/template-resolver/document-sanitizer.js +20 -0
  19. package/dist/template-resolver/document-schema.d.ts +2 -1
  20. package/dist/template-resolver/document-schema.js +4 -0
  21. package/dist/template-resolver/feature.d.ts +9 -0
  22. package/dist/template-resolver/feature.js +58 -33
  23. package/dist/template-resolver/handlers/collection-item.query.js +2 -1
  24. package/dist/template-resolver/handlers/collection-list.query.js +2 -1
  25. package/dist/template-resolver/handlers/collection-set.write.js +2 -2
  26. package/dist/template-resolver/handlers/collection-shared.d.ts +1 -2
  27. package/dist/template-resolver/handlers/collection-shared.js +0 -4
  28. package/dist/template-resolver/handlers/copy-locale.write.d.ts +2 -1
  29. package/dist/template-resolver/handlers/copy-locale.write.js +67 -63
  30. package/dist/template-resolver/handlers/discard-draft.write.d.ts +3 -2
  31. package/dist/template-resolver/handlers/discard-draft.write.js +18 -6
  32. package/dist/template-resolver/handlers/import-bundle.write.d.ts +2 -1
  33. package/dist/template-resolver/handlers/import-bundle.write.js +30 -25
  34. package/dist/template-resolver/handlers/overview.query.d.ts +2 -1
  35. package/dist/template-resolver/handlers/overview.query.js +46 -42
  36. package/dist/template-resolver/handlers/patch-draft.write.d.ts +2 -1
  37. package/dist/template-resolver/handlers/patch-draft.write.js +92 -86
  38. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +2 -1
  39. package/dist/template-resolver/handlers/reset-to-system.write.js +51 -44
  40. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +2 -1
  41. package/dist/template-resolver/handlers/resource-link-pdf.write.js +34 -28
  42. package/dist/template-resolver/handlers/resource-remove.write.d.ts +2 -1
  43. package/dist/template-resolver/handlers/resource-remove.write.js +39 -33
  44. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -1
  45. package/dist/template-resolver/handlers/resource-upload.write.js +104 -96
  46. package/dist/template-resolver/handlers/send-test-shared.d.ts +5 -0
  47. package/dist/template-resolver/handlers/send-test-shared.js +19 -0
  48. package/dist/template-resolver/handlers/send-test-status.query.d.ts +9 -0
  49. package/dist/template-resolver/handlers/send-test-status.query.js +18 -0
  50. package/dist/template-resolver/handlers/send-test.write.d.ts +2 -1
  51. package/dist/template-resolver/handlers/send-test.write.js +87 -89
  52. package/dist/template-resolver/handlers/set.write.d.ts +2 -1
  53. package/dist/template-resolver/handlers/set.write.js +94 -90
  54. package/dist/template-resolver/handlers/toggle-status.write.d.ts +6 -7
  55. package/dist/template-resolver/handlers/toggle-status.write.js +28 -10
  56. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +2 -1
  57. package/dist/template-resolver/handlers/upsert-tenant.write.js +30 -24
  58. package/dist/template-resolver/index.d.ts +4 -2
  59. package/dist/template-resolver/index.js +3 -1
  60. package/dist/template-resolver/qualified-names.d.ts +2 -0
  61. package/dist/template-resolver/qualified-names.js +2 -0
  62. package/dist/template-resolver/template-edit-policy.d.ts +22 -0
  63. package/dist/template-resolver/template-edit-policy.js +45 -0
  64. package/dist/template-resolver/template-preview.js +7 -2
  65. package/dist/template-resolver/template-validation.js +33 -13
  66. package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
  67. package/dist/template-resolver/web/document-editor-extensions.js +21 -1
  68. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  69. package/dist/template-resolver/web/document-tiptap-editor.js +22 -6
  70. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +2 -0
  71. package/dist/template-resolver/web/i18n.js +4 -0
  72. package/package.json +8 -8
  73. package/src/cap-counter/changes.json +12 -0
  74. package/src/inbound-mail-foundation/changes.json +6 -0
  75. package/src/mail-foundation/changes.json +6 -0
  76. package/src/template-resolver/changes.json +20 -0
@@ -38,6 +38,7 @@ const scalarVariableSchema = z.discriminatedUnion("type", [
38
38
  ]);
39
39
  const variableDefSchema = z.union([
40
40
  scalarVariableSchema,
41
+ z.object({ ...variableCommon, type: z.literal("images") }),
41
42
  z.object({
42
43
  ...variableCommon,
43
44
  type: z.literal("list"),
@@ -74,7 +75,9 @@ function listItemFields(variables, listName) {
74
75
  const def = listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
75
76
  return def?.type === "list" ? def.item : {};
76
77
  }
77
- function isKnownName(name, scope, itemFields) {
78
+ // An images variable holds file ids: as text it would leak them, so only an images block
79
+ // (and a condition on its presence) may name it.
80
+ function isKnownName(name, scope, itemFields, allowImages = false) {
78
81
  const { variables } = scope;
79
82
  const root = rootName(name);
80
83
  // Declared variables and item fields are scalars: a dotted path under one never renders a value.
@@ -82,12 +85,15 @@ function isKnownName(name, scope, itemFields) {
82
85
  return { known: true, used: "" };
83
86
  if (scope.systemPlaceholders.has(root))
84
87
  return { known: true, used: "" };
85
- if (Object.hasOwn(variables, name))
86
- return { known: true, used: name };
88
+ if (Object.hasOwn(variables, name)) {
89
+ return variables[name]?.type === "images" && !allowImages
90
+ ? { known: false, used: "" }
91
+ : { known: true, used: name };
92
+ }
87
93
  return { known: false, used: "" };
88
94
  }
89
- function recordName(name, itemFields, scope) {
90
- const resolution = isKnownName(name, scope, itemFields);
95
+ function recordName(name, itemFields, scope, allowImages = false) {
96
+ const resolution = isKnownName(name, scope, itemFields, allowImages);
91
97
  if (!resolution.known)
92
98
  scope.unknown.add(name);
93
99
  else if (resolution.used !== "")
@@ -96,10 +102,21 @@ function recordName(name, itemFields, scope) {
96
102
  function enclosingEachName(element) {
97
103
  return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
98
104
  }
99
- const NAME_ATTRIBUTE_BY_TAG = {
100
- span: "data-variable",
101
- div: "data-steps",
102
- };
105
+ function nameAttributeOf(element) {
106
+ const tag = element.tagName.toLowerCase();
107
+ if (tag === "span")
108
+ return "data-variable";
109
+ if (tag !== "div")
110
+ return undefined;
111
+ return element.hasAttribute("data-images") ? "data-images" : "data-steps";
112
+ }
113
+ function recordImagesName(name, scope) {
114
+ const def = Object.hasOwn(scope.variables, name) ? scope.variables[name] : undefined;
115
+ if (def?.type === "images")
116
+ scope.used.add(name);
117
+ else
118
+ scope.unknown.add(name);
119
+ }
103
120
  function scanRepeatRows(root, scope) {
104
121
  for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
105
122
  const listName = element.getAttribute("data-each") ?? "";
@@ -113,15 +130,18 @@ function scanRepeatRows(root, scope) {
113
130
  function scanConditions(root, scope) {
114
131
  for (const element of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
115
132
  const itemFields = listItemFields(scope.variables, enclosingEachName(conditionScopeElement(element)));
116
- recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope);
133
+ recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope, true);
117
134
  }
118
135
  }
119
136
  function scanReferences(root, scope) {
120
- for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], a[href], img[data-resource]"))) {
137
+ for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], div[data-images], a[href], img[data-resource]"))) {
121
138
  const itemFields = listItemFields(scope.variables, enclosingEachName(element));
122
139
  const tag = element.tagName.toLowerCase();
123
- const nameAttribute = NAME_ATTRIBUTE_BY_TAG[tag];
124
- if (nameAttribute !== undefined) {
140
+ const nameAttribute = nameAttributeOf(element);
141
+ if (nameAttribute === "data-images") {
142
+ recordImagesName(element.getAttribute(nameAttribute) ?? "", scope);
143
+ }
144
+ else if (nameAttribute !== undefined) {
125
145
  recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
126
146
  }
127
147
  else if (tag === "a") {
@@ -7,6 +7,7 @@ export type DocumentEditorLabels = {
7
7
  readonly pageBreak: string;
8
8
  readonly button?: string;
9
9
  readonly steps?: string;
10
+ readonly images?: string;
10
11
  /** Canvas block label of an image, e.g. "Image · inline". */
11
12
  readonly imageBlock?: string;
12
13
  readonly variableLabels?: Readonly<Record<string, string>>;
@@ -1,6 +1,6 @@
1
1
  import { ListKeymap } from "@tiptap/extension-list";
2
2
  import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
3
- import { DocumentButton, DocumentPageBreak, DocumentResource, DocumentSteps, DocumentVariable, documentExtensions, } from "../document-schema.js";
3
+ import { DocumentButton, DocumentImages, DocumentPageBreak, DocumentResource, DocumentSteps, DocumentVariable, documentExtensions, } from "../document-schema.js";
4
4
  import { readDocumentStepsAttribute } from "../document-steps.js";
5
5
  const DOCUMENT_RESOURCE_SIZE_TOKEN = "{size}";
6
6
  export const DOCUMENT_BLOCK_ATTRIBUTE = "data-document-block";
@@ -134,6 +134,24 @@ function createStepsView(labels) {
134
134
  return { dom };
135
135
  };
136
136
  }
137
+ // A sketch only: the photos are record data, known at render time.
138
+ function createImagesView(labels) {
139
+ return ({ node }) => {
140
+ const dom = createElementWithClass("div", "relative my-3 rounded border border-dashed border-input px-3 pb-3 pt-4");
141
+ dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
142
+ dom.contentEditable = "false";
143
+ const variable = String(node.attrs["variable"]);
144
+ const variableLabels = labels.variableLabels ?? {};
145
+ const variableLabel = Object.hasOwn(variableLabels, variable)
146
+ ? variableLabels[variable]
147
+ : undefined;
148
+ dom.append(createBlockLabel(`${labels.images ?? "Photos"} · ${variableLabel ?? variable}`));
149
+ const grid = createElementWithClass("div", "grid grid-cols-2 gap-1.5");
150
+ grid.append(createElementWithClass("div", "h-12 rounded bg-muted"), createElementWithClass("div", "h-12 rounded bg-muted"));
151
+ dom.append(grid);
152
+ return { dom };
153
+ };
154
+ }
137
155
  // Replaced by name, not appended: the schema must keep exactly one node per
138
156
  // name, and only the editor view differs from the shared document schema.
139
157
  export function createDocumentEditorExtensions(labels) {
@@ -151,6 +169,8 @@ export function createDocumentEditorExtensions(labels) {
151
169
  : extension;
152
170
  case DocumentSteps.name:
153
171
  return DocumentSteps.extend({ addNodeView: () => createStepsView(labels) });
172
+ case DocumentImages.name:
173
+ return DocumentImages.extend({ addNodeView: () => createImagesView(labels) });
154
174
  case DocumentPageBreak.name:
155
175
  return DocumentPageBreak.extend({
156
176
  addNodeView: () => createPageBreakView(labels.pageBreak),
@@ -1,3 +1,3 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { type DocumentEditorProps } from "./document-toolbar-blocks.ts";
3
- export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks, variableLabels, systemVariables, resourcePreviews, chrome, toolbar, renderPaper, toolbarHost, onEditorReady, }: DocumentEditorProps): ReactNode;
3
+ export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks: configuredBlocks, imageVariables, variableLabels, systemVariables, resourcePreviews, chrome, toolbar, renderPaper, toolbarHost, onEditorReady, }: DocumentEditorProps): ReactNode;
@@ -10,7 +10,7 @@ import { cn } from "@cosmicdrift/kumiko-renderer/web";
10
10
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
11
11
  import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
12
12
  import { BubbleMenu } from "@tiptap/react/menus";
13
- import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold as BoldIcon, Braces, ChevronDown, Image as ImageIcon, Italic as ItalicIcon, Link as LinkIcon, List, ListChecks, ListOrdered, Minus, MousePointerClick, Repeat, Scissors, Table as TableIcon, Underline as UnderlineIcon, } from "lucide-react";
13
+ import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold as BoldIcon, Braces, ChevronDown, GalleryThumbnails, Image as ImageIcon, Italic as ItalicIcon, Link as LinkIcon, List, ListChecks, ListOrdered, Minus, MousePointerClick, Repeat, Scissors, Table as TableIcon, Underline as UnderlineIcon, } from "lucide-react";
14
14
  import { Fragment, useEffect, useMemo, useState } from "react";
15
15
  import { createPortal } from "react-dom";
16
16
  import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "../document-schema.js";
@@ -35,6 +35,7 @@ const BLOCK_PANEL = {
35
35
  repeat: "repeat",
36
36
  align: "align",
37
37
  steps: "steps",
38
+ images: "images",
38
39
  };
39
40
  const ALIGNMENT_ICONS = {
40
41
  left: _jsx(AlignLeft, { size: 16 }),
@@ -243,7 +244,11 @@ function InsertPanel({ editor, editorId, kind, variables, variableLabels, onClos
243
244
  }
244
245
  insertAndClose({ type: "steps", attrs: { variable: selectedName, steps } });
245
246
  };
246
- const needsVariables = kind === "button" || kind === "repeat" || kind === "placeholder" || kind === "steps";
247
+ const needsVariables = kind === "button" ||
248
+ kind === "repeat" ||
249
+ kind === "placeholder" ||
250
+ kind === "steps" ||
251
+ kind === "images";
247
252
  const noVariables = needsVariables && selectable.length === 0;
248
253
  const idPrefix = `${editorId}-panel`;
249
254
  return (_jsxs("div", { className: "flex flex-col gap-2 border-b border-input bg-muted/40 p-2", children: [noVariables && (_jsx("p", { className: "text-sm text-muted-foreground", children: t("template-resolver.documentEditor.noVariables") })), kind === "link" && (_jsx(Field, { id: `${idPrefix}-href`, label: t("template-resolver.documentEditor.linkHref"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-href`, name: "href", value: href, onChange: setHref, placeholder: t("template-resolver.documentEditor.linkHrefPlaceholder") }) })), kind === "placeholder" && !noVariables && (_jsx("div", { className: "flex flex-wrap gap-1", children: _jsx(VariableChips, { variables: selectable, labels: variableLabels, onInsert: (name) => insertAndClose({ type: "variable", attrs: { name } }) }) })), kind === "align" && (_jsx("div", { className: "flex flex-wrap gap-1", children: DOCUMENT_TEXT_ALIGNMENTS.map((alignment) => (_jsxs(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => {
@@ -254,7 +259,7 @@ function InsertPanel({ editor, editorId, kind, variables, variableLabels, onClos
254
259
  .updateAttributes("heading", { textAlign: alignment })
255
260
  .run();
256
261
  onClose();
257
- }, children: [ALIGNMENT_ICONS[alignment], t(ALIGNMENT_LABEL_KEYS[alignment])] }, alignment))) })), kind === "image" && (_jsxs(_Fragment, { children: [_jsx(Field, { id: `${idPrefix}-resource`, label: t("template-resolver.documentEditor.resourceKey"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-resource`, name: "resource", value: resource, onChange: setResource }) }), _jsx(Field, { id: `${idPrefix}-alt`, label: t("template-resolver.documentEditor.imageAlt"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-alt`, name: "alt", value: alt, onChange: setAlt }) })] })), kind === "button" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-target`, label: t("template-resolver.documentEditor.buttonTarget"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-text`, label: t("template-resolver.documentEditor.buttonText"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-text`, name: "text", value: buttonText, onChange: setButtonText }) })] })), kind === "repeat" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-list`, label: t("template-resolver.documentEditor.repeatList"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName })), kind === "steps" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-steps-variable`, label: t("template-resolver.documentEditor.stepsVariable"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-steps-list`, label: t("template-resolver.documentEditor.stepsList"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-steps-list`, name: "steps", value: stepsText, onChange: setStepsText }) })] })), error !== null && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsx("div", { className: "flex flex-wrap gap-2", children: _jsx(PanelActions, { onCancel: onClose, ...(kind === "link" && { onInsert: submitLink }), ...(kind === "link" &&
262
+ }, children: [ALIGNMENT_ICONS[alignment], t(ALIGNMENT_LABEL_KEYS[alignment])] }, alignment))) })), kind === "image" && (_jsxs(_Fragment, { children: [_jsx(Field, { id: `${idPrefix}-resource`, label: t("template-resolver.documentEditor.resourceKey"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-resource`, name: "resource", value: resource, onChange: setResource }) }), _jsx(Field, { id: `${idPrefix}-alt`, label: t("template-resolver.documentEditor.imageAlt"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-alt`, name: "alt", value: alt, onChange: setAlt }) })] })), kind === "button" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-target`, label: t("template-resolver.documentEditor.buttonTarget"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-text`, label: t("template-resolver.documentEditor.buttonText"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-text`, name: "text", value: buttonText, onChange: setButtonText }) })] })), kind === "repeat" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-list`, label: t("template-resolver.documentEditor.repeatList"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName })), kind === "images" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-images-variable`, label: t("template-resolver.documentEditor.imagesVariable"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName })), kind === "steps" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-steps-variable`, label: t("template-resolver.documentEditor.stepsVariable"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-steps-list`, label: t("template-resolver.documentEditor.stepsList"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-steps-list`, name: "steps", value: stepsText, onChange: setStepsText }) })] })), error !== null && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsx("div", { className: "flex flex-wrap gap-2", children: _jsx(PanelActions, { onCancel: onClose, ...(kind === "link" && { onInsert: submitLink }), ...(kind === "link" &&
258
263
  editor.isActive("link") && {
259
264
  onRemoveLink: () => {
260
265
  editor.chain().focus().extendMarkRange("link").unsetLink().run();
@@ -268,9 +273,12 @@ function InsertPanel({ editor, editorId, kind, variables, variableLabels, onClos
268
273
  content: buttonText ? [{ type: "text", text: buttonText }] : [],
269
274
  }),
270
275
  }), ...(kind === "repeat" &&
271
- !noVariables && { onInsert: () => insertAndClose(repeatTableContent(selectedName)) }), ...(kind === "steps" && !noVariables && { onInsert: submitSteps }) }) })] }));
276
+ !noVariables && { onInsert: () => insertAndClose(repeatTableContent(selectedName)) }), ...(kind === "steps" && !noVariables && { onInsert: submitSteps }), ...(kind === "images" &&
277
+ !noVariables && {
278
+ onInsert: () => insertAndClose({ type: "images", attrs: { variable: selectedName } }),
279
+ }) }) })] }));
272
280
  }
273
- export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks = MAIL_DOCUMENT_BLOCKS, variableLabels = NO_LABELS, systemVariables = NO_NAMES, resourcePreviews = NO_PREVIEWS, chrome = "default", toolbar = "full", renderPaper, toolbarHost, onEditorReady, }) {
281
+ export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks: configuredBlocks = MAIL_DOCUMENT_BLOCKS, imageVariables = NO_NAMES, variableLabels = NO_LABELS, systemVariables = NO_NAMES, resourcePreviews = NO_PREVIEWS, chrome = "default", toolbar = "full", renderPaper, toolbarHost, onEditorReady, }) {
274
282
  const t = useTranslation();
275
283
  const { ActionOverflowMenu, Button, Field, Input } = usePrimitives();
276
284
  const [openPanel, setOpenPanel] = useState(null);
@@ -279,12 +287,17 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
279
287
  const canvas = chrome === "canvas";
280
288
  const compact = toolbar === "compact" && !canvas;
281
289
  const hosted = toolbarHost !== undefined;
290
+ // A photo block needs an images variable to bind to; without one the button would open an empty panel.
291
+ const blocks = imageVariables.length === 0
292
+ ? configuredBlocks.filter((block) => block !== "images")
293
+ : configuredBlocks;
282
294
  const placeholderLabel = t("template-resolver.documentEditor.placeholder");
283
295
  const imageLabel = t("template-resolver.documentEditor.image");
284
296
  const imageBlockLabel = t("template-resolver.documentEditor.imageBlock");
285
297
  const pageBreakLabel = t("template-resolver.documentEditor.pageBreak");
286
298
  const buttonLabel = t("template-resolver.documentEditor.button");
287
299
  const stepsLabel = t("template-resolver.documentEditor.steps");
300
+ const imagesLabel = t("template-resolver.documentEditor.images");
288
301
  const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
289
302
  const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
290
303
  const stablePreviews = useStableByKey(resourcePreviews, previewsKey(resourcePreviews));
@@ -294,6 +307,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
294
307
  pageBreak: pageBreakLabel,
295
308
  button: buttonLabel,
296
309
  steps: stepsLabel,
310
+ images: imagesLabel,
297
311
  imageBlock: imageBlockLabel,
298
312
  variableLabels: stableLabels,
299
313
  systemVariables: stableSystem,
@@ -306,6 +320,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
306
320
  pageBreakLabel,
307
321
  buttonLabel,
308
322
  stepsLabel,
323
+ imagesLabel,
309
324
  stableLabels,
310
325
  stableSystem,
311
326
  stablePreviews,
@@ -415,6 +430,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
415
430
  pageBreak: { label: pageBreakLabel, icon: _jsx(Scissors, { size: 16 }) },
416
431
  align: { label: t("template-resolver.documentEditor.align"), icon: _jsx(AlignLeft, { size: 16 }) },
417
432
  steps: { label: stepsLabel, icon: _jsx(ListChecks, { size: 16 }) },
433
+ images: { label: imagesLabel, icon: _jsx(GalleryThumbnails, { size: 16 }) },
418
434
  };
419
435
  const repeatTable = activeState?.repeatTable ?? null;
420
436
  const repeatListLabel = repeatTable !== null && repeatTable.lists.length === 1
@@ -507,7 +523,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
507
523
  : t("template-resolver.documentEditor.buttonTarget"), ": ", selectedBlock.type === "button" ? (_jsx("strong", { children: buttonTargetLabel(selectedBlock.detail, variableLabels) })) : (_jsx("code", { children: selectedBlock.detail }))] }), _jsx(Button, { type: "button", size: "sm", variant: "danger-ghost", onClick: () => {
508
524
  editor.chain().focus().deleteNode(selectedBlock.type).run();
509
525
  }, children: t("template-resolver.documentEditor.removeBlock") })] })) : (repeatTable !== null && (_jsxs("div", { "data-testid": "document-repeat-bar", className: BUBBLE_BAR_CLASS, children: [_jsx("span", { className: "px-1.5 text-[10px] uppercase tracking-wide text-muted-foreground", children: repeatBarLabel }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", pressed: repeatTable.headerRepeats, onClick: () => toggleRepeatHeader(editor), children: t("template-resolver.documentEditor.repeatHeader") })] }))) }, mountKeyOf(editor)));
510
- const insertPanel = openPanel !== null && !readOnly && (_jsx(InsertPanel, { editor: editor, editorId: id, kind: openPanel, variables: variables, variableLabels: stableLabels, onClose: () => setOpenPanel(null) }, openPanel));
526
+ const insertPanel = openPanel !== null && !readOnly && (_jsx(InsertPanel, { editor: editor, editorId: id, kind: openPanel, variables: openPanel === "images" ? imageVariables : variables, variableLabels: stableLabels, onClose: () => setOpenPanel(null) }, openPanel));
511
527
  if (hosted) {
512
528
  return (_jsxs(_Fragment, { children: [toolbarHost !== null &&
513
529
  createPortal(_jsxs(_Fragment, { children: [toolbarBar, insertPanel] }), toolbarHost), !readOnly && bubbleMenu, editorContent] }));
@@ -17,6 +17,8 @@ export type DocumentResourcePreview = {
17
17
  export type DocumentEditorChrome = "default" | "canvas";
18
18
  export type DocumentEditorProps = ContentEditorProps & {
19
19
  readonly blocks?: readonly DocumentToolbarBlock[];
20
+ /** Names of the template's images variables; the photo block offers exactly these and is hidden without any. */
21
+ readonly imageVariables?: readonly string[];
20
22
  /** Display label per variable name, as inserted (list item fields use the
21
23
  * dotted form, e.g. "lines.description"). A chip shows label and name. */
22
24
  readonly variableLabels?: Readonly<Record<string, string>>;
@@ -39,6 +39,8 @@ export const defaultTranslations = {
39
39
  "template-resolver.documentEditor.removeLink": "Remove link",
40
40
  "template-resolver.documentEditor.repeat": "Repeat",
41
41
  "template-resolver.documentEditor.repeatList": "List to repeat",
42
+ "template-resolver.documentEditor.images": "Photos",
43
+ "template-resolver.documentEditor.imagesVariable": "Photos from",
42
44
  "template-resolver.documentEditor.steps": "Steps",
43
45
  "template-resolver.documentEditor.stepsVariable": "Current step from",
44
46
  "template-resolver.documentEditor.stepsList": "Steps, separated by commas (value=label)",
@@ -52,6 +54,7 @@ export const defaultTranslations = {
52
54
  "template-resolver.editor.saving": "Saving…",
53
55
  "template-resolver.editor.created": "Created.",
54
56
  "template-resolver.editor.saved": "Saved.",
57
+ "templateResolver.errors.document.imagesBlockPdfOnly": "Photo blocks are only allowed in PDF documents.",
55
58
  "templateResolver.errors.document.buttonHrefNotPlaceholder": "The button target “{detail}” must be a placeholder.",
56
59
  "templateResolver.errors.document.disallowedLinkHref": "Link target “{detail}” is not allowed; use https, http, mailto, tel or a placeholder.",
57
60
  "templateResolver.errors.document.documentRequiresHtmlFormat": "This template kind requires the html format, got “{detail}”.",
@@ -91,6 +94,7 @@ export const defaultTranslations = {
91
94
  "templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
92
95
  "templateResolver.errors.noDraft": "There is no draft to discard.",
93
96
  "templateResolver.errors.systemOnlyTemplate": "Only the platform maintains this template; it cannot be overridden per tenant.",
97
+ "templateResolver.errors.templateNotTenantEditable": "Only the platform may edit this template.",
94
98
  "templateResolver.errors.template.invalidMeta": "The template settings are invalid.",
95
99
  "templateResolver.errors.template.meta.subject.required": "The subject is missing.",
96
100
  "templateResolver.errors.template.meta.subject.invalid": "The subject is invalid (at most 200 characters, no line breaks).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.377.0",
3
+ "version": "0.378.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -540,9 +540,9 @@
540
540
  }
541
541
  },
542
542
  "dependencies": {
543
- "@cosmicdrift/kumiko-framework": "0.377.0",
544
- "@cosmicdrift/kumiko-renderer": "0.377.0",
545
- "@cosmicdrift/kumiko-types": "0.377.0",
543
+ "@cosmicdrift/kumiko-framework": "0.378.0",
544
+ "@cosmicdrift/kumiko-renderer": "0.378.0",
545
+ "@cosmicdrift/kumiko-types": "0.378.0",
546
546
  "@mollie/api-client": "^4.5.0",
547
547
  "@node-rs/argon2": "^2.0.2",
548
548
  "@tiptap/core": "3.31.3",
@@ -1107,12 +1107,12 @@
1107
1107
  ],
1108
1108
  "devDependencies": {
1109
1109
  "@testing-library/user-event": "^14.6.1",
1110
- "@cosmicdrift/kumiko-locale-de": "0.377.0",
1111
- "@cosmicdrift/kumiko-locale-es": "0.377.0",
1112
- "@cosmicdrift/kumiko-renderer-mail-html": "0.377.0",
1110
+ "@cosmicdrift/kumiko-locale-de": "0.378.0",
1111
+ "@cosmicdrift/kumiko-locale-es": "0.378.0",
1112
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.378.0",
1113
1113
  "jsqr": "^1.4.0",
1114
1114
  "unpdf": "^1.8.1",
1115
- "@cosmicdrift/kumiko-testing": "0.377.0"
1115
+ "@cosmicdrift/kumiko-testing": "0.378.0"
1116
1116
  },
1117
1117
  "peerDependencies": {
1118
1118
  "@cosmicdrift/kumiko-testing": ">=0.366.0 <1.0.0"
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.378.0",
4
+ "type": "fix",
5
+ "title": "Stock caps are atomic under parallel writes",
6
+ "detail": "withStockCap now takes a transaction-scoped advisory lock per tenant and cap, so concurrent creates cannot both pass the check and exceed the cap."
7
+ },
8
+ {
9
+ "version": "0.378.0",
10
+ "type": "improvement",
11
+ "title": "withStockCap no longer widens the handler's grants",
12
+ "detail": "withStockCap serialises through TenantDb.advisoryXactLock instead of ctx.db.unsafeRaw(), so wrapped handlers no longer receive an implicit unsafeRaw grant."
13
+ },
2
14
  {
3
15
  "version": "0.370.0",
4
16
  "type": "breaking",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.378.0",
4
+ "type": "fix",
5
+ "title": "Mailbox list accepts the screen's limit",
6
+ "detail": "account:list accepted an empty payload only, so the projectionList screen's limit was rejected. It now takes limit up to MAX_LIST_LIMIT and returns the visible mailboxes ordered by id."
7
+ },
2
8
  {
3
9
  "version": "0.377.0",
4
10
  "type": "improvement",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.378.0",
4
+ "type": "improvement",
5
+ "title": "Email sending offers a platform default and an own SMTP server",
6
+ "detail": "The provider field is labelled \"Send via\" with the options \"Platform sending (default)\" (empty, saved as \"\") and \"Own SMTP server\"; the SMTP fields show only while the own server is picked."
7
+ },
2
8
  {
3
9
  "version": "0.370.0",
4
10
  "type": "breaking",
@@ -1,4 +1,24 @@
1
1
  [
2
+ {
3
+ "version": "0.378.0",
4
+ "type": "improvement",
5
+ "title": "Document PDFs embed the images of a record",
6
+ "detail": "A template variable of type images (templateVariable.images()) holds FileRef ids of the record. The new Images block (div[data-images], PDF templates only, rejected for mail kinds) renders them as data-URI images in the real PDF render. Only files of the rendering tenant are read, only png, jpeg, gif and webp, at most 24 images and 20 MB per document; missing, foreign, oversized or non-image ids are dropped silently. Previews and draft PDFs never read files and show one placeholder box per block. An images variable never renders as inline text. i18n keys template-resolver.documentEditor.images, template-resolver.documentEditor.imagesVariable and templateResolver.errors.document.imagesBlockPdfOnly ship in de, es, fr and it.",
7
+ "migration": "Declare a template variable with type `images`, place the Images block, and pass FileRef ids of the record in `variables`. Images resolve only in the real PDF render, previews show placeholders. The ids must come from the record on the server, never from client input: a resolved id is read for the whole tenant without an entity access check."
8
+ },
9
+ {
10
+ "version": "0.378.0",
11
+ "type": "breaking",
12
+ "title": "Template editing is SystemAdmin-only unless the app unlocks it",
13
+ "detail": "Every tenant write handler, the overview and resource upload accept SystemAdmin only, plus the roles of the tenantEditable entries. A write on a template that is not unlocked fails with templateNotTenantEditable. The test send follows the same rule. For a non-SystemAdmin an unknown template id answers 403 like a locked one, so the response does not reveal whether it exists. Collections without an access rule default to SystemAdmin.",
14
+ "migration": "Where TenantAdmins must keep editing a template, pass tenantEditable to createTemplateResolverFeature, for example tenantEditable: [{ kind: \"mail\", slug: \"billing:notify:invoice-issued\", access: { roles: [\"TenantAdmin\"] } }]. Give template collections an explicit access rule if tenants should manage them."
15
+ },
16
+ {
17
+ "version": "0.378.0",
18
+ "type": "improvement",
19
+ "title": "send-test-status query for the template test mail",
20
+ "detail": "template-resolver:query:send-test-status answers whether a test mail can be sent (canSend) and, if not, why (no_channel or no_address), so clients can disable the action instead of failing on send."
21
+ },
2
22
  {
3
23
  "version": "0.377.0",
4
24
  "type": "improvement",