@cosmicdrift/kumiko-document-designer 0.363.0 → 0.364.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/constants.d.ts +3 -0
- package/dist/constants.js +4 -0
- package/dist/i18n.js +144 -50
- package/dist/web/cells.js +1 -1
- package/dist/web/editor-header.d.ts +37 -0
- package/dist/web/editor-header.js +43 -0
- package/dist/web/editor-shell.d.ts +40 -0
- package/dist/web/editor-shell.js +36 -0
- package/dist/web/i18n.js +35 -59
- package/dist/web/layout-editor-body.d.ts +2 -2
- package/dist/web/layout-editor-body.js +94 -49
- package/dist/web/layout-editor.js +8 -6
- package/dist/web/layout-html.d.ts +2 -0
- package/dist/web/layout-html.js +13 -1
- package/dist/web/layout-meta.d.ts +29 -0
- package/dist/web/layout-meta.js +32 -0
- package/dist/web/layout-rails.d.ts +15 -5
- package/dist/web/layout-rails.js +61 -68
- package/dist/web/layout-types.d.ts +4 -0
- package/dist/web/layout-users.d.ts +1 -0
- package/dist/web/layout-users.js +4 -1
- package/dist/web/mail-dialogs.d.ts +1 -6
- package/dist/web/mail-dialogs.js +2 -2
- package/dist/web/mail-draft.d.ts +8 -6
- package/dist/web/mail-draft.js +33 -11
- package/dist/web/mail-edit-center.d.ts +3 -1
- package/dist/web/mail-edit-center.js +5 -4
- package/dist/web/mail-editor-body.d.ts +0 -1
- package/dist/web/mail-editor-body.js +57 -41
- package/dist/web/mail-editor.js +2 -5
- package/dist/web/mail-footer.d.ts +3 -14
- package/dist/web/mail-footer.js +12 -13
- package/dist/web/mail-left-rail.d.ts +9 -4
- package/dist/web/mail-left-rail.js +18 -15
- package/dist/web/mail-missing-locale.d.ts +10 -0
- package/dist/web/mail-missing-locale.js +45 -0
- package/dist/web/mail-origin-bar.d.ts +3 -5
- package/dist/web/mail-origin-bar.js +26 -50
- package/dist/web/mail-preview-side.d.ts +12 -5
- package/dist/web/mail-preview-side.js +14 -5
- package/dist/web/mail-preview.d.ts +9 -5
- package/dist/web/mail-preview.js +24 -13
- package/dist/web/mail-right-rail.js +23 -13
- package/dist/web/mail-types.d.ts +6 -1
- package/dist/web/mail-versions.js +3 -3
- package/dist/web/origin-bar.d.ts +11 -2
- package/dist/web/origin-bar.js +46 -24
- package/dist/web/overrides-drawer.d.ts +3 -2
- package/dist/web/overrides-drawer.js +87 -11
- package/dist/web/overview-row.d.ts +2 -0
- package/dist/web/overview-row.js +2 -1
- package/dist/web/page-canvas.d.ts +1 -0
- package/dist/web/page-canvas.js +23 -13
- package/dist/web/pdf-footer.d.ts +13 -0
- package/dist/web/pdf-footer.js +34 -0
- package/dist/web/pdf-left-rail.d.ts +9 -0
- package/dist/web/pdf-left-rail.js +16 -0
- package/dist/web/pdf-meta.d.ts +3 -5
- package/dist/web/pdf-meta.js +10 -16
- package/dist/web/pdf-preview.js +21 -8
- package/dist/web/pdf-template-editor.js +90 -76
- package/dist/web/rail.d.ts +13 -1
- package/dist/web/rail.js +15 -7
- package/dist/web/relative-time.d.ts +1 -0
- package/dist/web/relative-time.js +18 -0
- package/dist/web/settings-panel.d.ts +11 -5
- package/dist/web/settings-panel.js +24 -13
- package/dist/web/status-label.d.ts +7 -0
- package/dist/web/status-label.js +8 -0
- package/dist/web/types.d.ts +23 -0
- package/dist/web/use-insert-targets.d.ts +1 -1
- package/dist/web/use-mail-persistence.d.ts +0 -1
- package/dist/web/use-mail-persistence.js +1 -1
- package/dist/web/use-own-address.d.ts +5 -0
- package/dist/web/use-own-address.js +14 -0
- package/package.json +8 -8
- package/src/changes.json +24 -0
- package/dist/web/mail-header.d.ts +0 -23
- package/dist/web/mail-header.js +0 -16
- package/dist/web/placeholder-panel.d.ts +0 -21
- package/dist/web/placeholder-panel.js +0 -35
package/dist/web/page-canvas.js
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
|
-
import { DocumentContentEditor, PDF_DOCUMENT_BLOCKS, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
3
|
+
import { DocumentContentEditor, PDF_DEFAULT_MARGIN_MM, PDF_DOCUMENT_BLOCKS, PDF_FOOTER_MARGIN_MM, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
4
|
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
6
|
import { useCallback } from "react";
|
|
7
7
|
import { LayoutHtml } from "./layout-html.js";
|
|
8
8
|
import { MARGIN_SIDES } from "./pdf-meta.js";
|
|
9
9
|
const MM_TO_PX = 96 / 25.4;
|
|
10
|
-
|
|
11
|
-
// Page width in CSS px at 96 dpi, as a class so no inline width is needed.
|
|
10
|
+
// Page width in CSS px at 96 dpi as a cap; the page shrinks with a narrow column instead of scrolling sideways.
|
|
12
11
|
const PAGE_WIDTH_CLASSES = {
|
|
13
|
-
"A4-portrait": "w-[794px]",
|
|
14
|
-
"A4-landscape": "w-[1123px]",
|
|
15
|
-
"Letter-portrait": "w-[816px]",
|
|
16
|
-
"Letter-landscape": "w-[1056px]",
|
|
12
|
+
"A4-portrait": "max-w-[794px]",
|
|
13
|
+
"A4-landscape": "max-w-[1123px]",
|
|
14
|
+
"Letter-portrait": "max-w-[816px]",
|
|
15
|
+
"Letter-landscape": "max-w-[1056px]",
|
|
17
16
|
};
|
|
18
|
-
function RegionBlock({ html, systemLabels, caption, }) {
|
|
17
|
+
function RegionBlock({ html, systemLabels, resourceSrc, caption, }) {
|
|
19
18
|
if (html.trim() === "")
|
|
20
19
|
return null;
|
|
21
|
-
return (_jsxs("div", { className: "relative border border-dashed border-border bg-muted/40 p-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "absolute
|
|
20
|
+
return (_jsxs("div", { className: "relative border border-dashed border-border bg-muted/40 p-2 pt-4 text-xs text-muted-foreground", children: [_jsx("span", { className: "absolute left-2 top-0.5 text-[10px] uppercase tracking-wide", children: caption }), _jsx(LayoutHtml, { html: html, systemLabels: systemLabels, ...(resourceSrc !== undefined && { resourceSrc }) })] }));
|
|
21
|
+
}
|
|
22
|
+
// The letterhead is a band across the whole paper, outside the page margins, like on printed letter paper.
|
|
23
|
+
function HeaderBand({ html, systemLabels, resourceSrc, caption, }) {
|
|
24
|
+
return (_jsxs("div", { className: "relative border-b border-border bg-muted px-[8%] pb-4 pt-6 text-xs text-foreground [&_td:last-child]:whitespace-nowrap", "data-testid": "document-designer-letterhead", children: [_jsx("span", { className: "absolute right-[8%] top-2 text-[10px] uppercase tracking-wide text-muted-foreground", children: caption }), _jsx(LayoutHtml, { html: html, systemLabels: systemLabels, ...(resourceSrc !== undefined && { resourceSrc }) })] }));
|
|
22
25
|
}
|
|
23
26
|
export function PageCanvas(props) {
|
|
24
27
|
const t = useTranslation();
|
|
25
28
|
const { meta } = props;
|
|
26
29
|
const showRegions = meta.layoutHeaderFooter && props.regions !== null;
|
|
27
|
-
|
|
30
|
+
// The letterhead band sits in the top margin, so the content starts right below it.
|
|
31
|
+
const showLetterhead = showRegions && props.regions !== null && props.regions.header.trim() !== "";
|
|
32
|
+
// Same fallback as the PDF renderer: a footer region reserves a taller bottom margin (the header band replaces the top margin on the canvas).
|
|
33
|
+
const hasFooter = showRegions && props.regions !== null && props.regions.footer.trim() !== "";
|
|
34
|
+
const fromLayout = t("documentDesigner.canvas.fromLayout", { layout: props.layoutName });
|
|
35
|
+
const regionCaption = (region) => `${t(`documentDesigner.editor.region.${region}`)} · ${fromLayout}`;
|
|
28
36
|
// CSS variables instead of a style prop: margins are user data, classes are static.
|
|
29
37
|
const applyMargins = useCallback((element) => {
|
|
30
38
|
if (element === null)
|
|
31
39
|
return;
|
|
32
40
|
for (const side of MARGIN_SIDES) {
|
|
33
|
-
const
|
|
41
|
+
const fallbackMm = side === "bottom" && hasFooter ? PDF_FOOTER_MARGIN_MM : PDF_DEFAULT_MARGIN_MM;
|
|
42
|
+
const mm = meta.margins[side] === "" ? fallbackMm : meta.margins[side];
|
|
34
43
|
element.style.setProperty(`--page-margin-${side}`, `${Math.round(mm * MM_TO_PX)}px`);
|
|
35
44
|
}
|
|
36
|
-
}, [meta.margins]);
|
|
37
|
-
|
|
45
|
+
}, [meta.margins, hasFooter]);
|
|
46
|
+
const renderPaper = (editable) => (_jsxs("div", { className: "flex w-full flex-col items-center gap-2", children: [_jsxs("div", { "data-testid": "document-designer-page", className: cn("flex min-h-[1000px] w-full flex-col bg-card shadow-md", PAGE_WIDTH_CLASSES[`${meta.pageFormat}-${meta.orientation}`]), children: [showLetterhead && props.regions !== null && (_jsx(HeaderBand, { html: props.regions.header, systemLabels: props.systemLabels, resourceSrc: props.resourceSrc, caption: fromLayout })), _jsxs("div", { ref: applyMargins, className: cn("flex min-h-0 flex-1 flex-col pr-(--page-margin-right) pb-(--page-margin-bottom) pl-(--page-margin-left)", !showLetterhead && "pt-(--page-margin-top)"), children: [_jsxs("div", { className: "relative my-2 min-h-0 flex-1 border border-dashed border-primary/40 p-2 pt-5", "data-testid": "document-designer-content-area", children: [_jsx("span", { className: "absolute left-2 top-0.5 text-[10px] uppercase tracking-wide text-primary", children: t("documentDesigner.editor.region.content") }), editable] }), showRegions && props.regions !== null && (_jsxs(_Fragment, { children: [_jsx(RegionBlock, { html: props.regions.signature, systemLabels: props.systemLabels, resourceSrc: props.resourceSrc, caption: regionCaption("signature") }), _jsx(RegionBlock, { html: props.regions.footer, systemLabels: props.systemLabels, resourceSrc: props.resourceSrc, caption: regionCaption("footer") })] }))] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t("documentDesigner.canvas.caption", { format: meta.pageFormat }) })] }));
|
|
47
|
+
return (_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", "data-testid": "pdf-editor-center", children: _jsx(DocumentContentEditor, { id: "document-designer-content", value: props.content, onChange: props.onContentChange, variables: props.variableNames, readOnly: false, blocks: PDF_DOCUMENT_BLOCKS, variableLabels: props.variableLabels, systemVariables: props.systemVariables, resourcePreviews: props.resourcePreviews, chrome: "canvas", renderPaper: renderPaper, onEditorReady: props.onEditorReady }) }));
|
|
38
48
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FooterTone } from "./editor-shell.js";
|
|
2
|
+
import type { ValidationResult } from "./types.js";
|
|
3
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
|
+
export type GeneratedProbe = {
|
|
5
|
+
readonly pages: number;
|
|
6
|
+
readonly sizeBytes: number;
|
|
7
|
+
readonly durationMs: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function pdfFooterMessage(t: Translate, validation: ValidationResult, probe: GeneratedProbe | null): {
|
|
10
|
+
readonly tone: FooterTone;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function formatSize(bytes) {
|
|
2
|
+
return bytes < 1024 * 1024
|
|
3
|
+
? `${Math.max(1, Math.round(bytes / 1024))} KB`
|
|
4
|
+
: `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
5
|
+
}
|
|
6
|
+
// One sentence like the mail footer: missing required placeholders first, then other blocking
|
|
7
|
+
// issues, then warnings, else "ready" plus the last test run.
|
|
8
|
+
export function pdfFooterMessage(t, validation, probe) {
|
|
9
|
+
if (validation.missingRequired.length > 0) {
|
|
10
|
+
const names = validation.missingRequired
|
|
11
|
+
.map((name) => validation.variables[name]?.label ?? name)
|
|
12
|
+
.join(", ");
|
|
13
|
+
return { tone: "bad", text: t("documentDesigner.validation.missingRequired", { name: names }) };
|
|
14
|
+
}
|
|
15
|
+
const blocking = [...validation.issues, ...validation.publishIssues][0];
|
|
16
|
+
if (blocking !== undefined) {
|
|
17
|
+
const translated = t(blocking.i18nKey, blocking.params);
|
|
18
|
+
return { tone: "bad", text: translated === blocking.i18nKey ? blocking.code : translated };
|
|
19
|
+
}
|
|
20
|
+
const warning = validation.warnings[0];
|
|
21
|
+
if (warning !== undefined) {
|
|
22
|
+
const translated = t(warning.i18nKey, { detail: warning.detail });
|
|
23
|
+
return { tone: "warn", text: translated === warning.i18nKey ? warning.code : translated };
|
|
24
|
+
}
|
|
25
|
+
const ok = t("documentDesigner.validation.ok");
|
|
26
|
+
if (probe === null)
|
|
27
|
+
return { tone: "ok", text: ok };
|
|
28
|
+
const lastProbe = t("documentDesigner.validation.lastProbe", {
|
|
29
|
+
pages: probe.pages,
|
|
30
|
+
size: formatSize(probe.sizeBytes),
|
|
31
|
+
seconds: (probe.durationMs / 1000).toFixed(1),
|
|
32
|
+
});
|
|
33
|
+
return { tone: "ok", text: `${ok} ${lastProbe}` };
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export declare function visibleSystemPlaceholders(names: readonly string[]): readonly string[];
|
|
3
|
+
export declare function PdfResourceSection({ resourceKeys, canUpload, layoutName, onInsert, onUpload, }: {
|
|
4
|
+
readonly resourceKeys: readonly string[];
|
|
5
|
+
readonly canUpload: boolean;
|
|
6
|
+
readonly layoutName: string;
|
|
7
|
+
readonly onInsert: (key: string) => void;
|
|
8
|
+
readonly onUpload: (file: File) => Promise<void>;
|
|
9
|
+
}): ReactNode;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { UploadZone } from "@cosmicdrift/kumiko-renderer-web";
|
|
5
|
+
import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
|
|
6
|
+
// unsubscribeUrl belongs to mail; the PDF designer never offers it.
|
|
7
|
+
const HIDDEN_SYSTEM_PLACEHOLDERS = new Set(["unsubscribeUrl"]);
|
|
8
|
+
export function visibleSystemPlaceholders(names) {
|
|
9
|
+
return names.filter((name) => !HIDDEN_SYSTEM_PLACEHOLDERS.has(name));
|
|
10
|
+
}
|
|
11
|
+
const IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
12
|
+
export function PdfResourceSection({ resourceKeys, canUpload, layoutName, onInsert, onUpload, }) {
|
|
13
|
+
const t = useTranslation();
|
|
14
|
+
const { Button } = usePrimitives();
|
|
15
|
+
return (_jsxs(RailSection, { title: t("documentDesigner.resources.title"), testId: "pdf-editor-resources", children: [resourceKeys.length === 0 && _jsx(RailRow, { label: t("documentDesigner.resources.empty") }), resourceKeys.map((key) => (_jsx(RailRow, { testId: `pdf-resource-${key}`, symbol: "\u25A3", label: key, onPress: () => onInsert(key) }, key))), _jsx(RailField, { children: canUpload ? (_jsx(UploadZone, { multiple: false, accept: IMAGE_TYPES, title: t("documentDesigner.resources.upload"), onUpload: onUpload, testId: "document-designer-upload" })) : (_jsx(Button, { variant: "secondary", size: "sm", disabled: true, children: t("documentDesigner.resources.upload") })) }), !canUpload && _jsx(RailHint, { children: t("documentDesigner.resources.saveFirst") }), _jsx(RailHint, { children: t("documentDesigner.placeholders.layoutHint", { layout: layoutName }) })] }));
|
|
16
|
+
}
|
package/dist/web/pdf-meta.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export type
|
|
4
|
-
export type Orientation = (typeof ORIENTATIONS)[number];
|
|
5
|
-
export declare const DEFAULT_MARGIN_MM = 15;
|
|
1
|
+
import { PDF_ORIENTATIONS, PDF_PAGE_FORMATS } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
2
|
+
export type PageFormat = (typeof PDF_PAGE_FORMATS)[number];
|
|
3
|
+
export type Orientation = (typeof PDF_ORIENTATIONS)[number];
|
|
6
4
|
export type MarginSide = "top" | "right" | "bottom" | "left";
|
|
7
5
|
export declare const MARGIN_SIDES: readonly ["top", "right", "bottom", "left"];
|
|
8
6
|
export type PdfMetaDraft = {
|
package/dist/web/pdf-meta.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
+
import { PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
2
3
|
import { isOneOf, isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
-
export const PAGE_FORMATS = ["A4", "Letter"];
|
|
4
|
-
export const ORIENTATIONS = ["portrait", "landscape"];
|
|
5
|
-
// Mirrors the renderer's fallback margin; used to complete a partly filled set,
|
|
6
|
-
// because the server schema only accepts all four sides together.
|
|
7
|
-
export const DEFAULT_MARGIN_MM = 15;
|
|
8
4
|
export const MARGIN_SIDES = [
|
|
9
5
|
"top",
|
|
10
6
|
"right",
|
|
@@ -51,8 +47,8 @@ export function parsePdfMeta(raw) {
|
|
|
51
47
|
const metadata = propertyOf(parsed, "metadata");
|
|
52
48
|
return {
|
|
53
49
|
layoutSlug: stringOr(parsed["layoutSlug"], ""),
|
|
54
|
-
pageFormat: isOneOf(
|
|
55
|
-
orientation: isOneOf(
|
|
50
|
+
pageFormat: isOneOf(PDF_PAGE_FORMATS, pageFormat) ? pageFormat : EMPTY_PDF_META.pageFormat,
|
|
51
|
+
orientation: isOneOf(PDF_ORIENTATIONS, orientation) ? orientation : EMPTY_PDF_META.orientation,
|
|
56
52
|
printBackground: booleanOr(parsed["printBackground"], true),
|
|
57
53
|
layoutHeaderFooter: booleanOr(parsed["layoutHeaderFooter"], true),
|
|
58
54
|
margins: {
|
|
@@ -66,9 +62,6 @@ export function parsePdfMeta(raw) {
|
|
|
66
62
|
author: stringOr(propertyOf(metadata, "author"), ""),
|
|
67
63
|
};
|
|
68
64
|
}
|
|
69
|
-
function hasAnyMargin(margins) {
|
|
70
|
-
return MARGIN_SIDES.some((side) => margins[side] !== "");
|
|
71
|
-
}
|
|
72
65
|
/** Builds the meta object for the server; defaults and empty fields are left out. */
|
|
73
66
|
export function serializePdfMeta(draft) {
|
|
74
67
|
const meta = {};
|
|
@@ -82,12 +75,13 @@ export function serializePdfMeta(draft) {
|
|
|
82
75
|
meta["printBackground"] = false;
|
|
83
76
|
if (!draft.layoutHeaderFooter)
|
|
84
77
|
meta["layoutHeaderFooter"] = false;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
// Unset sides stay out so the renderer's per-side fallbacks (header/footer margins) still apply.
|
|
79
|
+
const setMargins = MARGIN_SIDES.flatMap((side) => {
|
|
80
|
+
const mm = draft.margins[side];
|
|
81
|
+
return mm === "" ? [] : [[side, mm]];
|
|
82
|
+
});
|
|
83
|
+
if (setMargins.length > 0)
|
|
84
|
+
meta["margins"] = Object.fromEntries(setMargins);
|
|
91
85
|
if (draft.fileName.trim() !== "")
|
|
92
86
|
meta["fileName"] = draft.fileName.trim();
|
|
93
87
|
const metadata = {};
|
package/dist/web/pdf-preview.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
// @runtime client
|
|
3
3
|
/// <reference path="./pdfjs-worker.d.ts" />
|
|
4
4
|
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
-
import { Suspense, use, useCallback } from "react";
|
|
5
|
+
import { Suspense, use, useCallback, useMemo } from "react";
|
|
6
6
|
// pdf.js runs in the main thread ("fake worker"): the worker module is bundled
|
|
7
7
|
// with the app and handed over through globalThis.pdfjsWorker, so no worker or
|
|
8
8
|
// CDN URL is ever fetched (self-hosting and CSP stay intact).
|
|
@@ -23,10 +23,8 @@ async function loadDocument(pdfBase64) {
|
|
|
23
23
|
import("pdfjs-dist/build/pdf.worker.min.mjs"),
|
|
24
24
|
]);
|
|
25
25
|
Object.assign(globalThis, { pdfjsWorker: worker });
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
}).promise;
|
|
29
|
-
return { ok: true, document };
|
|
26
|
+
const loadingTask = pdfjs.getDocument({ data: decodeBase64(pdfBase64) });
|
|
27
|
+
return { ok: true, document: await loadingTask.promise, loadingTask };
|
|
30
28
|
}
|
|
31
29
|
catch {
|
|
32
30
|
return { ok: false };
|
|
@@ -35,15 +33,21 @@ async function loadDocument(pdfBase64) {
|
|
|
35
33
|
// The promise must be stable across renders for use(); only the latest PDF is kept.
|
|
36
34
|
function documentFor(pdfBase64) {
|
|
37
35
|
if (cachedKey !== pdfBase64 || cachedLoad === null) {
|
|
36
|
+
const previous = cachedLoad;
|
|
38
37
|
cachedKey = pdfBase64;
|
|
39
38
|
cachedLoad = loadDocument(pdfBase64);
|
|
39
|
+
void previous?.then((loaded) => {
|
|
40
|
+
if (loaded.ok)
|
|
41
|
+
void loaded.loadingTask.destroy();
|
|
42
|
+
});
|
|
40
43
|
}
|
|
41
44
|
return cachedLoad;
|
|
42
45
|
}
|
|
43
46
|
function PageView({ document, pageNumber, }) {
|
|
44
47
|
const t = useTranslation();
|
|
45
48
|
const page = use(pageFor(document, pageNumber));
|
|
46
|
-
|
|
49
|
+
// Stable identity: the mount callbacks below depend on it, and a new viewport per render re-mounts canvas and text layer.
|
|
50
|
+
const viewport = useMemo(() => page.getViewport({ scale: RENDER_SCALE }), [page]);
|
|
47
51
|
const mountCanvas = useCallback((canvas) => {
|
|
48
52
|
if (canvas === null)
|
|
49
53
|
return;
|
|
@@ -57,7 +61,13 @@ function PageView({ document, pageNumber, }) {
|
|
|
57
61
|
container.style.setProperty("--total-scale-factor", String(RENDER_SCALE));
|
|
58
62
|
container.style.setProperty("--scale-factor", String(RENDER_SCALE));
|
|
59
63
|
let layer = null;
|
|
64
|
+
let cancelled = false;
|
|
60
65
|
void import("pdfjs-dist").then(({ TextLayer }) => {
|
|
66
|
+
// skip: cleanup ran before pdfjs finished loading
|
|
67
|
+
if (cancelled)
|
|
68
|
+
return undefined;
|
|
69
|
+
// TextLayer.cancel() keeps rendered spans; a re-mount must not append a second layer.
|
|
70
|
+
container.replaceChildren();
|
|
61
71
|
const textLayer = new TextLayer({
|
|
62
72
|
textContentSource: page.streamTextContent(),
|
|
63
73
|
container,
|
|
@@ -66,9 +76,12 @@ function PageView({ document, pageNumber, }) {
|
|
|
66
76
|
layer = textLayer;
|
|
67
77
|
return textLayer.render().catch(() => undefined);
|
|
68
78
|
});
|
|
69
|
-
return () =>
|
|
79
|
+
return () => {
|
|
80
|
+
cancelled = true;
|
|
81
|
+
layer?.cancel();
|
|
82
|
+
};
|
|
70
83
|
}, [page, viewport]);
|
|
71
|
-
return (_jsxs("figure", { className: "m-0 flex flex-col items-center gap-2", "data-testid": `pdf-page-${pageNumber}`, children: [_jsxs("div", { className: "relative w-fit bg-
|
|
84
|
+
return (_jsxs("figure", { className: "m-0 flex flex-col items-center gap-2", "data-testid": `pdf-page-${pageNumber}`, children: [_jsxs("div", { className: "relative w-fit bg-card shadow-md", children: [_jsx("canvas", { ref: mountCanvas, width: viewport.width, height: viewport.height }), _jsx("div", { ref: mountTextLayer, className: "absolute inset-0 overflow-hidden leading-none [&_span]:absolute [&_span]:origin-top-left [&_span]:cursor-text [&_span]:whitespace-pre [&_span]:text-transparent [&_span::selection]:bg-primary/30" })] }), _jsx("figcaption", { className: "text-xs text-muted-foreground", children: t("documentDesigner.preview.pageOf", { page: pageNumber, pages: document.numPages }) })] }));
|
|
72
85
|
}
|
|
73
86
|
const pageCache = new WeakMap();
|
|
74
87
|
function pageFor(document, pageNumber) {
|
|
@@ -3,36 +3,47 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
4
|
import { useDispatcher, usePrimitives, useQuery, useTranslation, useUserRoles, } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import { RendererPuppeteerClientQueries } from "@cosmicdrift/kumiko-renderer-puppeteer-client/qualified-names";
|
|
6
|
-
import { ErrorState, LoadingState,
|
|
7
|
-
import {
|
|
6
|
+
import { ErrorState, LoadingState, uploadFileRef } from "@cosmicdrift/kumiko-renderer-web";
|
|
7
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { BRANDING_QUERY_QN } from "../constants.js";
|
|
9
|
+
import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from "./editor-header.js";
|
|
10
|
+
import { EditorBody, EditorFooter, EditorFrame, EditorInfoBar } from "./editor-shell.js";
|
|
8
11
|
import { describeError, isAlreadyActive } from "./errors.js";
|
|
12
|
+
import { fileUrl } from "./file-name.js";
|
|
13
|
+
import { systemLabelMap, variableLabelMap } from "./mail-draft.js";
|
|
14
|
+
import { PlaceholderSection, SystemPlaceholderSection } from "./mail-left-rail.js";
|
|
9
15
|
import { OverridesDrawer } from "./overrides-drawer.js";
|
|
10
16
|
import { PageCanvas } from "./page-canvas.js";
|
|
17
|
+
import { pdfFooterMessage } from "./pdf-footer.js";
|
|
18
|
+
import { PdfResourceSection, visibleSystemPlaceholders } from "./pdf-left-rail.js";
|
|
11
19
|
import { EMPTY_PDF_META, parsePdfMeta, serializePdfMeta } from "./pdf-meta.js";
|
|
12
20
|
import { PdfPreview } from "./pdf-preview.js";
|
|
13
|
-
import { PlaceholderPanel, visibleSystemPlaceholders, } from "./placeholder-panel.js";
|
|
14
21
|
import { SettingsPanel } from "./settings-panel.js";
|
|
22
|
+
import { draftBesideActiveSentence } from "./status-label.js";
|
|
15
23
|
import { localesOf, parseJsonObject, pickRow, resolveLocale } from "./template-rows.js";
|
|
16
24
|
import { DEFAULT_LAYOUT_SLUG, DOCUMENT_PDF_KIND, PDF_LAYOUT_KIND, } from "./types.js";
|
|
25
|
+
import { useInsertTargets } from "./use-insert-targets.js";
|
|
26
|
+
import { useOverviewRows } from "./use-overview-rows.js";
|
|
17
27
|
const SETTLE_MS = 500;
|
|
18
28
|
const NO_VARIABLES = {};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
// Opening the preview prints the page without a click; a failed print is not retried for the same draft.
|
|
30
|
+
function RunOnMount({ run }) {
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
run();
|
|
33
|
+
}, [run]);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
22
36
|
const draftFromDetail = (detail) => ({
|
|
23
37
|
content: detail.content,
|
|
24
38
|
meta: parsePdfMeta(detail.meta),
|
|
25
39
|
});
|
|
26
40
|
const snapshotOf = (draft) => JSON.stringify({ content: draft.content, meta: serializePdfMeta(draft.meta) });
|
|
27
|
-
function formatSize(bytes) {
|
|
28
|
-
return bytes < 1024 * 1024
|
|
29
|
-
? `${Math.max(1, Math.round(bytes / 1024))} KB`
|
|
30
|
-
: `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
31
|
-
}
|
|
32
41
|
export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChange, }) {
|
|
33
42
|
const t = useTranslation();
|
|
34
43
|
const dispatcher = useDispatcher();
|
|
35
44
|
const { Button, Banner, Dialog } = usePrimitives();
|
|
45
|
+
const insert = useInsertTargets();
|
|
46
|
+
const overview = useOverviewRows(DOCUMENT_PDF_KIND);
|
|
36
47
|
const roles = useUserRoles();
|
|
37
48
|
const scope = roles?.includes("SystemAdmin") === true ? "system" : "tenant";
|
|
38
49
|
const rowsQuery = useQuery(TemplateResolverQueries.list, {
|
|
@@ -61,11 +72,12 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
61
72
|
clearTimeout(settleTimer.current);
|
|
62
73
|
}, []);
|
|
63
74
|
const [overridesOpen, setOverridesOpen] = useState(false);
|
|
64
|
-
const
|
|
75
|
+
const [draftEpoch, setDraftEpoch] = useState(0);
|
|
65
76
|
const [mode, setMode] = useState("edit");
|
|
66
77
|
const [busy, setBusy] = useState(null);
|
|
67
78
|
const [actionError, setActionError] = useState(null);
|
|
68
79
|
const [generated, setGenerated] = useState(null);
|
|
80
|
+
const autoPrintedFor = useRef(null);
|
|
69
81
|
const [pendingLocale, setPendingLocale] = useState(null);
|
|
70
82
|
const variableSchema = useMemo(() => parseJsonObject(detail?.variableSchema ?? null), [detail?.variableSchema]);
|
|
71
83
|
const settledMeta = current === null ? null : serializePdfMeta(current.settled.meta);
|
|
@@ -90,35 +102,25 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
90
102
|
variableSchema,
|
|
91
103
|
}, { enabled: detail !== null });
|
|
92
104
|
const layout = previewQuery.error === null ? (previewQuery.data?.layout ?? null) : null;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const layoutSlugs = [
|
|
98
|
-
...new Set((layoutsQuery.data ?? []).map((layoutRow) => layoutRow.slug)),
|
|
99
|
-
].sort();
|
|
105
|
+
const layouts = useOverviewRows(PDF_LAYOUT_KIND);
|
|
106
|
+
// The built-in "logo" resource of a layout is the tenant's branding logo.
|
|
107
|
+
const branding = useQuery(BRANDING_QUERY_QN, {});
|
|
108
|
+
const brandingLogoFile = branding.data?.logoFile ?? "";
|
|
100
109
|
const statusQuery = useQuery(RendererPuppeteerClientQueries.status, {});
|
|
101
110
|
const pdfAvailable = statusQuery.error === null && statusQuery.data?.available === true;
|
|
102
111
|
const unavailableMessage = statusQuery.data?.available === false ? (statusQuery.data.message ?? "") : "";
|
|
103
112
|
const showOrigin = scope === "system";
|
|
104
113
|
const overridesQuery = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind: DOCUMENT_PDF_KIND, locale }, { enabled: showOrigin && detail !== null });
|
|
105
114
|
const variables = validation?.variables ?? NO_VARIABLES;
|
|
106
|
-
const systemLabel = useCallback((name) => {
|
|
107
|
-
const label = t(`documentDesigner.system.${name}`);
|
|
108
|
-
return label === `documentDesigner.system.${name}` ? name : label;
|
|
109
|
-
}, [t]);
|
|
110
|
-
const variableLabels = useMemo(() => Object.fromEntries(Object.entries(variables).flatMap(([name, info]) => info.label === undefined ? [] : [[name, info.label]])), [variables]);
|
|
111
|
-
const systemLabels = useMemo(() => Object.fromEntries(SYSTEM_LABEL_NAMES.map((name) => [name, systemLabel(name)])), [systemLabel]);
|
|
112
115
|
const systemVariables = useMemo(() => visibleSystemPlaceholders(validation?.systemPlaceholders ?? []), [validation?.systemPlaceholders]);
|
|
116
|
+
const systemLabels = useMemo(() => systemLabelMap(t, systemVariables), [t, systemVariables]);
|
|
117
|
+
const variableLabels = useMemo(() => ({ ...systemLabels, ...variableLabelMap(variables) }), [systemLabels, variables]);
|
|
113
118
|
const variableNames = useMemo(() => Object.keys(variables), [variables]);
|
|
114
119
|
const linkedResources = detail?.linkedResources ?? null;
|
|
115
120
|
// The editor shows resource images from the stored copies; no extra request is made here.
|
|
116
121
|
const resourcePreviews = useMemo(() => Object.fromEntries(Object.entries(parseJsonObject(linkedResources)).flatMap(([key, fileRefId]) => typeof fileRefId === "string"
|
|
117
122
|
? [[key, { src: `/api/files/${encodeURIComponent(fileRefId)}`, caption: key }]]
|
|
118
123
|
: [])), [linkedResources]);
|
|
119
|
-
const onEditorReady = useCallback((handle) => {
|
|
120
|
-
editorHandle.current = handle;
|
|
121
|
-
}, []);
|
|
122
124
|
if (rowsQuery.loading && rowsQuery.data === null)
|
|
123
125
|
return _jsx(LoadingState, {});
|
|
124
126
|
if (rowsQuery.error !== null)
|
|
@@ -143,6 +145,7 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
143
145
|
if (settleTimer.current !== null)
|
|
144
146
|
clearTimeout(settleTimer.current);
|
|
145
147
|
setState({ key: source.id, draft, settled: draft, snapshot: snapshotOf(draft) });
|
|
148
|
+
setDraftEpoch((epoch) => epoch + 1);
|
|
146
149
|
setActionError(null);
|
|
147
150
|
}
|
|
148
151
|
async function reload() {
|
|
@@ -220,10 +223,11 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
220
223
|
setActionError(describeError(t, result.error));
|
|
221
224
|
setBusy(null);
|
|
222
225
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
function printOnce() {
|
|
227
|
+
if (current === null || autoPrintedFor.current === snapshotOf(current.draft))
|
|
228
|
+
return;
|
|
229
|
+
autoPrintedFor.current = snapshotOf(current.draft);
|
|
230
|
+
void generatePdf();
|
|
227
231
|
}
|
|
228
232
|
async function uploadResource(file) {
|
|
229
233
|
const sourceFileRefId = await uploadFileRef(file);
|
|
@@ -243,53 +247,63 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
243
247
|
else
|
|
244
248
|
onLocaleChange?.(next);
|
|
245
249
|
}
|
|
246
|
-
const used = validation?.used ?? [];
|
|
247
|
-
const missingRequired = validation?.missingRequired ?? [];
|
|
248
|
-
const layoutName = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
|
|
249
|
-
const systemNames = visibleSystemPlaceholders(validation?.systemPlaceholders ?? []).filter((name) => !MERGED_SYSTEM_PLACEHOLDERS.has(name));
|
|
250
|
-
const systemRows = systemNames.map((name) => ({
|
|
251
|
-
name,
|
|
252
|
-
label: systemLabel(name),
|
|
253
|
-
status: used.includes(name)
|
|
254
|
-
? "used"
|
|
255
|
-
: layout !== null &&
|
|
256
|
-
(layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`))
|
|
257
|
-
? "layout"
|
|
258
|
-
: "unused",
|
|
259
|
-
}));
|
|
260
|
-
const overrideCount = (overridesQuery.data?.activeOverrides ?? 0) + (overridesQuery.data?.draftOverrides ?? 0);
|
|
261
250
|
const canPublish = validation?.canPublish === true;
|
|
262
|
-
const
|
|
251
|
+
const layoutSlugShown = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
|
|
252
|
+
const layoutName = layouts.rows.find((row) => row.slug === layoutSlugShown)?.title ?? layoutSlugShown;
|
|
253
|
+
const layoutResourceSrc = (key) => key === "logo" && brandingLogoFile !== "" ? fileUrl(brandingLogoFile) : undefined;
|
|
254
|
+
const placeholderSource = validation === undefined || validation === null
|
|
255
|
+
? null
|
|
256
|
+
: {
|
|
257
|
+
...validation,
|
|
258
|
+
systemPlaceholders: visibleSystemPlaceholders(validation.systemPlaceholders),
|
|
259
|
+
};
|
|
260
|
+
const layoutFills = (name) => layout !== null &&
|
|
261
|
+
(layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`));
|
|
262
|
+
const overrideCount = (overridesQuery.data?.activeOverrides ?? 0) + (overridesQuery.data?.draftOverrides ?? 0);
|
|
263
263
|
const resourceKeys = Object.keys(parseJsonObject(detail.linkedResources));
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
264
|
+
const overviewRow = overview.rows.find((candidate) => candidate.slug === slug);
|
|
265
|
+
const title = overviewRow?.title ?? slug;
|
|
266
|
+
const systemVersion = overviewRow?.systemVersion ?? null;
|
|
267
|
+
const vocabulary = {
|
|
268
|
+
names: variableNames,
|
|
269
|
+
labels: variableLabels,
|
|
270
|
+
system: systemVariables,
|
|
271
|
+
};
|
|
272
|
+
const footer = validation === undefined || validation === null
|
|
273
|
+
? null
|
|
274
|
+
: pdfFooterMessage(t, validation, generated === null
|
|
275
|
+
? null
|
|
276
|
+
: {
|
|
277
|
+
pages: generated.result.pageCount,
|
|
278
|
+
sizeBytes: generated.result.sizeBytes,
|
|
279
|
+
durationMs: generated.result.durationMs,
|
|
280
|
+
});
|
|
281
|
+
const showOverrideLinks = showOrigin && detail.scope === "system";
|
|
282
|
+
const originText = showOverrideLinks
|
|
283
|
+
? overridesQuery.data?.totalTenants == null
|
|
284
|
+
? t("documentDesigner.originBar.noTotal", { count: overrideCount })
|
|
285
|
+
: t("documentDesigner.originBar.withTotal", {
|
|
286
|
+
count: overrideCount,
|
|
287
|
+
total: overridesQuery.data.totalTenants,
|
|
288
|
+
})
|
|
289
|
+
: detail.scope === "system"
|
|
290
|
+
? t("documentDesigner.mail.origin.system", { version: detail.version })
|
|
291
|
+
: systemVersion === null
|
|
292
|
+
? t("documentDesigner.mail.origin.tenantOnly")
|
|
293
|
+
: t("documentDesigner.mail.origin.overrides", { version: systemVersion });
|
|
294
|
+
const besideActiveSentence = draftBesideActiveSentence(t, detail);
|
|
295
|
+
const pdfUnavailableNote = !pdfAvailable && statusQuery.loading === false;
|
|
296
|
+
return (_jsxs(EditorFrame, { testId: "pdf-template-editor", children: [_jsx(EditorHeader, { title: title, stored: { status: detail.status, version: detail.version }, actions: {
|
|
297
|
+
busy: busy === "save",
|
|
298
|
+
canSave: busy === null && validation?.canSave !== false,
|
|
299
|
+
canPublish: busy === null && canPublish,
|
|
300
|
+
onSave: () => void save(),
|
|
301
|
+
onPublish: () => void publish(),
|
|
302
|
+
}, children: _jsx(EditorModeSwitch, { testIdPrefix: "pdf", mode: mode, onMode: setMode }) }), _jsxs(EditorInfoBar, { tone: pdfUnavailableNote ? "warn" : detail.scope === "tenant" ? "active" : "muted", testId: "pdf-origin-bar", origin: originOf(detail.scope === "system", systemVersion !== null), controls: _jsxs(_Fragment, { children: [_jsx(EditorLocaleSwitch, { testIdPrefix: "pdf", locales: locales.length > 0 ? locales : [locale], locale: locale, onLocale: requestLocale }), mode === "preview" && (_jsx(Button, { variant: "secondary", disabled: !pdfAvailable || busy !== null, loading: busy === "pdf", onClick: () => void generatePdf(), testId: "pdf-generate-header", children: t("documentDesigner.actions.generatePdf") }))] }), overlays: _jsx(OverridesDrawer, { open: overridesOpen, onOpenChange: setOverridesOpen, slug: slug, locale: locale, overrides: overridesQuery.data }), links: showOverrideLinks && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => void exportBundle(), children: t("documentDesigner.originBar.export") }), _jsx(Button, { variant: "link", onClick: () => setOverridesOpen(true), children: t("documentDesigner.originBar.showOverrides") })] })), children: [originText, besideActiveSentence !== null && ` ${besideActiveSentence}`, pdfUnavailableNote && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "data-testid": "pdf-unavailable", children: unavailableMessage === ""
|
|
303
|
+
? t("documentDesigner.pdfUnavailableNoReason")
|
|
304
|
+
: t("documentDesigner.pdfUnavailable", { message: unavailableMessage }) })] }))] }), _jsx(EditorBody, { left: mode === "edit" ? (_jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf" }), _jsx(SystemPlaceholderSection, { validation: placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf", isLayoutOwned: layoutFills }), _jsx(PdfResourceSection, { resourceKeys: resourceKeys, canUpload: detail.scope === scope, layoutName: layoutName, onInsert: insert.insertResource, onUpload: uploadResource })] })) : undefined, center: mode === "edit" ? (_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) => updateDraft({ ...current.draft, content }), regions: layout?.regions ?? null, layoutName: layoutName, resourceSrc: layoutResourceSrc, variableLabels: variableLabels, systemVariables: systemVariables, systemLabels: systemLabels, variableNames: variableNames, resourcePreviews: resourcePreviews, onEditorReady: insert.register("body") }) })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: generated !== null && generatedFresh ? (_jsx(PdfPreview, { pdfBase64: generated.result.pdfBase64 })) : (_jsxs("div", { className: "flex flex-col items-start gap-3 p-6", "data-testid": "document-designer-preview-stale", children: [pdfAvailable && busy === null && _jsx(RunOnMount, { run: printOnce }), _jsx("p", { className: "text-sm text-muted-foreground", children: busy === "pdf"
|
|
281
305
|
? t("documentDesigner.preview.loading")
|
|
282
|
-
: t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !pdfAvailable || busy !== null, onClick: () => void generatePdf(), children: t("documentDesigner.actions.generatePdf") })] })), _jsx(SettingsPanel, { meta: current.draft.meta, onMetaChange: (meta) => updateDraft({ ...current.draft, meta }),
|
|
283
|
-
version: detail.version,
|
|
284
|
-
status: detail.status,
|
|
285
|
-
modifiedAt: detail.modifiedAt,
|
|
286
|
-
} })] }), _jsxs("footer", { className: "flex flex-wrap items-start justify-between gap-3 border-t border-border pt-3", children: [_jsxs("ul", { className: "m-0 flex list-none flex-col gap-1 p-0 text-sm", "data-testid": "document-designer-validation", children: [missingRequired.length === 0 && validation !== undefined && validation !== null && (_jsx("li", { className: "text-status-ok", children: t("documentDesigner.validation.ok") })), missingRequired.map((name) => (_jsx("li", { className: "text-status-bad", children: t("documentDesigner.validation.missingRequired", {
|
|
287
|
-
name: variables[name]?.label ?? name,
|
|
288
|
-
}) }, name))), issueTexts.map((text) => (_jsx("li", { className: "text-status-bad", children: text }, text))), (validation?.warnings ?? []).map((warning) => (_jsx("li", { className: "text-status-warn", children: t(warning.i18nKey, { detail: warning.detail }) }, `${warning.code}:${warning.detail}`))), generated !== null && (_jsx("li", { className: "text-muted-foreground", children: t("documentDesigner.validation.lastProbe", {
|
|
289
|
-
pages: generated.result.pageCount,
|
|
290
|
-
size: formatSize(generated.result.sizeBytes),
|
|
291
|
-
seconds: (generated.result.durationMs / 1000).toFixed(1),
|
|
292
|
-
}) }))] }), _jsx("div", { className: "flex items-center gap-2", children: actions })] }), _jsx(Dialog, { open: pendingLocale !== null, onOpenChange: (open) => {
|
|
306
|
+
: t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !pdfAvailable || busy !== null, onClick: () => void generatePdf(), children: t("documentDesigner.actions.generatePdf") })] })) })), right: _jsx(SettingsPanel, { meta: current.draft.meta, onMetaChange: (meta) => updateDraft({ ...current.draft, meta }), layouts: layouts.rows, locale: locale, version: { id: detail.id, version: detail.version, status: detail.status }, vocabulary: vocabulary, insert: insert, draftKey: `${current.key}:${draftEpoch}` }) }), _jsx(EditorFooter, { testIdPrefix: "pdf-editor", message: footer, saveError: actionError, modifiedAt: detail.modifiedAt ?? null, dirty: dirty, busy: busy === "save", canSave: busy === null && validation?.canSave !== false, canPublish: busy === null && canPublish, onDiscard: () => resetDraft(detail), onSave: () => void save(), onPublish: () => void publish() }), _jsx(Dialog, { open: pendingLocale !== null, onOpenChange: (open) => {
|
|
293
307
|
if (!open)
|
|
294
308
|
setPendingLocale(null);
|
|
295
309
|
}, title: t("documentDesigner.localeDialog.title"), description: t("documentDesigner.localeDialog.description"), confirmLabel: t("documentDesigner.localeDialog.save"), cancelLabel: t("documentDesigner.localeDialog.cancel"), onConfirm: async () => {
|
package/dist/web/rail.d.ts
CHANGED
|
@@ -5,10 +5,21 @@ export declare function RailSection({ title, action, testId, children, }: {
|
|
|
5
5
|
readonly testId?: string;
|
|
6
6
|
readonly children: ReactNode;
|
|
7
7
|
}): ReactNode;
|
|
8
|
-
export declare function
|
|
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, required, trailing, trailingTone, action, bad, testId, onPress, }: {
|
|
9
17
|
readonly symbol?: string;
|
|
10
18
|
readonly label: string;
|
|
19
|
+
readonly required?: boolean;
|
|
11
20
|
readonly trailing?: ReactNode;
|
|
21
|
+
readonly trailingTone?: keyof typeof TRAILING_TONE;
|
|
22
|
+
readonly action?: ReactNode;
|
|
12
23
|
readonly bad?: boolean;
|
|
13
24
|
readonly testId?: string;
|
|
14
25
|
readonly onPress?: () => void;
|
|
@@ -16,3 +27,4 @@ export declare function RailRow({ symbol, label, trailing, bad, testId, onPress,
|
|
|
16
27
|
export declare function RailHint({ children }: {
|
|
17
28
|
readonly children: ReactNode;
|
|
18
29
|
}): ReactNode;
|
|
30
|
+
export {};
|
package/dist/web/rail.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
|
-
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
4
3
|
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
5
4
|
export function RailSection({ title, action, testId, children, }) {
|
|
6
|
-
return (_jsxs("section", { className: "flex flex-col
|
|
5
|
+
return (_jsxs("section", { className: "flex flex-col border-b pb-1", "data-testid": testId, children: [_jsxs("div", { className: "flex items-center gap-2 px-3.5 pt-2.5 pb-1", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: title }), _jsx("div", { className: "grow" }), action] }), children] }));
|
|
7
6
|
}
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export function RailField({ children }) {
|
|
8
|
+
return _jsx("div", { className: "flex flex-col gap-3 px-3.5 py-2", children: children });
|
|
9
|
+
}
|
|
10
|
+
const TRAILING_TONE = {
|
|
11
|
+
ok: "text-status-ok",
|
|
12
|
+
bad: "text-status-bad",
|
|
13
|
+
muted: "text-muted-foreground",
|
|
14
|
+
};
|
|
15
|
+
export function RailRow({ symbol, label, required, trailing, trailingTone = "muted", action, bad, testId, onPress, }) {
|
|
16
|
+
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 })), _jsxs("span", { className: cn("min-w-0 grow break-words text-sm", trailing !== undefined && "max-w-[65%] shrink-0", bad === true && "text-status-bad"), children: [label, required === true && _jsx("span", { className: "font-semibold text-status-bad", children: " *" })] }), trailing !== undefined && (_jsx("span", { className: cn("min-w-0 truncate text-right text-xs", bad === true ? "text-status-bad" : TRAILING_TONE[trailingTone]), title: typeof trailing === "string" ? trailing : undefined, children: trailing }))] }));
|
|
17
|
+
const rowClass = "flex min-h-10 min-w-0 grow items-center gap-2 px-3.5 text-left";
|
|
18
|
+
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 })] }));
|
|
11
19
|
}
|
|
12
20
|
export function RailHint({ children }) {
|
|
13
|
-
return _jsx("p", { className: "px-
|
|
21
|
+
return _jsx("p", { className: "px-3.5 pt-1 pb-2 text-xs text-muted-foreground", children: children });
|
|
14
22
|
}
|