@cosmicdrift/kumiko-document-designer 0.0.0-stage → 0.363.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 (114) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +37 -2
  3. package/dist/constants.d.ts +24 -0
  4. package/dist/constants.js +33 -0
  5. package/dist/feature.d.ts +2 -0
  6. package/dist/feature.js +85 -0
  7. package/dist/i18n.d.ts +6 -0
  8. package/dist/i18n.js +670 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/list-definition.d.ts +6 -0
  12. package/dist/list-definition.js +109 -0
  13. package/dist/web/cells.d.ts +10 -0
  14. package/dist/web/cells.js +87 -0
  15. package/dist/web/client-plugin.d.ts +12 -0
  16. package/dist/web/client-plugin.js +43 -0
  17. package/dist/web/compose-layout.d.ts +6 -0
  18. package/dist/web/compose-layout.js +13 -0
  19. package/dist/web/errors.d.ts +6 -0
  20. package/dist/web/errors.js +24 -0
  21. package/dist/web/field-issue-text.d.ts +4 -0
  22. package/dist/web/field-issue-text.js +12 -0
  23. package/dist/web/file-name.d.ts +2 -0
  24. package/dist/web/file-name.js +17 -0
  25. package/dist/web/i18n.d.ts +2 -0
  26. package/dist/web/i18n.js +170 -0
  27. package/dist/web/index.d.ts +6 -0
  28. package/dist/web/index.js +7 -0
  29. package/dist/web/layout-editor-body.d.ts +14 -0
  30. package/dist/web/layout-editor-body.js +142 -0
  31. package/dist/web/layout-editor.d.ts +2 -0
  32. package/dist/web/layout-editor.js +41 -0
  33. package/dist/web/layout-html.d.ts +7 -0
  34. package/dist/web/layout-html.js +81 -0
  35. package/dist/web/layout-rails.d.ts +18 -0
  36. package/dist/web/layout-rails.js +107 -0
  37. package/dist/web/layout-types.d.ts +62 -0
  38. package/dist/web/layout-types.js +1 -0
  39. package/dist/web/layout-users.d.ts +3 -0
  40. package/dist/web/layout-users.js +8 -0
  41. package/dist/web/mail-dialogs.d.ts +29 -0
  42. package/dist/web/mail-dialogs.js +15 -0
  43. package/dist/web/mail-draft.d.ts +17 -0
  44. package/dist/web/mail-draft.js +132 -0
  45. package/dist/web/mail-edit-center.d.ts +17 -0
  46. package/dist/web/mail-edit-center.js +11 -0
  47. package/dist/web/mail-editor-body.d.ts +18 -0
  48. package/dist/web/mail-editor-body.js +193 -0
  49. package/dist/web/mail-editor.d.ts +2 -0
  50. package/dist/web/mail-editor.js +71 -0
  51. package/dist/web/mail-footer.d.ts +21 -0
  52. package/dist/web/mail-footer.js +44 -0
  53. package/dist/web/mail-header.d.ts +23 -0
  54. package/dist/web/mail-header.js +16 -0
  55. package/dist/web/mail-left-rail.d.ts +18 -0
  56. package/dist/web/mail-left-rail.js +53 -0
  57. package/dist/web/mail-origin-bar.d.ts +17 -0
  58. package/dist/web/mail-origin-bar.js +84 -0
  59. package/dist/web/mail-pdf-link-modal.d.ts +11 -0
  60. package/dist/web/mail-pdf-link-modal.js +54 -0
  61. package/dist/web/mail-preview-side.d.ts +12 -0
  62. package/dist/web/mail-preview-side.js +48 -0
  63. package/dist/web/mail-preview.d.ts +20 -0
  64. package/dist/web/mail-preview.js +43 -0
  65. package/dist/web/mail-right-rail.d.ts +19 -0
  66. package/dist/web/mail-right-rail.js +47 -0
  67. package/dist/web/mail-types.d.ts +112 -0
  68. package/dist/web/mail-types.js +1 -0
  69. package/dist/web/mail-upload-modal.d.ts +16 -0
  70. package/dist/web/mail-upload-modal.js +56 -0
  71. package/dist/web/mail-versions.d.ts +6 -0
  72. package/dist/web/mail-versions.js +43 -0
  73. package/dist/web/origin-bar.d.ts +13 -0
  74. package/dist/web/origin-bar.js +82 -0
  75. package/dist/web/overrides-drawer.d.ts +9 -0
  76. package/dist/web/overrides-drawer.js +19 -0
  77. package/dist/web/overview-row.d.ts +21 -0
  78. package/dist/web/overview-row.js +47 -0
  79. package/dist/web/page-canvas.d.ts +18 -0
  80. package/dist/web/page-canvas.js +38 -0
  81. package/dist/web/pdf-meta.d.ts +23 -0
  82. package/dist/web/pdf-meta.js +101 -0
  83. package/dist/web/pdf-preview.d.ts +4 -0
  84. package/dist/web/pdf-preview.js +99 -0
  85. package/dist/web/pdf-template-editor.d.ts +8 -0
  86. package/dist/web/pdf-template-editor.js +326 -0
  87. package/dist/web/placeholder-panel.d.ts +21 -0
  88. package/dist/web/placeholder-panel.js +35 -0
  89. package/dist/web/preview-record-picker.d.ts +12 -0
  90. package/dist/web/preview-record-picker.js +7 -0
  91. package/dist/web/rail.d.ts +18 -0
  92. package/dist/web/rail.js +14 -0
  93. package/dist/web/settings-panel.d.ts +17 -0
  94. package/dist/web/settings-panel.js +25 -0
  95. package/dist/web/status-label.d.ts +7 -0
  96. package/dist/web/status-label.js +18 -0
  97. package/dist/web/template-rows.d.ts +9 -0
  98. package/dist/web/template-rows.js +34 -0
  99. package/dist/web/types.d.ts +103 -0
  100. package/dist/web/types.js +3 -0
  101. package/dist/web/use-insert-targets.d.ts +9 -0
  102. package/dist/web/use-insert-targets.js +17 -0
  103. package/dist/web/use-mail-persistence.d.ts +15 -0
  104. package/dist/web/use-mail-persistence.js +49 -0
  105. package/dist/web/use-overview-rows.d.ts +7 -0
  106. package/dist/web/use-overview-rows.js +19 -0
  107. package/dist/web/use-settled-value.d.ts +2 -0
  108. package/dist/web/use-settled-value.js +18 -0
  109. package/dist/web/use-test-mail.d.ts +15 -0
  110. package/dist/web/use-test-mail.js +27 -0
  111. package/dist/web/used-by-label.d.ts +9 -0
  112. package/dist/web/used-by-label.js +32 -0
  113. package/package.json +63 -4
  114. package/src/changes.json +20 -0
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from "react";
2
+ import type { TemplateRowView } from "./mail-types.js";
3
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
4
+ export declare function languageName(t: Translate, locale: string): string;
5
+ export declare function MailOriginBar({ row, title, systemVersion, slug, locale, fallbackLocale, isSystemAdmin, creating, onCreateBlank, onReload, }: {
6
+ readonly row: TemplateRowView | null;
7
+ readonly title: string;
8
+ readonly systemVersion: number | null;
9
+ readonly slug: string;
10
+ readonly locale: string;
11
+ readonly fallbackLocale: string | null;
12
+ readonly isSystemAdmin: boolean;
13
+ readonly creating: boolean;
14
+ readonly onCreateBlank: () => void;
15
+ readonly onReload: () => void;
16
+ }): ReactNode;
17
+ export {};
@@ -0,0 +1,84 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { dispatcherErrorText, formatWhen, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { useState } from "react";
6
+ import { DiffPanel } from "./origin-bar.js";
7
+ export function languageName(t, locale) {
8
+ const key = `documentDesigner.mail.language.${locale}`;
9
+ const translated = t(key);
10
+ return translated === key ? locale : translated;
11
+ }
12
+ function statusSentence(t, row) {
13
+ if (row.status === "active") {
14
+ return t("documentDesigner.mail.origin.activeSince", {
15
+ when: row.modifiedAt === null ? "–" : formatWhen(row.modifiedAt),
16
+ });
17
+ }
18
+ return t(row.status === "archived"
19
+ ? "documentDesigner.mail.origin.archived"
20
+ : "documentDesigner.mail.origin.notPublished");
21
+ }
22
+ const BAR_CLASS = "flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border px-4 py-2.5";
23
+ function MissingLocaleBar({ slug, locale, fallbackLocale, copyScope, creating, onCreateBlank, onCopied, }) {
24
+ const t = useTranslation();
25
+ const { Banner, Button, Text } = usePrimitives();
26
+ const copyLocale = useMutation(TemplateResolverHandlers.copyLocale);
27
+ const [error, setError] = useState(null);
28
+ if (creating) {
29
+ return (_jsx("div", { className: `${BAR_CLASS} bg-muted`, "data-testid": "mail-origin-bar", children: _jsx(Text, { children: t("documentDesigner.mail.origin.creating", { language: languageName(t, locale) }) }) }));
30
+ }
31
+ const copy = async (from) => {
32
+ setError(null);
33
+ const result = await copyLocale.mutate({
34
+ slug,
35
+ kind: "mail",
36
+ fromLocale: from,
37
+ toLocale: locale,
38
+ overwrite: false,
39
+ scope: copyScope,
40
+ });
41
+ if (result.isSuccess)
42
+ onCopied();
43
+ else
44
+ setError(dispatcherErrorText(result.error, t));
45
+ };
46
+ return (_jsxs("div", { className: `${BAR_CLASS} border-status-warn bg-status-warn-surface`, "data-testid": "mail-origin-bar", "data-variant": "warn", children: [_jsxs(Text, { children: [t(fallbackLocale === null
47
+ ? "documentDesigner.mail.origin.missingNoFallback"
48
+ : "documentDesigner.mail.origin.missing", {
49
+ language: languageName(t, locale),
50
+ locale,
51
+ fallback: fallbackLocale ?? "",
52
+ }), fallbackLocale !== null && ` ${t("documentDesigner.mail.origin.copyNote")}`] }), _jsx("div", { className: "grow" }), fallbackLocale !== null && (_jsx(Button, { size: "sm", variant: "secondary", loading: copyLocale.pending, onClick: () => copy(fallbackLocale), testId: "mail-copy-locale", children: t("documentDesigner.mail.origin.copyFrom", { fallback: fallbackLocale }) })), _jsx(Button, { size: "sm", variant: "secondary", onClick: onCreateBlank, testId: "mail-create-blank", children: t("documentDesigner.mail.origin.createBlank") }), error !== null && (_jsx(Banner, { variant: "error", testId: "mail-copy-error", children: error }))] }));
53
+ }
54
+ function TenantOriginBar({ row, title, systemVersion, onReset, }) {
55
+ const t = useTranslation();
56
+ const { Button, Dialog, Drawer, Modal, Text } = usePrimitives();
57
+ const [diffOpen, setDiffOpen] = useState(false);
58
+ const [resetOpen, setResetOpen] = useState(false);
59
+ const resetToSystem = useMutation(TemplateResolverHandlers.resetToSystem);
60
+ const hasSystem = systemVersion !== null;
61
+ const diffTitle = t("documentDesigner.editor.diff.title");
62
+ const diffBody = _jsx(DiffPanel, { tenantId: row.id });
63
+ return (_jsxs("div", { className: `${BAR_CLASS} bg-muted`, "data-testid": "mail-origin-bar", children: [_jsxs(Text, { children: [hasSystem
64
+ ? t("documentDesigner.mail.origin.overrides", { title, version: systemVersion })
65
+ : t("documentDesigner.mail.origin.tenantOnly"), " ", statusSentence(t, row)] }), _jsx("div", { className: "grow" }), hasSystem && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => setDiffOpen(true), testId: "document-designer-diff-open", children: t("documentDesigner.editor.diff.open") }), _jsx(Button, { variant: "link", onClick: () => setResetOpen(true), testId: "document-designer-reset-open", children: t("documentDesigner.mail.origin.reset") })] })), diffOpen &&
66
+ (Drawer === undefined ? (_jsx(Modal, { open: true, onOpenChange: setDiffOpen, title: diffTitle, children: diffBody })) : (_jsx(Drawer, { open: true, onOpenChange: setDiffOpen, title: diffTitle, children: diffBody }))), _jsx(Dialog, { open: resetOpen, onOpenChange: setResetOpen, title: t("documentDesigner.mail.reset.title"), description: t("documentDesigner.mail.reset.description"), confirmLabel: t("documentDesigner.mail.reset.confirm"), variant: "danger", onConfirm: async () => {
67
+ const result = await resetToSystem.mutate({ id: row.id });
68
+ if (result.isSuccess)
69
+ onReset();
70
+ } })] }));
71
+ }
72
+ export function MailOriginBar({ row, title, systemVersion, slug, locale, fallbackLocale, isSystemAdmin, creating, onCreateBlank, onReload, }) {
73
+ const t = useTranslation();
74
+ const { Text } = usePrimitives();
75
+ if (row === null) {
76
+ return (_jsx(MissingLocaleBar, { slug: slug, locale: locale, fallbackLocale: fallbackLocale, copyScope: isSystemAdmin ? "system" : "tenant", creating: creating, onCreateBlank: onCreateBlank, onCopied: onReload }));
77
+ }
78
+ if (row.scope === "tenant") {
79
+ return (_jsx(TenantOriginBar, { row: row, title: title, systemVersion: systemVersion, onReset: onReload }));
80
+ }
81
+ return (_jsx("div", { className: `${BAR_CLASS} bg-muted`, "data-testid": "mail-origin-bar", children: _jsx(Text, { children: t(isSystemAdmin
82
+ ? "documentDesigner.mail.origin.platform"
83
+ : "documentDesigner.mail.origin.system", { version: row.version }) }) }));
84
+ }
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from "react";
2
+ import type { OverviewRowView } from "./overview-row.js";
3
+ export declare function PdfLinkModal({ open, onOpenChange, templates, ...form }: {
4
+ readonly open: boolean;
5
+ readonly onOpenChange: (open: boolean) => void;
6
+ readonly templates: readonly OverviewRowView[];
7
+ readonly variableLabels: Readonly<Record<string, string>>;
8
+ readonly templateId: string;
9
+ readonly takenKeys: readonly string[];
10
+ readonly onLinked: () => void;
11
+ }): ReactNode;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { PlaceholderLineEditor, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { EmptyState } from "@cosmicdrift/kumiko-renderer-web";
6
+ import { useState } from "react";
7
+ import { lastSlugSegment, parseJsonRecord, uniqueResourceKey } from "./mail-draft.js";
8
+ function pdfRowIdOf(summaries, slug) {
9
+ const ofSlug = (summaries ?? []).filter((row) => row.slug === slug);
10
+ return (ofSlug.find((row) => row.scope === "tenant") ?? ofSlug[0])?.id ?? null;
11
+ }
12
+ function PdfLinkForm({ templates, variableLabels, templateId, takenKeys, onLinked, }) {
13
+ const t = useTranslation();
14
+ const { Banner, Button, Field, Input } = usePrimitives();
15
+ const [slug, setSlug] = useState(templates[0]?.slug ?? "");
16
+ const [editedName, setEditedName] = useState(null);
17
+ const [error, setError] = useState(null);
18
+ const linkPdf = useMutation(TemplateResolverHandlers.resourceLinkPdf);
19
+ const summaries = useQuery(TemplateResolverQueries.list, {
20
+ kind: "document-pdf",
21
+ includeSystem: true,
22
+ });
23
+ const rowId = pdfRowIdOf(summaries.data, slug);
24
+ const row = useQuery(TemplateResolverQueries.findById, { id: rowId ?? "" }, { enabled: rowId !== null });
25
+ const metaName = parseJsonRecord(row.data?.meta)["fileName"];
26
+ const defaultName = typeof metaName === "string" && metaName !== "" ? metaName : `${lastSlugSegment(slug)}.pdf`;
27
+ const fileName = editedName ?? defaultName;
28
+ const link = async () => {
29
+ setError(null);
30
+ const result = await linkPdf.mutate({
31
+ templateId,
32
+ key: uniqueResourceKey(lastSlugSegment(slug), takenKeys),
33
+ templateSlug: slug,
34
+ fileName,
35
+ });
36
+ if (!result.isSuccess) {
37
+ setError(dispatcherErrorText(result.error, t));
38
+ return;
39
+ }
40
+ onLinked();
41
+ };
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
+ setSlug(next);
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") }) })] }));
46
+ }
47
+ export function PdfLinkModal({ open, onOpenChange, templates, ...form }) {
48
+ const t = useTranslation();
49
+ const { Modal } = usePrimitives();
50
+ return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("documentDesigner.mail.pdfLink.title"), testId: "mail-pdf-link-modal", children: templates.length === 0 ? (_jsx(EmptyState, { title: t("documentDesigner.mail.pdfLink.none") })) : (_jsx(PdfLinkForm, { templates: templates, ...form, onLinked: () => {
51
+ onOpenChange(false);
52
+ form.onLinked();
53
+ } })) }));
54
+ }
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { MailPreview, MailValidation } from "./mail-types.js";
3
+ export declare function PreviewSide({ preview, validation, slug, locale, testMail, }: {
4
+ readonly preview: MailPreview | null;
5
+ readonly validation: MailValidation | null;
6
+ readonly slug: string;
7
+ readonly locale: string;
8
+ readonly testMail: {
9
+ readonly pending: boolean;
10
+ readonly onSend: () => void;
11
+ };
12
+ }): ReactNode;
@@ -0,0 +1,48 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
+ import { useState } from "react";
5
+ import { displayValue } from "./mail-draft.js";
6
+ import { usePreviewRecordPicker } from "./preview-record-picker.js";
7
+ import { RailHint, RailRow, RailSection } from "./rail.js";
8
+ // Check params list names as arrays; the message templates expect one string.
9
+ function checkText(t, check) {
10
+ const params = Object.fromEntries(Object.entries(check.params ?? {}).map(([name, value]) => [
11
+ name,
12
+ Array.isArray(value) ? value.join(", ") : value,
13
+ ]));
14
+ const translated = t(check.i18nKey, params);
15
+ return translated === check.i18nKey ? check.code : translated;
16
+ }
17
+ function SampleValues({ preview, validation, picked, }) {
18
+ const t = useTranslation();
19
+ const values = { ...(preview?.sampleValues ?? {}), ...(picked ?? {}) };
20
+ const names = Object.keys(validation?.variables ?? values);
21
+ return (_jsx(_Fragment, { children: names.map((name) => {
22
+ const missing = validation?.missingRequired.includes(name) === true;
23
+ return (_jsx(RailRow, { testId: `mail-sample-${name}`, label: validation?.variables[name]?.label ?? name, bad: missing, trailing: missing
24
+ ? t("documentDesigner.mail.preview.missingInContent")
25
+ : displayValue(t, values[name]) }, name));
26
+ }) }));
27
+ }
28
+ function RecordPickerLink({ slug, locale, onPick, }) {
29
+ const picker = usePreviewRecordPicker();
30
+ const { Button, Modal } = usePrimitives();
31
+ const [open, setOpen] = useState(false);
32
+ if (picker === null)
33
+ return null;
34
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => setOpen(true), testId: "mail-record-picker-open", children: picker.label }), _jsx(Modal, { open: open, onOpenChange: setOpen, title: picker.label, testId: "mail-record-picker-modal", children: picker.renderPicker({
35
+ slug,
36
+ locale,
37
+ onPick: (values) => {
38
+ onPick(values);
39
+ setOpen(false);
40
+ },
41
+ }) })] }));
42
+ }
43
+ export function PreviewSide({ preview, validation, slug, locale, testMail, }) {
44
+ const t = useTranslation();
45
+ const { Button, Field, Text } = usePrimitives();
46
+ const [picked, setPicked] = useState(null);
47
+ return (_jsxs("div", { className: "flex flex-col", "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: setPicked }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsxs("div", { className: "flex flex-col gap-2 px-2", children: [_jsx(Field, { id: "mail-test-recipient", label: t("documentDesigner.mail.testMail.recipient"), children: _jsx(Text, { children: t("documentDesigner.mail.testMail.ownAddress") }) }), _jsxs(Text, { variant: "muted", children: ["\u2713 ", t("documentDesigner.mail.testMail.prefix")] }), _jsxs(Text, { variant: "muted", children: ["\u2713 ", t("documentDesigner.mail.testMail.attachments")] }), _jsx(Button, { variant: "secondary", 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") })] }), _jsx(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), bad: !check.ok }, check.code))) })] }));
48
+ }
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from "react";
2
+ import type { MailValidation } from "./mail-types.js";
3
+ export type PreviewPayload = {
4
+ readonly slug: string;
5
+ readonly kind: "mail";
6
+ readonly locale: string;
7
+ readonly content: string;
8
+ readonly meta: Readonly<Record<string, unknown>>;
9
+ readonly variableSchema: unknown;
10
+ };
11
+ export declare function MailPreviewMode({ payload, validation, slug, locale, testMail, }: {
12
+ readonly payload: PreviewPayload;
13
+ readonly validation: MailValidation | null;
14
+ readonly slug: string;
15
+ readonly locale: string;
16
+ readonly testMail: {
17
+ readonly pending: boolean;
18
+ readonly onSend: () => void;
19
+ };
20
+ }): ReactNode;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { dispatcherErrorText, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { LoadingState, ModeSwitch } from "@cosmicdrift/kumiko-renderer-web";
6
+ import { useState } from "react";
7
+ import { formatKilobytes } from "./mail-draft.js";
8
+ import { PreviewSide } from "./mail-preview-side.js";
9
+ function attachmentText(t, attachment) {
10
+ switch (attachment.kind) {
11
+ case "inline":
12
+ return null;
13
+ case "file":
14
+ return `⎘ ${attachment.name} · ${formatKilobytes(attachment.sizeBytes)}`;
15
+ case "rendered-pdf":
16
+ return `⎘ ${attachment.name} · ${t("documentDesigner.mail.preview.generatedOnSend")}`;
17
+ }
18
+ }
19
+ function MailFrame({ preview, format, width, }) {
20
+ const t = useTranslation();
21
+ const attachments = preview.attachments.flatMap((a) => attachmentText(t, a) ?? []);
22
+ return (_jsxs("div", { className: "flex flex-col overflow-hidden rounded-md border", "data-testid": "mail-preview-frame", children: [_jsxs("div", { className: "flex flex-col gap-1 border-b bg-muted px-4 py-3 text-sm", children: [_jsxs("div", { className: "flex gap-3", children: [_jsx("span", { className: "w-16 shrink-0 text-muted-foreground", children: t("documentDesigner.mail.meta.subject") }), _jsx("span", { className: "font-medium", "data-testid": "mail-preview-subject", children: preview.subject ?? "" })] }), attachments.length > 0 && (_jsxs("div", { className: "flex gap-3", children: [_jsx("span", { className: "w-16 shrink-0 text-muted-foreground", children: t("documentDesigner.mail.preview.attachments") }), _jsx("span", { className: "flex flex-col", "data-testid": "mail-preview-attachments", children: attachments.map((line) => (_jsx("span", { 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 ?? "" }))] }));
23
+ }
24
+ function layoutText(t, preview) {
25
+ return preview?.layout == null
26
+ ? t("documentDesigner.mail.preview.barPlain")
27
+ : t("documentDesigner.mail.preview.bar", { layout: preview.layout.slug });
28
+ }
29
+ export function MailPreviewMode({ payload, validation, slug, locale, testMail, }) {
30
+ const t = useTranslation();
31
+ const { Banner } = usePrimitives();
32
+ const [format, setFormat] = useState("html");
33
+ const [width, setWidth] = useState("desktop");
34
+ const query = useQuery(TemplateResolverQueries.preview, payload);
35
+ const preview = query.data;
36
+ return (_jsxs("div", { className: "flex flex-col gap-3", "data-testid": "mail-editor-preview", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border bg-muted px-4 py-2.5", children: [_jsx("span", { className: "text-sm", "data-testid": "mail-preview-bar", children: layoutText(t, preview) }), _jsx("div", { className: "grow" }), _jsx(ModeSwitch, { value: format, onChange: setFormat, ariaLabel: t("documentDesigner.mail.preview.formatSwitch"), testId: "mail-preview-format", options: [
37
+ { value: "html", label: t("documentDesigner.mail.preview.html") },
38
+ { value: "text", label: t("documentDesigner.mail.preview.text") },
39
+ ] }), _jsx(ModeSwitch, { value: width, onChange: setWidth, ariaLabel: t("documentDesigner.mail.preview.widthSwitch"), testId: "mail-preview-width", options: [
40
+ { value: "desktop", label: t("documentDesigner.mail.preview.desktop") },
41
+ { value: "mobile", label: t("documentDesigner.mail.preview.mobile") },
42
+ ] })] }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[minmax(0,1fr)_300px]", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [query.error !== null && (_jsx(Banner, { variant: "error", testId: "mail-preview-error", children: dispatcherErrorText(query.error, t) })), preview === null ? (query.error === null && _jsx(LoadingState, {})) : (_jsx(MailFrame, { preview: preview, format: format, width: width }))] }), _jsx(PreviewSide, { preview: preview, validation: validation, slug: slug, locale: locale, testMail: testMail })] })] }));
43
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+ import type { LinkedResourceView, MailDraft } from "./mail-types.js";
3
+ import type { OverviewRowView } from "./overview-row.js";
4
+ export declare function layoutOptionLabel(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, layout: OverviewRowView): string;
5
+ export declare function AttachmentSection({ attachments, pdfTitles, canEdit, onAddPdf, onRemove, }: {
6
+ readonly attachments: readonly LinkedResourceView[];
7
+ readonly pdfTitles: ReadonlyMap<string, string>;
8
+ readonly canEdit: boolean;
9
+ readonly onAddPdf: () => void;
10
+ readonly onRemove: (key: string) => void;
11
+ }): ReactNode;
12
+ export declare function PropertiesSection({ draft, layouts, onChange, }: {
13
+ readonly draft: MailDraft;
14
+ readonly layouts: readonly OverviewRowView[];
15
+ readonly onChange: (patch: Partial<MailDraft>) => void;
16
+ }): ReactNode;
17
+ export declare function UsageSection({ slug }: {
18
+ readonly slug: string;
19
+ }): ReactNode;
@@ -0,0 +1,47 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
5
+ import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
6
+ import { RailHint, RailRow, RailSection } from "./rail.js";
7
+ import { usageParts } from "./used-by-label.js";
8
+ export function layoutOptionLabel(t, layout) {
9
+ return t(layout.origin === "tenant"
10
+ ? "documentDesigner.mail.props.layoutTenant"
11
+ : "documentDesigner.mail.props.layoutSystem", { title: layout.title, version: layout.version });
12
+ }
13
+ function UsageRows({ slug }) {
14
+ const t = useTranslation();
15
+ const { data } = useQuery(TemplateResolverQueries.usage, {
16
+ slug,
17
+ kind: "mail",
18
+ });
19
+ return (_jsx(_Fragment, { children: (data?.usedBy ?? []).map((usage) => {
20
+ const parts = usageParts(t, usage);
21
+ return (_jsx(RailRow, { label: t("documentDesigner.mail.usage.line", parts), testId: "mail-usage-row" }, `${usage.feature}:${usage.notification}`));
22
+ }) }));
23
+ }
24
+ export function AttachmentSection({ attachments, pdfTitles, canEdit, onAddPdf, onRemove, }) {
25
+ const t = useTranslation();
26
+ const { Button } = usePrimitives();
27
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.attachments.title"), testId: "mail-editor-attachments", action: _jsx(Button, { variant: "link", disabled: !canEdit, onClick: onAddPdf, testId: "mail-attachment-pdf-open", children: t("documentDesigner.mail.attachments.addPdf") }), children: [attachments.length === 0 && (_jsx(RailHint, { children: t("documentDesigner.mail.attachments.none") })), attachments.map((attachment) => (_jsxs("div", { className: "flex items-center gap-1", "data-testid": "mail-attachment-row", children: [_jsx("div", { className: "min-w-0 grow", children: attachment.kind === "pdf" ? (_jsx(RailRow, { symbol: "\u2398", label: t("documentDesigner.mail.attachments.pdfRow", {
28
+ title: pdfTitles.get(attachment.templateSlug) ?? attachment.templateSlug,
29
+ }), trailing: attachment.fileName })) : (_jsx(RailRow, { symbol: "\u2398", label: attachment.kind === "file" ? attachment.fileName : attachment.key, trailing: t("documentDesigner.mail.attachments.file") })) }), _jsx(Button, { variant: "danger-ghost", size: "sm", onClick: () => onRemove(attachment.key), testId: `mail-attachment-remove-${attachment.key}`, children: t("documentDesigner.mail.resource.remove") })] }, attachment.key)))] }));
30
+ }
31
+ export function PropertiesSection({ draft, layouts, onChange, }) {
32
+ const t = useTranslation();
33
+ const { Field, Input, Text } = usePrimitives();
34
+ const layoutValue = draft.layoutSlug === "" ? DEFAULT_LAYOUT_SLUG : draft.layoutSlug;
35
+ const categoryValue = draft.category === "" ? "transactional" : draft.category;
36
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.props.title"), testId: "mail-editor-properties", children: [_jsxs("div", { className: "flex flex-col gap-3 px-2", children: [_jsx(Field, { id: "mail-layout", label: t("documentDesigner.mail.props.layout"), children: _jsx(Input, { kind: "select", id: "mail-layout", name: "mail-layout", value: layoutValue, onChange: (next) => onChange({ layoutSlug: next }), options: layouts.map((layout) => ({
37
+ value: layout.slug,
38
+ label: layoutOptionLabel(t, layout),
39
+ })) }) }), _jsx(Field, { id: "mail-sender", label: t("documentDesigner.mail.props.sender"), children: _jsx(Text, { testId: "mail-sender", children: t("documentDesigner.mail.props.senderDefault") }) }), _jsx(Field, { id: "mail-category", label: t("documentDesigner.mail.props.category"), children: _jsx(Input, { kind: "select", id: "mail-category", name: "mail-category", value: categoryValue, onChange: (next) => onChange({ category: next === "marketing" ? "marketing" : "transactional" }), options: [
40
+ { value: "transactional", label: t("documentDesigner.mail.props.transactional") },
41
+ { value: "marketing", label: t("documentDesigner.mail.props.marketing") },
42
+ ] }) })] }), _jsx(RailHint, { children: t("documentDesigner.mail.props.categoryHint") })] }));
43
+ }
44
+ export function UsageSection({ slug }) {
45
+ const t = useTranslation();
46
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.usage.title"), testId: "mail-editor-usage", children: [_jsx(UsageRows, { slug: slug }), _jsx(RailHint, { children: t("documentDesigner.mail.usage.hint") })] }));
47
+ }
@@ -0,0 +1,112 @@
1
+ import type { ValidationIssue } from "./layout-types.js";
2
+ export type PlaceholderType = "text" | "money" | "date" | "link" | "list";
3
+ export type MailVariable = {
4
+ readonly type: PlaceholderType;
5
+ readonly label?: string;
6
+ readonly required?: boolean;
7
+ readonly example?: unknown;
8
+ readonly item?: Readonly<Record<string, {
9
+ readonly type: PlaceholderType;
10
+ readonly label?: string;
11
+ }>>;
12
+ };
13
+ export type MailWarning = {
14
+ readonly code: string;
15
+ readonly detail: string;
16
+ readonly i18nKey: string;
17
+ };
18
+ export type MailValidation = {
19
+ readonly canSave: boolean;
20
+ readonly canPublish: boolean;
21
+ readonly issues: readonly ValidationIssue[];
22
+ readonly publishIssues: readonly ValidationIssue[];
23
+ readonly used: readonly string[];
24
+ readonly unknown: readonly string[];
25
+ readonly missingRequired: readonly string[];
26
+ readonly warnings: readonly MailWarning[];
27
+ readonly variables: Readonly<Record<string, MailVariable>>;
28
+ readonly systemPlaceholders: readonly string[];
29
+ };
30
+ export type PreviewAttachment = {
31
+ readonly kind: "inline";
32
+ readonly name: string;
33
+ readonly sizeBytes: number;
34
+ } | {
35
+ readonly kind: "file";
36
+ readonly name: string;
37
+ readonly sizeBytes: number;
38
+ } | {
39
+ readonly kind: "rendered-pdf";
40
+ readonly name: string;
41
+ readonly templateSlug: string;
42
+ };
43
+ export type PreviewCheck = {
44
+ readonly code: string;
45
+ readonly ok: boolean;
46
+ readonly i18nKey: string;
47
+ readonly params?: Readonly<Record<string, unknown>>;
48
+ };
49
+ export type MailPreview = {
50
+ readonly subject: string | null;
51
+ readonly html: string;
52
+ readonly displayHtml?: string;
53
+ readonly text: string | null;
54
+ readonly attachments: readonly PreviewAttachment[];
55
+ readonly layout: {
56
+ readonly slug: string;
57
+ readonly scope: string;
58
+ } | null;
59
+ readonly sampleValues: Readonly<Record<string, unknown>>;
60
+ readonly checks: readonly PreviewCheck[];
61
+ };
62
+ export type TemplateRowSummary = {
63
+ readonly id: string;
64
+ readonly slug: string;
65
+ readonly kind: string;
66
+ readonly locale: string;
67
+ readonly scope: "tenant" | "system";
68
+ readonly status: string;
69
+ readonly modifiedAt: string;
70
+ };
71
+ export type TemplateRowView = {
72
+ readonly id: string;
73
+ readonly version: number;
74
+ readonly slug: string;
75
+ readonly locale: string;
76
+ readonly scope: "tenant" | "system";
77
+ readonly status: string;
78
+ readonly content: string;
79
+ readonly variableSchema: unknown;
80
+ readonly linkedResources: unknown;
81
+ readonly meta: unknown;
82
+ readonly modifiedAt: string | null;
83
+ };
84
+ export type MailCategory = "" | "transactional" | "marketing";
85
+ export type MailDraft = {
86
+ readonly subject: string;
87
+ readonly previewText: string;
88
+ readonly layoutSlug: string;
89
+ readonly category: MailCategory;
90
+ readonly body: string;
91
+ };
92
+ export type LinkedResourceView = {
93
+ readonly kind: "image";
94
+ readonly key: string;
95
+ readonly fileRefId: string;
96
+ } | {
97
+ readonly kind: "file";
98
+ readonly key: string;
99
+ readonly fileRefId: string;
100
+ readonly fileName: string;
101
+ } | {
102
+ readonly kind: "pdf";
103
+ readonly key: string;
104
+ readonly templateSlug: string;
105
+ readonly fileName: string;
106
+ };
107
+ export type UpsertResponse = {
108
+ readonly id: string;
109
+ };
110
+ export type SendTestResponse = {
111
+ readonly subject: string;
112
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from "react";
2
+ export type UploadedResource = {
3
+ readonly key: string;
4
+ readonly purpose: "image" | "attachment";
5
+ };
6
+ type UploadTarget = {
7
+ readonly templateId: string;
8
+ readonly takenKeys: readonly string[];
9
+ readonly onUploaded: (uploaded: UploadedResource) => void;
10
+ };
11
+ export declare function UploadModal({ open, onOpenChange, target, }: {
12
+ readonly open: boolean;
13
+ readonly onOpenChange: (open: boolean) => void;
14
+ readonly target: UploadTarget;
15
+ }): ReactNode;
16
+ export {};
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { dispatcherErrorText, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { useState } from "react";
6
+ import { fetchStoredFileName } from "./file-name.js";
7
+ import { uniqueResourceKey } from "./mail-draft.js";
8
+ const IMAGE_ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/webp"];
9
+ function useResourceUpload(target) {
10
+ const t = useTranslation();
11
+ const resourceUpload = useMutation(TemplateResolverHandlers.resourceUpload);
12
+ const [error, setError] = useState(null);
13
+ const [busy, setBusy] = useState(false);
14
+ const upload = async (fileRefId, purpose) => {
15
+ if (fileRefId === null)
16
+ return;
17
+ setError(null);
18
+ setBusy(true);
19
+ try {
20
+ const fileName = await fetchStoredFileName(fileRefId);
21
+ if (fileName === null) {
22
+ setError(t("documentDesigner.mail.upload.noFileName"));
23
+ return;
24
+ }
25
+ const key = uniqueResourceKey(fileName, target.takenKeys);
26
+ const result = await resourceUpload.mutate({
27
+ sourceFileRefId: fileRefId,
28
+ templateId: target.templateId,
29
+ key,
30
+ purpose,
31
+ ...(purpose === "attachment" && { fileName }),
32
+ });
33
+ if (!result.isSuccess) {
34
+ setError(dispatcherErrorText(result.error, t));
35
+ return;
36
+ }
37
+ target.onUploaded({ key, purpose });
38
+ }
39
+ finally {
40
+ setBusy(false);
41
+ }
42
+ };
43
+ return { upload, error, busy };
44
+ }
45
+ export function UploadModal({ open, onOpenChange, target, }) {
46
+ const t = useTranslation();
47
+ const { Banner, Field, Input, Modal, Text } = usePrimitives();
48
+ const { upload, error, busy } = useResourceUpload({
49
+ ...target,
50
+ onUploaded: (uploaded) => {
51
+ onOpenChange(false);
52
+ target.onUploaded(uploaded);
53
+ },
54
+ });
55
+ return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("documentDesigner.mail.upload.title"), testId: "mail-upload-modal", children: _jsxs("div", { className: "flex flex-col gap-4", children: [error !== null && (_jsx(Banner, { variant: "error", testId: "mail-upload-error", children: error })), busy && _jsx(Text, { variant: "muted", children: t("documentDesigner.mail.upload.busy") }), _jsx(Field, { id: "mail-upload-image", label: t("documentDesigner.mail.upload.image"), description: t("documentDesigner.mail.upload.imageHint"), children: _jsx(Input, { kind: "file", id: "mail-upload-image", name: "mail-upload-image", value: null, accept: IMAGE_ACCEPT, disabled: busy, onChange: (fileId) => void upload(fileId, "image") }) }), _jsx(Field, { id: "mail-upload-attachment", label: t("documentDesigner.mail.upload.attachment"), description: t("documentDesigner.mail.upload.attachmentHint"), children: _jsx(Input, { kind: "file", id: "mail-upload-attachment", name: "mail-upload-attachment", value: null, disabled: busy, onChange: (fileId) => void upload(fileId, "attachment") }) })] }) }));
56
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from "react";
2
+ export declare function VersionsSection({ rowId, currentVersion, currentStatus, }: {
3
+ readonly rowId: string | null;
4
+ readonly currentVersion: number;
5
+ readonly currentStatus: string;
6
+ }): ReactNode;
@@ -0,0 +1,43 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { formatWhen, useAppFeatures, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
+ import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
5
+ import { RailRow, RailSection } from "./rail.js";
6
+ import { asRowStatus, statusLabel, statusTone } from "./status-label.js";
7
+ const AUDIT_FEATURE = "audit";
8
+ const AUDIT_LIST_QUERY = "audit:query:list";
9
+ const MEMBER_DIRECTORY_QUERY = "tenant:query:member-directory";
10
+ const VERSION_LIMIT = 20;
11
+ function VersionRows({ rowId, currentVersion, currentStatus, }) {
12
+ const t = useTranslation();
13
+ const audit = useQuery(AUDIT_LIST_QUERY, {
14
+ aggregateType: "template-resource",
15
+ aggregateId: rowId,
16
+ limit: VERSION_LIMIT,
17
+ });
18
+ const members = useQuery(MEMBER_DIRECTORY_QUERY, { limit: 200 });
19
+ const names = new Map((members.data?.rows ?? []).map((member) => [member.id, member.label]));
20
+ const status = asRowStatus(currentStatus);
21
+ // The event log has one entry per change; show each version once, newest first.
22
+ const seen = new Set();
23
+ const versions = (audit.data?.rows ?? []).filter((row) => {
24
+ if (seen.has(row.version))
25
+ return false;
26
+ seen.add(row.version);
27
+ return true;
28
+ });
29
+ return (_jsx(_Fragment, { children: versions.map((row) => (_jsx(RailRow, { testId: `mail-version-${row.version}`, label: t("documentDesigner.mail.versions.line", {
30
+ version: row.version,
31
+ when: formatWhen(row.createdAt),
32
+ name: names.get(row.createdBy) ?? "–",
33
+ }), trailing: row.version === currentVersion ? (_jsx(StatusBadge, { tone: statusTone(status), children: statusLabel(t, status, row.version) })) : undefined }, row.version))) }));
34
+ }
35
+ // Hidden when the audit feature is not mounted, so no failing query shows up as an error.
36
+ export function VersionsSection({ rowId, currentVersion, currentStatus, }) {
37
+ const t = useTranslation();
38
+ const features = useAppFeatures();
39
+ if (rowId === null || !features.some((feature) => feature.featureName === AUDIT_FEATURE)) {
40
+ return null;
41
+ }
42
+ return (_jsx(RailSection, { title: t("documentDesigner.mail.versions.title"), testId: "mail-editor-versions", children: _jsx(VersionRows, { rowId: rowId, currentVersion: currentVersion, currentStatus: currentStatus }) }));
43
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ import type { LayoutKind, LayoutSourceView, TenantOverrideCounts } from "./layout-types.js";
3
+ export declare function DiffPanel({ tenantId }: {
4
+ readonly tenantId: string;
5
+ }): ReactNode;
6
+ export declare function OriginBar({ isSystemAdmin, source, kind, userCount, overrides, onReset, }: {
7
+ readonly isSystemAdmin: boolean;
8
+ readonly source: LayoutSourceView;
9
+ readonly kind: LayoutKind;
10
+ readonly userCount: number;
11
+ readonly overrides: TenantOverrideCounts | null;
12
+ readonly onReset: () => void;
13
+ }): ReactNode;