@cosmicdrift/kumiko-document-designer 0.413.0 → 0.414.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.
- package/dist/web/client-plugin.js +3 -0
- package/dist/web/constants.d.ts +1 -0
- package/dist/web/constants.js +2 -0
- package/dist/web/editor-header.d.ts +4 -14
- package/dist/web/editor-header.js +9 -22
- package/dist/web/editor-shell.d.ts +6 -33
- package/dist/web/editor-shell.js +12 -35
- package/dist/web/errors.d.ts +1 -3
- package/dist/web/field-issue-text.d.ts +1 -4
- package/dist/web/field-issue-text.js +1 -11
- package/dist/web/index.d.ts +2 -1
- package/dist/web/index.js +2 -1
- package/dist/web/layout-editor-center.d.ts +1 -1
- package/dist/web/layout-editor-logic.d.ts +1 -1
- package/dist/web/layout-editor-paper.d.ts +1 -1
- package/dist/web/mail-draft.d.ts +1 -2
- package/dist/web/mail-editor-logic.d.ts +1 -2
- package/dist/web/mail-editor-logic.js +2 -4
- package/dist/web/mail-editor-model.d.ts +3 -4
- package/dist/web/mail-editor-model.js +1 -1
- package/dist/web/mail-editor-render.d.ts +1 -1
- package/dist/web/mail-footer.d.ts +1 -1
- package/dist/web/mail-origin-bar.d.ts +1 -2
- package/dist/web/mail-preview-render.js +1 -1
- package/dist/web/mail-preview-renderer.d.ts +15 -0
- package/dist/web/mail-preview-renderer.js +61 -0
- package/dist/web/mail-preview.js +15 -34
- package/dist/web/pdf-editor-actions.d.ts +1 -2
- package/dist/web/pdf-editor-actions.js +8 -10
- package/dist/web/pdf-editor-context.d.ts +1 -1
- package/dist/web/pdf-editor-panes.d.ts +1 -1
- package/dist/web/pdf-editor-panes.js +21 -18
- package/dist/web/pdf-editor-view.d.ts +4 -5
- package/dist/web/pdf-footer.d.ts +1 -2
- package/dist/web/pdf-preview-renderer.d.ts +14 -0
- package/dist/web/pdf-preview-renderer.js +22 -0
- package/dist/web/pdf-template-editor.d.ts +1 -1
- package/dist/web/pdf-template-editor.js +14 -1
- package/dist/web/rail.d.ts +1 -32
- package/dist/web/rail.js +1 -31
- package/dist/web/status-label.d.ts +1 -2
- package/dist/web/use-debounced-value.d.ts +1 -1
- package/dist/web/use-debounced-value.js +1 -11
- package/dist/web/use-layout-editor-state.d.ts +2 -1
- package/dist/web/use-mail-editor-state.d.ts +3 -4
- package/dist/web/use-pdf-editor-state.d.ts +1 -2
- package/dist/web/used-by-label.d.ts +1 -2
- package/package.json +6 -6
- package/src/changes.json +18 -0
- package/dist/web/relative-time.d.ts +0 -1
- package/dist/web/relative-time.js +0 -18
|
@@ -7,6 +7,8 @@ import { createLocalesCell, KindCell, ModifiedCell, OriginCell, OverriddenByCell
|
|
|
7
7
|
import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, } from "./constants.js";
|
|
8
8
|
import { documentDesignerTranslations } from "./default-translations.js";
|
|
9
9
|
import { DOCUMENT_DESIGNER_I18N } from "./i18n.js";
|
|
10
|
+
import { MailPreviewRenderer } from "./mail-preview-renderer.js";
|
|
11
|
+
import { PdfPreviewRenderer } from "./pdf-preview-renderer.js";
|
|
10
12
|
// The editors pull tiptap, MJML preview and the page canvas; load them on first mount.
|
|
11
13
|
const LazyLayoutEditor = lazy(() => import("./layout-editor.js").then((module) => ({ default: module.LayoutEditor })));
|
|
12
14
|
const LazyPdfTemplateEditor = lazy(() => import("./pdf-template-editor.js").then((module) => ({ default: module.PdfTemplateEditor })));
|
|
@@ -47,6 +49,7 @@ export function documentDesignerClient(options = {}) {
|
|
|
47
49
|
[DocumentDesignerCells.modifiedAt]: ModifiedCell,
|
|
48
50
|
[DocumentDesignerCells.overriddenBy]: OverriddenByCell,
|
|
49
51
|
},
|
|
52
|
+
contentPreviews: { mail: MailPreviewRenderer, pdf: PdfPreviewRenderer },
|
|
50
53
|
components: {
|
|
51
54
|
[LAYOUT_EDITOR_SCREEN_ID]: LayoutEditorScreen,
|
|
52
55
|
[PLATFORM_LAYOUT_EDITOR_SCREEN_ID]: PlatformLayoutEditorScreen,
|
package/dist/web/constants.d.ts
CHANGED
|
@@ -29,3 +29,4 @@ export declare const DocumentDesignerCells: {
|
|
|
29
29
|
};
|
|
30
30
|
export declare const DEFAULT_TEMPLATE_LOCALES: readonly string[];
|
|
31
31
|
export declare const TENANT_MAIL_SENDING_SCREEN_ID = "mail-foundation-tenant";
|
|
32
|
+
export declare const TEMPLATE_RESOURCE_ENTITY: "template-resource";
|
package/dist/web/constants.js
CHANGED
|
@@ -40,3 +40,5 @@ export const DocumentDesignerCells = {
|
|
|
40
40
|
export const DEFAULT_TEMPLATE_LOCALES = ["de", "en"];
|
|
41
41
|
// Generated settings screen where a tenant admin picks its mail provider.
|
|
42
42
|
export const TENANT_MAIL_SENDING_SCREEN_ID = "mail-foundation-tenant";
|
|
43
|
+
// Pinned like the names above: the entity name is registered by the template-resolver feature.
|
|
44
|
+
export const TEMPLATE_RESOURCE_ENTITY = "template-resource";
|
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
export type EditorMode
|
|
2
|
+
export type { EditorMode } from "@cosmicdrift/kumiko-renderer/web";
|
|
3
|
+
export { EditorLocaleSwitch, EditorModeSwitch } from "@cosmicdrift/kumiko-renderer/web";
|
|
3
4
|
/** "override" is a tenant row that replaces a system version; "tenant" has no system counterpart. */
|
|
4
5
|
export type EditorOrigin = "system" | "override" | "tenant";
|
|
5
6
|
export declare function originOf(isSystemRow: boolean, hasSystemVersion: boolean): EditorOrigin;
|
|
6
7
|
export declare function OriginBadge({ origin }: {
|
|
7
8
|
readonly origin: EditorOrigin;
|
|
8
9
|
}): ReactNode;
|
|
9
|
-
export declare function EditorModeSwitch({ testIdPrefix, mode, onMode, }: {
|
|
10
|
-
readonly testIdPrefix: string;
|
|
11
|
-
readonly mode: EditorMode;
|
|
12
|
-
readonly onMode: (mode: EditorMode) => void;
|
|
13
|
-
}): ReactNode;
|
|
14
|
-
export declare function EditorLocaleSwitch({ testIdPrefix, locales, locale, onLocale, }: {
|
|
15
|
-
readonly testIdPrefix: string;
|
|
16
|
-
readonly locales: readonly string[];
|
|
17
|
-
readonly locale: string;
|
|
18
|
-
readonly onLocale: (locale: string) => void;
|
|
19
|
-
}): ReactNode;
|
|
20
10
|
export type StoredVersion = {
|
|
21
11
|
readonly status: string;
|
|
22
12
|
readonly version: number;
|
|
23
13
|
};
|
|
24
|
-
export type
|
|
14
|
+
export type DesignerSaveActions = {
|
|
25
15
|
readonly busy: boolean;
|
|
26
16
|
readonly canSave: boolean;
|
|
27
17
|
readonly canPublish: boolean;
|
|
@@ -31,7 +21,7 @@ export type EditorSaveActions = {
|
|
|
31
21
|
export declare function EditorHeader({ title, stored, actions, children, }: {
|
|
32
22
|
readonly title: string;
|
|
33
23
|
readonly stored: StoredVersion | null;
|
|
34
|
-
readonly actions:
|
|
24
|
+
readonly actions: DesignerSaveActions;
|
|
35
25
|
/** View controls left of the save actions (mode or kind switch). */
|
|
36
26
|
readonly children: ReactNode;
|
|
37
27
|
}): ReactNode;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { StatusBadge, EditorHeader as WorkbenchHeader } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
4
|
import { asRowStatus, statusLabel, statusTone } from "./status-label.js";
|
|
5
|
+
export { EditorLocaleSwitch, EditorModeSwitch } from "@cosmicdrift/kumiko-renderer/web";
|
|
5
6
|
const ORIGIN_LABEL = {
|
|
6
7
|
system: "documentDesigner.origin.system",
|
|
7
8
|
override: "documentDesigner.origin.override",
|
|
@@ -14,29 +15,15 @@ export function OriginBadge({ origin }) {
|
|
|
14
15
|
const t = useTranslation();
|
|
15
16
|
return (_jsx(StatusBadge, { tone: origin === "system" ? "muted" : "accent", testId: "document-designer-editor-origin", children: t(ORIGIN_LABEL[origin]) }));
|
|
16
17
|
}
|
|
17
|
-
export function EditorModeSwitch({ testIdPrefix, mode, onMode, }) {
|
|
18
|
-
const t = useTranslation();
|
|
19
|
-
return (_jsx(ModeSwitch, { value: mode, onChange: onMode, ariaLabel: t("documentDesigner.mail.header.modeSwitch"), testId: `${testIdPrefix}-mode-switch`, options: [
|
|
20
|
-
{ value: "edit", label: t("documentDesigner.mail.header.edit") },
|
|
21
|
-
{ value: "preview", label: t("documentDesigner.mail.header.preview") },
|
|
22
|
-
] }));
|
|
23
|
-
}
|
|
24
|
-
export function EditorLocaleSwitch({ testIdPrefix, locales, locale, onLocale, }) {
|
|
25
|
-
const t = useTranslation();
|
|
26
|
-
return (_jsx(ModeSwitch, { value: locale, onChange: onLocale, ariaLabel: t("documentDesigner.mail.header.localeSwitch"), testId: `${testIdPrefix}-locale-switch`, options: locales.map((code) => ({ value: code, label: code })) }));
|
|
27
|
-
}
|
|
28
18
|
function StoredStatus({ stored }) {
|
|
29
19
|
const t = useTranslation();
|
|
30
20
|
const status = asRowStatus(stored.status);
|
|
31
21
|
return (_jsx(StatusBadge, { tone: statusTone(status), testId: "document-designer-editor-status", children: statusLabel(t, status, stored.version) }));
|
|
32
22
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
const DESIGNER_HEADER_TEST_IDS = {
|
|
24
|
+
save: "document-designer-header-save",
|
|
25
|
+
publish: "document-designer-header-publish",
|
|
26
|
+
};
|
|
36
27
|
export function EditorHeader({ title, stored, actions, children, }) {
|
|
37
|
-
|
|
38
|
-
const { Button, PageHeader } = usePrimitives();
|
|
39
|
-
if (PageHeader === undefined)
|
|
40
|
-
return null;
|
|
41
|
-
return (_jsx(PageHeader, { title: title, status: stored !== null && _jsx(StoredStatus, { stored: stored }), actions: _jsxs("span", { className: "flex items-center gap-2 [&_button]:px-2.5 [&_button]:text-sm", children: [children, _jsxs("span", { className: "hidden items-center gap-2 xl:flex", children: [_jsx(Button, { variant: "secondary", size: "sm", disabled: !actions.canSave, loading: actions.busy, onClick: actions.onSave, testId: "document-designer-header-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { size: "sm", disabled: !actions.canPublish, onClick: actions.onPublish, testId: "document-designer-header-publish", children: t("documentDesigner.editor.publish") })] })] }) }));
|
|
28
|
+
return (_jsx(WorkbenchHeader, { title: title, status: stored !== null && _jsx(StoredStatus, { stored: stored }), actions: actions, testIds: DESIGNER_HEADER_TEST_IDS, children: children }));
|
|
42
29
|
}
|
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InfoBarTone, EditorFooter as WorkbenchFooter, EditorInfoBar as WorkbenchInfoBar } from "@cosmicdrift/kumiko-renderer/web";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
3
|
import { type EditorOrigin } from "./editor-header.tsx";
|
|
3
|
-
export type
|
|
4
|
-
export
|
|
5
|
-
export declare function
|
|
6
|
-
readonly testId: string;
|
|
7
|
-
readonly children: ReactNode;
|
|
8
|
-
}): ReactNode;
|
|
9
|
-
export declare function EditorInfoBar({ tone, testId, origin, controls, links, overlays, children, }: {
|
|
4
|
+
export type { FooterTone, InfoBarTone } from "@cosmicdrift/kumiko-renderer/web";
|
|
5
|
+
export { EditorBody, EditorFrame } from "@cosmicdrift/kumiko-renderer/web";
|
|
6
|
+
export declare function EditorInfoBar({ origin, ...props }: Omit<ComponentProps<typeof WorkbenchInfoBar>, "origin" | "tone"> & {
|
|
10
7
|
readonly tone: InfoBarTone;
|
|
11
|
-
readonly testId: string;
|
|
12
8
|
readonly origin?: EditorOrigin | null;
|
|
13
|
-
/** Right side, before the links: locale switch and the editor's secondary action. */
|
|
14
|
-
readonly controls?: ReactNode;
|
|
15
|
-
readonly links?: ReactNode;
|
|
16
|
-
readonly overlays?: ReactNode;
|
|
17
|
-
readonly children: ReactNode;
|
|
18
|
-
}): ReactNode;
|
|
19
|
-
export declare function EditorBody({ left, center, right, testId, }: {
|
|
20
|
-
readonly left?: ReactNode;
|
|
21
|
-
readonly center: ReactNode;
|
|
22
|
-
readonly right: ReactNode;
|
|
23
|
-
readonly testId?: string;
|
|
24
9
|
}): ReactNode;
|
|
25
|
-
export declare function EditorFooter(
|
|
26
|
-
readonly testIdPrefix: string;
|
|
27
|
-
readonly message: {
|
|
28
|
-
readonly tone: FooterTone;
|
|
29
|
-
readonly text: string;
|
|
30
|
-
} | null;
|
|
31
|
-
readonly saveError: string | null;
|
|
32
|
-
readonly modifiedAt: string | null;
|
|
33
|
-
readonly dirty: boolean;
|
|
34
|
-
readonly busy: boolean;
|
|
35
|
-
readonly canSave: boolean;
|
|
10
|
+
export declare function EditorFooter(props: Omit<ComponentProps<typeof WorkbenchFooter>, "actionTestIds" | "pendingMessage" | "onPublish" | "canPublish"> & {
|
|
36
11
|
readonly canPublish: boolean;
|
|
37
|
-
readonly onDiscard: () => void;
|
|
38
|
-
readonly onSave: () => void;
|
|
39
12
|
readonly onPublish: () => void;
|
|
40
13
|
}): ReactNode;
|
package/dist/web/editor-shell.js
CHANGED
|
@@ -1,40 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { EditorFooter as WorkbenchFooter, EditorInfoBar as WorkbenchInfoBar, } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
4
|
import { OriginBadge } from "./editor-header.js";
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
export { EditorBody, EditorFrame } from "@cosmicdrift/kumiko-renderer/web";
|
|
6
|
+
const DESIGNER_ACTION_TEST_IDS = {
|
|
7
|
+
discard: "document-designer-discard",
|
|
8
|
+
save: "document-designer-save",
|
|
9
|
+
publish: "document-designer-publish",
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
warn: { text: "text-status-warn", glyph: "!" },
|
|
14
|
-
bad: { text: "text-status-bad", glyph: "!" },
|
|
15
|
-
};
|
|
16
|
-
// Height below the shell header: the rails and the center scroll on their own.
|
|
17
|
-
export function EditorFrame({ testId, children, }) {
|
|
18
|
-
return (_jsx("div", { className: "flex h-[calc(100svh-var(--shell-header-height))] min-h-[600px] flex-col", "data-testid": testId, children: children }));
|
|
19
|
-
}
|
|
20
|
-
export function EditorInfoBar({ tone, testId, origin, controls, links, overlays, children, }) {
|
|
21
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn("flex items-start gap-4 border-b px-6 py-2 text-sm", INFO_BAR_TONE[tone]), "data-testid": testId, "data-variant": tone === "warn" ? "warn" : undefined, children: [_jsxs("span", { className: "min-w-0 flex-1", children: [origin != null && _jsx(OriginBadge, { origin: origin }), " ", children] }), controls !== undefined && (_jsx("div", { className: "flex shrink-0 items-center gap-2", children: controls })), links !== undefined && _jsx("div", { className: "flex shrink-0 items-center gap-4", children: links })] }), overlays] }));
|
|
22
|
-
}
|
|
23
|
-
export function EditorBody({ left, center, right, testId, }) {
|
|
24
|
-
const centerColumn = (_jsx("div", { className: "flex min-h-0 min-w-0 flex-col overflow-hidden bg-muted", children: center }));
|
|
25
|
-
if (left === undefined) {
|
|
26
|
-
return (_jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_280px]", "data-testid": testId, children: [centerColumn, _jsx("aside", { className: "min-h-0 overflow-y-auto border-l bg-background", children: right })] }));
|
|
27
|
-
}
|
|
28
|
-
// Three columns leave a narrow window (1024px beside the app sidebar) too little room for the
|
|
29
|
-
// page: there both rails share the left column, one below the other, and the center keeps its width.
|
|
30
|
-
// The rail column is display:contents while wide, so both rails stay grid items of their own.
|
|
31
|
-
return (_jsx("div", { className: "@container flex min-h-0 min-w-0 flex-1", "data-testid": testId, children: _jsxs("div", { className: "grid min-h-0 min-w-0 flex-1 grid-cols-[240px_minmax(0,1fr)_280px] @max-4xl:grid-cols-[240px_minmax(0,1fr)]", children: [_jsxs("div", { className: "contents @max-4xl:block @max-4xl:min-h-0 @max-4xl:overflow-y-auto @max-4xl:border-r @max-4xl:bg-background", children: [_jsx("aside", { className: "min-h-0 overflow-y-auto border-r bg-background @max-4xl:overflow-visible @max-4xl:border-r-0 @max-4xl:border-b", children: left }), _jsx("aside", { className: "order-last min-h-0 overflow-y-auto border-l bg-background @max-4xl:overflow-visible @max-4xl:border-l-0", children: right })] }), centerColumn] }) }));
|
|
11
|
+
export function EditorInfoBar({ origin, ...props }) {
|
|
12
|
+
return (_jsx(WorkbenchInfoBar, { ...props, origin: origin == null ? undefined : _jsx(OriginBadge, { origin: origin }) }));
|
|
32
13
|
}
|
|
33
|
-
export function EditorFooter(
|
|
14
|
+
export function EditorFooter(props) {
|
|
34
15
|
const t = useTranslation();
|
|
35
|
-
|
|
36
|
-
const tone = message === null ? null : FOOTER_TONE[message.tone];
|
|
37
|
-
return (_jsxs("div", { className: "flex items-center gap-4 border-t bg-background px-6 py-3", "data-testid": `${testIdPrefix}-footer`, children: [_jsxs("div", { className: "min-w-0 flex-1 text-sm", children: [saveError !== null ? (_jsx(Banner, { variant: "error", testId: `${testIdPrefix}-save-error`, children: saveError })) : (_jsx("span", { className: cn(tone === null ? "text-muted-foreground" : tone.text), "data-testid": `${testIdPrefix}-footer-message`, "data-tone": message?.tone, children: message === null
|
|
38
|
-
? t("documentDesigner.editor.footer.checking")
|
|
39
|
-
: `${tone?.glyph ?? ""} ${message.text}` })), modifiedAt !== null && (_jsxs(_Fragment, { children: [" ", _jsx(Text, { variant: "muted", testId: `${testIdPrefix}-saved-at`, children: t("documentDesigner.mail.footer.savedAt", { when: formatRelativeWhen(modifiedAt) }) })] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [_jsx(Button, { variant: "ghost", disabled: !dirty, onClick: onDiscard, testId: "document-designer-discard", children: t("documentDesigner.editor.discard") }), _jsx(Button, { variant: "secondary", disabled: !canSave, loading: busy, onClick: onSave, testId: "document-designer-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { disabled: !canPublish, onClick: onPublish, testId: "document-designer-publish", children: t("documentDesigner.editor.publish") })] })] }));
|
|
16
|
+
return (_jsx(WorkbenchFooter, { ...props, actionTestIds: DESIGNER_ACTION_TEST_IDS, pendingMessage: t("documentDesigner.editor.footer.checking") }));
|
|
40
17
|
}
|
package/dist/web/errors.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { DispatcherError } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
1
|
+
import type { DispatcherError, Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
3
2
|
export declare function isAlreadyActive(error: DispatcherError): boolean;
|
|
4
3
|
/** Plain-language text for a failed call; falls back to the server message when no translation exists. */
|
|
5
4
|
export declare function describeError(t: Translate, error: DispatcherError): string;
|
|
6
|
-
export {};
|
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
3
|
-
export declare function errorTextWithFieldIssues(error: DispatcherError, translate: Translate): string;
|
|
4
|
-
export {};
|
|
1
|
+
export { dispatcherErrorTextWithFieldIssues as errorTextWithFieldIssues } from "@cosmicdrift/kumiko-renderer";
|
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// A validation_error alone says nothing; the server names the offending field
|
|
3
|
-
// and reason per issue, so the user gets those as text.
|
|
4
|
-
export function errorTextWithFieldIssues(error, translate) {
|
|
5
|
-
const headline = dispatcherErrorText(error, translate);
|
|
6
|
-
const issues = (error.details?.fields ?? []).map((issue) => {
|
|
7
|
-
const text = translate(issue.i18nKey, issue.params);
|
|
8
|
-
return text === issue.i18nKey ? `${issue.path}: ${issue.code}` : text;
|
|
9
|
-
});
|
|
10
|
-
return issues.length === 0 ? headline : `${headline} ${issues.join(" ")}`;
|
|
11
|
-
}
|
|
1
|
+
export { dispatcherErrorTextWithFieldIssues as errorTextWithFieldIssues } from "@cosmicdrift/kumiko-renderer";
|
package/dist/web/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { type DocumentDesignerClientOptions, documentDesignerClient, PdfTemplateEditorScreen, } from "./client-plugin.tsx";
|
|
2
2
|
export { composeLayoutHtml, type LayoutRegionValues } from "./compose-layout.ts";
|
|
3
|
+
export { DOCUMENT_DESIGNER_FEATURE, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, type TemplateEditorKind, } from "./constants.ts";
|
|
3
4
|
export { documentDesignerTranslations } from "./default-translations.ts";
|
|
4
5
|
export { PdfTemplateEditor, type PdfTemplateEditorProps } from "./pdf-template-editor.tsx";
|
|
5
6
|
export { type PreviewRecordPicker, type PreviewRecordPickerProps, PreviewRecordPickerProvider, } from "./preview-record-picker.tsx";
|
|
6
|
-
export { usageLabel, usedByLabel } from "./used-by-label.ts";
|
|
7
|
+
export { templateTitle, templateTitleText, usageLabel, usedByLabel } from "./used-by-label.ts";
|
package/dist/web/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { documentDesignerClient, PdfTemplateEditorScreen, } from "./client-plugin.js";
|
|
2
2
|
export { composeLayoutHtml } from "./compose-layout.js";
|
|
3
|
+
export { DOCUMENT_DESIGNER_FEATURE, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, } from "./constants.js";
|
|
3
4
|
export { documentDesignerTranslations } from "./default-translations.js";
|
|
4
5
|
export { PdfTemplateEditor } from "./pdf-template-editor.js";
|
|
5
6
|
export { PreviewRecordPickerProvider, } from "./preview-record-picker.js";
|
|
6
|
-
export { usageLabel, usedByLabel } from "./used-by-label.js";
|
|
7
|
+
export { templateTitle, templateTitleText, usageLabel, usedByLabel } from "./used-by-label.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
|
-
import type { Translate } from "./layout-editor-logic.ts";
|
|
4
4
|
import type { LayoutEditorBodyProps, LayoutEditorModel } from "./use-layout-editor-state.ts";
|
|
5
5
|
type Banner = ReturnType<typeof usePrimitives>["Banner"];
|
|
6
6
|
export declare function renderCenter(Banner: Banner, t: Translate, props: LayoutEditorBodyProps, m: LayoutEditorModel): ReactNode;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type useMutation } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
3
|
import type { FooterTone } from "./editor-shell.tsx";
|
|
3
4
|
import { type LayoutAppearance, type LayoutWidth } from "./layout-meta.ts";
|
|
4
5
|
import type { BrandingView, LayoutKind, LayoutSourceView, LayoutValidation, UpsertResponse, ValidationIssue } from "./layout-types.ts";
|
|
5
6
|
export type EditorTab = "draft" | "mjml";
|
|
6
7
|
export type Region = "header" | "signature" | "footer";
|
|
7
|
-
export type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
8
8
|
export declare const REGION_EDITOR_IDS: Readonly<Record<Region, string>>;
|
|
9
9
|
export declare const PAPER_WIDTH_CLASS: Readonly<Record<LayoutWidth, string>>;
|
|
10
10
|
export type FooterMessage = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { Translate } from "./layout-editor-logic.ts";
|
|
3
3
|
import type { LayoutEditorModel } from "./use-layout-editor-state.ts";
|
|
4
4
|
export declare function renderPaper(t: Translate, m: LayoutEditorModel): ReactNode;
|
package/dist/web/mail-draft.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { LinkedResourceView, MailDraft, MailVariable, PickedValues, PlaceholderType, TemplateRowView } from "./mail-types.ts";
|
|
2
3
|
export declare const EMPTY_DRAFT: MailDraft;
|
|
3
4
|
export declare const PLACEHOLDER_SYMBOL: Readonly<Record<PlaceholderType, string>>;
|
|
4
5
|
export declare const LAYOUT_OWNED_PLACEHOLDERS: ReadonlySet<string>;
|
|
5
6
|
export declare const SYSTEM_PLACEHOLDER_TYPE: Readonly<Record<string, PlaceholderType>>;
|
|
6
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
7
7
|
export declare function systemPlaceholderLabel(t: Translate, name: string): string;
|
|
8
8
|
export declare function labelPlaceholders(line: string, variableLabels: Readonly<Record<string, string>>): string;
|
|
9
9
|
export declare function systemLabelMap(t: Translate, names: readonly string[]): Readonly<Record<string, string>>;
|
|
@@ -22,4 +22,3 @@ export type SampleValueFormat = {
|
|
|
22
22
|
readonly tenantCurrency: string | undefined;
|
|
23
23
|
};
|
|
24
24
|
export declare function displayValue(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, value: unknown, type: PlaceholderType | undefined, format: SampleValueFormat): string;
|
|
25
|
-
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { EditorVocabulary } from "./mail-edit-center.tsx";
|
|
2
3
|
import type { LinkedResourceView, MailDraft, MailValidation, PickedValues, TemplateRowView } from "./mail-types.ts";
|
|
3
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
4
|
export type MailEditorBodyProps = {
|
|
5
5
|
readonly slug: string;
|
|
6
6
|
readonly locale: string;
|
|
@@ -67,4 +67,3 @@ export declare function previewPayload(input: {
|
|
|
67
67
|
variableSchema: Readonly<Record<string, unknown>>;
|
|
68
68
|
variables?: Readonly<Record<string, unknown>> | undefined;
|
|
69
69
|
};
|
|
70
|
-
export {};
|
|
@@ -42,13 +42,11 @@ export function resourcePreviewsOfMail(resources, t) {
|
|
|
42
42
|
}
|
|
43
43
|
// Nothing is written yet in a missing language, so no placeholder may read as missing.
|
|
44
44
|
export function railValidationOf(locked, validation) {
|
|
45
|
-
return locked && validation
|
|
46
|
-
? { ...validation, missingRequired: [] }
|
|
47
|
-
: (validation ?? null);
|
|
45
|
+
return locked && validation !== null ? { ...validation, missingRequired: [] } : validation;
|
|
48
46
|
}
|
|
49
47
|
export function vocabularyOf(t, validation) {
|
|
50
48
|
return {
|
|
51
|
-
names: placeholderNames(validation
|
|
49
|
+
names: placeholderNames(validation),
|
|
52
50
|
labels: {
|
|
53
51
|
...systemLabelMap(t, validation?.systemPlaceholders ?? []),
|
|
54
52
|
...variableLabelMap(t, validation?.variables ?? {}),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { LeaveTarget } from "./mail-dialogs.tsx";
|
|
2
3
|
import { type MailEditorBodyProps } from "./mail-editor-logic.ts";
|
|
3
4
|
import type { MailDraft, MailValidation, PickedValues } from "./mail-types.ts";
|
|
4
5
|
import type { MailServices, MailUiState } from "./use-mail-editor-parts.ts";
|
|
5
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
6
6
|
export type MailModelInput = {
|
|
7
7
|
readonly props: MailEditorBodyProps;
|
|
8
8
|
readonly t: Translate;
|
|
@@ -31,8 +31,8 @@ export declare function buildMailModel(input: MailModelInput): {
|
|
|
31
31
|
setDraft: import("react").Dispatch<import("react").SetStateAction<MailDraft>>;
|
|
32
32
|
settled: MailDraft;
|
|
33
33
|
settle: (next: MailDraft) => void;
|
|
34
|
-
mode: import("
|
|
35
|
-
setMode: import("react").Dispatch<import("react").SetStateAction<import("
|
|
34
|
+
mode: import("@cosmicdrift/kumiko-renderer/web").EditorMode;
|
|
35
|
+
setMode: import("react").Dispatch<import("react").SetStateAction<import("@cosmicdrift/kumiko-renderer/web").EditorMode>>;
|
|
36
36
|
creating: boolean;
|
|
37
37
|
setCreating: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
38
38
|
dialog: import("./mail-dialogs.tsx").ConfirmDialog;
|
|
@@ -102,4 +102,3 @@ export declare function buildMailModel(input: MailModelInput): {
|
|
|
102
102
|
fieldErrors: Partial<Record<"previewText" | "subject", string>>;
|
|
103
103
|
};
|
|
104
104
|
export type MailEditorModel = ReturnType<typeof buildMailModel>;
|
|
105
|
-
export {};
|
|
@@ -82,7 +82,7 @@ export function buildMailModel(input) {
|
|
|
82
82
|
templateTitleText(t, template.title),
|
|
83
83
|
]));
|
|
84
84
|
const uploadTemplateId = row?.scope === "tenant" ? row.id : null;
|
|
85
|
-
const footer = locked || validation ===
|
|
85
|
+
const footer = locked || validation === null
|
|
86
86
|
? null
|
|
87
87
|
: footerMessage(t, validation, ui.mode === "preview" && services.testMail.availability.canSend);
|
|
88
88
|
return {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
|
|
4
5
|
import type { MailEditorModel } from "./mail-editor-model.ts";
|
|
5
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
6
6
|
type Banner = ReturnType<typeof usePrimitives>["Banner"];
|
|
7
7
|
export declare function renderMailControls(props: MailEditorBodyProps, m: MailEditorModel): ReactNode;
|
|
8
8
|
export declare function renderMailBanners(Banner: Banner, t: Translate, m: MailEditorModel): ReactNode;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { MailValidation } from "./mail-types.ts";
|
|
2
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
3
3
|
export declare function footerMessage(t: Translate, validation: MailValidation, offersTestMail: boolean): {
|
|
4
4
|
readonly tone: "bad" | "warn" | "ok";
|
|
5
5
|
readonly text: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import { type ReactNode } from "react";
|
|
2
3
|
import type { TemplateRowView } from "./mail-types.ts";
|
|
3
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
4
|
export declare function languageName(t: Translate, locale: string): string;
|
|
5
5
|
export declare function MailOriginBar({ row, systemVersion, locale, isSystemAdmin, creating, controls, onReload, onResetError, }: {
|
|
6
6
|
readonly row: TemplateRowView | null;
|
|
@@ -12,4 +12,3 @@ export declare function MailOriginBar({ row, systemVersion, locale, isSystemAdmi
|
|
|
12
12
|
readonly onReload: () => void;
|
|
13
13
|
readonly onResetError: (message: string) => void;
|
|
14
14
|
}): ReactNode;
|
|
15
|
-
export {};
|
|
@@ -10,7 +10,7 @@ export function renderMailPreview(props, m, controls, origin) {
|
|
|
10
10
|
settled: ui.settled,
|
|
11
11
|
variableSchema: m.variableSchema,
|
|
12
12
|
pickedVariables: m.pickedVariables,
|
|
13
|
-
}), picked: m.picked, onPick: m.setPicked, validation: m.validation
|
|
13
|
+
}), picked: m.picked, onPick: m.setPicked, validation: m.validation, slug: props.slug, locale: props.locale, fromName: ui.settled.senderName, own: services.own, origin: origin, controls: controls, testMail: {
|
|
14
14
|
availability: testMail.availability,
|
|
15
15
|
pending: testMail.pending,
|
|
16
16
|
onSend: m.sendTestMail,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PreviewRenderer } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { MailPreview } from "./mail-types.ts";
|
|
3
|
+
import type { OwnAddress } from "./use-own-address.ts";
|
|
4
|
+
export type PreviewFormat = "html" | "text";
|
|
5
|
+
export type PreviewWidth = "desktop" | "mobile";
|
|
6
|
+
/** View state the mail editor keeps in its info bar; the renderer shows what it says. */
|
|
7
|
+
export type MailPreviewView = {
|
|
8
|
+
readonly format: PreviewFormat;
|
|
9
|
+
readonly width: PreviewWidth;
|
|
10
|
+
readonly fromName: string;
|
|
11
|
+
readonly own: OwnAddress | null;
|
|
12
|
+
};
|
|
13
|
+
export declare const MailPreviewViewContext: import("react").Context<MailPreviewView>;
|
|
14
|
+
export declare function isMailPreview(value: unknown): value is MailPreview;
|
|
15
|
+
export declare const MailPreviewRenderer: PreviewRenderer;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
3
|
+
import { dispatcherErrorText, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { LoadingState } from "@cosmicdrift/kumiko-renderer/web";
|
|
5
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
|
+
import { createContext, useContext, useEffect } from "react";
|
|
7
|
+
import { formatKilobytes } from "./mail-draft.js";
|
|
8
|
+
const DEFAULT_VIEW = { format: "html", width: "desktop", fromName: "", own: null };
|
|
9
|
+
export const MailPreviewViewContext = createContext(DEFAULT_VIEW);
|
|
10
|
+
export function isMailPreview(value) {
|
|
11
|
+
return isPlainObject(value) && typeof value["html"] === "string";
|
|
12
|
+
}
|
|
13
|
+
function attachmentText(t, attachment) {
|
|
14
|
+
switch (attachment.kind) {
|
|
15
|
+
case "inline":
|
|
16
|
+
return null;
|
|
17
|
+
case "file":
|
|
18
|
+
return `⎘ ${attachment.name} · ${formatKilobytes(attachment.sizeBytes)}`;
|
|
19
|
+
case "rendered-pdf":
|
|
20
|
+
return `⎘ ${attachment.name} · ${t("documentDesigner.mail.preview.generatedOnSend")}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function HeadRow({ label, children, }) {
|
|
24
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "min-w-0", children: children })] }));
|
|
25
|
+
}
|
|
26
|
+
// The preview mail goes to the example recipient of the sample values, not to the test-mail address.
|
|
27
|
+
function recipientOf(preview, own) {
|
|
28
|
+
const { recipientName: name, recipientEmail: email } = preview.sampleValues;
|
|
29
|
+
if (typeof name !== "string" || name === "")
|
|
30
|
+
return own?.email ?? null;
|
|
31
|
+
return typeof email === "string" && email !== "" ? `${name} <${email}>` : name;
|
|
32
|
+
}
|
|
33
|
+
function MailFrame({ preview, view, }) {
|
|
34
|
+
const t = useTranslation();
|
|
35
|
+
const { format, width, fromName } = view;
|
|
36
|
+
const recipient = recipientOf(preview, view.own);
|
|
37
|
+
const attachments = preview.attachments.flatMap((a) => attachmentText(t, a) ?? []);
|
|
38
|
+
return (_jsxs("div", { className: "mx-auto flex min-h-0 w-full max-w-[680px] flex-1 flex-col border bg-card shadow-md", "data-testid": "mail-preview-frame", children: [_jsxs("div", { className: "grid grid-cols-[80px_minmax(0,1fr)] gap-y-1 border-b px-5 py-3.5 text-sm", children: [_jsx(HeadRow, { label: t("documentDesigner.mail.preview.from"), children: _jsx("span", { "data-testid": "mail-preview-from", children: fromName === "" ? t("documentDesigner.mail.props.senderDefault") : fromName }) }), recipient !== null && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.to"), children: _jsx("span", { "data-testid": "mail-preview-to", children: recipient }) })), _jsx(HeadRow, { label: t("documentDesigner.mail.meta.subject"), children: _jsx("span", { className: "font-medium", "data-testid": "mail-preview-subject", children: preview.subject ?? "" }) }), attachments.length > 0 && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.attachments"), children: _jsx("span", { className: "flex flex-col items-start gap-1", "data-testid": "mail-preview-attachments", children: attachments.map((line) => (_jsx("span", { className: "rounded border bg-muted px-2 py-0.5 text-xs", children: line }, line))) }) }))] }), format === "html" ? (_jsx("iframe", { title: t("documentDesigner.mail.preview.frameTitle"), sandbox: "", srcDoc: preview.displayHtml ?? preview.html, className: `min-h-[420px] flex-1 border-0 bg-background ${width === "mobile" ? "w-[375px] self-center" : "w-full"}`, "data-testid": "mail-preview-iframe" })) : (_jsx("pre", { className: "min-h-[420px] flex-1 overflow-auto whitespace-pre-wrap p-4 font-mono text-sm", "data-testid": "mail-preview-text", children: preview.text ?? "" }))] }));
|
|
39
|
+
}
|
|
40
|
+
// The mail target: renders through `query:preview` (admin access), which composes the layout,
|
|
41
|
+
// substitutes sample values and returns subject, html and text.
|
|
42
|
+
export const MailPreviewRenderer = ({ content, variables, subject, onResult, }) => {
|
|
43
|
+
const t = useTranslation();
|
|
44
|
+
const { Banner } = usePrimitives();
|
|
45
|
+
const view = useContext(MailPreviewViewContext);
|
|
46
|
+
const hasVariables = Object.keys(variables).length > 0;
|
|
47
|
+
const query = useQuery(TemplateResolverQueries.preview, {
|
|
48
|
+
slug: subject.kind === "template" ? subject.slug : "",
|
|
49
|
+
kind: "mail",
|
|
50
|
+
locale: subject.kind === "template" ? subject.locale : "",
|
|
51
|
+
content,
|
|
52
|
+
meta: subject.kind === "template" ? subject.meta : {},
|
|
53
|
+
variableSchema: subject.kind === "template" ? subject.variableSchema : undefined,
|
|
54
|
+
...(hasVariables && { variables }),
|
|
55
|
+
}, { enabled: subject.kind === "template" });
|
|
56
|
+
const preview = query.data;
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
onResult?.(preview);
|
|
59
|
+
}, [preview, onResult]);
|
|
60
|
+
return (_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, view: view }))] }));
|
|
61
|
+
};
|
package/dist/web/mail-preview.js
CHANGED
|
@@ -1,36 +1,10 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { useState } from "react";
|
|
2
|
+
import { PreviewPane, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { ModeSwitch } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
|
+
import { useCallback, useMemo, useState } from "react";
|
|
6
5
|
import { EditorBody, EditorInfoBar } from "./editor-shell.js";
|
|
7
|
-
import {
|
|
6
|
+
import { isMailPreview, MailPreviewViewContext, } from "./mail-preview-renderer.js";
|
|
8
7
|
import { PreviewSide } from "./mail-preview-side.js";
|
|
9
|
-
function attachmentText(t, attachment) {
|
|
10
|
-
switch (attachment.kind) {
|
|
11
|
-
case "inline":
|
|
12
|
-
return null;
|
|
13
|
-
case "file":
|
|
14
|
-
return `⎘ ${attachment.name} · ${formatKilobytes(attachment.sizeBytes)}`;
|
|
15
|
-
case "rendered-pdf":
|
|
16
|
-
return `⎘ ${attachment.name} · ${t("documentDesigner.mail.preview.generatedOnSend")}`;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function HeadRow({ label, children, }) {
|
|
20
|
-
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "min-w-0", children: children })] }));
|
|
21
|
-
}
|
|
22
|
-
function MailFrame({ preview, format, width, fromName, recipient, }) {
|
|
23
|
-
const t = useTranslation();
|
|
24
|
-
const attachments = preview.attachments.flatMap((a) => attachmentText(t, a) ?? []);
|
|
25
|
-
return (_jsxs("div", { className: "mx-auto flex min-h-0 w-full max-w-[680px] flex-1 flex-col border bg-card shadow-md", "data-testid": "mail-preview-frame", children: [_jsxs("div", { className: "grid grid-cols-[80px_minmax(0,1fr)] gap-y-1 border-b px-5 py-3.5 text-sm", children: [_jsx(HeadRow, { label: t("documentDesigner.mail.preview.from"), children: _jsx("span", { "data-testid": "mail-preview-from", children: fromName === "" ? t("documentDesigner.mail.props.senderDefault") : fromName }) }), recipient !== null && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.to"), children: _jsx("span", { "data-testid": "mail-preview-to", children: recipient }) })), _jsx(HeadRow, { label: t("documentDesigner.mail.meta.subject"), children: _jsx("span", { className: "font-medium", "data-testid": "mail-preview-subject", children: preview.subject ?? "" }) }), attachments.length > 0 && (_jsx(HeadRow, { label: t("documentDesigner.mail.preview.attachments"), children: _jsx("span", { className: "flex flex-col items-start gap-1", "data-testid": "mail-preview-attachments", children: attachments.map((line) => (_jsx("span", { className: "rounded border bg-muted px-2 py-0.5 text-xs", children: line }, line))) }) }))] }), format === "html" ? (_jsx("iframe", { title: t("documentDesigner.mail.preview.frameTitle"), sandbox: "", srcDoc: preview.displayHtml ?? preview.html, className: `min-h-[420px] flex-1 border-0 bg-background ${width === "mobile" ? "w-[375px] self-center" : "w-full"}`, "data-testid": "mail-preview-iframe" })) : (_jsx("pre", { className: "min-h-[420px] flex-1 overflow-auto whitespace-pre-wrap p-4 font-mono text-sm", "data-testid": "mail-preview-text", children: preview.text ?? "" }))] }));
|
|
26
|
-
}
|
|
27
|
-
// The preview mail goes to the example recipient of the sample values, not to the test-mail address.
|
|
28
|
-
function recipientOf(preview, own) {
|
|
29
|
-
const { recipientName: name, recipientEmail: email } = preview.sampleValues;
|
|
30
|
-
if (typeof name !== "string" || name === "")
|
|
31
|
-
return own.email;
|
|
32
|
-
return typeof email === "string" && email !== "" ? `${name} <${email}>` : name;
|
|
33
|
-
}
|
|
34
8
|
function layoutText(t, preview) {
|
|
35
9
|
return preview?.mailLayout == null
|
|
36
10
|
? t("documentDesigner.mail.preview.barPlain")
|
|
@@ -38,16 +12,23 @@ function layoutText(t, preview) {
|
|
|
38
12
|
}
|
|
39
13
|
export function MailPreviewMode({ payload, validation, slug, locale, fromName, own, controls, origin, testMail, picked, onPick, }) {
|
|
40
14
|
const t = useTranslation();
|
|
41
|
-
const { Banner } = usePrimitives();
|
|
42
15
|
const [format, setFormat] = useState("html");
|
|
43
16
|
const [width, setWidth] = useState("desktop");
|
|
44
|
-
const
|
|
45
|
-
const
|
|
17
|
+
const [preview, setPreview] = useState(null);
|
|
18
|
+
const onResult = useCallback((result) => setPreview(isMailPreview(result) ? result : null), []);
|
|
19
|
+
const view = useMemo(() => ({ format, width, fromName, own }), [format, width, fromName, own]);
|
|
46
20
|
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: [
|
|
47
21
|
{ value: "html", label: t("documentDesigner.mail.preview.html") },
|
|
48
22
|
{ value: "text", label: t("documentDesigner.mail.preview.text") },
|
|
49
23
|
] }), _jsx(ModeSwitch, { value: width, onChange: setWidth, ariaLabel: t("documentDesigner.mail.preview.widthSwitch"), testId: "mail-preview-width", options: [
|
|
50
24
|
{ value: "desktop", label: t("documentDesigner.mail.preview.desktop") },
|
|
51
25
|
{ value: "mobile", label: t("documentDesigner.mail.preview.mobile") },
|
|
52
|
-
] })] }), children: layoutText(t, preview) }), _jsx(EditorBody, { testId: "mail-editor-preview", center:
|
|
26
|
+
] })] }), children: layoutText(t, preview) }), _jsx(EditorBody, { testId: "mail-editor-preview", center: _jsx(MailPreviewViewContext.Provider, { value: view, children: _jsx(PreviewPane, { target: "mail", content: payload.content, editFormat: "document", variables: payload.variables ?? {}, subject: {
|
|
27
|
+
kind: "template",
|
|
28
|
+
templateKind: payload.kind,
|
|
29
|
+
slug: payload.slug,
|
|
30
|
+
locale: payload.locale,
|
|
31
|
+
meta: payload.meta,
|
|
32
|
+
variableSchema: payload.variableSchema,
|
|
33
|
+
}, onResult: onResult }) }), right: _jsx(PreviewSide, { preview: preview, validation: validation, slug: slug, locale: locale, own: own, testMail: testMail, picked: picked, onPick: onPick }) })] }));
|
|
53
34
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { useDispatcher } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
3
|
import { type Draft, type EditorState } from "./pdf-editor-draft.ts";
|
|
3
4
|
import { type TemplateDetail } from "./types.ts";
|
|
4
5
|
import type { PdfEditorState } from "./use-pdf-editor-state.ts";
|
|
5
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
6
6
|
export type PdfActionsInput = {
|
|
7
7
|
readonly s: PdfEditorState;
|
|
8
8
|
readonly t: Translate;
|
|
@@ -24,4 +24,3 @@ export declare function buildPdfActions(input: PdfActionsInput): {
|
|
|
24
24
|
requestLocale: (next: string) => void;
|
|
25
25
|
};
|
|
26
26
|
export type PdfActions = ReturnType<typeof buildPdfActions>;
|
|
27
|
-
export {};
|
|
@@ -38,7 +38,7 @@ function writeActions(input) {
|
|
|
38
38
|
}
|
|
39
39
|
async function persist() {
|
|
40
40
|
const { draft } = current;
|
|
41
|
-
const parentTemplateId = detail
|
|
41
|
+
const parentTemplateId = detail.scope === "system" ? detail.id : detail.parentTemplateId;
|
|
42
42
|
const result = await dispatcher.write(scope === "system"
|
|
43
43
|
? TemplateResolverHandlers.upsertSystem
|
|
44
44
|
: TemplateResolverHandlers.upsertTenant, {
|
|
@@ -72,16 +72,14 @@ function writeActions(input) {
|
|
|
72
72
|
setActionError(null);
|
|
73
73
|
// A system save activates immediately; a tenant row needs a save first when it
|
|
74
74
|
// does not exist yet or carries unsaved edits, then an explicit publish.
|
|
75
|
-
const needsSave = dirty || detail
|
|
75
|
+
const needsSave = dirty || detail.scope === "system";
|
|
76
76
|
const saved = needsSave ? await persist() : null;
|
|
77
77
|
const saveFailed = needsSave && saved === null;
|
|
78
|
-
// upsertSystem
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
? TemplateResolverHandlers.publishSystem
|
|
84
|
-
: TemplateResolverHandlers.publish, { id: templateId });
|
|
78
|
+
// A system host always saves above and upsertSystem activates, so only a tenant row publishes.
|
|
79
|
+
if (scope === "tenant" && !saveFailed) {
|
|
80
|
+
const result = await dispatcher.write(TemplateResolverHandlers.publish, {
|
|
81
|
+
id: saved?.id ?? detail.id,
|
|
82
|
+
});
|
|
85
83
|
if (!result.isSuccess && !isAlreadyActive(result.error))
|
|
86
84
|
setActionError(describeError(t, result.error));
|
|
87
85
|
}
|
|
@@ -123,7 +121,7 @@ function pdfActions(input) {
|
|
|
123
121
|
async function uploadResource(file) {
|
|
124
122
|
const sourceFileRefId = await uploadFileRef(file);
|
|
125
123
|
const result = await dispatcher.write(TemplateResolverHandlers.resourceUpload, {
|
|
126
|
-
templateId: detail
|
|
124
|
+
templateId: detail.id,
|
|
127
125
|
sourceFileRefId,
|
|
128
126
|
});
|
|
129
127
|
if (!result.isSuccess)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
3
|
import type { PdfActions } from "./pdf-editor-actions.ts";
|
|
3
4
|
import type { EditorState } from "./pdf-editor-draft.ts";
|
|
4
5
|
import type { PdfView } from "./pdf-editor-view.ts";
|
|
5
6
|
import type { TemplateDetail } from "./types.ts";
|
|
6
7
|
import type { PdfEditorState } from "./use-pdf-editor-state.ts";
|
|
7
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
8
8
|
type Primitives = ReturnType<typeof usePrimitives>;
|
|
9
9
|
export type PdfRenderContext = {
|
|
10
10
|
readonly t: Translate;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
import type { PdfRenderContext } from "./pdf-editor-context.ts";
|
|
3
3
|
export declare function renderPdfLeftRail(c: PdfRenderContext): ReactNode;
|
|
4
4
|
export declare function renderPdfEditCanvas(c: PdfRenderContext): ReactNode;
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect } from "react";
|
|
2
|
+
import { PreviewPane } from "@cosmicdrift/kumiko-renderer";
|
|
4
3
|
import { PlaceholderSection, SystemPlaceholderSection } from "./mail-left-rail.js";
|
|
5
4
|
import { PageCanvas } from "./page-canvas.js";
|
|
6
5
|
import { PdfResourceSection } from "./pdf-left-rail.js";
|
|
6
|
+
import { serializePdfMeta } from "./pdf-meta.js";
|
|
7
|
+
import { PdfPreviewControllerContext } from "./pdf-preview-renderer.js";
|
|
7
8
|
import { SettingsPanel } from "./settings-panel.js";
|
|
8
|
-
|
|
9
|
-
function RunOnMount({ run }) {
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
run();
|
|
12
|
-
}, [run]);
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
9
|
+
import { DOCUMENT_PDF_KIND } from "./types.js";
|
|
15
10
|
export function renderPdfLeftRail(c) {
|
|
16
11
|
const { s, view, detail, actions } = c;
|
|
17
12
|
const { insert, filesAvailable, scope } = s.host;
|
|
@@ -23,16 +18,24 @@ export function renderPdfEditCanvas(c) {
|
|
|
23
18
|
const { vocabulary } = s;
|
|
24
19
|
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
20
|
}
|
|
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
21
|
export function renderPdfPreviewPane(c) {
|
|
34
|
-
const {
|
|
35
|
-
|
|
22
|
+
const { s, view, actions, current, slug } = c;
|
|
23
|
+
const { busy, generated } = s.ui;
|
|
24
|
+
const controller = {
|
|
25
|
+
fresh: generated !== null && view.generatedFresh ? generated.result : null,
|
|
26
|
+
printing: busy === "pdf",
|
|
27
|
+
available: view.pdfAvailable && busy === null,
|
|
28
|
+
generate: () => void actions.generatePdf(),
|
|
29
|
+
printOnce: actions.printOnce,
|
|
30
|
+
};
|
|
31
|
+
return (_jsx(PdfPreviewControllerContext.Provider, { value: controller, children: _jsx(PreviewPane, { target: "pdf", content: current.draft.content, editFormat: "document", variables: {}, subject: {
|
|
32
|
+
kind: "template",
|
|
33
|
+
templateKind: DOCUMENT_PDF_KIND,
|
|
34
|
+
slug,
|
|
35
|
+
locale: s.doc.locale,
|
|
36
|
+
meta: serializePdfMeta(current.draft.meta),
|
|
37
|
+
variableSchema: s.server.variableSchema,
|
|
38
|
+
} }) }));
|
|
36
39
|
}
|
|
37
40
|
export function renderPdfSettings(c) {
|
|
38
41
|
const { s, view, current, detail, actions } = c;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { EditorState } from "./pdf-editor-draft.ts";
|
|
2
3
|
import { type TemplateDetail } from "./types.ts";
|
|
3
4
|
import type { PdfEditorState } from "./use-pdf-editor-state.ts";
|
|
4
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
5
5
|
export declare function buildPdfView(input: {
|
|
6
6
|
readonly t: Translate;
|
|
7
7
|
readonly s: PdfEditorState;
|
|
@@ -20,8 +20,8 @@ export declare function buildPdfView(input: {
|
|
|
20
20
|
placeholderSource: {
|
|
21
21
|
canSave: boolean;
|
|
22
22
|
canPublish: boolean;
|
|
23
|
-
issues: readonly import("@cosmicdrift/kumiko-
|
|
24
|
-
publishIssues: readonly import("@cosmicdrift/kumiko-
|
|
23
|
+
issues: readonly import("@cosmicdrift/kumiko-renderer/headless").FieldIssue[];
|
|
24
|
+
publishIssues: readonly import("@cosmicdrift/kumiko-renderer/headless").FieldIssue[];
|
|
25
25
|
used: readonly string[];
|
|
26
26
|
unknown: readonly string[];
|
|
27
27
|
missingRequired: readonly string[];
|
|
@@ -41,7 +41,7 @@ export declare function buildPdfView(input: {
|
|
|
41
41
|
system: readonly string[];
|
|
42
42
|
};
|
|
43
43
|
footer: {
|
|
44
|
-
readonly tone: import("
|
|
44
|
+
readonly tone: import("@cosmicdrift/kumiko-renderer/web").FooterTone;
|
|
45
45
|
readonly text: string;
|
|
46
46
|
} | null;
|
|
47
47
|
showOverrideLinks: boolean;
|
|
@@ -49,4 +49,3 @@ export declare function buildPdfView(input: {
|
|
|
49
49
|
besideActiveSentence: string | null;
|
|
50
50
|
};
|
|
51
51
|
export type PdfView = ReturnType<typeof buildPdfView>;
|
|
52
|
-
export {};
|
package/dist/web/pdf-footer.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { FooterTone } from "./editor-shell.tsx";
|
|
2
3
|
import type { ValidationResult } from "./types.ts";
|
|
3
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
4
|
export type GeneratedProbe = {
|
|
5
5
|
readonly pages: number;
|
|
6
6
|
readonly sizeBytes: number;
|
|
@@ -10,4 +10,3 @@ export declare function pdfFooterMessage(t: Translate, validation: ValidationRes
|
|
|
10
10
|
readonly tone: FooterTone;
|
|
11
11
|
readonly text: string;
|
|
12
12
|
};
|
|
13
|
-
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type PreviewRenderer } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { PdfPreviewResult } from "./types.ts";
|
|
3
|
+
/** What the PDF editor knows about the printed page: print state is shared with its header and footer buttons. */
|
|
4
|
+
export type PdfPreviewController = {
|
|
5
|
+
/** The printed page when it matches the current draft, else null. */
|
|
6
|
+
readonly fresh: PdfPreviewResult | null;
|
|
7
|
+
readonly printing: boolean;
|
|
8
|
+
readonly available: boolean;
|
|
9
|
+
readonly generate: () => void;
|
|
10
|
+
/** Prints once per draft; opening the preview prints without a click. */
|
|
11
|
+
readonly printOnce: () => void;
|
|
12
|
+
};
|
|
13
|
+
export declare const PdfPreviewControllerContext: import("react").Context<PdfPreviewController | null>;
|
|
14
|
+
export declare const PdfPreviewRenderer: PreviewRenderer;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { PdfPreview } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
|
+
import { createContext, useContext, useEffect } from "react";
|
|
5
|
+
export const PdfPreviewControllerContext = createContext(null);
|
|
6
|
+
function RunOnMount({ run }) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
run();
|
|
9
|
+
}, [run]);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
// The pdf target: the page is printed by the print service on demand (`previewPdf`, started by the
|
|
13
|
+
// editor so its header and footer share the state) and shown in the PDF viewer.
|
|
14
|
+
export const PdfPreviewRenderer = () => {
|
|
15
|
+
const t = useTranslation();
|
|
16
|
+
const { Button } = usePrimitives();
|
|
17
|
+
const controller = useContext(PdfPreviewControllerContext);
|
|
18
|
+
if (controller === null)
|
|
19
|
+
return null;
|
|
20
|
+
const { fresh, printing, available, generate, printOnce } = controller;
|
|
21
|
+
return (_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: fresh !== null ? (_jsx(PdfPreview, { source: { base64: fresh.pdfBase64 }, testId: "document-designer-pdf-preview" })) : (_jsxs("div", { className: "flex flex-col items-start gap-3 p-6", "data-testid": "document-designer-preview-stale", children: [available && !printing && _jsx(RunOnMount, { run: printOnce }), _jsx("p", { className: "text-sm text-muted-foreground", children: printing ? t("documentDesigner.preview.loading") : t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !available || printing, onClick: generate, children: t("documentDesigner.actions.generatePdf") })] })) }));
|
|
22
|
+
};
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
3
|
-
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { useDispatcher, usePrimitives, useSetScreenFocus, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
4
4
|
import { ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer/web";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { TEMPLATE_RESOURCE_ENTITY } from "./constants.js";
|
|
5
7
|
import { EditorBody, EditorFrame } from "./editor-shell.js";
|
|
6
8
|
import { describeError } from "./errors.js";
|
|
7
9
|
import { buildPdfActions } from "./pdf-editor-actions.js";
|
|
8
10
|
import { renderPdfFooter, renderPdfHeader, renderPdfInfoBar, renderPdfLocaleDialog, } from "./pdf-editor-chrome.js";
|
|
9
11
|
import { renderPdfEditCanvas, renderPdfLeftRail, renderPdfPreviewPane, renderPdfSettings, } from "./pdf-editor-panes.js";
|
|
10
12
|
import { buildPdfView } from "./pdf-editor-view.js";
|
|
13
|
+
import { parsePdfMeta } from "./pdf-meta.js";
|
|
11
14
|
import { DOCUMENT_PDF_KIND } from "./types.js";
|
|
12
15
|
import { usePdfEditorState } from "./use-pdf-editor-state.js";
|
|
13
16
|
export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChange, }) {
|
|
@@ -16,6 +19,16 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
16
19
|
const { Button, Banner, Dialog } = usePrimitives();
|
|
17
20
|
const s = usePdfEditorState({ slug, requestedLocale }, t);
|
|
18
21
|
const { rowsQuery, row, current, detail, detailQuery } = s.doc;
|
|
22
|
+
// Persisted values only: unsaved draft content never reaches the focus.
|
|
23
|
+
const focus = useMemo(() => row === undefined
|
|
24
|
+
? null
|
|
25
|
+
: {
|
|
26
|
+
entityName: TEMPLATE_RESOURCE_ENTITY,
|
|
27
|
+
recordId: row.id,
|
|
28
|
+
label: (detail !== null ? parsePdfMeta(detail.meta).title : "") || slug,
|
|
29
|
+
facts: { slug: row.slug, kind: row.kind, locale: row.locale },
|
|
30
|
+
}, [row, detail, slug]);
|
|
31
|
+
useSetScreenFocus(focus);
|
|
19
32
|
if (rowsQuery.loading && rowsQuery.data === null)
|
|
20
33
|
return _jsx(LoadingState, {});
|
|
21
34
|
if (rowsQuery.error !== null)
|
package/dist/web/rail.d.ts
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function RailSection({ title, action, testId, children, }: {
|
|
3
|
-
readonly title: string;
|
|
4
|
-
readonly action?: ReactNode;
|
|
5
|
-
readonly testId?: string;
|
|
6
|
-
readonly children: ReactNode;
|
|
7
|
-
}): ReactNode;
|
|
8
|
-
export declare function RailField({ children }: {
|
|
9
|
-
readonly children: ReactNode;
|
|
10
|
-
}): ReactNode;
|
|
11
|
-
declare const TRAILING_TONE: {
|
|
12
|
-
readonly ok: "text-status-ok";
|
|
13
|
-
readonly bad: "text-status-bad";
|
|
14
|
-
readonly muted: "text-muted-foreground";
|
|
15
|
-
};
|
|
16
|
-
export declare function RailRow({ symbol, label, nameHint, required, trailing, trailingTone, action, bad, testId, onPress, }: {
|
|
17
|
-
readonly symbol?: string;
|
|
18
|
-
readonly label: string;
|
|
19
|
-
/** Technical name behind a translated label, shown as tooltip. */
|
|
20
|
-
readonly nameHint?: string;
|
|
21
|
-
readonly required?: boolean;
|
|
22
|
-
readonly trailing?: ReactNode;
|
|
23
|
-
readonly trailingTone?: keyof typeof TRAILING_TONE;
|
|
24
|
-
readonly action?: ReactNode;
|
|
25
|
-
readonly bad?: boolean;
|
|
26
|
-
readonly testId?: string;
|
|
27
|
-
readonly onPress?: () => void;
|
|
28
|
-
}): ReactNode;
|
|
29
|
-
export declare function RailHint({ children }: {
|
|
30
|
-
readonly children: ReactNode;
|
|
31
|
-
}): ReactNode;
|
|
32
|
-
export {};
|
|
1
|
+
export { RailField, RailHint, RailRow, RailSection } from "@cosmicdrift/kumiko-renderer/web";
|
package/dist/web/rail.js
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { cn } from "@cosmicdrift/kumiko-renderer/web";
|
|
3
|
-
export function RailSection({ title, action, testId, children, }) {
|
|
4
|
-
return (_jsxs("section", { className: "flex flex-col border-b pb-1", "data-testid": testId, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-3.5 pt-2.5 pb-1", children: [_jsx("span", { className: "min-w-0 text-xs font-medium uppercase tracking-wider text-muted-foreground", children: title }), action !== undefined && _jsx("div", { className: "ml-auto", children: action })] }), children] }));
|
|
5
|
-
}
|
|
6
|
-
export function RailField({ children }) {
|
|
7
|
-
return _jsx("div", { className: "flex flex-col gap-3 px-3.5 py-2", children: children });
|
|
8
|
-
}
|
|
9
|
-
const TRAILING_TONE = {
|
|
10
|
-
ok: "text-status-ok",
|
|
11
|
-
bad: "text-status-bad",
|
|
12
|
-
muted: "text-muted-foreground",
|
|
13
|
-
};
|
|
14
|
-
// The star rides on the last word so a wrapping label never leaves it alone on a line.
|
|
15
|
-
function RequiredLabel({ label }) {
|
|
16
|
-
const lastSpace = label.lastIndexOf(" ");
|
|
17
|
-
return (_jsxs(_Fragment, { children: [label.slice(0, lastSpace + 1), _jsxs("span", { className: "whitespace-nowrap", children: [label.slice(lastSpace + 1), _jsx("span", { className: "font-semibold text-status-bad", children: " *" })] })] }));
|
|
18
|
-
}
|
|
19
|
-
export function RailRow({ symbol, label, nameHint, required, trailing, trailingTone = "muted", action, bad, testId, onPress, }) {
|
|
20
|
-
const content = (_jsxs(_Fragment, { children: [symbol !== undefined && (_jsx("span", { className: "inline-flex h-[18px] w-[22px] shrink-0 items-center justify-center rounded-sm bg-muted font-mono text-xs text-muted-foreground", children: symbol })), _jsx("span", { className: cn(
|
|
21
|
-
// anywhere, not break-word: only it lets a file name without spaces narrow the min-content
|
|
22
|
-
// width, so the label wraps instead of running under the action.
|
|
23
|
-
"min-w-0 grow wrap-anywhere text-sm", trailing !== undefined && "min-w-min max-w-[65%] shrink-0", bad === true && "text-status-bad"), title: nameHint !== undefined && nameHint !== label ? nameHint : undefined, children: required === true ? _jsx(RequiredLabel, { label: label }) : label }), trailing !== undefined && (_jsx("span", { className: cn("min-w-0 text-xs", action !== undefined ? "basis-full wrap-anywhere" : "truncate text-right", bad === true ? "text-status-bad" : TRAILING_TONE[trailingTone]), title: typeof trailing === "string" ? trailing : undefined, children: trailing }))] }));
|
|
24
|
-
// Beside an action the row is too narrow for label and trailing text: the trailing text takes its
|
|
25
|
-
// own line and wraps there instead of being clipped to an ellipsis.
|
|
26
|
-
const rowClass = cn("flex min-h-10 min-w-0 grow items-center gap-2 px-3.5 text-left", action !== undefined && "flex-wrap gap-y-0.5 py-1");
|
|
27
|
-
return (_jsxs("div", { className: cn("flex items-center border-b", bad === true && "bg-status-bad-surface"), "data-testid": testId, "data-bad": bad === true ? "true" : undefined, children: [onPress === undefined ? (_jsx("div", { className: rowClass, children: content })) : (_jsx("button", { type: "button", onClick: onPress, className: cn(rowClass, "hover:bg-muted"), ...(testId !== undefined && { "data-testid": `${testId}-insert` }), children: content })), action !== undefined && _jsx("div", { className: "shrink-0 pr-2", children: action })] }));
|
|
28
|
-
}
|
|
29
|
-
export function RailHint({ children }) {
|
|
30
|
-
return _jsx("p", { className: "px-3.5 pt-1 pb-2 text-xs text-muted-foreground", children: children });
|
|
31
|
-
}
|
|
1
|
+
export { RailField, RailHint, RailRow, RailSection } from "@cosmicdrift/kumiko-renderer/web";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { StatusBadgeTone } from "@cosmicdrift/kumiko-renderer/web";
|
|
2
3
|
import type { OverviewRowView } from "./overview-row.ts";
|
|
3
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
4
|
export declare function statusTone(status: OverviewRowView["status"]): StatusBadgeTone;
|
|
5
5
|
export declare function statusLabel(t: Translate, status: OverviewRowView["status"], version: number): string;
|
|
6
6
|
export declare function draftBesideActiveSentence(t: Translate, row: {
|
|
@@ -11,4 +11,3 @@ export declare function draftBesideActiveSentence(t: Translate, row: {
|
|
|
11
11
|
} | null;
|
|
12
12
|
}): string | null;
|
|
13
13
|
export declare function asRowStatus(status: string): OverviewRowView["status"];
|
|
14
|
-
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { useDebouncedValue } from "@cosmicdrift/kumiko-renderer";
|
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// Unlike useDeferredValue this waits for a quiet period, so a burst of
|
|
3
|
-
// keystrokes triggers one server round-trip instead of one per keystroke.
|
|
4
|
-
export function useDebouncedValue(value, delayMs) {
|
|
5
|
-
const [debounced, setDebounced] = useState(value);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const timer = setTimeout(() => setDebounced(value), delayMs);
|
|
8
|
-
return () => clearTimeout(timer);
|
|
9
|
-
}, [value, delayMs]);
|
|
10
|
-
return debounced;
|
|
11
|
-
}
|
|
1
|
+
export { useDebouncedValue } from "@cosmicdrift/kumiko-renderer";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
|
+
import { type EditorTab, type Region } from "./layout-editor-logic.ts";
|
|
2
3
|
import type { LayoutKind, LayoutSourceView, LayoutValidation, TenantOverrideCounts } from "./layout-types.ts";
|
|
3
4
|
import type { OverviewRowView } from "./overview-row.ts";
|
|
4
5
|
export type LayoutEditorBodyProps = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
|
|
2
3
|
import type { MailValidation, PickedValues } from "./mail-types.ts";
|
|
3
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
4
|
export declare function useMailEditorState(props: MailEditorBodyProps, t: Translate): {
|
|
5
5
|
update: (patch: Partial<import("./mail-types.ts").MailDraft>) => void;
|
|
6
6
|
reset: () => void;
|
|
@@ -16,8 +16,8 @@ export declare function useMailEditorState(props: MailEditorBodyProps, t: Transl
|
|
|
16
16
|
setDraft: import("react").Dispatch<import("react").SetStateAction<import("./mail-types.ts").MailDraft>>;
|
|
17
17
|
settled: import("./mail-types.ts").MailDraft;
|
|
18
18
|
settle: (next: import("./mail-types.ts").MailDraft) => void;
|
|
19
|
-
mode: import("
|
|
20
|
-
setMode: import("react").Dispatch<import("react").SetStateAction<import("
|
|
19
|
+
mode: import("@cosmicdrift/kumiko-renderer/web").EditorMode;
|
|
20
|
+
setMode: import("react").Dispatch<import("react").SetStateAction<import("@cosmicdrift/kumiko-renderer/web").EditorMode>>;
|
|
21
21
|
creating: boolean;
|
|
22
22
|
setCreating: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
23
23
|
dialog: import("./mail-dialogs.tsx").ConfirmDialog;
|
|
@@ -86,4 +86,3 @@ export declare function useMailEditorState(props: MailEditorBodyProps, t: Transl
|
|
|
86
86
|
} | null;
|
|
87
87
|
fieldErrors: Partial<Record<"previewText" | "subject", string>>;
|
|
88
88
|
};
|
|
89
|
-
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { EditorMode } from "./editor-header.tsx";
|
|
2
3
|
import { type EditorState, type GeneratedPdf } from "./pdf-editor-draft.ts";
|
|
3
4
|
import { type PuppeteerStatus, type TemplateDetail, type TemplateListRow, type TemplateScope, type TenantOverrides, type ValidationResult } from "./types.ts";
|
|
4
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
5
5
|
export declare function usePdfEditorState(input: {
|
|
6
6
|
readonly slug: string;
|
|
7
7
|
readonly requestedLocale: string | undefined;
|
|
@@ -74,4 +74,3 @@ export declare function usePdfEditorState(input: {
|
|
|
74
74
|
};
|
|
75
75
|
};
|
|
76
76
|
export type PdfEditorState = ReturnType<typeof usePdfEditorState>;
|
|
77
|
-
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { Translate } from "@cosmicdrift/kumiko-renderer/headless";
|
|
1
2
|
import type { OverviewRowView, OverviewUsageView } from "./overview-row.ts";
|
|
2
|
-
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
3
3
|
export declare function variableLabelText(t: Translate, name: string, label: string | undefined): string;
|
|
4
4
|
export declare function templateTitleText(t: Translate, title: string): string;
|
|
5
5
|
export declare function templateTitle(t: Translate, row: Pick<OverviewRowView, "title" | "slug" | "kind">): string;
|
|
@@ -9,4 +9,3 @@ export declare function usageParts(t: Translate, usage: OverviewUsageView): {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function usageLabel(t: Translate, usage: OverviewUsageView): string;
|
|
11
11
|
export declare function usedByLabel(t: Translate, row: OverviewRowView): string;
|
|
12
|
-
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-document-designer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.414.0",
|
|
4
4
|
"description": "Document designer for Kumiko: template overview, layout editor (mail layout, PDF letterhead) and the PDF template editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"./web/styles.css": "./src/web/styles.css"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
24
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
25
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
26
|
-
"@cosmicdrift/kumiko-renderer-puppeteer-client": "0.
|
|
27
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
23
|
+
"@cosmicdrift/kumiko-bundled-features": "0.414.0",
|
|
24
|
+
"@cosmicdrift/kumiko-framework": "0.414.0",
|
|
25
|
+
"@cosmicdrift/kumiko-renderer": "0.414.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer-puppeteer-client": "0.414.0",
|
|
27
|
+
"@cosmicdrift/kumiko-types": "0.414.0",
|
|
28
28
|
"react": "^19.3.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.414.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "mail and PDF previews register in the preview registry",
|
|
6
|
+
"detail": "`documentDesignerClient()` registers `contentPreviews.mail` and `contentPreviews.pdf`; the mail, PDF and layout editors keep their look and mount their preview through `PreviewPane`. Their shell parts are adapters over the renderer's editor shell. Apps that mount the editors through `documentDesignerClient()` need no change; tests that render the editors without `createKumikoApp` wrap them in `ContentPreviewsProvider` with `documentDesignerClient().contentPreviews`."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.414.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "review round 2 document designer fixes",
|
|
12
|
+
"detail": "`toWorkingRow` keeps `archived` for an archived row with an open draft instead of reporting it as a draft, so find-by-id and layout-source show the template as archived and publish brings the draft back. `useEditorHandleReport` re-reports the handle when `onEditorReady` changes on a standing editor and unregisters the old callback. The repeat-bar list label ignores inherited object keys. Dead null guards in the PDF and mail editors, the redundant variable guard in the insert panel and the unused bullet-list text are removed, and the copies of `Translate`, `Primitives` and the selected-block type now come from one place. Tests cover the PDF editor's branding query without managed-pages and drop an unsafe cast."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.414.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Template title helpers and editor screen ids exported",
|
|
18
|
+
"detail": "`@cosmicdrift/kumiko-document-designer/web` exports `templateTitle`, `templateTitleText`, `DOCUMENT_DESIGNER_FEATURE`, `MAIL_EDITOR_SCREEN_ID`, `PDF_TEMPLATE_EDITOR_SCREEN_ID`, `TEMPLATE_EDITOR_SCREENS` and the type `TemplateEditorKind`. An app that reads the `slug` search parameter of an editor route can resolve the shown title the same way the overview does."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.409.0",
|
|
4
22
|
"type": "improvement",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function formatRelativeWhen(value: string, now?: number): string;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const UNITS = [
|
|
2
|
-
["day", 86_400_000],
|
|
3
|
-
["hour", 3_600_000],
|
|
4
|
-
["minute", 60_000],
|
|
5
|
-
];
|
|
6
|
-
// Falls back to the raw value for an unparseable timestamp, like formatWhen.
|
|
7
|
-
export function formatRelativeWhen(value, now = Date.now()) {
|
|
8
|
-
const then = Date.parse(value);
|
|
9
|
-
if (Number.isNaN(then))
|
|
10
|
-
return value;
|
|
11
|
-
const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
|
|
12
|
-
const elapsed = then - now;
|
|
13
|
-
for (const [unit, size] of UNITS) {
|
|
14
|
-
if (Math.abs(elapsed) >= size)
|
|
15
|
-
return formatter.format(Math.round(elapsed / size), unit);
|
|
16
|
-
}
|
|
17
|
-
return formatter.format(0, "minute");
|
|
18
|
-
}
|