@cosmicdrift/kumiko-document-designer 0.377.0 → 0.379.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 (78) hide show
  1. package/dist/feature.d.ts +6 -2
  2. package/dist/feature.js +10 -10
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/list-definition.js +14 -2
  6. package/dist/web/cells.js +3 -4
  7. package/dist/web/client-plugin.js +3 -4
  8. package/dist/{constants.d.ts → web/constants.d.ts} +1 -0
  9. package/dist/{constants.js → web/constants.js} +2 -1
  10. package/dist/web/default-translations.d.ts +2 -0
  11. package/dist/web/default-translations.js +218 -0
  12. package/dist/web/editor-header.js +1 -2
  13. package/dist/web/editor-locale.js +1 -2
  14. package/dist/web/editor-shell.js +8 -4
  15. package/dist/web/errors.js +0 -1
  16. package/dist/web/field-issue-text.js +0 -1
  17. package/dist/web/file-name.js +0 -1
  18. package/dist/web/i18n.d.ts +6 -2
  19. package/dist/web/i18n.js +873 -219
  20. package/dist/web/index.d.ts +1 -1
  21. package/dist/web/index.js +1 -2
  22. package/dist/web/layout-editor-body.js +6 -6
  23. package/dist/web/layout-editor.js +1 -2
  24. package/dist/web/layout-html.js +0 -1
  25. package/dist/web/layout-meta.js +0 -1
  26. package/dist/web/layout-rails.js +3 -3
  27. package/dist/web/layout-users.js +1 -2
  28. package/dist/web/mail-dialogs.js +0 -1
  29. package/dist/web/mail-draft.js +4 -3
  30. package/dist/web/mail-edit-center.js +0 -1
  31. package/dist/web/mail-editor-body.js +7 -4
  32. package/dist/web/mail-editor.js +1 -2
  33. package/dist/web/mail-footer.d.ts +1 -1
  34. package/dist/web/mail-footer.js +2 -2
  35. package/dist/web/mail-left-rail.d.ts +2 -1
  36. package/dist/web/mail-left-rail.js +2 -3
  37. package/dist/web/mail-missing-locale.js +0 -1
  38. package/dist/web/mail-origin-bar.js +0 -1
  39. package/dist/web/mail-pdf-link-modal.js +5 -2
  40. package/dist/web/mail-preview-side.d.ts +2 -0
  41. package/dist/web/mail-preview-side.js +3 -2
  42. package/dist/web/mail-preview.js +1 -2
  43. package/dist/web/mail-right-rail.js +1 -2
  44. package/dist/web/mail-types.d.ts +4 -0
  45. package/dist/web/mail-upload-modal.js +0 -1
  46. package/dist/web/mail-versions.js +0 -1
  47. package/dist/web/origin-bar.js +2 -3
  48. package/dist/web/overrides-drawer.js +0 -1
  49. package/dist/web/overview-row.d.ts +4 -1
  50. package/dist/web/overview-row.js +3 -1
  51. package/dist/web/page-canvas.d.ts +1 -0
  52. package/dist/web/page-canvas.js +1 -2
  53. package/dist/web/pdf-left-rail.d.ts +2 -1
  54. package/dist/web/pdf-left-rail.js +2 -3
  55. package/dist/web/pdf-meta.js +0 -1
  56. package/dist/web/pdf-template-editor.js +40 -7
  57. package/dist/web/preview-record-picker.js +0 -1
  58. package/dist/web/rail.js +8 -4
  59. package/dist/web/settings-panel.js +0 -1
  60. package/dist/web/template-rows.js +0 -1
  61. package/dist/web/types.d.ts +1 -1
  62. package/dist/web/use-files-capability.d.ts +1 -0
  63. package/dist/web/use-files-capability.js +7 -0
  64. package/dist/web/use-mail-persistence.js +0 -1
  65. package/dist/web/use-overview-rows.js +0 -1
  66. package/dist/web/use-own-address.js +1 -2
  67. package/dist/web/use-settled-value.js +0 -1
  68. package/dist/web/use-test-mail.d.ts +6 -0
  69. package/dist/web/use-test-mail.js +8 -4
  70. package/dist/web/used-by-label.d.ts +1 -0
  71. package/dist/web/used-by-label.js +10 -5
  72. package/package.json +11 -9
  73. package/src/changes.json +43 -0
  74. package/src/web/styles.css +12 -0
  75. package/dist/i18n.d.ts +0 -6
  76. package/dist/i18n.js +0 -836
  77. package/dist/web/pdf-preview.d.ts +0 -4
  78. package/dist/web/pdf-preview.js +0 -112
@@ -1,6 +1,6 @@
1
1
  export { type DocumentDesignerClientOptions, documentDesignerClient, PdfTemplateEditorScreen, } from "./client-plugin.tsx";
2
2
  export { composeLayoutHtml, type LayoutRegionValues } from "./compose-layout.ts";
3
- export { documentDesignerTranslations } from "./i18n.ts";
3
+ export { documentDesignerTranslations } from "./default-translations.ts";
4
4
  export { PdfTemplateEditor, type PdfTemplateEditorProps } from "./pdf-template-editor.tsx";
5
5
  export { type PreviewRecordPicker, type PreviewRecordPickerProps, PreviewRecordPickerProvider, } from "./preview-record-picker.tsx";
6
6
  export { usageLabel, usedByLabel } from "./used-by-label.ts";
package/dist/web/index.js CHANGED
@@ -1,7 +1,6 @@
1
- // @runtime client
2
1
  export { documentDesignerClient, PdfTemplateEditorScreen, } from "./client-plugin.js";
3
2
  export { composeLayoutHtml } from "./compose-layout.js";
4
- export { documentDesignerTranslations } from "./i18n.js";
3
+ export { documentDesignerTranslations } from "./default-translations.js";
5
4
  export { PdfTemplateEditor } from "./pdf-template-editor.js";
6
5
  export { PreviewRecordPickerProvider, } from "./preview-record-picker.js";
7
6
  export { usageLabel, usedByLabel } from "./used-by-label.js";
@@ -1,11 +1,11 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
4
+ import { pluralKey } from "@cosmicdrift/kumiko-renderer/headless";
5
5
  import { cn, ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer/web";
6
6
  import { useState } from "react";
7
- import { BRANDING_QUERY_QN } from "../constants.js";
8
7
  import { composeLayoutHtml } from "./compose-layout.js";
8
+ import { BRANDING_QUERY_QN } from "./constants.js";
9
9
  import { EditorHeader } from "./editor-header.js";
10
10
  import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
11
11
  import { fileUrl } from "./file-name.js";
@@ -137,14 +137,14 @@ export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRo
137
137
  ? { tone: "bad", text: issueText(t, blockingIssue) }
138
138
  : {
139
139
  tone: "ok",
140
- text: t(isMail
140
+ text: t(pluralKey(isMail
141
141
  ? "documentDesigner.editor.footer.okMail"
142
- : "documentDesigner.editor.footer.okPdf", { count: userCount }),
142
+ : "documentDesigner.editor.footer.okPdf", userCount), { count: userCount }),
143
143
  };
144
144
  const accentColor = branding.data?.accentColor ?? "";
145
145
  const showAccentLine = isMail && appearanceEditable && appearance.accentLine === "branding" && isHexColor(accentColor);
146
- // A layout region has no per-mail variables, so a steps block there could never show a stage.
147
- const blocks = (isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS).filter((block) => block !== "steps");
146
+ // A layout region has no per-mail variables, so a steps block there could never show a stage and an images block no photos.
147
+ const blocks = (isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS).filter((block) => block !== "steps" && block !== "images");
148
148
  // The layout's "logo" resource is the branding logo, so the editor shows it instead of a chip.
149
149
  const logoFile = branding.data?.logoFile ?? "";
150
150
  const resourcePreviews = logoFile === ""
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { useNav, useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
5
4
  import { ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer/web";
6
5
  import { useState } from "react";
7
- import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
6
+ import { DEFAULT_LAYOUT_SLUG } from "./constants.js";
8
7
  import { LayoutEditorBody } from "./layout-editor-body.js";
9
8
  import { layoutUserKind } from "./layout-users.js";
10
9
  import { parseOverviewRow } from "./overview-row.js";
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { cn } from "@cosmicdrift/kumiko-renderer/web";
4
3
  import { createElement, useMemo } from "react";
5
4
  // Layout regions arrive server-sanitised, but the preview still renders them
@@ -1,4 +1,3 @@
1
- // @runtime client
2
1
  // Mirrors the mail-layout meta of template-resolver (template-meta.ts); the server parses it again on every write and render.
3
2
  export const LAYOUT_WIDTHS = [560, 600, 640];
4
3
  export const LAYOUT_FONTS = ["sans", "serif"];
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { useAppFeatures, useNav, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
4
- import { BRANDING_SCREEN_QN } from "../constants.js";
3
+ import { BRANDING_SCREEN_QN } from "./constants.js";
5
4
  import { isHexColor, LAYOUT_ACCENT_LINES, LAYOUT_BUTTON_STYLES, LAYOUT_FONTS, LAYOUT_WIDTHS, } from "./layout-meta.js";
6
5
  import { layoutUsers } from "./layout-users.js";
7
6
  import { PLACEHOLDER_SYMBOL, SYSTEM_PLACEHOLDER_TYPE, systemPlaceholderLabel, } from "./mail-draft.js";
8
7
  import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
9
8
  import { statusLabel } from "./status-label.js";
9
+ import { templateTitleText } from "./used-by-label.js";
10
10
  const BRANDING_SCREEN_SHORT_ID = BRANDING_SCREEN_QN.slice(BRANDING_SCREEN_QN.lastIndexOf(":") + 1);
11
11
  function logoName(logoUrl) {
12
12
  const path = logoUrl.split("?")[0] ?? "";
@@ -90,7 +90,7 @@ export function AppearanceRail({ appearance, onChange, fromMjml, }) {
90
90
  export function UsedByRail({ layoutSlug, kind, rows, }) {
91
91
  const t = useTranslation();
92
92
  const users = layoutUsers(rows, kind, layoutSlug);
93
- return (_jsx(RailSection, { title: t("documentDesigner.usedBy.title"), testId: "document-designer-used-by", children: users.length === 0 ? (_jsx(RailHint, { children: t("documentDesigner.usedBy.none") })) : (users.map((row) => (_jsx(RailRow, { testId: `used-by-${row.id}`, label: row.title, trailing: row.origin === "system"
93
+ return (_jsx(RailSection, { title: t("documentDesigner.usedBy.title"), testId: "document-designer-used-by", children: users.length === 0 ? (_jsx(RailHint, { children: t("documentDesigner.usedBy.none") })) : (users.map((row) => (_jsx(RailRow, { testId: `used-by-${row.id}`, label: templateTitleText(t, row.title), trailing: row.origin === "system"
94
94
  ? t("documentDesigner.source.system")
95
95
  : statusLabel(t, row.status, row.version) }, row.id)))) }));
96
96
  }
@@ -1,5 +1,4 @@
1
- // @runtime client
2
- import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
1
+ import { DEFAULT_LAYOUT_SLUG } from "./constants.js";
3
2
  export function layoutUserKind(kind) {
4
3
  return kind === "mail-layout" ? "mail" : "document-pdf";
5
4
  }
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
3
  export function MailConfirmDialogs({ dialog, onClose, onDiscard, onRemove, onLeaveSave, onLeaveDiscard, }) {
5
4
  const t = useTranslation();
@@ -1,5 +1,4 @@
1
- // @runtime client
2
- import { DOCUMENT_FALLBACK_CURRENCY, formatDocumentDate, formatDocumentMoney, formatDocumentPercent, } from "@cosmicdrift/kumiko-renderer/headless";
1
+ import { DOCUMENT_FALLBACK_CURRENCY, formatDocumentDate, formatDocumentMoney, formatDocumentPercent, pluralKey, } from "@cosmicdrift/kumiko-renderer/headless";
3
2
  import { variableLabelText } from "./used-by-label.js";
4
3
  export const EMPTY_DRAFT = {
5
4
  subject: "",
@@ -167,7 +166,9 @@ export function variableLabelMap(t, variables) {
167
166
  }
168
167
  export function displayValue(t, value, type, format) {
169
168
  if (Array.isArray(value))
170
- return t("documentDesigner.mail.preview.entries", { count: value.length });
169
+ return t(pluralKey("documentDesigner.mail.preview.entries", value.length), {
170
+ count: value.length,
171
+ });
171
172
  if (type === "money") {
172
173
  if (typeof value === "number" && format.tenantCurrency === undefined)
173
174
  return String(value);
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PlaceholderLineEditor, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { useTranslation } from "@cosmicdrift/kumiko-renderer";
5
4
  const renderMailPaper = (editable) => (_jsx("div", { className: "mx-auto w-full max-w-[600px] bg-card px-10 py-7 shadow-md", children: editable }));
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { useState } from "react";
@@ -18,12 +17,14 @@ import { MailPreviewMode } from "./mail-preview.js";
18
17
  import { AttachmentSection, ChromeSection, PropertiesSection, UsageSection, } from "./mail-right-rail.js";
19
18
  import { UploadModal } from "./mail-upload-modal.js";
20
19
  import { VersionsSection } from "./mail-versions.js";
20
+ import { useFilesCapability } from "./use-files-capability.js";
21
21
  import { useInsertTargets } from "./use-insert-targets.js";
22
22
  import { useMailPersistence } from "./use-mail-persistence.js";
23
23
  import { useOverviewRows } from "./use-overview-rows.js";
24
24
  import { useOwnAddress } from "./use-own-address.js";
25
25
  import { useSettledValue } from "./use-settled-value.js";
26
26
  import { useTestMail } from "./use-test-mail.js";
27
+ import { templateTitleText } from "./used-by-label.js";
27
28
  // The editor replaces this token with the decoded image size once the preview image loaded.
28
29
  const SIZE_TOKEN = "{size}";
29
30
  function uniqueValues(values) {
@@ -66,6 +67,7 @@ export function MailEditorBody(props) {
66
67
  });
67
68
  const testMail = useTestMail();
68
69
  const own = useOwnAddress();
70
+ const filesAvailable = useFilesCapability();
69
71
  const resourceRemove = useMutation(TemplateResolverHandlers.resourceRemove);
70
72
  const layouts = useOverviewRows("mail-layout");
71
73
  const pdfTemplates = useOverviewRows("document-pdf");
@@ -114,7 +116,7 @@ export function MailEditorBody(props) {
114
116
  perform(target);
115
117
  };
116
118
  const resources = parseLinkedResources(row?.linkedResources);
117
- const pdfTitles = new Map(pdfTemplates.rows.map((template) => [template.slug, template.title]));
119
+ const pdfTitles = new Map(pdfTemplates.rows.map((template) => [template.slug, templateTitleText(t, template.title)]));
118
120
  const resourcePreviews = Object.fromEntries(resources.flatMap((resource) => resource.kind === "image"
119
121
  ? [
120
122
  [
@@ -167,7 +169,7 @@ export function MailEditorBody(props) {
167
169
  const attachments = resources.filter((resource) => resource.kind !== "image");
168
170
  const footer = locked || validation === undefined || validation === null
169
171
  ? null
170
- : footerMessage(t, validation, mode === "preview");
172
+ : footerMessage(t, validation, mode === "preview" && testMail.availability.canSend);
171
173
  const fieldErrors = locked ? {} : metaFieldMessages(t, validation ?? null);
172
174
  const publish = () => void saveAndReload(true);
173
175
  const controls = (_jsx(EditorLocaleSwitch, { testIdPrefix: "mail", locales: props.locales, locale: locale, onLocale: (next) => leave({ kind: "locale", locale: next }) }));
@@ -175,7 +177,7 @@ export function MailEditorBody(props) {
175
177
  return (_jsxs(EditorFrame, { testId: "document-designer-mail-editor", children: [_jsx(EditorHeader, { title: title, stored: row === null ? null : { status: row.status, version: row.version }, actions: { busy, canSave, canPublish, onSave: saveDraft, onPublish: publish }, children: _jsx(EditorModeSwitch, { testIdPrefix: "mail", mode: mode, onMode: setMode }) }), mode === "edit" && (_jsx(MailOriginBar, { row: row, systemVersion: props.systemVersion, locale: locale, isSystemAdmin: isSystemAdmin, creating: creating, controls: controls, onReload: () => {
176
178
  setResetError(null);
177
179
  void onReload();
178
- }, onResetError: setResetError })), testMail.state.kind === "sent" && (_jsx(Banner, { variant: "primary", testId: "mail-test-sent", children: t("documentDesigner.mail.testMail.sent", { subject: testMail.state.subject }) })), testMail.state.kind === "error" && (_jsx(Banner, { variant: "error", testId: "mail-test-error", children: testMail.state.text })), resetError !== null && (_jsx(Banner, { variant: "error", testId: "mail-reset-error", children: resetError })), resourceError !== null && (_jsx(Banner, { variant: "error", testId: "mail-resource-error", children: resourceError })), mode === "edit" ? (_jsx(EditorBody, { left: _jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: railValidation, onInsert: insert.insertPlaceholder }), _jsx(SystemPlaceholderSection, { validation: railValidation, onInsert: insert.insertPlaceholder }), _jsx(ResourceSection, { resources: resources, pdfTitles: pdfTitles, canUpload: uploadTemplateId !== null, onUpload: () => setUploadOpen(true), onInsert: insert.insertResource, onRemove: (key) => setDialog({ kind: "remove", key }) })] }), center: locked ? (_jsx(MissingLocaleCanvas, { slug: slug, locale: locale, fallbackLocale: props.fallbackLocale, copyScope: isSystemAdmin ? "system" : "tenant", onCreateBlank: () => setCreating(true), onCopied: () => void onReload() })) : (_jsx(MailEditCenter, { draft: draft, vocabulary: vocabulary, resourcePreviews: resourcePreviews, insert: insert, readOnly: false, fieldErrors: fieldErrors, onChange: update }, editorEpoch)), right: _jsxs(_Fragment, { children: [_jsx(PropertiesSection, { slug: slug, draft: draft, layouts: layouts.rows, onChange: update }), _jsx(ChromeSection, { draft: draft, vocabulary: vocabulary, insert: insert, onChange: update }), _jsx(AttachmentSection, { attachments: attachments, pdfTitles: pdfTitles, canEdit: uploadTemplateId !== null, onAddPdf: () => setPdfOpen(true), onRemove: (key) => setDialog({ kind: "remove", key }) }), _jsx(UsageSection, { slug: slug }), _jsx(VersionsSection, { rowId: row?.id ?? null, currentVersion: row?.version ?? 0, currentStatus: row?.status ?? "draft" })] }) })) : (_jsx(MailPreviewMode, { payload: {
180
+ }, onResetError: setResetError })), testMail.state.kind === "sent" && (_jsx(Banner, { variant: "primary", testId: "mail-test-sent", children: t("documentDesigner.mail.testMail.sent", { subject: testMail.state.subject }) })), testMail.state.kind === "error" && (_jsx(Banner, { variant: "error", testId: "mail-test-error", children: testMail.state.text })), resetError !== null && (_jsx(Banner, { variant: "error", testId: "mail-reset-error", children: resetError })), resourceError !== null && (_jsx(Banner, { variant: "error", testId: "mail-resource-error", children: resourceError })), mode === "edit" ? (_jsx(EditorBody, { left: _jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: railValidation, onInsert: insert.insertPlaceholder }), _jsx(SystemPlaceholderSection, { validation: railValidation, onInsert: insert.insertPlaceholder }), _jsx(ResourceSection, { resources: resources, pdfTitles: pdfTitles, canUpload: uploadTemplateId !== null, filesAvailable: filesAvailable, onUpload: () => setUploadOpen(true), onInsert: insert.insertResource, onRemove: (key) => setDialog({ kind: "remove", key }) })] }), center: locked ? (_jsx(MissingLocaleCanvas, { slug: slug, locale: locale, fallbackLocale: props.fallbackLocale, copyScope: isSystemAdmin ? "system" : "tenant", onCreateBlank: () => setCreating(true), onCopied: () => void onReload() })) : (_jsx(MailEditCenter, { draft: draft, vocabulary: vocabulary, resourcePreviews: resourcePreviews, insert: insert, readOnly: false, fieldErrors: fieldErrors, onChange: update }, editorEpoch)), right: _jsxs(_Fragment, { children: [_jsx(PropertiesSection, { slug: slug, draft: draft, layouts: layouts.rows, onChange: update }), _jsx(ChromeSection, { draft: draft, vocabulary: vocabulary, insert: insert, onChange: update }), _jsx(AttachmentSection, { attachments: attachments, pdfTitles: pdfTitles, canEdit: uploadTemplateId !== null, onAddPdf: () => setPdfOpen(true), onRemove: (key) => setDialog({ kind: "remove", key }) }), _jsx(UsageSection, { slug: slug }), _jsx(VersionsSection, { rowId: row?.id ?? null, currentVersion: row?.version ?? 0, currentStatus: row?.status ?? "draft" })] }) })) : (_jsx(MailPreviewMode, { payload: {
179
181
  slug,
180
182
  kind: "mail",
181
183
  locale,
@@ -184,6 +186,7 @@ export function MailEditorBody(props) {
184
186
  variableSchema,
185
187
  ...(picked !== null && { variables: picked }),
186
188
  }, picked: picked, onPick: setPicked, validation: validation ?? null, slug: slug, locale: locale, fromName: settled.senderName, own: own, origin: origin, controls: controls, testMail: {
189
+ availability: testMail.availability,
187
190
  pending: testMail.pending,
188
191
  onSend: sendTestMail,
189
192
  includeAttachments,
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { useLocale, useNav, useQuery, useTranslation, useUserRoles, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { LoadingState } from "@cosmicdrift/kumiko-renderer/web";
6
5
  import { useState } from "react";
7
- import { DEFAULT_TEMPLATE_LOCALES } from "../constants.js";
6
+ import { DEFAULT_TEMPLATE_LOCALES } from "./constants.js";
8
7
  import { defaultLocaleOf } from "./editor-locale.js";
9
8
  import { MailEditorBody } from "./mail-editor-body.js";
10
9
  import { useOverviewRows } from "./use-overview-rows.js";
@@ -1,6 +1,6 @@
1
1
  import type { MailValidation } from "./mail-types.ts";
2
2
  type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
3
- export declare function footerMessage(t: Translate, validation: MailValidation, previewMode: boolean): {
3
+ export declare function footerMessage(t: Translate, validation: MailValidation, offersTestMail: boolean): {
4
4
  readonly tone: "bad" | "warn" | "ok";
5
5
  readonly text: string;
6
6
  };
@@ -10,11 +10,11 @@ function missingLabels(t, validation) {
10
10
  }
11
11
  // The footer sentence: missing required placeholders first, then other blocking issues,
12
12
  // then hints (not-yet-uploaded resources), else "ready".
13
- export function footerMessage(t, validation, previewMode) {
13
+ export function footerMessage(t, validation, offersTestMail) {
14
14
  if (validation.missingRequired.length > 0) {
15
15
  return {
16
16
  tone: "bad",
17
- text: t(previewMode
17
+ text: t(offersTestMail
18
18
  ? "documentDesigner.mail.footer.missingPreview"
19
19
  : "documentDesigner.mail.footer.missing", { names: missingLabels(t, validation) }),
20
20
  };
@@ -13,10 +13,11 @@ export declare function SystemPlaceholderSection({ validation, onInsert, idPrefi
13
13
  /** Placeholders the layout fills in, so a template that never uses them is fine. */
14
14
  readonly isLayoutOwned?: (name: string) => boolean;
15
15
  }): ReactNode;
16
- export declare function ResourceSection({ resources, pdfTitles, canUpload, onUpload, onInsert, onRemove, }: {
16
+ export declare function ResourceSection({ resources, pdfTitles, canUpload, filesAvailable, onUpload, onInsert, onRemove, }: {
17
17
  readonly resources: readonly LinkedResourceView[];
18
18
  readonly pdfTitles: ReadonlyMap<string, string>;
19
19
  readonly canUpload: boolean;
20
+ readonly filesAvailable: boolean;
20
21
  readonly onUpload: () => void;
21
22
  readonly onInsert: (key: string) => void;
22
23
  readonly onRemove: (key: string) => void;
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
3
  import { LAYOUT_OWNED_PLACEHOLDERS, PLACEHOLDER_SYMBOL, SYSTEM_PLACEHOLDER_TYPE, systemPlaceholderLabel, } from "./mail-draft.js";
5
4
  import { RailHint, RailRow, RailSection } from "./rail.js";
@@ -47,10 +46,10 @@ function resourceRow(t, resource, pdfTitles) {
47
46
  };
48
47
  }
49
48
  }
50
- export function ResourceSection({ resources, pdfTitles, canUpload, onUpload, onInsert, onRemove, }) {
49
+ export function ResourceSection({ resources, pdfTitles, canUpload, filesAvailable, onUpload, onInsert, onRemove, }) {
51
50
  const t = useTranslation();
52
51
  const { Button } = usePrimitives();
53
- return (_jsxs(RailSection, { title: t("documentDesigner.mail.resource.title"), testId: "mail-editor-resources", action: _jsx(Button, { variant: "link", disabled: !canUpload, onClick: onUpload, testId: "mail-resource-upload-open", children: t("documentDesigner.mail.resource.upload") }), children: [!canUpload && _jsx(RailHint, { children: t("documentDesigner.mail.resource.saveFirst") }), resources.map((resource) => {
52
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.resource.title"), testId: "mail-editor-resources", action: _jsx(Button, { variant: "link", disabled: !canUpload || !filesAvailable, onClick: onUpload, testId: "mail-resource-upload-open", children: t("documentDesigner.mail.resource.upload") }), children: [!filesAvailable && _jsx(RailHint, { children: t("documentDesigner.mail.resource.noFilesFeature") }), filesAvailable && !canUpload && (_jsx(RailHint, { children: t("documentDesigner.mail.resource.saveFirst") })), resources.map((resource) => {
54
53
  const row = resourceRow(t, resource, pdfTitles);
55
54
  return (_jsx(RailRow, { testId: "mail-resource-row", symbol: row.symbol, label: row.label, trailing: row.meta, action: _jsx(Button, { variant: "danger-ghost", size: "sm", onClick: () => onRemove(resource.key), testId: `mail-resource-remove-${resource.key}`, children: t("documentDesigner.mail.resource.remove") }), ...(resource.kind === "image" && { onPress: () => onInsert(resource.key) }) }, resource.key));
56
55
  }), _jsx(RailHint, { children: t("documentDesigner.mail.resource.hint") })] }));
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { useState } from "react";
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, formatWhen, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-renderer/headless";
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { PlaceholderLineEditor, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { EmptyState } from "@cosmicdrift/kumiko-renderer/web";
6
5
  import { useState } from "react";
7
6
  import { lastSlugSegment, parseJsonRecord, uniqueResourceKey } from "./mail-draft.js";
7
+ import { templateTitleText } from "./used-by-label.js";
8
8
  function pdfRowIdOf(summaries, slug) {
9
9
  const ofSlug = (summaries ?? []).filter((row) => row.slug === slug);
10
10
  return (ofSlug.find((row) => row.scope === "tenant") ?? ofSlug[0])?.id ?? null;
@@ -42,7 +42,10 @@ function PdfLinkForm({ templates, variableLabels, templateId, takenKeys, onLinke
42
42
  return (_jsxs("div", { className: "flex flex-col gap-4", children: [error !== null && (_jsx(Banner, { variant: "error", testId: "mail-pdf-link-error", children: error })), _jsx(Field, { id: "mail-pdf-template", label: t("documentDesigner.mail.pdfLink.template"), children: _jsx(Input, { kind: "select", id: "mail-pdf-template", name: "mail-pdf-template", value: slug, onChange: (next) => {
43
43
  setSlug(next);
44
44
  setEditedName(null);
45
- }, options: templates.map((template) => ({ value: template.slug, label: template.title })) }) }), _jsx(Field, { id: "mail-pdf-file-name", label: t("documentDesigner.mail.pdfLink.fileName"), description: t("documentDesigner.mail.pdfLink.fileNameHint"), children: _jsx(PlaceholderLineEditor, { id: "mail-pdf-file-name", value: fileName, onChange: setEditedName, variableLabels: variableLabels, "aria-label": t("documentDesigner.mail.pdfLink.fileName") }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { disabled: slug === "" || fileName === "" || linkPdf.pending, loading: linkPdf.pending, onClick: link, testId: "mail-pdf-link-confirm", children: t("documentDesigner.mail.pdfLink.confirm") }) })] }));
45
+ }, options: templates.map((template) => ({
46
+ value: template.slug,
47
+ label: templateTitleText(t, template.title),
48
+ })) }) }), _jsx(Field, { id: "mail-pdf-file-name", label: t("documentDesigner.mail.pdfLink.fileName"), description: t("documentDesigner.mail.pdfLink.fileNameHint"), children: _jsx(PlaceholderLineEditor, { id: "mail-pdf-file-name", value: fileName, onChange: setEditedName, variableLabels: variableLabels, "aria-label": t("documentDesigner.mail.pdfLink.fileName") }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { disabled: slug === "" || fileName === "" || linkPdf.pending, loading: linkPdf.pending, onClick: link, testId: "mail-pdf-link-confirm", children: t("documentDesigner.mail.pdfLink.confirm") }) })] }));
46
49
  }
47
50
  export function PdfLinkModal({ open, onOpenChange, templates, ...form }) {
48
51
  const t = useTranslation();
@@ -1,7 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { MailPreview, MailValidation, PickedValues } from "./mail-types.ts";
3
3
  import type { OwnAddress } from "./use-own-address.ts";
4
+ import type { TestMailAvailability } from "./use-test-mail.ts";
4
5
  export type TestMailControls = {
6
+ readonly availability: TestMailAvailability;
5
7
  readonly pending: boolean;
6
8
  readonly onSend: () => void;
7
9
  readonly includeAttachments: boolean;
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { ConfigQueries } from "@cosmicdrift/kumiko-bundled-features/config/web";
4
3
  import { useNav, usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
5
4
  import { DOCUMENT_FALLBACK_CURRENCY } from "@cosmicdrift/kumiko-renderer/headless";
@@ -77,5 +76,7 @@ export function PreviewSide({ preview, validation, slug, locale, own, testMail,
77
76
  const t = useTranslation();
78
77
  const { Button, Field, Input, Text } = usePrimitives();
79
78
  const hasAttachments = preview?.attachments.some((a) => a.kind !== "inline") === true;
80
- return (_jsxs("div", { "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: onPick }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked, locale: locale }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsx(TestMailRecipient, { own: own }), _jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-prefix", label: t("documentDesigner.mail.testMail.prefixSubject"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-prefix", name: "mail-test-prefix", value: testMail.prefixSubject, onChange: testMail.onPrefixSubject }) }), hasAttachments ? (_jsx(Field, { id: "mail-test-attachments", label: t("documentDesigner.mail.testMail.attachments"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-attachments", name: "mail-test-attachments", value: testMail.includeAttachments, onChange: testMail.onIncludeAttachments }) })) : (_jsx(Text, { variant: "muted", testId: "mail-test-no-attachments", children: t("documentDesigner.mail.testMail.noAttachments") })), _jsx(Button, { variant: "secondary", width: "full", disabled: testMail.pending, loading: testMail.pending, onClick: testMail.onSend, testId: "mail-test-send", children: t("documentDesigner.mail.testMail.send") })] }), _jsx(RailHint, { children: t("documentDesigner.mail.testMail.hint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.preview.checks"), testId: "mail-preview-checks", children: [(preview?.checks ?? []).map((check) => (_jsx(RailRow, { testId: `mail-check-${check.code}`, symbol: check.ok ? "✓" : "!", label: checkText(t, check, validation), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
79
+ return (_jsxs("div", { "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: onPick }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked, locale: locale }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsx(TestMailRecipient, { own: own }), _jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-prefix", label: t("documentDesigner.mail.testMail.prefixSubject"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-prefix", name: "mail-test-prefix", value: testMail.prefixSubject, onChange: testMail.onPrefixSubject }) }), hasAttachments ? (_jsx(Field, { id: "mail-test-attachments", label: t("documentDesigner.mail.testMail.attachments"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-attachments", name: "mail-test-attachments", value: testMail.includeAttachments, onChange: testMail.onIncludeAttachments }) })) : (_jsx(Text, { variant: "muted", testId: "mail-test-no-attachments", children: t("documentDesigner.mail.testMail.noAttachments") })), _jsx(Button, { variant: "secondary", width: "full", disabled: testMail.pending || !testMail.availability.canSend, loading: testMail.pending, onClick: testMail.onSend, testId: "mail-test-send", children: t("documentDesigner.mail.testMail.send") })] }), testMail.availability.reason !== null && (_jsx(RailHint, { children: t(testMail.availability.reason === "no_channel"
80
+ ? "documentDesigner.mail.testMail.unavailable.noChannel"
81
+ : "documentDesigner.mail.testMail.unavailable.noAddress") })), _jsx(RailHint, { children: t("documentDesigner.mail.testMail.hint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.preview.checks"), testId: "mail-preview-checks", children: [(preview?.checks ?? []).map((check) => (_jsx(RailRow, { testId: `mail-check-${check.code}`, symbol: check.ok ? "✓" : "!", label: checkText(t, check, validation), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
81
82
  }
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { LoadingState, ModeSwitch } from "@cosmicdrift/kumiko-renderer/web";
@@ -23,7 +22,7 @@ function HeadRow({ label, children, }) {
23
22
  function MailFrame({ preview, format, width, fromName, recipient, }) {
24
23
  const t = useTranslation();
25
24
  const attachments = preview.attachments.flatMap((a) => attachmentText(t, a) ?? []);
26
- return (_jsxs("div", { className: "mx-auto flex w-full max-w-[680px] flex-col border bg-card shadow-md", "data-testid": "mail-preview-frame", children: [_jsxs("div", { className: "grid grid-cols-[80px_minmax(0,1fr)] gap-y-1 border-b px-5 py-3.5 text-sm", children: [_jsx(HeadRow, { label: t("documentDesigner.mail.preview.from"), children: _jsx("span", { "data-testid": "mail-preview-from", children: fromName === "" ? t("documentDesigner.mail.props.senderDefault") : fromName }) }), recipient !== null && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.to"), children: _jsx("span", { "data-testid": "mail-preview-to", children: recipient }) })), _jsx(HeadRow, { label: t("documentDesigner.mail.meta.subject"), children: _jsx("span", { className: "font-medium", "data-testid": "mail-preview-subject", children: preview.subject ?? "" }) }), attachments.length > 0 && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.attachments"), children: _jsx("span", { className: "flex flex-col items-start gap-1", "data-testid": "mail-preview-attachments", children: attachments.map((line) => (_jsx("span", { className: "rounded border bg-muted px-2 py-0.5 text-xs", children: line }, line))) }) }))] }), format === "html" ? (_jsx("iframe", { title: t("documentDesigner.mail.preview.frameTitle"), sandbox: "", srcDoc: preview.displayHtml ?? preview.html, className: `h-[560px] border-0 bg-background ${width === "mobile" ? "w-[375px] self-center" : "w-full"}`, "data-testid": "mail-preview-iframe" })) : (_jsx("pre", { className: "h-[560px] overflow-auto whitespace-pre-wrap p-4 font-mono text-sm", "data-testid": "mail-preview-text", children: preview.text ?? "" }))] }));
25
+ return (_jsxs("div", { className: "mx-auto flex min-h-0 w-full max-w-[680px] flex-1 flex-col border bg-card shadow-md", "data-testid": "mail-preview-frame", children: [_jsxs("div", { className: "grid grid-cols-[80px_minmax(0,1fr)] gap-y-1 border-b px-5 py-3.5 text-sm", children: [_jsx(HeadRow, { label: t("documentDesigner.mail.preview.from"), children: _jsx("span", { "data-testid": "mail-preview-from", children: fromName === "" ? t("documentDesigner.mail.props.senderDefault") : fromName }) }), recipient !== null && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.to"), children: _jsx("span", { "data-testid": "mail-preview-to", children: recipient }) })), _jsx(HeadRow, { label: t("documentDesigner.mail.meta.subject"), children: _jsx("span", { className: "font-medium", "data-testid": "mail-preview-subject", children: preview.subject ?? "" }) }), attachments.length > 0 && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.attachments"), children: _jsx("span", { className: "flex flex-col items-start gap-1", "data-testid": "mail-preview-attachments", children: attachments.map((line) => (_jsx("span", { className: "rounded border bg-muted px-2 py-0.5 text-xs", children: line }, line))) }) }))] }), format === "html" ? (_jsx("iframe", { title: t("documentDesigner.mail.preview.frameTitle"), sandbox: "", srcDoc: preview.displayHtml ?? preview.html, className: `min-h-[420px] flex-1 border-0 bg-background ${width === "mobile" ? "w-[375px] self-center" : "w-full"}`, "data-testid": "mail-preview-iframe" })) : (_jsx("pre", { className: "min-h-[420px] flex-1 overflow-auto whitespace-pre-wrap p-4 font-mono text-sm", "data-testid": "mail-preview-text", children: preview.text ?? "" }))] }));
27
26
  }
28
27
  // The preview mail goes to the example recipient of the sample values, not to the test-mail address.
29
28
  function recipientOf(preview, own) {
@@ -1,9 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { MAIL_TONES, PlaceholderLineEditor, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { useAppFeatures, useNav, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
6
- import { DEFAULT_LAYOUT_SLUG, TENANT_MAIL_SENDING_SCREEN_ID } from "../constants.js";
5
+ import { DEFAULT_LAYOUT_SLUG, TENANT_MAIL_SENDING_SCREEN_ID } from "./constants.js";
7
6
  import { labelPlaceholders } from "./mail-draft.js";
8
7
  import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
9
8
  import { usageParts } from "./used-by-label.js";
@@ -119,4 +119,8 @@ export type UpsertResponse = {
119
119
  export type SendTestResponse = {
120
120
  readonly subject: string;
121
121
  };
122
+ export type SendTestStatus = {
123
+ readonly canSend: boolean;
124
+ readonly reason: "no_channel" | "no_address" | null;
125
+ };
122
126
  export type PickedValues = Readonly<Record<string, unknown>>;
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
4
  import { useRef, useState } from "react";
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { formatWhen, useAppFeatures, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
3
  import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-renderer/headless";
5
4
  import { StatusBadge } from "@cosmicdrift/kumiko-renderer/web";
@@ -1,8 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
- import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-renderer/headless";
4
+ import { EMPTY_VALUE_PLACEHOLDER, pluralKey } from "@cosmicdrift/kumiko-renderer/headless";
6
5
  import { EmptyState, ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer/web";
7
6
  import { useState } from "react";
8
7
  import { originOf } from "./editor-header.js";
@@ -73,7 +72,7 @@ function originText(t, args) {
73
72
  }
74
73
  const usesKey = kind === "mail-layout" ? "mails" : "pdfs";
75
74
  if (source.tenant !== null) {
76
- return t(`documentDesigner.editor.origin.tenant.${usesKey}`, {
75
+ return t(pluralKey(`documentDesigner.editor.origin.tenant.${usesKey}`, userCount), {
77
76
  version: systemVersion,
78
77
  count: userCount,
79
78
  });
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { formatWhen, usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
5
4
  import { Drawer, EmptyState, LoadingState } from "@cosmicdrift/kumiko-renderer/web";
@@ -1,4 +1,4 @@
1
- import type { TemplateEditorKind } from "../constants.ts";
1
+ import type { TemplateEditorKind } from "./constants.ts";
2
2
  export type OverviewUsageView = {
3
3
  readonly feature: string;
4
4
  readonly notification: string;
@@ -16,6 +16,9 @@ export type OverviewRowView = {
16
16
  /** An open draft beside the served version, never another tenant's. */
17
17
  readonly draftVersion: number | null;
18
18
  readonly systemVersion: number | null;
19
+ /** SystemAdmin only: a stored tenant override that is not delivered. */
20
+ readonly lockedOverride: boolean;
21
+ readonly lockedOverrideI18nKey: string | null;
19
22
  readonly modifiedAt: string;
20
23
  readonly usedBy: readonly OverviewUsageView[];
21
24
  readonly usedByCount: number;
@@ -21,7 +21,7 @@ function usageViewOf(value) {
21
21
  const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
22
22
  const numberOr = (value, fallback) => typeof value === "number" ? value : fallback;
23
23
  export function parseOverviewRow(row) {
24
- const { id, title, layoutSlug, slug, kind, origin, locales, status, version, draftVersion, systemVersion, modifiedAt, usedBy, usedByCount, } = row;
24
+ const { id, title, layoutSlug, slug, kind, origin, locales, status, version, draftVersion, systemVersion, lockedOverride, lockedOverrideI18nKey, modifiedAt, usedBy, usedByCount, } = row;
25
25
  if (typeof slug !== "string" || !isTemplateKind(kind) || !isStatus(status))
26
26
  return null;
27
27
  if (origin !== "tenant" && origin !== "system")
@@ -41,6 +41,8 @@ export function parseOverviewRow(row) {
41
41
  version: numberOr(version, 0),
42
42
  draftVersion: numberOr(draftVersion, null),
43
43
  systemVersion: numberOr(systemVersion, null),
44
+ lockedOverride: lockedOverride === true,
45
+ lockedOverrideI18nKey: typeof lockedOverrideI18nKey === "string" ? lockedOverrideI18nKey : null,
44
46
  modifiedAt: stringOr(modifiedAt, ""),
45
47
  usedBy: usages,
46
48
  usedByCount: numberOr(usedByCount, usages.length),
@@ -12,6 +12,7 @@ export type PageCanvasProps = {
12
12
  readonly systemVariables: readonly string[];
13
13
  readonly systemLabels: Readonly<Record<string, string>>;
14
14
  readonly variableNames: readonly string[];
15
+ readonly imageVariables: readonly string[];
15
16
  readonly resourcePreviews: Readonly<Record<string, DocumentResourcePreview>>;
16
17
  readonly onEditorReady: (handle: DocumentEditorHandle | null) => void;
17
18
  readonly resourceSrc?: (key: string) => string | undefined;
@@ -1,5 +1,4 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { DocumentContentEditor, PDF_DEFAULT_MARGIN_MM, PDF_DOCUMENT_BLOCKS, PDF_FOOTER_MARGIN_MM, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
3
  import { useTranslation } from "@cosmicdrift/kumiko-renderer";
5
4
  import { cn } from "@cosmicdrift/kumiko-renderer/web";
@@ -44,5 +43,5 @@ export function PageCanvas(props) {
44
43
  }
45
44
  }, [meta.margins, hasFooter]);
46
45
  const renderPaper = (editable) => (_jsxs("div", { className: "flex w-full flex-col items-center gap-2", children: [_jsxs("div", { "data-testid": "document-designer-page", className: cn("flex min-h-[1000px] w-full flex-col bg-card shadow-md", PAGE_WIDTH_CLASSES[`${meta.pageFormat}-${meta.orientation}`]), children: [showLetterhead && props.regions !== null && (_jsx(HeaderBand, { html: props.regions.header, systemLabels: props.systemLabels, resourceSrc: props.resourceSrc, caption: fromLayout })), _jsxs("div", { ref: applyMargins, className: cn("flex min-h-0 flex-1 flex-col pr-(--page-margin-right) pb-(--page-margin-bottom) pl-(--page-margin-left)", !showLetterhead && "pt-(--page-margin-top)"), children: [_jsxs("div", { className: "relative my-2 min-h-0 flex-1 border border-dashed border-primary/40 p-2 pt-5", "data-testid": "document-designer-content-area", children: [_jsx("span", { className: "absolute left-2 top-0.5 text-[10px] uppercase tracking-wide text-primary", children: t("documentDesigner.editor.region.content") }), editable] }), showRegions && props.regions !== null && (_jsxs(_Fragment, { children: [_jsx(RegionBlock, { html: props.regions.signature, systemLabels: props.systemLabels, resourceSrc: props.resourceSrc, caption: regionCaption("signature") }), _jsx(RegionBlock, { html: props.regions.footer, systemLabels: props.systemLabels, resourceSrc: props.resourceSrc, caption: regionCaption("footer") })] }))] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t("documentDesigner.canvas.caption", { format: meta.pageFormat }) })] }));
47
- return (_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", "data-testid": "pdf-editor-center", children: _jsx(DocumentContentEditor, { id: "document-designer-content", value: props.content, onChange: props.onContentChange, variables: props.variableNames, readOnly: false, blocks: PDF_DOCUMENT_BLOCKS, variableLabels: props.variableLabels, systemVariables: props.systemVariables, resourcePreviews: props.resourcePreviews, chrome: "canvas", renderPaper: renderPaper, onEditorReady: props.onEditorReady }) }));
46
+ return (_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", "data-testid": "pdf-editor-center", children: _jsx(DocumentContentEditor, { id: "document-designer-content", value: props.content, onChange: props.onContentChange, variables: props.variableNames, imageVariables: props.imageVariables, readOnly: false, blocks: PDF_DOCUMENT_BLOCKS, variableLabels: props.variableLabels, systemVariables: props.systemVariables, resourcePreviews: props.resourcePreviews, chrome: "canvas", renderPaper: renderPaper, onEditorReady: props.onEditorReady }) }));
48
47
  }
@@ -1,8 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
2
  export declare function visibleSystemPlaceholders(names: readonly string[]): readonly string[];
3
- export declare function PdfResourceSection({ resourceKeys, canUpload, layoutName, onInsert, onUpload, }: {
3
+ export declare function PdfResourceSection({ resourceKeys, canUpload, filesAvailable, layoutName, onInsert, onUpload, }: {
4
4
  readonly resourceKeys: readonly string[];
5
5
  readonly canUpload: boolean;
6
+ readonly filesAvailable: boolean;
6
7
  readonly layoutName: string;
7
8
  readonly onInsert: (key: string) => void;
8
9
  readonly onUpload: (file: File) => Promise<void>;
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
2
  import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
3
  import { UploadZone } from "@cosmicdrift/kumiko-renderer/web";
5
4
  import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
@@ -9,8 +8,8 @@ export function visibleSystemPlaceholders(names) {
9
8
  return names.filter((name) => !HIDDEN_SYSTEM_PLACEHOLDERS.has(name));
10
9
  }
11
10
  const IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
12
- export function PdfResourceSection({ resourceKeys, canUpload, layoutName, onInsert, onUpload, }) {
11
+ export function PdfResourceSection({ resourceKeys, canUpload, filesAvailable, layoutName, onInsert, onUpload, }) {
13
12
  const t = useTranslation();
14
13
  const { Button } = usePrimitives();
15
- return (_jsxs(RailSection, { title: t("documentDesigner.resources.title"), testId: "pdf-editor-resources", children: [resourceKeys.length === 0 && _jsx(RailRow, { label: t("documentDesigner.resources.empty") }), resourceKeys.map((key) => (_jsx(RailRow, { testId: `pdf-resource-${key}`, symbol: "\u25A3", label: key, onPress: () => onInsert(key) }, key))), _jsx(RailField, { children: canUpload ? (_jsx(UploadZone, { multiple: false, accept: IMAGE_TYPES, title: t("documentDesigner.resources.upload"), onUpload: onUpload, testId: "document-designer-upload" })) : (_jsx(Button, { variant: "secondary", size: "sm", disabled: true, children: t("documentDesigner.resources.upload") })) }), !canUpload && _jsx(RailHint, { children: t("documentDesigner.resources.saveFirst") }), _jsx(RailHint, { children: t("documentDesigner.placeholders.layoutHint", { layout: layoutName }) })] }));
14
+ return (_jsxs(RailSection, { title: t("documentDesigner.resources.title"), testId: "pdf-editor-resources", children: [resourceKeys.length === 0 && _jsx(RailRow, { label: t("documentDesigner.resources.empty") }), resourceKeys.map((key) => (_jsx(RailRow, { testId: `pdf-resource-${key}`, symbol: "\u25A3", label: key, onPress: () => onInsert(key) }, key))), _jsx(RailField, { children: canUpload && filesAvailable ? (_jsx(UploadZone, { multiple: false, accept: IMAGE_TYPES, title: t("documentDesigner.resources.upload"), onUpload: onUpload, testId: "document-designer-upload" })) : (_jsx(Button, { variant: "secondary", size: "sm", disabled: true, children: t("documentDesigner.resources.upload") })) }), !filesAvailable && _jsx(RailHint, { children: t("documentDesigner.resources.noFilesFeature") }), filesAvailable && !canUpload && (_jsx(RailHint, { children: t("documentDesigner.resources.saveFirst") })), _jsx(RailHint, { children: t("documentDesigner.placeholders.layoutHint", { layout: layoutName }) })] }));
16
15
  }
@@ -1,4 +1,3 @@
1
- // @runtime client
2
1
  import { PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
3
2
  import { isOneOf, isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
3
  export const MARGIN_SIDES = [