@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,142 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { ModeSwitch, StatusBadge, TextareaField } from "@cosmicdrift/kumiko-renderer-web";
6
+ import { useDeferredValue, useState } from "react";
7
+ import { composeLayoutHtml } from "./compose-layout.js";
8
+ import { AppearanceRail, BrandingRail, MjmlPreviewRail, PlaceholderRail, UsedByRail, } from "./layout-rails.js";
9
+ import { layoutUsers } from "./layout-users.js";
10
+ import { OriginBar } from "./origin-bar.js";
11
+ import { asRowStatus, statusLabel, statusTone } from "./status-label.js";
12
+ const REGION_EDITOR_IDS = {
13
+ header: "document-designer-region-header",
14
+ signature: "document-designer-region-signature",
15
+ footer: "document-designer-region-footer",
16
+ };
17
+ function RegionLabel({ children }) {
18
+ return (_jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: children }));
19
+ }
20
+ function PaperRegion({ label, testId, children, }) {
21
+ return (_jsxs("div", { className: "flex flex-col gap-1.5 px-8 py-3", "data-testid": testId, children: [_jsx(RegionLabel, { children: label }), children] }));
22
+ }
23
+ function issueText(t, issue) {
24
+ const translated = t(issue.i18nKey, issue.params);
25
+ return translated === issue.i18nKey ? issue.code : translated;
26
+ }
27
+ function FooterMessage({ blockingIssue, checked, userCount, isMail, }) {
28
+ const t = useTranslation();
29
+ if (!checked) {
30
+ return (_jsx("span", { className: "text-sm text-muted-foreground", children: t("documentDesigner.editor.footer.checking") }));
31
+ }
32
+ if (blockingIssue !== undefined) {
33
+ return (_jsx("span", { className: "text-sm text-status-bad", "data-testid": "document-designer-footer-issue", children: issueText(t, blockingIssue) }));
34
+ }
35
+ return (_jsx("span", { className: "text-sm text-status-ok", "data-testid": "document-designer-footer-ok", children: t(isMail ? "documentDesigner.editor.footer.okMail" : "documentDesigner.editor.footer.okPdf", {
36
+ count: userCount,
37
+ }) }));
38
+ }
39
+ export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, overrides, onKindChange, onReload, onBack, }) {
40
+ const t = useTranslation();
41
+ const { Banner, Button, Dialog, Heading, Link, Text } = usePrimitives();
42
+ const isMail = kind === "mail-layout";
43
+ const showMjmlTab = isSystemAdmin && source.mjmlSource !== null;
44
+ const [tab, setTab] = useState(showMjmlTab && source.effectiveFormat === "mjml" ? "mjml" : "draft");
45
+ const [header, setHeader] = useState(source.regions.header);
46
+ const [signature, setSignature] = useState(source.regions.signature);
47
+ const [footer, setFooter] = useState(source.regions.footer);
48
+ const [mjml, setMjml] = useState(source.mjmlSource ?? "");
49
+ const [pendingKind, setPendingKind] = useState(null);
50
+ const [saveError, setSaveError] = useState(null);
51
+ const upsertTenant = useMutation(TemplateResolverHandlers.upsertTenant);
52
+ const upsertSystem = useMutation(TemplateResolverHandlers.upsertSystem);
53
+ const publishSystem = useMutation(TemplateResolverHandlers.publishSystem);
54
+ const mjmlMode = tab === "mjml";
55
+ const htmlDirty = header !== source.regions.header ||
56
+ signature !== source.regions.signature ||
57
+ footer !== source.regions.footer;
58
+ const dirty = mjmlMode ? mjml !== (source.mjmlSource ?? "") : htmlDirty;
59
+ const content = mjmlMode
60
+ ? mjml
61
+ : composeLayoutHtml({ header, signature: isMail ? signature : "", footer });
62
+ const deferredContent = useDeferredValue(content);
63
+ const validation = useQuery(TemplateResolverQueries.validate, {
64
+ slug,
65
+ kind,
66
+ contentFormat: mjmlMode ? "mjml" : "html",
67
+ content: deferredContent,
68
+ locale: source.locale,
69
+ scope: isSystemAdmin ? "system" : "tenant",
70
+ });
71
+ const checked = validation.data;
72
+ const shownRow = isSystemAdmin ? source.system : (source.tenant ?? source.system);
73
+ const userCount = layoutUsers(overviewRows, kind, slug).length;
74
+ const placeholders = checked?.systemPlaceholders ?? [];
75
+ const requestKind = (next) => {
76
+ if (next === kind)
77
+ return;
78
+ if (dirty)
79
+ setPendingKind(next);
80
+ else
81
+ onKindChange(next);
82
+ };
83
+ const save = async (publish) => {
84
+ setSaveError(null);
85
+ const base = {
86
+ slug,
87
+ kind,
88
+ locale: source.locale,
89
+ content,
90
+ contentFormat: mjmlMode ? "mjml" : "html",
91
+ meta: {},
92
+ };
93
+ const saved = isSystemAdmin
94
+ ? await upsertSystem.mutate(base)
95
+ : await upsertTenant.mutate({ ...base, status: publish ? "active" : "draft" });
96
+ if (!saved.isSuccess) {
97
+ setSaveError(dispatcherErrorText(saved.error, t));
98
+ return;
99
+ }
100
+ if (isSystemAdmin && publish) {
101
+ const published = await publishSystem.mutate({ id: saved.data.id });
102
+ if (!published.isSuccess) {
103
+ setSaveError(dispatcherErrorText(published.error, t));
104
+ return;
105
+ }
106
+ }
107
+ await onReload();
108
+ };
109
+ const busy = upsertTenant.pending || upsertSystem.pending || publishSystem.pending;
110
+ const shownStatus = shownRow === null ? null : asRowStatus(shownRow.status);
111
+ const canSave = dirty && checked?.canSave === true && !busy;
112
+ const canPublish = checked?.canPublish === true && (dirty || shownStatus === "draft") && !busy;
113
+ const blockingIssue = checked === null ? undefined : [...checked.issues, ...checked.publishIssues][0];
114
+ const backLink = (_jsx(Link, { href: "#", onPress: onBack, testId: "document-designer-back", children: t("documentDesigner.editor.crumb") }));
115
+ return (_jsxs("div", { className: "flex flex-col gap-4 p-6", "data-testid": "document-designer-layout-editor", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [backLink, _jsx(Text, { variant: "muted", children: "\u203A" }), _jsx(Heading, { variant: "page", testId: "document-designer-editor-title", children: t(isMail ? "documentDesigner.editor.title.mail" : "documentDesigner.editor.title.pdf") }), shownRow !== null && shownStatus !== null && (_jsx(StatusBadge, { tone: statusTone(shownStatus), testId: "document-designer-editor-status", children: statusLabel(t, shownStatus, shownRow.version) })), _jsx(StatusBadge, { tone: source.tenant !== null ? "accent" : "muted", testId: "document-designer-editor-origin", children: t(source.tenant !== null
116
+ ? "documentDesigner.source.tenant"
117
+ : "documentDesigner.source.system") }), _jsx("div", { className: "grow" }), _jsx(ModeSwitch, { value: kind, onChange: requestKind, ariaLabel: t("documentDesigner.editor.kindSwitch"), testId: "document-designer-kind-switch", options: [
118
+ { value: "mail-layout", label: t("documentDesigner.editor.kind.mail") },
119
+ { value: "pdf-layout", label: t("documentDesigner.editor.kind.pdf") },
120
+ ] })] }), _jsx(OriginBar, { isSystemAdmin: isSystemAdmin, source: source, kind: kind, userCount: userCount, overrides: overrides, onReset: () => void onReload() }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[260px_minmax(0,1fr)_280px]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(BrandingRail, {}), _jsx(PlaceholderRail, { validation: checked })] }), _jsxs("div", { className: "flex flex-col gap-3", children: [showMjmlTab && (_jsx("div", { className: "flex justify-end", children: _jsx(ModeSwitch, { value: tab, onChange: setTab, ariaLabel: t("documentDesigner.editor.tabSwitch"), testId: "layout-mjml-tab", options: [
121
+ { value: "draft", label: t("documentDesigner.editor.tab.draft") },
122
+ { value: "mjml", label: t("documentDesigner.editor.tab.mjml") },
123
+ ] }) })), mjmlMode ? (_jsx(TextareaField, { id: "document-designer-mjml-source", name: "mjml", label: t("documentDesigner.editor.mjml.label"), value: mjml, onChange: setMjml, rows: 24, testId: "document-designer-mjml-source" })) : (_jsxs(_Fragment, { children: [source.effectiveFormat === "mjml" && (_jsx(Banner, { variant: "info", testId: "document-designer-mjml-notice", children: t(isSystemAdmin
124
+ ? "documentDesigner.editor.mjmlNotice.platform"
125
+ : "documentDesigner.editor.mjmlNotice.tenant") })), _jsxs("div", { className: isMail
126
+ ? "mx-auto w-[600px] max-w-full divide-y rounded-md border bg-card py-2"
127
+ : "mx-auto min-h-[842px] w-[595px] max-w-full divide-y rounded-md border bg-card py-2", "data-testid": "document-designer-paper", children: [_jsx(PaperRegion, { label: t("documentDesigner.editor.region.header"), testId: "document-designer-region-header", children: _jsx(DocumentContentEditor, { id: REGION_EDITOR_IDS.header, value: header, onChange: setHeader, variables: placeholders, readOnly: false, blocks: isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS, toolbar: "compact" }) }), _jsx(PaperRegion, { label: t("documentDesigner.editor.region.content"), testId: "document-designer-region-content", children: _jsx("div", { className: "rounded-md border border-dashed px-4 py-6 text-center text-sm text-muted-foreground", children: t(isMail
128
+ ? "documentDesigner.editor.slot.mail"
129
+ : "documentDesigner.editor.slot.pdf") }) }), isMail && (_jsx(PaperRegion, { label: t("documentDesigner.editor.region.signature"), testId: "document-designer-region-signature", children: _jsx(DocumentContentEditor, { id: REGION_EDITOR_IDS.signature, value: signature, onChange: setSignature, variables: placeholders, readOnly: false, blocks: MAIL_DOCUMENT_BLOCKS, toolbar: "compact" }) })), _jsx(PaperRegion, { label: t("documentDesigner.editor.region.footer"), testId: "document-designer-region-footer", children: _jsx(DocumentContentEditor, { id: REGION_EDITOR_IDS.footer, value: footer, onChange: setFooter, variables: placeholders, readOnly: false, blocks: isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS, toolbar: "compact" }) })] })] }))] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(AppearanceRail, { kind: kind }), showMjmlTab && !mjmlMode && source.mjmlSource !== null && (_jsx(MjmlPreviewRail, { mjmlSource: source.mjmlSource })), _jsx(UsedByRail, { layoutSlug: slug, kind: kind, rows: overviewRows })] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3 border-t pt-3", "data-testid": "document-designer-footer-bar", children: [saveError !== null ? (_jsx(Banner, { variant: "error", testId: "document-designer-save-error", children: saveError })) : (_jsx(FooterMessage, { blockingIssue: blockingIssue, checked: checked !== null, userCount: userCount, isMail: isMail })), _jsx("div", { className: "grow" }), dirty && (_jsx(Button, { variant: "ghost", onClick: () => {
130
+ setHeader(source.regions.header);
131
+ setSignature(source.regions.signature);
132
+ setFooter(source.regions.footer);
133
+ setMjml(source.mjmlSource ?? "");
134
+ setSaveError(null);
135
+ }, testId: "document-designer-discard", children: t("documentDesigner.editor.discard") })), _jsx(Button, { variant: "secondary", disabled: !canSave, loading: busy, onClick: () => save(false), testId: "document-designer-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { disabled: !canPublish, onClick: () => save(true), testId: "document-designer-publish", children: t("documentDesigner.editor.publish") })] }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
136
+ if (!open)
137
+ setPendingKind(null);
138
+ }, title: t("documentDesigner.editor.dirtySwitch.title"), description: t("documentDesigner.editor.dirtySwitch.description"), confirmLabel: t("documentDesigner.editor.dirtySwitch.confirm"), variant: "danger", onConfirm: () => {
139
+ if (pendingKind !== null)
140
+ onKindChange(pendingKind);
141
+ } })] }));
142
+ }
@@ -0,0 +1,2 @@
1
+ import { type ReactNode } from "react";
2
+ export declare function LayoutEditor(): ReactNode;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { useNav, useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
5
+ import { ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
6
+ import { useState } from "react";
7
+ import { DEFAULT_LAYOUT_SLUG, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_SCREEN_ID, } from "../constants.js";
8
+ import { LayoutEditorBody } from "./layout-editor-body.js";
9
+ import { parseOverviewRow } from "./overview-row.js";
10
+ function parseLayoutKind(raw) {
11
+ return raw === "pdf-layout" ? "pdf-layout" : "mail-layout";
12
+ }
13
+ function LayoutEditorLoader({ kind, slug, isSystemAdmin, onKindChange, }) {
14
+ const nav = useNav();
15
+ const source = useQuery(TemplateResolverQueries.layoutSource, { kind, slug });
16
+ const overview = useQuery(TemplateResolverQueries.overview, { limit: 200 });
17
+ const overrides = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind }, { enabled: isSystemAdmin });
18
+ if (source.error !== null) {
19
+ return (_jsx(ErrorState, { error: source.error, onRetry: () => void source.refetch(), testId: "document-designer-layout-error" }));
20
+ }
21
+ if (source.data === null) {
22
+ return _jsx(LoadingState, {});
23
+ }
24
+ const data = source.data;
25
+ const rows = (overview.data?.rows ?? []).flatMap((row) => parseOverviewRow(row) ?? []);
26
+ const reload = async () => {
27
+ await Promise.all([source.refetch(), overview.refetch(), overrides.refetch()]);
28
+ };
29
+ // A saved or reset layout is a new version: remount so the editors take the stored regions.
30
+ const bodyKey = [kind, slug, data.tenant?.id, data.tenant?.version, data.system?.version].join("|");
31
+ return (_jsx(LayoutEditorBody, { kind: kind, slug: slug, isSystemAdmin: isSystemAdmin, source: data, overviewRows: rows, overrides: isSystemAdmin ? overrides.data : null, onKindChange: onKindChange, onReload: reload, onBack: () => nav.navigate({
32
+ screenId: isSystemAdmin ? PLATFORM_TEMPLATES_SCREEN_ID : TEMPLATES_SCREEN_ID,
33
+ }) }, bodyKey));
34
+ }
35
+ export function LayoutEditor() {
36
+ const nav = useNav();
37
+ const roles = useUserRoles();
38
+ const [kind, setKind] = useState(() => parseLayoutKind(nav.searchParams["kind"]));
39
+ const slug = nav.searchParams["slug"] ?? DEFAULT_LAYOUT_SLUG;
40
+ return (_jsx(LayoutEditorLoader, { kind: kind, slug: slug, isSystemAdmin: roles?.includes("SystemAdmin") === true, onKindChange: setKind }, kind));
41
+ }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from "react";
2
+ export type LayoutHtmlProps = {
3
+ readonly html: string;
4
+ /** Label per system placeholder (`data-variable`), shown as a chip. */
5
+ readonly systemLabels: Readonly<Record<string, string>>;
6
+ };
7
+ export declare function LayoutHtml(props: LayoutHtmlProps): ReactNode;
@@ -0,0 +1,81 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { cn } from "@cosmicdrift/kumiko-renderer-web";
4
+ import { createElement, useMemo } from "react";
5
+ // Layout regions arrive server-sanitised, but the preview still renders them
6
+ // through a whitelist instead of innerHTML so a sanitiser gap can never run
7
+ // markup in the admin session. Unknown elements are unwrapped, scripts dropped.
8
+ const TAG_CLASSES = {
9
+ div: "",
10
+ p: "m-0",
11
+ span: "",
12
+ strong: "font-semibold",
13
+ b: "font-semibold",
14
+ em: "italic",
15
+ i: "italic",
16
+ u: "underline",
17
+ small: "text-xs",
18
+ h1: "m-0 text-lg font-semibold",
19
+ h2: "m-0 text-base font-semibold",
20
+ h3: "m-0 text-sm font-semibold",
21
+ table: "w-full border-collapse",
22
+ thead: "",
23
+ tbody: "",
24
+ tr: "",
25
+ td: "align-top",
26
+ th: "align-top text-left font-semibold",
27
+ ul: "m-0 list-disc pl-5",
28
+ ol: "m-0 list-decimal pl-5",
29
+ li: "",
30
+ a: "underline",
31
+ hr: "my-1 border-border",
32
+ br: "",
33
+ img: "max-h-12 max-w-full object-contain",
34
+ };
35
+ const VOID_TAGS = new Set(["br", "hr", "img"]);
36
+ const DROPPED_TAGS = new Set(["script", "style", "iframe", "object", "embed"]);
37
+ const SAFE_IMAGE_SRC = /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/=]+$/;
38
+ const ALIGN_CLASSES = {
39
+ left: "text-left",
40
+ center: "text-center",
41
+ right: "text-right",
42
+ justify: "text-justify",
43
+ };
44
+ const ELEMENT_NODE = 1;
45
+ const TEXT_NODE = 3;
46
+ function renderChildren(node, props, path) {
47
+ return Array.from(node.childNodes).map((child, index) => renderNode(child, props, `${path}.${index}`));
48
+ }
49
+ function renderNode(node, props, path) {
50
+ if (node.nodeType === TEXT_NODE)
51
+ return node.textContent;
52
+ if (node.nodeType !== ELEMENT_NODE)
53
+ return null;
54
+ const tag = node.nodeName.toLowerCase();
55
+ if (DROPPED_TAGS.has(tag))
56
+ return null;
57
+ if (!(node instanceof Element))
58
+ return null;
59
+ if (!(tag in TAG_CLASSES))
60
+ return _jsx("span", { children: renderChildren(node, props, path) }, path);
61
+ const variable = node.getAttribute("data-variable");
62
+ if (variable !== null) {
63
+ return (_jsx("span", { className: "mx-0.5 inline-flex rounded border border-primary/30 bg-primary/10 px-1.5 text-xs text-primary", children: props.systemLabels[variable] ?? variable }, path));
64
+ }
65
+ const align = ALIGN_CLASSES[node.getAttribute("data-align") ?? ""];
66
+ const className = cn(TAG_CLASSES[tag], align);
67
+ if (tag === "img") {
68
+ const src = node.getAttribute("src") ?? "";
69
+ if (!SAFE_IMAGE_SRC.test(src))
70
+ return null;
71
+ return createElement("img", { key: path, className, src, alt: node.getAttribute("alt") ?? "" });
72
+ }
73
+ if (VOID_TAGS.has(tag))
74
+ return createElement(tag, { key: path, className });
75
+ return createElement(tag, { key: path, className }, ...renderChildren(node, props, path));
76
+ }
77
+ export function LayoutHtml(props) {
78
+ const { html } = props;
79
+ const body = useMemo(() => new DOMParser().parseFromString(html, "text/html").body, [html]);
80
+ return _jsx(_Fragment, { children: renderChildren(body, props, "r") });
81
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react";
2
+ import type { LayoutKind, LayoutValidation } from "./layout-types.js";
3
+ import type { OverviewRowView } from "./overview-row.js";
4
+ export declare function BrandingRail(): ReactNode;
5
+ export declare function PlaceholderRail({ validation, }: {
6
+ readonly validation: LayoutValidation | null;
7
+ }): ReactNode;
8
+ export declare function AppearanceRail({ kind }: {
9
+ readonly kind: LayoutKind;
10
+ }): ReactNode;
11
+ export declare function UsedByRail({ layoutSlug, kind, rows, }: {
12
+ readonly layoutSlug: string;
13
+ readonly kind: LayoutKind;
14
+ readonly rows: readonly OverviewRowView[];
15
+ }): ReactNode;
16
+ export declare function MjmlPreviewRail({ mjmlSource }: {
17
+ readonly mjmlSource: string;
18
+ }): ReactNode;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { useAppFeatures, useNav, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
4
+ import { DetailList, EmptyState, SectionCard } from "@cosmicdrift/kumiko-renderer-web";
5
+ import { BRANDING_QUERY_QN, BRANDING_SCREEN_QN } from "../constants.js";
6
+ import { layoutUsers } from "./layout-users.js";
7
+ import { statusLabel } from "./status-label.js";
8
+ const NOT_SET_MARK = "–";
9
+ const HEX_COLOR = /^#[0-9a-f]{3,8}$/i;
10
+ const BRANDING_SCREEN_SHORT_ID = BRANDING_SCREEN_QN.slice(BRANDING_SCREEN_QN.lastIndexOf(":") + 1);
11
+ function logoName(logoUrl) {
12
+ const path = logoUrl.split("?")[0] ?? "";
13
+ return path.slice(path.lastIndexOf("/") + 1);
14
+ }
15
+ function hostOf(siteUrl) {
16
+ return siteUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
17
+ }
18
+ function BrandingSwatch({ color }) {
19
+ if (!HEX_COLOR.test(color))
20
+ return color;
21
+ return (_jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { "data-testid": "document-designer-accent-swatch", className: "inline-block size-3.5 rounded-sm border", style: { backgroundColor: color } }), _jsx("span", { className: "font-mono text-xs", children: color })] }));
22
+ }
23
+ export function BrandingRail() {
24
+ const t = useTranslation();
25
+ const nav = useNav();
26
+ const features = useAppFeatures();
27
+ const { Link, Text } = usePrimitives();
28
+ const { data, loading, error } = useQuery(BRANDING_QUERY_QN, {});
29
+ const hasBrandingScreen = features.some((feature) => feature.featureName === "managed-pages" &&
30
+ feature.screens.some((screen) => screen.id === BRANDING_SCREEN_SHORT_ID));
31
+ const notSet = t("documentDesigner.branding.notSet");
32
+ const editLink = hasBrandingScreen ? (_jsx(Link, { href: nav.hrefFor({ screenId: BRANDING_SCREEN_SHORT_ID }), onPress: () => nav.navigate({ screenId: BRANDING_SCREEN_SHORT_ID }), testId: "document-designer-branding-edit", children: t("documentDesigner.branding.edit") })) : undefined;
33
+ if (error !== null || (data === null && !loading)) {
34
+ return (_jsx(SectionCard, { title: t("documentDesigner.branding.title"), testId: "document-designer-branding", children: _jsx(EmptyState, { title: t("documentDesigner.branding.unavailable") }) }));
35
+ }
36
+ const value = (text) => text === undefined || text === "" ? notSet : text;
37
+ const logo = data?.logoUrl !== undefined && data.logoUrl !== ""
38
+ ? logoName(data.logoUrl)
39
+ : data?.logoFile !== undefined && data.logoFile !== ""
40
+ ? t("documentDesigner.branding.logoUploaded")
41
+ : notSet;
42
+ return (_jsxs(SectionCard, { title: t("documentDesigner.branding.title"), action: editLink, testId: "document-designer-branding", children: [_jsx(DetailList, { rows: [
43
+ { label: t("documentDesigner.branding.productName"), value: value(data?.title) },
44
+ {
45
+ label: t("documentDesigner.branding.accentColor"),
46
+ value: data?.accentColor === undefined || data.accentColor === "" ? (notSet) : (_jsx(BrandingSwatch, { color: data.accentColor })),
47
+ },
48
+ { label: t("documentDesigner.branding.logo"), value: logo },
49
+ {
50
+ label: t("documentDesigner.branding.website"),
51
+ value: data?.siteUrl === undefined || data.siteUrl === "" ? notSet : hostOf(data.siteUrl),
52
+ },
53
+ ] }), _jsx(Text, { variant: "muted", children: t("documentDesigner.branding.hint") })] }));
54
+ }
55
+ export function PlaceholderRail({ validation, }) {
56
+ const t = useTranslation();
57
+ const used = new Set(validation?.used ?? []);
58
+ const placeholderLabel = (name) => {
59
+ const key = `documentDesigner.placeholder.${name}`;
60
+ const label = t(key);
61
+ return label === key ? name : label;
62
+ };
63
+ return (_jsx(SectionCard, { title: t("documentDesigner.layoutPlaceholders.title"), testId: "document-designer-placeholders", children: _jsx(DetailList, { rows: [
64
+ {
65
+ label: `${t("documentDesigner.placeholders.content")} *`,
66
+ value: "✓",
67
+ },
68
+ ...(validation?.systemPlaceholders ?? []).map((name) => ({
69
+ id: `placeholder-${name}`,
70
+ label: placeholderLabel(name),
71
+ value: used.has(name) ? "✓" : NOT_SET_MARK,
72
+ })),
73
+ ] }) }));
74
+ }
75
+ export function AppearanceRail({ kind }) {
76
+ const t = useTranslation();
77
+ const { Text } = usePrimitives();
78
+ const rows = kind === "mail-layout"
79
+ ? [
80
+ { label: t("documentDesigner.appearance.width"), value: "600 px" },
81
+ {
82
+ label: t("documentDesigner.appearance.font"),
83
+ value: t("documentDesigner.appearance.systemFont"),
84
+ },
85
+ {
86
+ label: t("documentDesigner.appearance.background"),
87
+ value: t("documentDesigner.appearance.backgroundValue"),
88
+ },
89
+ ]
90
+ : [{ label: t("documentDesigner.appearance.pageFormat"), value: "A4" }];
91
+ return (_jsxs(SectionCard, { title: t("documentDesigner.appearance.title"), testId: "document-designer-appearance", children: [_jsx(DetailList, { rows: rows }), _jsx(Text, { variant: "muted", children: t("documentDesigner.appearance.hint") })] }));
92
+ }
93
+ export function UsedByRail({ layoutSlug, kind, rows, }) {
94
+ const t = useTranslation();
95
+ const users = layoutUsers(rows, kind, layoutSlug);
96
+ return (_jsx(SectionCard, { title: t("documentDesigner.usedBy.title"), testId: "document-designer-used-by", children: users.length === 0 ? (_jsx(EmptyState, { title: t("documentDesigner.usedBy.none") })) : (_jsx(DetailList, { rows: users.map((row) => ({
97
+ id: `used-by-${row.id}`,
98
+ label: row.title,
99
+ value: row.origin === "system"
100
+ ? t("documentDesigner.source.system")
101
+ : statusLabel(t, row.status, row.version),
102
+ })) })) }));
103
+ }
104
+ export function MjmlPreviewRail({ mjmlSource }) {
105
+ const t = useTranslation();
106
+ return (_jsx(SectionCard, { title: t("documentDesigner.mjml.title"), testId: "document-designer-mjml-preview", children: _jsx("pre", { className: "max-h-56 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-3 font-mono text-xs", children: mjmlSource }) }));
107
+ }
@@ -0,0 +1,62 @@
1
+ export type LayoutKind = "mail-layout" | "pdf-layout";
2
+ export type ValidationIssue = {
3
+ readonly path: string;
4
+ readonly code: string;
5
+ readonly i18nKey: string;
6
+ readonly params?: Readonly<Record<string, unknown>>;
7
+ };
8
+ export type LayoutValidation = {
9
+ readonly canSave: boolean;
10
+ readonly canPublish: boolean;
11
+ readonly issues: readonly ValidationIssue[];
12
+ readonly publishIssues: readonly ValidationIssue[];
13
+ readonly used: readonly string[];
14
+ readonly systemPlaceholders: readonly string[];
15
+ };
16
+ export type BrandingView = {
17
+ readonly title: string;
18
+ readonly accentColor: string;
19
+ readonly logoUrl: string;
20
+ readonly logoFile: string;
21
+ readonly siteUrl: string;
22
+ };
23
+ export type TenantOverrideCounts = {
24
+ readonly activeOverrides: number;
25
+ readonly draftOverrides: number;
26
+ readonly totalTenants: number | null;
27
+ };
28
+ export type TemplateDiff = {
29
+ readonly identical: boolean;
30
+ readonly blocks: readonly {
31
+ readonly change: "same" | "added" | "removed";
32
+ readonly content: string;
33
+ }[];
34
+ readonly meta: readonly {
35
+ readonly key: string;
36
+ readonly tenant?: unknown;
37
+ readonly system?: unknown;
38
+ }[];
39
+ };
40
+ export type UpsertResponse = {
41
+ readonly id: string;
42
+ };
43
+ export type LayoutSourceRow = {
44
+ readonly id: string;
45
+ readonly version: number;
46
+ readonly status: string;
47
+ readonly locale: string;
48
+ };
49
+ export type LayoutSourceView = {
50
+ readonly tenant: LayoutSourceRow | null;
51
+ readonly system: (LayoutSourceRow & {
52
+ readonly contentFormat: string;
53
+ }) | null;
54
+ readonly regions: {
55
+ readonly header: string;
56
+ readonly signature: string;
57
+ readonly footer: string;
58
+ };
59
+ readonly effectiveFormat: "html" | "mjml" | null;
60
+ readonly mjmlSource: string | null;
61
+ readonly locale: string;
62
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { LayoutKind } from "./layout-types.js";
2
+ import type { OverviewRowView } from "./overview-row.js";
3
+ export declare function layoutUsers(rows: readonly OverviewRowView[], kind: LayoutKind, layoutSlug: string): readonly OverviewRowView[];
@@ -0,0 +1,8 @@
1
+ // @runtime client
2
+ import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
3
+ // Templates whose content is wrapped by this layout. A template without an explicit
4
+ // layoutSlug uses the default layout.
5
+ export function layoutUsers(rows, kind, layoutSlug) {
6
+ const documentKind = kind === "mail-layout" ? "mail" : "document-pdf";
7
+ return rows.filter((row) => row.kind === documentKind && (row.layoutSlug ?? DEFAULT_LAYOUT_SLUG) === layoutSlug);
8
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from "react";
2
+ export type LeaveTarget = {
3
+ readonly kind: "locale";
4
+ readonly locale: string;
5
+ } | {
6
+ readonly kind: "back";
7
+ };
8
+ export type ConfirmDialog = {
9
+ readonly kind: "none";
10
+ } | {
11
+ readonly kind: "discard";
12
+ } | {
13
+ readonly kind: "activeSave";
14
+ } | {
15
+ readonly kind: "remove";
16
+ readonly key: string;
17
+ } | {
18
+ readonly kind: "leave";
19
+ readonly target: LeaveTarget;
20
+ };
21
+ export declare function MailConfirmDialogs({ dialog, onClose, onDiscard, onSaveDraft, onRemove, onLeaveSave, onLeaveDiscard, }: {
22
+ readonly dialog: ConfirmDialog;
23
+ readonly onClose: () => void;
24
+ readonly onDiscard: () => void;
25
+ readonly onSaveDraft: () => Promise<void>;
26
+ readonly onRemove: (key: string) => Promise<void>;
27
+ readonly onLeaveSave: (target: LeaveTarget) => Promise<void>;
28
+ readonly onLeaveDiscard: (target: LeaveTarget) => void;
29
+ }): ReactNode;
@@ -0,0 +1,15 @@
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
+ export function MailConfirmDialogs({ dialog, onClose, onDiscard, onSaveDraft, onRemove, onLeaveSave, onLeaveDiscard, }) {
5
+ const t = useTranslation();
6
+ const { Button, Dialog } = usePrimitives();
7
+ const closeUnless = (open) => {
8
+ if (!open)
9
+ onClose();
10
+ };
11
+ return (_jsxs(_Fragment, { children: [_jsx(Dialog, { open: dialog.kind === "discard", onOpenChange: closeUnless, title: t("documentDesigner.mail.discard.title"), description: t("documentDesigner.mail.discard.description"), confirmLabel: t("documentDesigner.mail.discard.confirm"), variant: "danger", testId: "mail-discard-dialog", onConfirm: onDiscard }), _jsx(Dialog, { open: dialog.kind === "activeSave", onOpenChange: closeUnless, title: t("documentDesigner.mail.activeSave.title"), description: t("documentDesigner.mail.activeSave.description"), confirmLabel: t("documentDesigner.mail.activeSave.confirm"), testId: "mail-active-save-dialog", onConfirm: onSaveDraft }), _jsx(Dialog, { open: dialog.kind === "remove", onOpenChange: closeUnless, title: t("documentDesigner.mail.remove.title"), description: t("documentDesigner.mail.remove.description"), confirmLabel: t("documentDesigner.mail.remove.confirm"), variant: "danger", testId: "mail-remove-dialog", onConfirm: () => (dialog.kind === "remove" ? onRemove(dialog.key) : Promise.resolve()) }), _jsx(Dialog, { open: dialog.kind === "leave", onOpenChange: closeUnless, title: t("documentDesigner.mail.leave.title"), description: t("documentDesigner.mail.leave.description"), confirmLabel: t("documentDesigner.mail.leave.save"), testId: "mail-leave-dialog", onConfirm: () => (dialog.kind === "leave" ? onLeaveSave(dialog.target) : Promise.resolve()), children: _jsx(Button, { variant: "danger-ghost", onClick: () => {
12
+ if (dialog.kind === "leave")
13
+ onLeaveDiscard(dialog.target);
14
+ }, testId: "mail-leave-discard", children: t("documentDesigner.mail.leave.discard") }) })] }));
15
+ }
@@ -0,0 +1,17 @@
1
+ import type { LinkedResourceView, MailDraft, MailVariable, PlaceholderType, TemplateRowView } from "./mail-types.js";
2
+ export declare const EMPTY_DRAFT: MailDraft;
3
+ export declare const PLACEHOLDER_SYMBOL: Readonly<Record<PlaceholderType, string>>;
4
+ export declare function parseJsonRecord(value: unknown): Readonly<Record<string, unknown>>;
5
+ export type LoadedMail = {
6
+ readonly draft: MailDraft;
7
+ readonly sender: unknown;
8
+ };
9
+ export declare function loadedMailOf(row: TemplateRowView | null): LoadedMail;
10
+ export declare function buildMeta(draft: MailDraft, sender: unknown): Record<string, unknown>;
11
+ export declare function isDraftDirty(current: MailDraft, saved: MailDraft): boolean;
12
+ export declare function parseLinkedResources(value: unknown): readonly LinkedResourceView[];
13
+ export declare function uniqueResourceKey(source: string, taken: readonly string[]): string;
14
+ export declare function lastSlugSegment(slug: string): string;
15
+ export declare function formatKilobytes(bytes: number): string;
16
+ export declare function variableLabelMap(variables: Readonly<Record<string, MailVariable>>): Record<string, string>;
17
+ export declare function displayValue(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, value: unknown): string;