@cosmicdrift/kumiko-document-designer 0.389.0 → 0.390.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 (42) hide show
  1. package/dist/web/layout-editor-body.d.ts +3 -14
  2. package/dist/web/layout-editor-body.js +25 -172
  3. package/dist/web/layout-editor-center.d.ts +7 -0
  4. package/dist/web/layout-editor-center.js +23 -0
  5. package/dist/web/layout-editor-logic.d.ts +97 -0
  6. package/dist/web/layout-editor-logic.js +143 -0
  7. package/dist/web/layout-editor-paper.d.ts +4 -0
  8. package/dist/web/layout-editor-paper.js +24 -0
  9. package/dist/web/mail-editor-body.d.ts +4 -16
  10. package/dist/web/mail-editor-body.js +27 -218
  11. package/dist/web/mail-editor-logic.d.ts +70 -0
  12. package/dist/web/mail-editor-logic.js +81 -0
  13. package/dist/web/mail-editor-model.d.ts +105 -0
  14. package/dist/web/mail-editor-model.js +114 -0
  15. package/dist/web/mail-editor-render.d.ts +11 -0
  16. package/dist/web/mail-editor-render.js +51 -0
  17. package/dist/web/mail-preview-render.d.ts +5 -0
  18. package/dist/web/mail-preview-render.js +22 -0
  19. package/dist/web/pdf-editor-actions.d.ts +27 -0
  20. package/dist/web/pdf-editor-actions.js +146 -0
  21. package/dist/web/pdf-editor-chrome.d.ts +6 -0
  22. package/dist/web/pdf-editor-chrome.js +60 -0
  23. package/dist/web/pdf-editor-context.d.ts +22 -0
  24. package/dist/web/pdf-editor-context.js +1 -0
  25. package/dist/web/pdf-editor-draft.d.ts +23 -0
  26. package/dist/web/pdf-editor-draft.js +33 -0
  27. package/dist/web/pdf-editor-panes.d.ts +6 -0
  28. package/dist/web/pdf-editor-panes.js +40 -0
  29. package/dist/web/pdf-editor-view.d.ts +52 -0
  30. package/dist/web/pdf-editor-view.js +89 -0
  31. package/dist/web/pdf-template-editor.d.ts +1 -1
  32. package/dist/web/pdf-template-editor.js +40 -342
  33. package/dist/web/use-layout-editor-state.d.ts +62 -0
  34. package/dist/web/use-layout-editor-state.js +155 -0
  35. package/dist/web/use-mail-editor-parts.d.ts +42 -0
  36. package/dist/web/use-mail-editor-parts.js +74 -0
  37. package/dist/web/use-mail-editor-state.d.ts +89 -0
  38. package/dist/web/use-mail-editor-state.js +55 -0
  39. package/dist/web/use-pdf-editor-state.d.ts +77 -0
  40. package/dist/web/use-pdf-editor-state.js +161 -0
  41. package/package.json +6 -6
  42. package/src/changes.json +18 -0
@@ -0,0 +1,51 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EditorLocaleSwitch } from "./editor-header.js";
3
+ import { EditorBody } from "./editor-shell.js";
4
+ import { MailEditCenter } from "./mail-edit-center.js";
5
+ import { PlaceholderSection, ResourceSection, SystemPlaceholderSection, } from "./mail-left-rail.js";
6
+ import { MissingLocaleCanvas } from "./mail-missing-locale.js";
7
+ import { PdfLinkModal } from "./mail-pdf-link-modal.js";
8
+ import { AttachmentSection, ChromeSection, PropertiesSection, UsageSection, } from "./mail-right-rail.js";
9
+ import { UploadModal } from "./mail-upload-modal.js";
10
+ import { VersionsSection } from "./mail-versions.js";
11
+ export function renderMailControls(props, m) {
12
+ return (_jsx(EditorLocaleSwitch, { testIdPrefix: "mail", locales: props.locales, locale: props.locale, onLocale: (next) => m.leave({ kind: "locale", locale: next }) }));
13
+ }
14
+ export function renderMailBanners(Banner, t, m) {
15
+ const { testMail } = m.services;
16
+ const { resetError, resourceError } = m.ui;
17
+ return (_jsxs(_Fragment, { children: [testMail.state.kind === "sent" && (_jsx(Banner, { variant: "primary", testId: "mail-test-sent", children: t("documentDesigner.mail.testMail.sent", { subject: testMail.state.subject }) })), testMail.state.kind === "error" && (_jsx(Banner, { variant: "error", testId: "mail-test-error", children: testMail.state.text })), resetError !== null && (_jsx(Banner, { variant: "error", testId: "mail-reset-error", children: resetError })), resourceError !== null && (_jsx(Banner, { variant: "error", testId: "mail-resource-error", children: resourceError }))] }));
18
+ }
19
+ function renderLeftRail(m) {
20
+ const { insert, filesAvailable } = m.services;
21
+ const { setUploadOpen, setDialog } = m.ui;
22
+ return (_jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: m.railValidation, onInsert: insert.insertPlaceholder }), _jsx(SystemPlaceholderSection, { validation: m.railValidation, onInsert: insert.insertPlaceholder }), _jsx(ResourceSection, { resources: m.resources, pdfTitles: m.pdfTitles, canUpload: m.uploadTemplateId !== null, filesAvailable: filesAvailable, onUpload: () => setUploadOpen(true), onInsert: insert.insertResource, onRemove: (key) => setDialog({ kind: "remove", key }) })] }));
23
+ }
24
+ function renderCenterCanvas(props, m) {
25
+ if (m.locked) {
26
+ return (_jsx(MissingLocaleCanvas, { slug: props.slug, locale: props.locale, fallbackLocale: props.fallbackLocale, copyScope: props.isSystemAdmin ? "system" : "tenant", onCreateBlank: () => m.ui.setCreating(true), onCopied: () => void props.onReload() }));
27
+ }
28
+ return (_jsx(MailEditCenter, { draft: m.ui.draft, vocabulary: m.vocabulary, resourcePreviews: m.resourcePreviews, insert: m.services.insert, readOnly: false, fieldErrors: m.fieldErrors, onChange: m.update }, m.ui.editorEpoch));
29
+ }
30
+ function renderRightRail(props, m) {
31
+ const { row } = props;
32
+ return (_jsxs(_Fragment, { children: [_jsx(PropertiesSection, { slug: props.slug, draft: m.ui.draft, layouts: m.services.layouts.rows, onChange: m.update }), _jsx(ChromeSection, { draft: m.ui.draft, vocabulary: m.vocabulary, insert: m.services.insert, onChange: m.update }), _jsx(AttachmentSection, { attachments: m.attachments, pdfTitles: m.pdfTitles, canEdit: m.uploadTemplateId !== null, onAddPdf: () => m.ui.setPdfOpen(true), onRemove: (key) => m.ui.setDialog({ kind: "remove", key }) }), _jsx(UsageSection, { slug: props.slug }), _jsx(VersionsSection, { rowId: row?.id ?? null, currentVersion: row?.version ?? 0, currentStatus: row?.status ?? "draft" })] }));
33
+ }
34
+ export function renderMailEditBody(props, m) {
35
+ return (_jsx(EditorBody, { left: renderLeftRail(m), center: renderCenterCanvas(props, m), right: renderRightRail(props, m) }));
36
+ }
37
+ export function renderMailModals(props, m) {
38
+ const { uploadTemplateId, takenKeys } = m;
39
+ if (uploadTemplateId === null)
40
+ return false; // skip: only tenant rows accept uploads
41
+ const { insert, pdfTemplates } = m.services;
42
+ return (_jsxs(_Fragment, { children: [_jsx(UploadModal, { open: m.ui.uploadOpen, onOpenChange: m.ui.setUploadOpen, target: {
43
+ templateId: uploadTemplateId,
44
+ takenKeys,
45
+ onUploaded: (uploaded) => {
46
+ if (uploaded.purpose === "image")
47
+ insert.insertResource(uploaded.key);
48
+ void props.onRefreshRow();
49
+ },
50
+ } }), _jsx(PdfLinkModal, { open: m.ui.pdfOpen, onOpenChange: m.ui.setPdfOpen, templates: pdfTemplates.rows, variableLabels: m.vocabulary.labels, templateId: uploadTemplateId, takenKeys: takenKeys, onLinked: () => void props.onRefreshRow() })] }));
51
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+ import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
3
+ import type { MailEditorModel } from "./mail-editor-model.ts";
4
+ import { MailPreviewMode } from "./mail-preview.tsx";
5
+ export declare function renderMailPreview(props: MailEditorBodyProps, m: MailEditorModel, controls: ReactNode, origin: React.ComponentProps<typeof MailPreviewMode>["origin"]): ReactNode;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { previewPayload } from "./mail-editor-logic.js";
3
+ import { MailPreviewMode } from "./mail-preview.js";
4
+ export function renderMailPreview(props, m, controls, origin) {
5
+ const { ui, services } = m;
6
+ const { testMail } = services;
7
+ return (_jsx(MailPreviewMode, { payload: previewPayload({
8
+ slug: props.slug,
9
+ locale: props.locale,
10
+ settled: ui.settled,
11
+ variableSchema: m.variableSchema,
12
+ pickedVariables: m.pickedVariables,
13
+ }), picked: m.picked, onPick: m.setPicked, validation: m.validation ?? null, slug: props.slug, locale: props.locale, fromName: ui.settled.senderName, own: services.own, origin: origin, controls: controls, testMail: {
14
+ availability: testMail.availability,
15
+ pending: testMail.pending,
16
+ onSend: m.sendTestMail,
17
+ includeAttachments: ui.includeAttachments,
18
+ onIncludeAttachments: ui.setIncludeAttachments,
19
+ prefixSubject: ui.prefixSubject,
20
+ onPrefixSubject: ui.setPrefixSubject,
21
+ } }));
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { useDispatcher } from "@cosmicdrift/kumiko-renderer";
2
+ import { type Draft, type EditorState } from "./pdf-editor-draft.ts";
3
+ import { type TemplateDetail } from "./types.ts";
4
+ import type { PdfEditorState } from "./use-pdf-editor-state.ts";
5
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
6
+ export type PdfActionsInput = {
7
+ readonly s: PdfEditorState;
8
+ readonly t: Translate;
9
+ readonly dispatcher: ReturnType<typeof useDispatcher>;
10
+ readonly slug: string;
11
+ readonly current: EditorState;
12
+ readonly detail: TemplateDetail;
13
+ readonly dirty: boolean;
14
+ readonly onLocaleChange: ((locale: string) => void) | undefined;
15
+ };
16
+ export declare function buildPdfActions(input: PdfActionsInput): {
17
+ updateDraft: (next: Draft) => void;
18
+ resetDraft: (source: TemplateDetail) => void;
19
+ save: () => Promise<boolean>;
20
+ publish: () => Promise<void>;
21
+ generatePdf: () => Promise<void>;
22
+ printOnce: () => void;
23
+ uploadResource: (file: File) => Promise<void>;
24
+ requestLocale: (next: string) => void;
25
+ };
26
+ export type PdfActions = ReturnType<typeof buildPdfActions>;
27
+ export {};
@@ -0,0 +1,146 @@
1
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
2
+ import { uploadFileRef } from "@cosmicdrift/kumiko-renderer/web";
3
+ import { RendererPuppeteerClientQueries } from "@cosmicdrift/kumiko-renderer-puppeteer-client/qualified-names";
4
+ import { describeError, isAlreadyActive } from "./errors.js";
5
+ import { draftFromDetail, SETTLE_MS, snapshotOf, } from "./pdf-editor-draft.js";
6
+ import { serializePdfMeta } from "./pdf-meta.js";
7
+ import { DOCUMENT_PDF_KIND, } from "./types.js";
8
+ function draftActions(input) {
9
+ const { s } = input;
10
+ const { setState } = s.doc;
11
+ const { settleTimer } = s;
12
+ function updateDraft(next) {
13
+ setState((previous) => (previous === null ? previous : { ...previous, draft: next }));
14
+ if (settleTimer.current !== null)
15
+ clearTimeout(settleTimer.current);
16
+ settleTimer.current = setTimeout(() => {
17
+ setState((previous) => previous === null ? previous : { ...previous, settled: previous.draft });
18
+ }, SETTLE_MS);
19
+ }
20
+ function resetDraft(source) {
21
+ const draft = draftFromDetail(source);
22
+ if (settleTimer.current !== null)
23
+ clearTimeout(settleTimer.current);
24
+ setState({ key: source.id, draft, settled: draft, snapshot: snapshotOf(draft) });
25
+ s.ui.setDraftEpoch((epoch) => epoch + 1);
26
+ s.ui.setActionError(null);
27
+ }
28
+ return { updateDraft, resetDraft };
29
+ }
30
+ function writeActions(input) {
31
+ const { s, t, dispatcher, slug, current, detail, dirty } = input;
32
+ const { locale, rowsQuery, detailQuery, setState } = s.doc;
33
+ const { variableSchema, validationQuery } = s.server;
34
+ const { scope } = s.host;
35
+ const { setBusy, setActionError } = s.ui;
36
+ async function reload() {
37
+ await Promise.all([rowsQuery.refetch(), detailQuery.refetch(), validationQuery.refetch()]);
38
+ }
39
+ async function persist() {
40
+ const { draft } = current;
41
+ const parentTemplateId = detail?.scope === "system" ? detail.id : detail?.parentTemplateId;
42
+ const result = await dispatcher.write(scope === "system"
43
+ ? TemplateResolverHandlers.upsertSystem
44
+ : TemplateResolverHandlers.upsertTenant, {
45
+ slug,
46
+ kind: DOCUMENT_PDF_KIND,
47
+ locale,
48
+ content: draft.content,
49
+ contentFormat: "html",
50
+ variableSchema,
51
+ meta: serializePdfMeta(draft.meta),
52
+ ...(scope === "tenant" && parentTemplateId != null && { parentTemplateId }),
53
+ });
54
+ if (!result.isSuccess) {
55
+ setActionError(describeError(t, result.error));
56
+ return null;
57
+ }
58
+ setState((previous) => previous === null ? previous : { ...previous, snapshot: snapshotOf(draft) });
59
+ return result.data;
60
+ }
61
+ async function save() {
62
+ setBusy("save");
63
+ setActionError(null);
64
+ const saved = await persist();
65
+ if (saved !== null)
66
+ await reload();
67
+ setBusy(null);
68
+ return saved !== null;
69
+ }
70
+ async function publish() {
71
+ setBusy("publish");
72
+ setActionError(null);
73
+ // A system save activates immediately; a tenant row needs a save first when it
74
+ // does not exist yet or carries unsaved edits, then an explicit publish.
75
+ const needsSave = dirty || detail?.scope === "system";
76
+ const saved = needsSave ? await persist() : null;
77
+ const saveFailed = needsSave && saved === null;
78
+ // upsertSystem already activates, so only a tenant row needs the explicit publish.
79
+ const alreadyActivatedBySave = scope === "system" && saved !== null;
80
+ const templateId = saved?.id ?? detail?.id ?? null;
81
+ if (templateId !== null && !saveFailed && !alreadyActivatedBySave) {
82
+ const result = await dispatcher.write(scope === "system"
83
+ ? TemplateResolverHandlers.publishSystem
84
+ : TemplateResolverHandlers.publish, { id: templateId });
85
+ if (!result.isSuccess && !isAlreadyActive(result.error))
86
+ setActionError(describeError(t, result.error));
87
+ }
88
+ await reload();
89
+ setBusy(null);
90
+ }
91
+ return { save, publish };
92
+ }
93
+ function pdfActions(input) {
94
+ const { s, t, dispatcher, slug, current, detail, dirty, onLocaleChange } = input;
95
+ const { locale, detailQuery } = s.doc;
96
+ const { variableSchema } = s.server;
97
+ const { setMode, setBusy, setActionError, setGenerated, autoPrintedFor, setPendingLocale } = s.ui;
98
+ async function generatePdf() {
99
+ setMode("preview");
100
+ setBusy("pdf");
101
+ setActionError(null);
102
+ const forSnapshot = snapshotOf(current.draft);
103
+ const result = await dispatcher.query(RendererPuppeteerClientQueries.previewPdf, {
104
+ slug,
105
+ locale,
106
+ content: current.draft.content,
107
+ meta: serializePdfMeta(current.draft.meta),
108
+ variableSchema,
109
+ });
110
+ if (result.isSuccess)
111
+ setGenerated({ forSnapshot, result: result.data });
112
+ else
113
+ setActionError(describeError(t, result.error));
114
+ setBusy(null);
115
+ }
116
+ function printOnce() {
117
+ // skip: this draft was already printed
118
+ if (autoPrintedFor.current === snapshotOf(current.draft))
119
+ return;
120
+ autoPrintedFor.current = snapshotOf(current.draft);
121
+ void generatePdf();
122
+ }
123
+ async function uploadResource(file) {
124
+ const sourceFileRefId = await uploadFileRef(file);
125
+ const result = await dispatcher.write(TemplateResolverHandlers.resourceUpload, {
126
+ templateId: detail?.id,
127
+ sourceFileRefId,
128
+ });
129
+ if (!result.isSuccess)
130
+ throw new Error(describeError(t, result.error));
131
+ await detailQuery.refetch();
132
+ }
133
+ function requestLocale(next) {
134
+ // skip: the requested locale is already open
135
+ if (next === locale)
136
+ return;
137
+ if (dirty)
138
+ setPendingLocale(next);
139
+ else
140
+ onLocaleChange?.(next);
141
+ }
142
+ return { generatePdf, printOnce, uploadResource, requestLocale };
143
+ }
144
+ export function buildPdfActions(input) {
145
+ return { ...draftActions(input), ...writeActions(input), ...pdfActions(input) };
146
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from "react";
2
+ import type { PdfRenderContext } from "./pdf-editor-context.ts";
3
+ export declare function renderPdfHeader(c: PdfRenderContext): ReactNode;
4
+ export declare function renderPdfInfoBar(c: PdfRenderContext): ReactNode;
5
+ export declare function renderPdfFooter(c: PdfRenderContext): ReactNode;
6
+ export declare function renderPdfLocaleDialog(c: PdfRenderContext): ReactNode;
@@ -0,0 +1,60 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf } from "./editor-header.js";
3
+ import { EditorFooter, EditorInfoBar } from "./editor-shell.js";
4
+ import { OverridesDrawer } from "./overrides-drawer.js";
5
+ export function renderPdfHeader(c) {
6
+ const { s, view, detail, actions } = c;
7
+ const { busy, mode, setMode } = s.ui;
8
+ return (_jsx(EditorHeader, { title: view.title, stored: { status: detail.status, version: detail.version }, actions: {
9
+ busy: busy === "save",
10
+ canSave: busy === null && s.server.validation?.canSave !== false,
11
+ canPublish: busy === null && view.canPublish,
12
+ onSave: () => void actions.save(),
13
+ onPublish: () => void actions.publish(),
14
+ }, children: _jsx(EditorModeSwitch, { testIdPrefix: "pdf", mode: mode, onMode: setMode }) }));
15
+ }
16
+ function renderInfoControls(c) {
17
+ const { t, Button, s, view, actions } = c;
18
+ const { busy, mode } = s.ui;
19
+ const { locale, locales } = s.doc;
20
+ return (_jsxs(_Fragment, { children: [_jsx(EditorLocaleSwitch, { testIdPrefix: "pdf", locales: locales.length > 0 ? locales : [locale], locale: locale, onLocale: actions.requestLocale }), mode === "preview" && (_jsx(Button, { variant: "secondary", disabled: !view.pdfAvailable || busy !== null, loading: busy === "pdf", onClick: () => void actions.generatePdf(), testId: "pdf-generate-header", children: t("documentDesigner.actions.generatePdf") }))] }));
21
+ }
22
+ function renderOverrideLinks(c) {
23
+ const { t, Button, s, view } = c;
24
+ return (view.showOverrideLinks && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => void c.exportBundle(), children: t("documentDesigner.originBar.export") }), _jsx(Button, { variant: "link", onClick: () => s.ui.setOverridesOpen(true), children: t("documentDesigner.originBar.showOverrides") })] })));
25
+ }
26
+ function renderUnavailableNote(c) {
27
+ const { t, view } = c;
28
+ return (view.pdfUnavailableNote && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "data-testid": "pdf-unavailable", children: view.unavailableMessage === ""
29
+ ? t("documentDesigner.pdfUnavailableNoReason")
30
+ : t("documentDesigner.pdfUnavailable", { message: view.unavailableMessage }) })] })));
31
+ }
32
+ export function renderPdfInfoBar(c) {
33
+ const { s, view, detail, slug } = c;
34
+ const { overridesOpen, setOverridesOpen } = s.ui;
35
+ return (_jsxs(EditorInfoBar, { tone: view.pdfUnavailableNote ? "warn" : detail.scope === "tenant" ? "active" : "muted", testId: "pdf-origin-bar", origin: originOf(detail.scope === "system", view.systemVersion !== null), controls: renderInfoControls(c), overlays: _jsx(OverridesDrawer, { open: overridesOpen, onOpenChange: setOverridesOpen, slug: slug, locale: s.doc.locale, overrides: s.server.overridesQuery.data }), links: renderOverrideLinks(c), children: [view.originText, view.besideActiveSentence !== null && ` ${view.besideActiveSentence}`, renderUnavailableNote(c)] }));
36
+ }
37
+ export function renderPdfFooter(c) {
38
+ const { s, view, detail, actions } = c;
39
+ const { busy } = s.ui;
40
+ return (_jsx(EditorFooter, { testIdPrefix: "pdf-editor", message: view.footer, saveError: s.ui.actionError, modifiedAt: detail.modifiedAt ?? null, dirty: view.dirty, busy: busy === "save", canSave: busy === null && s.server.validation?.canSave !== false, canPublish: busy === null && view.canPublish, onDiscard: () => actions.resetDraft(detail), onSave: () => void actions.save(), onPublish: () => void actions.publish() }));
41
+ }
42
+ export function renderPdfLocaleDialog(c) {
43
+ const { t, Button, Dialog, s, actions, detail, onLocaleChange } = c;
44
+ const { pendingLocale, setPendingLocale } = s.ui;
45
+ return (_jsx(Dialog, { open: pendingLocale !== null, onOpenChange: (open) => {
46
+ if (!open)
47
+ setPendingLocale(null);
48
+ }, title: t("documentDesigner.localeDialog.title"), description: t("documentDesigner.localeDialog.description"), confirmLabel: t("documentDesigner.localeDialog.save"), cancelLabel: t("documentDesigner.localeDialog.cancel"), onConfirm: async () => {
49
+ const target = pendingLocale;
50
+ if (target !== null && (await actions.save()))
51
+ onLocaleChange?.(target);
52
+ setPendingLocale(null);
53
+ }, children: _jsx(Button, { variant: "danger-ghost", onClick: () => {
54
+ const target = pendingLocale;
55
+ setPendingLocale(null);
56
+ actions.resetDraft(detail);
57
+ if (target !== null)
58
+ onLocaleChange?.(target);
59
+ }, children: t("documentDesigner.localeDialog.discard") }) }));
60
+ }
@@ -0,0 +1,22 @@
1
+ import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
2
+ import type { PdfActions } from "./pdf-editor-actions.ts";
3
+ import type { EditorState } from "./pdf-editor-draft.ts";
4
+ import type { PdfView } from "./pdf-editor-view.ts";
5
+ import type { TemplateDetail } from "./types.ts";
6
+ import type { PdfEditorState } from "./use-pdf-editor-state.ts";
7
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
8
+ type Primitives = ReturnType<typeof usePrimitives>;
9
+ export type PdfRenderContext = {
10
+ readonly t: Translate;
11
+ readonly Button: Primitives["Button"];
12
+ readonly Dialog: Primitives["Dialog"];
13
+ readonly slug: string;
14
+ readonly s: PdfEditorState;
15
+ readonly actions: PdfActions;
16
+ readonly view: PdfView;
17
+ readonly current: EditorState;
18
+ readonly detail: TemplateDetail;
19
+ readonly onLocaleChange: ((locale: string) => void) | undefined;
20
+ readonly exportBundle: () => Promise<void>;
21
+ };
22
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { MailVariable } from "./mail-types.ts";
2
+ import { type PdfMetaDraft } from "./pdf-meta.ts";
3
+ import type { PdfPreviewResult, TemplateDetail, VariableInfo } from "./types.ts";
4
+ export declare const SETTLE_MS = 500;
5
+ export declare const NO_VARIABLES: Readonly<Record<string, VariableInfo>>;
6
+ export type Draft = {
7
+ readonly content: string;
8
+ readonly meta: PdfMetaDraft;
9
+ };
10
+ export type EditorState = {
11
+ readonly key: string;
12
+ readonly draft: Draft;
13
+ readonly settled: Draft;
14
+ readonly snapshot: string;
15
+ };
16
+ export type GeneratedPdf = {
17
+ readonly forSnapshot: string;
18
+ readonly result: PdfPreviewResult;
19
+ };
20
+ export declare const draftFromDetail: (detail: TemplateDetail) => Draft;
21
+ export declare const snapshotOf: (draft: Draft) => string;
22
+ export declare function imageLabelSource(variables: Readonly<Record<string, VariableInfo>>): Readonly<Record<string, MailVariable>>;
23
+ export declare function textVariablesOf(variables: Readonly<Record<string, VariableInfo>>): Readonly<Record<string, MailVariable>>;
@@ -0,0 +1,33 @@
1
+ import { parsePdfMeta, serializePdfMeta } from "./pdf-meta.js";
2
+ export const SETTLE_MS = 500;
3
+ export const NO_VARIABLES = {};
4
+ export const draftFromDetail = (detail) => ({
5
+ content: detail.content,
6
+ meta: parsePdfMeta(detail.meta),
7
+ });
8
+ export const snapshotOf = (draft) => JSON.stringify({ content: draft.content, meta: serializePdfMeta(draft.meta) });
9
+ // Only the label of an images variable is needed, for the Images block and its picker.
10
+ export function imageLabelSource(variables) {
11
+ const result = {};
12
+ for (const [name, info] of Object.entries(variables)) {
13
+ if (info.type === "images" && info.label !== undefined) {
14
+ result[name] = { type: "text", label: info.label };
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ export function textVariablesOf(variables) {
20
+ const result = {};
21
+ for (const [name, info] of Object.entries(variables)) {
22
+ if (info.type === "images")
23
+ continue;
24
+ const { item, ...rest } = info;
25
+ const columns = Object.entries(item ?? {}).flatMap(([column, def]) => def.type === "images" ? [] : [[column, { ...def, type: def.type }]]);
26
+ result[name] = {
27
+ ...rest,
28
+ type: info.type,
29
+ ...(item !== undefined && { item: Object.fromEntries(columns) }),
30
+ };
31
+ }
32
+ return result;
33
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from "react";
2
+ import type { PdfRenderContext } from "./pdf-editor-context.ts";
3
+ export declare function renderPdfLeftRail(c: PdfRenderContext): ReactNode;
4
+ export declare function renderPdfEditCanvas(c: PdfRenderContext): ReactNode;
5
+ export declare function renderPdfPreviewPane(c: PdfRenderContext): ReactNode;
6
+ export declare function renderPdfSettings(c: PdfRenderContext): ReactNode;
@@ -0,0 +1,40 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PdfPreview } from "@cosmicdrift/kumiko-renderer/web";
3
+ import { useEffect } from "react";
4
+ import { PlaceholderSection, SystemPlaceholderSection } from "./mail-left-rail.js";
5
+ import { PageCanvas } from "./page-canvas.js";
6
+ import { PdfResourceSection } from "./pdf-left-rail.js";
7
+ import { SettingsPanel } from "./settings-panel.js";
8
+ // Opening the preview prints the page without a click; a failed print is not retried for the same draft.
9
+ function RunOnMount({ run }) {
10
+ useEffect(() => {
11
+ run();
12
+ }, [run]);
13
+ return null;
14
+ }
15
+ export function renderPdfLeftRail(c) {
16
+ const { s, view, detail, actions } = c;
17
+ const { insert, filesAvailable, scope } = s.host;
18
+ return (_jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: view.placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf" }), _jsx(SystemPlaceholderSection, { validation: view.placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf", isLayoutOwned: view.layoutFills }), _jsx(PdfResourceSection, { resourceKeys: view.resourceKeys, canUpload: detail.scope === scope, filesAvailable: filesAvailable, layoutName: view.layoutName, onInsert: insert.insertResource, onUpload: actions.uploadResource })] }));
19
+ }
20
+ export function renderPdfEditCanvas(c) {
21
+ const { s, view, current, actions } = c;
22
+ const { insert } = s.host;
23
+ const { vocabulary } = s;
24
+ return (_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", onFocusCapture: insert.markFocused("body"), children: _jsx(PageCanvas, { meta: current.draft.meta, content: current.draft.content, onContentChange: (content) => actions.updateDraft({ ...current.draft, content }), regions: s.server.layout?.regions ?? null, layoutName: view.layoutName, resourceSrc: view.layoutResourceSrc, variableLabels: vocabulary.variableLabels, systemVariables: vocabulary.systemVariables, systemLabels: vocabulary.systemLabels, variableNames: vocabulary.variableNames, imageVariables: vocabulary.imageVariables, resourcePreviews: vocabulary.resourcePreviews, onEditorReady: insert.register("body") }) }));
25
+ }
26
+ function renderStalePreview(c) {
27
+ const { t, Button, s, view, actions } = c;
28
+ const { busy } = s.ui;
29
+ return (_jsxs("div", { className: "flex flex-col items-start gap-3 p-6", "data-testid": "document-designer-preview-stale", children: [view.pdfAvailable && busy === null && _jsx(RunOnMount, { run: actions.printOnce }), _jsx("p", { className: "text-sm text-muted-foreground", children: busy === "pdf"
30
+ ? t("documentDesigner.preview.loading")
31
+ : t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !view.pdfAvailable || busy !== null, onClick: () => void actions.generatePdf(), children: t("documentDesigner.actions.generatePdf") })] }));
32
+ }
33
+ export function renderPdfPreviewPane(c) {
34
+ const { generated } = c.s.ui;
35
+ return (_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: generated !== null && c.view.generatedFresh ? (_jsx(PdfPreview, { source: { base64: generated.result.pdfBase64 }, testId: "document-designer-pdf-preview" })) : (renderStalePreview(c)) }));
36
+ }
37
+ export function renderPdfSettings(c) {
38
+ const { s, view, current, detail, actions } = c;
39
+ return (_jsx(SettingsPanel, { meta: current.draft.meta, onMetaChange: (meta) => actions.updateDraft({ ...current.draft, meta }), layouts: s.server.layouts.rows, locale: s.doc.locale, version: { id: detail.id, version: detail.version, status: detail.status }, vocabulary: view.vocabulary, insert: s.host.insert, draftKey: `${current.key}:${s.ui.draftEpoch}` }));
40
+ }
@@ -0,0 +1,52 @@
1
+ import type { EditorState } from "./pdf-editor-draft.ts";
2
+ import { type TemplateDetail } from "./types.ts";
3
+ import type { PdfEditorState } from "./use-pdf-editor-state.ts";
4
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
5
+ export declare function buildPdfView(input: {
6
+ readonly t: Translate;
7
+ readonly s: PdfEditorState;
8
+ readonly slug: string;
9
+ readonly current: EditorState;
10
+ readonly detail: TemplateDetail;
11
+ }): {
12
+ dirty: boolean;
13
+ generatedFresh: boolean;
14
+ canPublish: boolean;
15
+ pdfAvailable: boolean;
16
+ unavailableMessage: string;
17
+ pdfUnavailableNote: boolean;
18
+ layoutName: string;
19
+ layoutResourceSrc: (key: string) => string | undefined;
20
+ placeholderSource: {
21
+ canSave: boolean;
22
+ canPublish: boolean;
23
+ issues: readonly import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
24
+ publishIssues: readonly import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
25
+ used: readonly string[];
26
+ unknown: readonly string[];
27
+ missingRequired: readonly string[];
28
+ warnings: readonly import("./types.ts").ValidationWarning[];
29
+ variables: Readonly<Record<string, import("./mail-types.ts").MailVariable>>;
30
+ systemPlaceholders: readonly string[];
31
+ } | null;
32
+ layoutFills: (name: string) => boolean;
33
+ resourceKeys: string[];
34
+ title: string;
35
+ systemVersion: number | null;
36
+ vocabulary: {
37
+ names: string[];
38
+ labels: {
39
+ [x: string]: string;
40
+ };
41
+ system: readonly string[];
42
+ };
43
+ footer: {
44
+ readonly tone: import("./editor-shell.tsx").FooterTone;
45
+ readonly text: string;
46
+ } | null;
47
+ showOverrideLinks: boolean;
48
+ originText: string;
49
+ besideActiveSentence: string | null;
50
+ };
51
+ export type PdfView = ReturnType<typeof buildPdfView>;
52
+ export {};
@@ -0,0 +1,89 @@
1
+ import { fileUrl } from "./file-name.js";
2
+ import { snapshotOf } from "./pdf-editor-draft.js";
3
+ import { pdfFooterMessage } from "./pdf-footer.js";
4
+ import { visibleSystemPlaceholders } from "./pdf-left-rail.js";
5
+ import { draftBesideActiveSentence } from "./status-label.js";
6
+ import { parseJsonObject } from "./template-rows.js";
7
+ import { DEFAULT_LAYOUT_SLUG } from "./types.js";
8
+ import { templateTitle, templateTitleText } from "./used-by-label.js";
9
+ function originTextOf(t, s, detail, systemVersion, showOverrideLinks) {
10
+ const overrides = s.server.overridesQuery.data;
11
+ if (showOverrideLinks) {
12
+ const overrideCount = (overrides?.activeOverrides ?? 0) + (overrides?.draftOverrides ?? 0);
13
+ return overrides?.totalTenants == null
14
+ ? t("documentDesigner.originBar.noTotal", { count: overrideCount })
15
+ : t("documentDesigner.originBar.withTotal", {
16
+ count: overrideCount,
17
+ total: overrides.totalTenants,
18
+ });
19
+ }
20
+ if (detail.scope === "system")
21
+ return t("documentDesigner.mail.origin.system", { version: detail.version });
22
+ return systemVersion === null
23
+ ? t("documentDesigner.mail.origin.tenantOnly")
24
+ : t("documentDesigner.mail.origin.overrides", { version: systemVersion });
25
+ }
26
+ function footerOf(t, s) {
27
+ const { validation } = s.server;
28
+ const { generated } = s.ui;
29
+ if (validation === undefined || validation === null)
30
+ return null; // skip: nothing validated yet
31
+ return pdfFooterMessage(t, validation, generated === null
32
+ ? null
33
+ : {
34
+ pages: generated.result.pageCount,
35
+ sizeBytes: generated.result.sizeBytes,
36
+ durationMs: generated.result.durationMs,
37
+ });
38
+ }
39
+ function layoutNameOf(t, s, layoutSlugShown) {
40
+ const layoutTitle = s.server.layouts.rows.find((row) => row.slug === layoutSlugShown)?.title;
41
+ return layoutTitle === undefined ? layoutSlugShown : templateTitleText(t, layoutTitle);
42
+ }
43
+ function placeholderSourceOf(s) {
44
+ const { validation } = s.server;
45
+ return validation === undefined || validation === null
46
+ ? null
47
+ : {
48
+ ...validation,
49
+ variables: s.vocabulary.variables,
50
+ systemPlaceholders: visibleSystemPlaceholders(validation.systemPlaceholders),
51
+ };
52
+ }
53
+ export function buildPdfView(input) {
54
+ const { t, s, slug, current, detail } = input;
55
+ const { layout, validation, statusQuery, brandingLogoFile } = s.server;
56
+ const { generated } = s.ui;
57
+ const pdfAvailable = statusQuery.error === null && statusQuery.data?.available === true;
58
+ const unavailableMessage = statusQuery.data?.available === false ? (statusQuery.data.message ?? "") : "";
59
+ const layoutSlugShown = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
60
+ const layoutName = layoutNameOf(t, s, layoutSlugShown);
61
+ const overviewRow = s.host.overview.rows.find((candidate) => candidate.slug === slug);
62
+ const systemVersion = overviewRow?.systemVersion ?? null;
63
+ const showOverrideLinks = s.server.showOrigin && detail.scope === "system";
64
+ return {
65
+ dirty: snapshotOf(current.draft) !== current.snapshot,
66
+ generatedFresh: generated !== null && generated.forSnapshot === snapshotOf(current.draft),
67
+ canPublish: validation?.canPublish === true,
68
+ pdfAvailable,
69
+ unavailableMessage,
70
+ pdfUnavailableNote: !pdfAvailable && statusQuery.loading === false,
71
+ layoutName,
72
+ layoutResourceSrc: (key) => key === "logo" && brandingLogoFile !== "" ? fileUrl(brandingLogoFile) : undefined,
73
+ placeholderSource: placeholderSourceOf(s),
74
+ layoutFills: (name) => layout !== null &&
75
+ (layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`)),
76
+ resourceKeys: Object.keys(parseJsonObject(detail.linkedResources)),
77
+ title: overviewRow === undefined ? slug : templateTitle(t, overviewRow),
78
+ systemVersion,
79
+ vocabulary: {
80
+ names: s.vocabulary.variableNames,
81
+ labels: s.vocabulary.variableLabels,
82
+ system: s.vocabulary.systemVariables,
83
+ },
84
+ footer: footerOf(t, s),
85
+ showOverrideLinks,
86
+ originText: originTextOf(t, s, detail, systemVersion, showOverrideLinks),
87
+ besideActiveSentence: draftBesideActiveSentence(t, detail),
88
+ };
89
+ }
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from "react";
1
+ import type { ReactNode } from "react";
2
2
  export type PdfTemplateEditorProps = {
3
3
  readonly slug: string;
4
4
  /** Template locale; defaults to "de", or the first locale the slug has. */