@cosmicdrift/kumiko-document-designer 0.363.0 → 0.365.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 (90) hide show
  1. package/dist/constants.d.ts +3 -0
  2. package/dist/constants.js +4 -0
  3. package/dist/i18n.js +208 -50
  4. package/dist/web/cells.js +4 -3
  5. package/dist/web/editor-header.d.ts +37 -0
  6. package/dist/web/editor-header.js +43 -0
  7. package/dist/web/editor-locale.d.ts +1 -0
  8. package/dist/web/editor-locale.js +15 -0
  9. package/dist/web/editor-shell.d.ts +40 -0
  10. package/dist/web/editor-shell.js +36 -0
  11. package/dist/web/i18n.js +35 -59
  12. package/dist/web/layout-editor-body.d.ts +2 -2
  13. package/dist/web/layout-editor-body.js +101 -52
  14. package/dist/web/layout-editor.js +14 -7
  15. package/dist/web/layout-html.d.ts +2 -0
  16. package/dist/web/layout-html.js +13 -1
  17. package/dist/web/layout-meta.d.ts +29 -0
  18. package/dist/web/layout-meta.js +32 -0
  19. package/dist/web/layout-rails.d.ts +15 -5
  20. package/dist/web/layout-rails.js +61 -68
  21. package/dist/web/layout-types.d.ts +4 -0
  22. package/dist/web/layout-users.d.ts +1 -0
  23. package/dist/web/layout-users.js +4 -1
  24. package/dist/web/mail-dialogs.d.ts +1 -6
  25. package/dist/web/mail-dialogs.js +2 -2
  26. package/dist/web/mail-draft.d.ts +8 -6
  27. package/dist/web/mail-draft.js +45 -11
  28. package/dist/web/mail-edit-center.d.ts +3 -1
  29. package/dist/web/mail-edit-center.js +5 -4
  30. package/dist/web/mail-editor-body.d.ts +0 -1
  31. package/dist/web/mail-editor-body.js +60 -45
  32. package/dist/web/mail-editor.js +9 -14
  33. package/dist/web/mail-footer.d.ts +3 -14
  34. package/dist/web/mail-footer.js +12 -13
  35. package/dist/web/mail-left-rail.d.ts +9 -4
  36. package/dist/web/mail-left-rail.js +18 -15
  37. package/dist/web/mail-missing-locale.d.ts +10 -0
  38. package/dist/web/mail-missing-locale.js +45 -0
  39. package/dist/web/mail-origin-bar.d.ts +3 -5
  40. package/dist/web/mail-origin-bar.js +26 -50
  41. package/dist/web/mail-preview-side.d.ts +12 -5
  42. package/dist/web/mail-preview-side.js +14 -5
  43. package/dist/web/mail-preview.d.ts +9 -5
  44. package/dist/web/mail-preview.js +24 -13
  45. package/dist/web/mail-right-rail.d.ts +8 -0
  46. package/dist/web/mail-right-rail.js +65 -14
  47. package/dist/web/mail-types.d.ts +10 -1
  48. package/dist/web/mail-upload-modal.js +12 -2
  49. package/dist/web/mail-versions.js +3 -3
  50. package/dist/web/origin-bar.d.ts +11 -2
  51. package/dist/web/origin-bar.js +46 -24
  52. package/dist/web/overrides-drawer.d.ts +3 -2
  53. package/dist/web/overrides-drawer.js +87 -11
  54. package/dist/web/overview-row.d.ts +2 -0
  55. package/dist/web/overview-row.js +2 -1
  56. package/dist/web/page-canvas.d.ts +2 -1
  57. package/dist/web/page-canvas.js +23 -13
  58. package/dist/web/pdf-footer.d.ts +13 -0
  59. package/dist/web/pdf-footer.js +34 -0
  60. package/dist/web/pdf-left-rail.d.ts +9 -0
  61. package/dist/web/pdf-left-rail.js +16 -0
  62. package/dist/web/pdf-meta.d.ts +3 -5
  63. package/dist/web/pdf-meta.js +10 -16
  64. package/dist/web/pdf-preview.js +21 -8
  65. package/dist/web/pdf-template-editor.js +91 -76
  66. package/dist/web/rail.d.ts +13 -1
  67. package/dist/web/rail.js +15 -7
  68. package/dist/web/relative-time.d.ts +1 -0
  69. package/dist/web/relative-time.js +18 -0
  70. package/dist/web/settings-panel.d.ts +11 -5
  71. package/dist/web/settings-panel.js +24 -13
  72. package/dist/web/status-label.d.ts +7 -0
  73. package/dist/web/status-label.js +8 -0
  74. package/dist/web/types.d.ts +23 -0
  75. package/dist/web/use-debounced-value.d.ts +1 -0
  76. package/dist/web/use-debounced-value.js +11 -0
  77. package/dist/web/use-insert-targets.d.ts +2 -2
  78. package/dist/web/use-insert-targets.js +4 -1
  79. package/dist/web/use-mail-persistence.d.ts +2 -2
  80. package/dist/web/use-mail-persistence.js +4 -4
  81. package/dist/web/use-own-address.d.ts +5 -0
  82. package/dist/web/use-own-address.js +14 -0
  83. package/dist/web/used-by-label.d.ts +1 -0
  84. package/dist/web/used-by-label.js +11 -0
  85. package/package.json +8 -8
  86. package/src/changes.json +48 -0
  87. package/dist/web/mail-header.d.ts +0 -23
  88. package/dist/web/mail-header.js +0 -16
  89. package/dist/web/placeholder-panel.d.ts +0 -21
  90. package/dist/web/placeholder-panel.js +0 -35
@@ -1,12 +1,19 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { MailPreview, MailValidation } from "./mail-types.js";
3
- export declare function PreviewSide({ preview, validation, slug, locale, testMail, }: {
3
+ import type { OwnAddress } from "./use-own-address.js";
4
+ export type TestMailControls = {
5
+ readonly pending: boolean;
6
+ readonly onSend: () => void;
7
+ readonly includeAttachments: boolean;
8
+ readonly onIncludeAttachments: (next: boolean) => void;
9
+ readonly prefixSubject: boolean;
10
+ readonly onPrefixSubject: (next: boolean) => void;
11
+ };
12
+ export declare function PreviewSide({ preview, validation, slug, locale, own, testMail, }: {
4
13
  readonly preview: MailPreview | null;
5
14
  readonly validation: MailValidation | null;
6
15
  readonly slug: string;
7
16
  readonly locale: string;
8
- readonly testMail: {
9
- readonly pending: boolean;
10
- readonly onSend: () => void;
11
- };
17
+ readonly own: OwnAddress;
18
+ readonly testMail: TestMailControls;
12
19
  }): ReactNode;
@@ -1,10 +1,11 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
- import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
3
+ import { useNav, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
4
  import { useState } from "react";
5
5
  import { displayValue } from "./mail-draft.js";
6
6
  import { usePreviewRecordPicker } from "./preview-record-picker.js";
7
- import { RailHint, RailRow, RailSection } from "./rail.js";
7
+ import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
8
+ const noop = () => undefined;
8
9
  // Check params list names as arrays; the message templates expect one string.
9
10
  function checkText(t, check) {
10
11
  const params = Object.fromEntries(Object.entries(check.params ?? {}).map(([name, value]) => [
@@ -40,9 +41,17 @@ function RecordPickerLink({ slug, locale, onPick, }) {
40
41
  },
41
42
  }) })] }));
42
43
  }
43
- export function PreviewSide({ preview, validation, slug, locale, testMail, }) {
44
+ function TestMailRecipient({ own }) {
44
45
  const t = useTranslation();
45
- const { Button, Field, Text } = usePrimitives();
46
+ const nav = useNav();
47
+ const { Field, Input, Link } = usePrimitives();
48
+ const { profileScreenId } = own;
49
+ return (_jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-recipient", label: t("documentDesigner.mail.testMail.recipient"), children: _jsx(Input, { kind: "text", id: "mail-test-recipient", name: "mail-test-recipient", testId: "mail-test-recipient", value: own.email ?? t("documentDesigner.mail.testMail.ownAddress"), onChange: noop, disabled: true }) }), profileScreenId !== null && (_jsx(Link, { href: nav.hrefFor({ screenId: profileScreenId }), onPress: () => nav.navigate({ screenId: profileScreenId }), testId: "mail-test-recipient-edit", children: t("documentDesigner.mail.testMail.changeAddress") }))] }));
50
+ }
51
+ export function PreviewSide({ preview, validation, slug, locale, own, testMail, }) {
52
+ const t = useTranslation();
53
+ const { Button, Field, Input, Text } = usePrimitives();
54
+ const hasAttachments = preview?.attachments.some((a) => a.kind !== "inline") === true;
46
55
  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))) })] }));
56
+ 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: 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: [_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), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
48
57
  }
@@ -1,5 +1,8 @@
1
1
  import { type ReactNode } from "react";
2
+ import type { EditorOrigin } from "./editor-header.js";
3
+ import { type TestMailControls } from "./mail-preview-side.js";
2
4
  import type { MailValidation } from "./mail-types.js";
5
+ import type { OwnAddress } from "./use-own-address.js";
3
6
  export type PreviewPayload = {
4
7
  readonly slug: string;
5
8
  readonly kind: "mail";
@@ -8,13 +11,14 @@ export type PreviewPayload = {
8
11
  readonly meta: Readonly<Record<string, unknown>>;
9
12
  readonly variableSchema: unknown;
10
13
  };
11
- export declare function MailPreviewMode({ payload, validation, slug, locale, testMail, }: {
14
+ export declare function MailPreviewMode({ payload, validation, slug, locale, fromName, own, controls, origin, testMail, }: {
12
15
  readonly payload: PreviewPayload;
13
16
  readonly validation: MailValidation | null;
14
17
  readonly slug: string;
15
18
  readonly locale: string;
16
- readonly testMail: {
17
- readonly pending: boolean;
18
- readonly onSend: () => void;
19
- };
19
+ readonly fromName: string;
20
+ readonly own: OwnAddress;
21
+ readonly controls: ReactNode;
22
+ readonly origin: EditorOrigin | null;
23
+ readonly testMail: TestMailControls;
20
24
  }): ReactNode;
@@ -1,9 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
4
  import { dispatcherErrorText, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
5
  import { LoadingState, ModeSwitch } from "@cosmicdrift/kumiko-renderer-web";
6
6
  import { useState } from "react";
7
+ import { EditorBody, EditorInfoBar } from "./editor-shell.js";
7
8
  import { formatKilobytes } from "./mail-draft.js";
8
9
  import { PreviewSide } from "./mail-preview-side.js";
9
10
  function attachmentText(t, attachment) {
@@ -16,28 +17,38 @@ function attachmentText(t, attachment) {
16
17
  return `⎘ ${attachment.name} · ${t("documentDesigner.mail.preview.generatedOnSend")}`;
17
18
  }
18
19
  }
19
- function MailFrame({ preview, format, width, }) {
20
+ function HeadRow({ label, children, }) {
21
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "min-w-0", children: children })] }));
22
+ }
23
+ function MailFrame({ preview, format, width, fromName, recipient, }) {
20
24
  const t = useTranslation();
21
25
  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 ?? "" }))] }));
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 ?? "" }))] }));
27
+ }
28
+ // The preview mail goes to the example recipient of the sample values, not to the test-mail address.
29
+ function recipientOf(preview, own) {
30
+ const { recipientName: name, recipientEmail: email } = preview.sampleValues;
31
+ if (typeof name !== "string" || name === "")
32
+ return own.email;
33
+ return typeof email === "string" && email !== "" ? `${name} <${email}>` : name;
23
34
  }
24
35
  function layoutText(t, preview) {
25
- return preview?.layout == null
36
+ return preview?.mailLayout == null
26
37
  ? t("documentDesigner.mail.preview.barPlain")
27
- : t("documentDesigner.mail.preview.bar", { layout: preview.layout.slug });
38
+ : t("documentDesigner.mail.preview.bar", { version: preview.mailLayout.version });
28
39
  }
29
- export function MailPreviewMode({ payload, validation, slug, locale, testMail, }) {
40
+ export function MailPreviewMode({ payload, validation, slug, locale, fromName, own, controls, origin, testMail, }) {
30
41
  const t = useTranslation();
31
42
  const { Banner } = usePrimitives();
32
43
  const [format, setFormat] = useState("html");
33
44
  const [width, setWidth] = useState("desktop");
34
45
  const query = useQuery(TemplateResolverQueries.preview, payload);
35
46
  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 })] })] }));
47
+ return (_jsxs(_Fragment, { children: [_jsx(EditorInfoBar, { tone: "muted", testId: "mail-preview-bar", origin: origin, controls: controls, links: _jsxs(_Fragment, { children: [_jsx(ModeSwitch, { value: format, onChange: setFormat, ariaLabel: t("documentDesigner.mail.preview.formatSwitch"), testId: "mail-preview-format", options: [
48
+ { value: "html", label: t("documentDesigner.mail.preview.html") },
49
+ { value: "text", label: t("documentDesigner.mail.preview.text") },
50
+ ] }), _jsx(ModeSwitch, { value: width, onChange: setWidth, ariaLabel: t("documentDesigner.mail.preview.widthSwitch"), testId: "mail-preview-width", options: [
51
+ { value: "desktop", label: t("documentDesigner.mail.preview.desktop") },
52
+ { value: "mobile", label: t("documentDesigner.mail.preview.mobile") },
53
+ ] })] }), children: layoutText(t, preview) }), _jsx(EditorBody, { testId: "mail-editor-preview", center: _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto px-8 py-6", 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, fromName: fromName, recipient: recipientOf(preview, own) }))] }), right: _jsx(PreviewSide, { preview: preview, validation: validation, slug: slug, locale: locale, own: own, testMail: testMail }) })] }));
43
54
  }
@@ -1,6 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { EditorVocabulary } from "./mail-edit-center.js";
2
3
  import type { LinkedResourceView, MailDraft } from "./mail-types.js";
3
4
  import type { OverviewRowView } from "./overview-row.js";
5
+ import type { InsertTargets } from "./use-insert-targets.js";
4
6
  export declare function layoutOptionLabel(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, layout: OverviewRowView): string;
5
7
  export declare function AttachmentSection({ attachments, pdfTitles, canEdit, onAddPdf, onRemove, }: {
6
8
  readonly attachments: readonly LinkedResourceView[];
@@ -14,6 +16,12 @@ export declare function PropertiesSection({ draft, layouts, onChange, }: {
14
16
  readonly layouts: readonly OverviewRowView[];
15
17
  readonly onChange: (patch: Partial<MailDraft>) => void;
16
18
  }): ReactNode;
19
+ export declare function ChromeSection({ draft, vocabulary, insert, onChange, }: {
20
+ readonly draft: MailDraft;
21
+ readonly vocabulary: EditorVocabulary;
22
+ readonly insert: InsertTargets;
23
+ readonly onChange: (patch: Partial<MailDraft>) => void;
24
+ }): ReactNode;
17
25
  export declare function UsageSection({ slug }: {
18
26
  readonly slug: string;
19
27
  }): ReactNode;
@@ -1,14 +1,33 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
- import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
3
+ import { MAIL_TONES, PlaceholderLineEditor, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
4
  import { usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
5
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
5
6
  import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
6
- import { RailHint, RailRow, RailSection } from "./rail.js";
7
+ import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
7
8
  import { usageParts } from "./used-by-label.js";
8
9
  export function layoutOptionLabel(t, layout) {
10
+ // A layout has no registry title; the slug would read like an id, so name it by kind.
11
+ const title = layout.title === layout.slug
12
+ ? t(`documentDesigner.editor.title.${layout.kind === "mail-layout" ? "mail" : "pdf"}`)
13
+ : layout.title;
9
14
  return t(layout.origin === "tenant"
10
15
  ? "documentDesigner.mail.props.layoutTenant"
11
- : "documentDesigner.mail.props.layoutSystem", { title: layout.title, version: layout.version });
16
+ : "documentDesigner.mail.props.layoutSystem", { title, version: layout.version });
17
+ }
18
+ // Without a stored default layout the renderer uses its built-in one, so the
19
+ // select still needs an option for the value it shows.
20
+ function layoutOptions(t, layouts) {
21
+ const stored = layouts.map((layout) => ({
22
+ value: layout.slug,
23
+ label: layoutOptionLabel(t, layout),
24
+ }));
25
+ return layouts.some((layout) => layout.slug === DEFAULT_LAYOUT_SLUG)
26
+ ? stored
27
+ : [
28
+ { value: DEFAULT_LAYOUT_SLUG, label: t("documentDesigner.mail.props.layoutBuiltIn") },
29
+ ...stored,
30
+ ];
12
31
  }
13
32
  function UsageRows({ slug }) {
14
33
  const t = useTranslation();
@@ -24,22 +43,54 @@ function UsageRows({ slug }) {
24
43
  export function AttachmentSection({ attachments, pdfTitles, canEdit, onAddPdf, onRemove, }) {
25
44
  const t = useTranslation();
26
45
  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)))] }));
46
+ 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(RailRow, { label: t("documentDesigner.mail.attachments.none") }), attachments.map((attachment) => (_jsx(RailRow, { testId: "mail-attachment-row", symbol: "\u2398", label: attachment.kind === "pdf"
47
+ ? t("documentDesigner.mail.attachments.pdfRow", {
48
+ title: pdfTitles.get(attachment.templateSlug) ?? attachment.templateSlug,
49
+ })
50
+ : attachment.kind === "file"
51
+ ? attachment.fileName
52
+ : attachment.key, trailing: attachment.kind === "pdf"
53
+ ? attachment.fileName
54
+ : t("documentDesigner.mail.attachments.file"), action: _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))), _jsx(RailHint, { children: t("documentDesigner.mail.attachments.hint") })] }));
30
55
  }
31
56
  export function PropertiesSection({ draft, layouts, onChange, }) {
32
57
  const t = useTranslation();
33
- const { Field, Input, Text } = usePrimitives();
58
+ const { Field, Input } = usePrimitives();
34
59
  const layoutValue = draft.layoutSlug === "" ? DEFAULT_LAYOUT_SLUG : draft.layoutSlug;
35
60
  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") })] }));
61
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.props.title"), testId: "mail-editor-properties", children: [_jsx(RailField, { 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, display: "dropdown", onChange: (next) => onChange({ layoutSlug: next }), options: layoutOptions(t, layouts) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-sender", label: t("documentDesigner.mail.props.sender"), children: _jsx(Input, { kind: "text", id: "mail-sender", name: "mail-sender", testId: "mail-sender", value: draft.senderName, onChange: (next) => onChange({ senderName: next }), placeholder: t("documentDesigner.mail.props.senderPlaceholder") }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.props.senderHint") }), _jsx(RailField, { children: _jsx(Field, { id: "mail-reply-to", label: t("documentDesigner.mail.props.replyTo"), children: _jsx(Input, { kind: "email", id: "mail-reply-to", name: "mail-reply-to", testId: "mail-reply-to", value: draft.replyTo, onChange: (next) => onChange({ replyTo: next }) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-category", label: t("documentDesigner.mail.props.category"), children: _jsx(Input, { kind: "select", id: "mail-category", name: "mail-category", value: categoryValue, display: "dropdown", onChange: (next) => onChange({ category: next === "marketing" ? "marketing" : "transactional" }), options: [
62
+ { value: "transactional", label: t("documentDesigner.mail.props.transactional") },
63
+ { value: "marketing", label: t("documentDesigner.mail.props.marketing") },
64
+ ] }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.props.categoryHint") })] }));
65
+ }
66
+ const TONE_LABEL_KEYS = {
67
+ accent: "documentDesigner.mail.chrome.toneAccent",
68
+ info: "documentDesigner.mail.chrome.toneInfo",
69
+ success: "documentDesigner.mail.chrome.toneSuccess",
70
+ warning: "documentDesigner.mail.chrome.toneWarning",
71
+ danger: "documentDesigner.mail.chrome.toneDanger",
72
+ neutral: "documentDesigner.mail.chrome.toneNeutral",
73
+ };
74
+ // The stored tone may be a placeholder that resolves per mail; the select keeps it as its own
75
+ // option so opening the template never silently drops it.
76
+ function toneOptions(t, current) {
77
+ const fixed = MAIL_TONES.map((tone) => ({ value: tone, label: t(TONE_LABEL_KEYS[tone]) }));
78
+ return current === "" || isOneOf(MAIL_TONES, current)
79
+ ? fixed
80
+ : [
81
+ ...fixed,
82
+ {
83
+ value: current,
84
+ label: t("documentDesigner.mail.chrome.toneFromPlaceholder", { value: current }),
85
+ },
86
+ ];
87
+ }
88
+ export function ChromeSection({ draft, vocabulary, insert, onChange, }) {
89
+ const t = useTranslation();
90
+ const { Field, Input } = usePrimitives();
91
+ // "accent" is the renderer default, so it is stored as an empty value.
92
+ const toneValue = draft.tone === "" ? "accent" : draft.tone;
93
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.chrome.title"), testId: "mail-editor-chrome", children: [_jsx(RailField, { children: _jsx(Field, { id: "mail-header-chip", label: t("documentDesigner.mail.chrome.headerChip"), children: _jsx("div", { onFocusCapture: insert.markFocused("headerChip"), "data-testid": "mail-header-chip", children: _jsx(PlaceholderLineEditor, { id: "mail-header-chip", value: draft.headerChip, onChange: (headerChip) => onChange({ headerChip }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.headerChipPlaceholder"), onEditorReady: insert.register("headerChip"), "aria-label": t("documentDesigner.mail.chrome.headerChip") }) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-tone", label: t("documentDesigner.mail.chrome.tone"), children: _jsx(Input, { kind: "select", id: "mail-tone", name: "mail-tone", value: toneValue, display: "dropdown", onChange: (next) => onChange({ tone: next === "accent" ? "" : next }), options: toneOptions(t, draft.tone) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-footer-note", label: t("documentDesigner.mail.chrome.footerNote"), children: _jsx("div", { onFocusCapture: insert.markFocused("footerNote"), "data-testid": "mail-footer-note", children: _jsx(PlaceholderLineEditor, { id: "mail-footer-note", value: draft.footerNote, onChange: (footerNote) => onChange({ footerNote }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.footerNotePlaceholder"), onEditorReady: insert.register("footerNote"), "aria-label": t("documentDesigner.mail.chrome.footerNote") }) }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.chrome.hint") })] }));
43
94
  }
44
95
  export function UsageSection({ slug }) {
45
96
  const t = useTranslation();
@@ -1,4 +1,5 @@
1
1
  import type { ValidationIssue } from "./layout-types.js";
2
+ import type { ServedVersionView } from "./types.js";
2
3
  export type PlaceholderType = "text" | "money" | "date" | "link" | "list";
3
4
  export type MailVariable = {
4
5
  readonly type: PlaceholderType;
@@ -52,9 +53,10 @@ export type MailPreview = {
52
53
  readonly displayHtml?: string;
53
54
  readonly text: string | null;
54
55
  readonly attachments: readonly PreviewAttachment[];
55
- readonly layout: {
56
+ readonly mailLayout?: {
56
57
  readonly slug: string;
57
58
  readonly scope: string;
59
+ readonly version: number;
58
60
  } | null;
59
61
  readonly sampleValues: Readonly<Record<string, unknown>>;
60
62
  readonly checks: readonly PreviewCheck[];
@@ -80,6 +82,7 @@ export type TemplateRowView = {
80
82
  readonly linkedResources: unknown;
81
83
  readonly meta: unknown;
82
84
  readonly modifiedAt: string | null;
85
+ readonly active?: ServedVersionView | null;
83
86
  };
84
87
  export type MailCategory = "" | "transactional" | "marketing";
85
88
  export type MailDraft = {
@@ -87,6 +90,12 @@ export type MailDraft = {
87
90
  readonly previewText: string;
88
91
  readonly layoutSlug: string;
89
92
  readonly category: MailCategory;
93
+ readonly senderName: string;
94
+ readonly replyTo: string;
95
+ /** Meta lines without rail fields yet; carried so a tenant save keeps them. */
96
+ readonly headerChip: string;
97
+ readonly tone: string;
98
+ readonly footerNote: string;
90
99
  readonly body: string;
91
100
  };
92
101
  export type LinkedResourceView = {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
4
  import { dispatcherErrorText, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
- import { useState } from "react";
5
+ import { useRef, useState } from "react";
6
6
  import { fetchStoredFileName } from "./file-name.js";
7
7
  import { uniqueResourceKey } from "./mail-draft.js";
8
8
  const IMAGE_ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -11,6 +11,12 @@ function useResourceUpload(target) {
11
11
  const resourceUpload = useMutation(TemplateResolverHandlers.resourceUpload);
12
12
  const [error, setError] = useState(null);
13
13
  const [busy, setBusy] = useState(false);
14
+ // takenKeys only refreshes after the fire-and-forget row refetch, so keys handed out
15
+ // here must be remembered or a quick second upload of the same file name collides.
16
+ const uploadedKeys = useRef({
17
+ templateId: target.templateId,
18
+ keys: new Set(),
19
+ });
14
20
  const upload = async (fileRefId, purpose) => {
15
21
  if (fileRefId === null)
16
22
  return;
@@ -22,7 +28,10 @@ function useResourceUpload(target) {
22
28
  setError(t("documentDesigner.mail.upload.noFileName"));
23
29
  return;
24
30
  }
25
- const key = uniqueResourceKey(fileName, target.takenKeys);
31
+ if (uploadedKeys.current.templateId !== target.templateId) {
32
+ uploadedKeys.current = { templateId: target.templateId, keys: new Set() };
33
+ }
34
+ const key = uniqueResourceKey(fileName, [...target.takenKeys, ...uploadedKeys.current.keys]);
26
35
  const result = await resourceUpload.mutate({
27
36
  sourceFileRefId: fileRefId,
28
37
  templateId: target.templateId,
@@ -34,6 +43,7 @@ function useResourceUpload(target) {
34
43
  setError(dispatcherErrorText(result.error, t));
35
44
  return;
36
45
  }
46
+ uploadedKeys.current.keys.add(key);
37
47
  target.onUploaded({ key, purpose });
38
48
  }
39
49
  finally {
@@ -1,8 +1,8 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { formatWhen, useAppFeatures, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
4
  import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
5
- import { RailRow, RailSection } from "./rail.js";
5
+ import { RailHint, RailRow, RailSection } from "./rail.js";
6
6
  import { asRowStatus, statusLabel, statusTone } from "./status-label.js";
7
7
  const AUDIT_FEATURE = "audit";
8
8
  const AUDIT_LIST_QUERY = "audit:query:list";
@@ -39,5 +39,5 @@ export function VersionsSection({ rowId, currentVersion, currentStatus, }) {
39
39
  if (rowId === null || !features.some((feature) => feature.featureName === AUDIT_FEATURE)) {
40
40
  return null;
41
41
  }
42
- return (_jsx(RailSection, { title: t("documentDesigner.mail.versions.title"), testId: "mail-editor-versions", children: _jsx(VersionRows, { rowId: rowId, currentVersion: currentVersion, currentStatus: currentStatus }) }));
42
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.versions.title"), testId: "mail-editor-versions", children: [_jsx(VersionRows, { rowId: rowId, currentVersion: currentVersion, currentStatus: currentStatus }), _jsx(RailHint, { children: t("documentDesigner.mail.versions.hint") })] }));
43
43
  }
@@ -1,13 +1,22 @@
1
1
  import { type ReactNode } from "react";
2
- import type { LayoutKind, LayoutSourceView, TenantOverrideCounts } from "./layout-types.js";
2
+ import type { LayoutKind, LayoutSourceView, TemplateDiff, TenantOverrideCounts } from "./layout-types.js";
3
+ export declare function DiffView({ data, mode, }: {
4
+ readonly data: TemplateDiff;
5
+ readonly mode?: "diff" | "full";
6
+ }): ReactNode;
3
7
  export declare function DiffPanel({ tenantId }: {
4
8
  readonly tenantId: string;
5
9
  }): ReactNode;
6
- export declare function OriginBar({ isSystemAdmin, source, kind, userCount, overrides, onReset, }: {
10
+ export declare function DiffDrawer({ tenantId, onClose, }: {
11
+ readonly tenantId: string;
12
+ readonly onClose: () => void;
13
+ }): ReactNode;
14
+ export declare function OriginBar({ isSystemAdmin, source, kind, userCount, overrides, onReset, onResetError, }: {
7
15
  readonly isSystemAdmin: boolean;
8
16
  readonly source: LayoutSourceView;
9
17
  readonly kind: LayoutKind;
10
18
  readonly userCount: number;
11
19
  readonly overrides: TenantOverrideCounts | null;
12
20
  readonly onReset: () => void;
21
+ readonly onResetError: (message: string) => void;
13
22
  }): ReactNode;
@@ -1,9 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
- import { useMutation, usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
5
- import { EmptyState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
4
+ import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { EmptyState, ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
6
6
  import { useState } from "react";
7
+ import { originOf } from "./editor-header.js";
8
+ import { EditorInfoBar } from "./editor-shell.js";
9
+ import { draftBesideActiveSentence } from "./status-label.js";
7
10
  const DIFF_MARK = {
8
11
  same: " ",
9
12
  added: "+",
@@ -19,18 +22,36 @@ function metaValueText(value) {
19
22
  return "–";
20
23
  return typeof value === "string" ? value : JSON.stringify(value);
21
24
  }
22
- export function DiffPanel({ tenantId }) {
25
+ // "full" shows the tenant's version as it reads (everything but the removed system blocks).
26
+ export function DiffView({ data, mode = "diff", }) {
23
27
  const t = useTranslation();
24
- const { data, loading } = useQuery(TemplateResolverQueries.diffToSystem, {
25
- id: tenantId,
26
- });
27
- if (loading || data === null)
28
- return _jsx(LoadingState, {});
29
- if (data.identical)
28
+ if (mode === "diff" && data.identical) {
30
29
  return _jsx(EmptyState, { title: t("documentDesigner.editor.diff.identical") });
30
+ }
31
31
  // Diff blocks have no identity beyond their position.
32
- const entries = data.blocks.map((block, position) => ({ block, key: `block-${position}` }));
33
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("ul", { className: "flex flex-col gap-1", "data-testid": "document-designer-diff", children: entries.map(({ block, key }) => (_jsxs("li", { "data-change": block.change, className: `whitespace-pre-wrap rounded px-2 py-1 font-mono text-xs ${DIFF_TONE[block.change]}`, children: [DIFF_MARK[block.change], " ", block.content] }, key))) }), data.meta.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1", "data-testid": "document-designer-diff-meta", children: data.meta.map((entry) => (_jsxs("li", { className: "rounded bg-muted px-2 py-1 font-mono text-xs", children: [entry.key, ": ", metaValueText(entry.system), " \u2192 ", metaValueText(entry.tenant)] }, entry.key))) }))] }));
32
+ const entries = data.blocks
33
+ .map((block, position) => ({ block, key: `block-${position}` }))
34
+ .filter(({ block }) => mode === "diff" || block.change !== "removed");
35
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("ul", { className: "flex flex-col gap-1", "data-testid": "document-designer-diff", children: entries.map(({ block, key }) => (_jsxs("li", { "data-change": block.change, className: `whitespace-pre-wrap rounded px-2 py-1 font-mono text-xs ${DIFF_TONE[block.change]}`, children: [mode === "diff" && `${DIFF_MARK[block.change]} `, block.content] }, key))) }), mode === "diff" && data.meta.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1", "data-testid": "document-designer-diff-meta", children: data.meta.map((entry) => (_jsxs("li", { className: "rounded bg-muted px-2 py-1 font-mono text-xs", children: [entry.key, ": ", metaValueText(entry.system), " \u2192 ", metaValueText(entry.tenant)] }, entry.key))) }))] }));
36
+ }
37
+ export function DiffPanel({ tenantId }) {
38
+ const { data, loading, error, refetch } = useQuery(TemplateResolverQueries.diffToSystem, { id: tenantId });
39
+ if (error !== null)
40
+ return _jsx(ErrorState, { error: error, onRetry: () => void refetch() });
41
+ if (loading || data === null)
42
+ return _jsx(LoadingState, {});
43
+ return _jsx(DiffView, { data: data });
44
+ }
45
+ export function DiffDrawer({ tenantId, onClose, }) {
46
+ const t = useTranslation();
47
+ const { Drawer, Modal } = usePrimitives();
48
+ const title = t("documentDesigner.editor.diff.title");
49
+ const body = _jsx(DiffPanel, { tenantId: tenantId });
50
+ const onOpenChange = (open) => {
51
+ if (!open)
52
+ onClose();
53
+ };
54
+ return Drawer === undefined ? (_jsx(Modal, { open: true, onOpenChange: onOpenChange, title: title, children: body })) : (_jsx(Drawer, { open: true, onOpenChange: onOpenChange, title: title, children: body }));
34
55
  }
35
56
  function originText(t, args) {
36
57
  const { isSystemAdmin, source, kind, userCount, overrides } = args;
@@ -60,23 +81,24 @@ function originText(t, args) {
60
81
  ? t("documentDesigner.editor.origin.none")
61
82
  : t("documentDesigner.editor.origin.system");
62
83
  }
63
- export function OriginBar({ isSystemAdmin, source, kind, userCount, overrides, onReset, }) {
84
+ export function OriginBar({ isSystemAdmin, source, kind, userCount, overrides, onReset, onResetError, }) {
64
85
  const t = useTranslation();
65
- const { Button, Dialog, Drawer, Modal, Text } = usePrimitives();
86
+ const { Button, Dialog } = usePrimitives();
66
87
  const [diffOpen, setDiffOpen] = useState(false);
67
88
  const [resetOpen, setResetOpen] = useState(false);
68
89
  const resetToSystem = useMutation(TemplateResolverHandlers.resetToSystem);
69
90
  const { tenant, system } = source;
91
+ const besideActive = isSystemAdmin || tenant === null ? null : draftBesideActiveSentence(t, tenant);
70
92
  const canDiff = tenant !== null && system?.contentFormat === "html";
71
93
  const canReset = tenant !== null && system !== null && !isSystemAdmin;
72
- const diffTitle = t("documentDesigner.editor.diff.title");
73
- const diffBody = tenant === null ? null : _jsx(DiffPanel, { tenantId: tenant.id });
74
- return (_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border bg-muted px-4 py-2.5", "data-testid": "document-designer-origin-bar", children: [_jsx(Text, { children: originText(t, { isSystemAdmin, source, kind, userCount, overrides }) }), _jsx("div", { className: "grow" }), canDiff && (_jsx(Button, { variant: "link", onClick: () => setDiffOpen(true), testId: "document-designer-diff-open", children: t("documentDesigner.editor.diff.open") })), canReset && (_jsx(Button, { variant: "link", onClick: () => setResetOpen(true), testId: "document-designer-reset-open", children: t("documentDesigner.editor.reset.open") })), diffOpen &&
75
- (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.editor.reset.title"), description: t("documentDesigner.editor.reset.description"), confirmLabel: t("documentDesigner.editor.reset.confirm"), variant: "danger", onConfirm: async () => {
76
- if (tenant === null)
77
- return;
78
- const result = await resetToSystem.mutate({ id: tenant.id });
79
- if (result.isSuccess)
80
- onReset();
81
- } })] }));
94
+ const links = (_jsxs(_Fragment, { children: [canDiff && (_jsx(Button, { variant: "link", onClick: () => setDiffOpen(true), testId: "document-designer-diff-open", children: t("documentDesigner.editor.diff.open") })), canReset && (_jsx(Button, { variant: "link", onClick: () => setResetOpen(true), testId: "document-designer-reset-open", children: t("documentDesigner.editor.reset.open") }))] }));
95
+ return (_jsxs(EditorInfoBar, { tone: tenant !== null ? "active" : "muted", testId: "document-designer-origin-bar", origin: originOf(tenant === null, system !== null), links: links, overlays: _jsxs(_Fragment, { children: [diffOpen && tenant !== null && (_jsx(DiffDrawer, { tenantId: tenant.id, onClose: () => setDiffOpen(false) })), _jsx(Dialog, { open: resetOpen, onOpenChange: setResetOpen, title: t("documentDesigner.editor.reset.title"), description: t("documentDesigner.editor.reset.description"), confirmLabel: t("documentDesigner.editor.reset.confirm"), variant: "danger", onConfirm: async () => {
96
+ if (tenant === null)
97
+ return;
98
+ const result = await resetToSystem.mutate({ id: tenant.id });
99
+ if (result.isSuccess)
100
+ onReset();
101
+ else
102
+ onResetError(dispatcherErrorText(result.error, t));
103
+ } })] }), children: [originText(t, { isSystemAdmin, source, kind, userCount, overrides }), besideActive !== null && ` ${besideActive}`] }));
82
104
  }
@@ -1,9 +1,10 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import { type TenantOverrides } from "./types.js";
3
3
  export type OverridesDrawerProps = {
4
4
  readonly open: boolean;
5
5
  readonly onOpenChange: (open: boolean) => void;
6
6
  readonly slug: string;
7
+ readonly locale: string;
7
8
  readonly overrides: TenantOverrides | null;
8
9
  };
9
- export declare function OverridesDrawer({ open, onOpenChange, slug, overrides, }: OverridesDrawerProps): ReactNode;
10
+ export declare function OverridesDrawer({ open, onOpenChange, slug, locale, overrides, }: OverridesDrawerProps): ReactNode;