@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.
Files changed (81) hide show
  1. package/dist/constants.d.ts +3 -0
  2. package/dist/constants.js +4 -0
  3. package/dist/i18n.js +144 -50
  4. package/dist/web/cells.js +1 -1
  5. package/dist/web/editor-header.d.ts +37 -0
  6. package/dist/web/editor-header.js +43 -0
  7. package/dist/web/editor-shell.d.ts +40 -0
  8. package/dist/web/editor-shell.js +36 -0
  9. package/dist/web/i18n.js +35 -59
  10. package/dist/web/layout-editor-body.d.ts +2 -2
  11. package/dist/web/layout-editor-body.js +94 -49
  12. package/dist/web/layout-editor.js +8 -6
  13. package/dist/web/layout-html.d.ts +2 -0
  14. package/dist/web/layout-html.js +13 -1
  15. package/dist/web/layout-meta.d.ts +29 -0
  16. package/dist/web/layout-meta.js +32 -0
  17. package/dist/web/layout-rails.d.ts +15 -5
  18. package/dist/web/layout-rails.js +61 -68
  19. package/dist/web/layout-types.d.ts +4 -0
  20. package/dist/web/layout-users.d.ts +1 -0
  21. package/dist/web/layout-users.js +4 -1
  22. package/dist/web/mail-dialogs.d.ts +1 -6
  23. package/dist/web/mail-dialogs.js +2 -2
  24. package/dist/web/mail-draft.d.ts +8 -6
  25. package/dist/web/mail-draft.js +33 -11
  26. package/dist/web/mail-edit-center.d.ts +3 -1
  27. package/dist/web/mail-edit-center.js +5 -4
  28. package/dist/web/mail-editor-body.d.ts +0 -1
  29. package/dist/web/mail-editor-body.js +57 -41
  30. package/dist/web/mail-editor.js +2 -5
  31. package/dist/web/mail-footer.d.ts +3 -14
  32. package/dist/web/mail-footer.js +12 -13
  33. package/dist/web/mail-left-rail.d.ts +9 -4
  34. package/dist/web/mail-left-rail.js +18 -15
  35. package/dist/web/mail-missing-locale.d.ts +10 -0
  36. package/dist/web/mail-missing-locale.js +45 -0
  37. package/dist/web/mail-origin-bar.d.ts +3 -5
  38. package/dist/web/mail-origin-bar.js +26 -50
  39. package/dist/web/mail-preview-side.d.ts +12 -5
  40. package/dist/web/mail-preview-side.js +14 -5
  41. package/dist/web/mail-preview.d.ts +9 -5
  42. package/dist/web/mail-preview.js +24 -13
  43. package/dist/web/mail-right-rail.js +23 -13
  44. package/dist/web/mail-types.d.ts +6 -1
  45. package/dist/web/mail-versions.js +3 -3
  46. package/dist/web/origin-bar.d.ts +11 -2
  47. package/dist/web/origin-bar.js +46 -24
  48. package/dist/web/overrides-drawer.d.ts +3 -2
  49. package/dist/web/overrides-drawer.js +87 -11
  50. package/dist/web/overview-row.d.ts +2 -0
  51. package/dist/web/overview-row.js +2 -1
  52. package/dist/web/page-canvas.d.ts +1 -0
  53. package/dist/web/page-canvas.js +23 -13
  54. package/dist/web/pdf-footer.d.ts +13 -0
  55. package/dist/web/pdf-footer.js +34 -0
  56. package/dist/web/pdf-left-rail.d.ts +9 -0
  57. package/dist/web/pdf-left-rail.js +16 -0
  58. package/dist/web/pdf-meta.d.ts +3 -5
  59. package/dist/web/pdf-meta.js +10 -16
  60. package/dist/web/pdf-preview.js +21 -8
  61. package/dist/web/pdf-template-editor.js +90 -76
  62. package/dist/web/rail.d.ts +13 -1
  63. package/dist/web/rail.js +15 -7
  64. package/dist/web/relative-time.d.ts +1 -0
  65. package/dist/web/relative-time.js +18 -0
  66. package/dist/web/settings-panel.d.ts +11 -5
  67. package/dist/web/settings-panel.js +24 -13
  68. package/dist/web/status-label.d.ts +7 -0
  69. package/dist/web/status-label.js +8 -0
  70. package/dist/web/types.d.ts +23 -0
  71. package/dist/web/use-insert-targets.d.ts +1 -1
  72. package/dist/web/use-mail-persistence.d.ts +0 -1
  73. package/dist/web/use-mail-persistence.js +1 -1
  74. package/dist/web/use-own-address.d.ts +5 -0
  75. package/dist/web/use-own-address.js +14 -0
  76. package/package.json +8 -8
  77. package/src/changes.json +24 -0
  78. package/dist/web/mail-header.d.ts +0 -23
  79. package/dist/web/mail-header.js +0 -16
  80. package/dist/web/placeholder-panel.d.ts +0 -21
  81. package/dist/web/placeholder-panel.js +0 -35
@@ -0,0 +1 @@
1
+ export declare function formatRelativeWhen(value: string, now?: number): string;
@@ -0,0 +1,18 @@
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
+ }
@@ -1,17 +1,23 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { EditorVocabulary } from "./mail-edit-center.js";
3
+ import type { OverviewRowView } from "./overview-row.js";
2
4
  import { type PdfMetaDraft } from "./pdf-meta.js";
3
- import type { TemplateStatus } from "./types.js";
5
+ import { type TemplateStatus } from "./types.js";
6
+ import type { InsertTargets } from "./use-insert-targets.js";
4
7
  export type VersionInfo = {
8
+ readonly id: string;
5
9
  readonly version: number;
6
10
  readonly status: TemplateStatus;
7
- readonly modifiedAt: string | null;
8
11
  };
9
12
  export type SettingsPanelProps = {
10
13
  readonly meta: PdfMetaDraft;
11
14
  readonly onMetaChange: (next: PdfMetaDraft) => void;
12
- /** Layout slugs available for this locale; the default layout is always offered. */
13
- readonly layoutSlugs: readonly string[];
15
+ readonly layouts: readonly OverviewRowView[];
14
16
  readonly locale: string;
15
17
  readonly version: VersionInfo;
18
+ readonly vocabulary: EditorVocabulary;
19
+ readonly insert: InsertTargets;
20
+ /** Changes when the draft is replaced from outside, so the single-line editors re-read it. */
21
+ readonly draftKey: string;
16
22
  };
17
- export declare function SettingsPanel({ meta, onMetaChange, layoutSlugs, locale, version, }: SettingsPanelProps): ReactNode;
23
+ export declare function SettingsPanel({ meta, onMetaChange, layouts, locale, version, vocabulary, insert, draftKey, }: SettingsPanelProps): ReactNode;
@@ -1,25 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
+ import { PDF_ORIENTATIONS, PDF_PAGE_FORMATS, PlaceholderLineEditor, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
3
4
  import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
- import { ModeSwitch, SectionCard, StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
5
- import { MARGIN_SIDES, ORIENTATIONS, PAGE_FORMATS, } from "./pdf-meta.js";
5
+ import { languageName } from "./mail-origin-bar.js";
6
+ import { layoutOptionLabel } from "./mail-right-rail.js";
7
+ import { VersionsSection } from "./mail-versions.js";
8
+ import { MARGIN_SIDES } from "./pdf-meta.js";
9
+ import { RailField, RailHint, RailSection } from "./rail.js";
10
+ import { DEFAULT_LAYOUT_SLUG } from "./types.js";
6
11
  const SIDE_LABEL_KEYS = {
7
12
  top: "documentDesigner.settings.marginTop",
8
13
  right: "documentDesigner.settings.marginRight",
9
14
  bottom: "documentDesigner.settings.marginBottom",
10
15
  left: "documentDesigner.settings.marginLeft",
11
16
  };
12
- export function SettingsPanel({ meta, onMetaChange, layoutSlugs, locale, version, }) {
17
+ export function SettingsPanel({ meta, onMetaChange, layouts, locale, version, vocabulary, insert, draftKey, }) {
13
18
  const t = useTranslation();
14
- const { Field, Input } = usePrimitives();
19
+ const { Field, Input, Text } = usePrimitives();
15
20
  const patch = (changes) => onMetaChange({ ...meta, ...changes });
16
- return (_jsxs("div", { className: "flex flex-col gap-4", "data-testid": "document-designer-settings", children: [_jsxs(SectionCard, { title: t("documentDesigner.settings.page"), children: [_jsx(Field, { id: "dd-layout", label: t("documentDesigner.settings.layout"), children: _jsx(Input, { kind: "select", id: "dd-layout", name: "layoutSlug", value: meta.layoutSlug, onChange: (layoutSlug) => patch({ layoutSlug }), display: "dropdown", options: [
17
- { value: "", label: t("documentDesigner.settings.layoutDefault") },
18
- ...layoutSlugs.map((slug) => ({ value: slug, label: slug })),
19
- ] }) }), _jsx(Field, { id: "dd-file-name", label: t("documentDesigner.settings.fileName"), children: _jsx(Input, { kind: "text", id: "dd-file-name", name: "fileName", value: meta.fileName, onChange: (fileName) => patch({ fileName }), placeholder: "rechnung-{{number}}.pdf" }) }), _jsx(ModeSwitch, { ariaLabel: t("documentDesigner.settings.pageFormat"), value: meta.pageFormat, onChange: (pageFormat) => patch({ pageFormat }), options: PAGE_FORMATS.map((value) => ({ value, label: value })) }), _jsx(ModeSwitch, { ariaLabel: t("documentDesigner.settings.orientation"), value: meta.orientation, onChange: (orientation) => patch({ orientation }), options: ORIENTATIONS.map((value) => ({
20
- value,
21
- label: t(`documentDesigner.settings.${value}`),
22
- })) }), _jsx("div", { className: "grid grid-cols-2 gap-2", children: MARGIN_SIDES.map((side) => (_jsx(Field, { id: `dd-margin-${side}`, label: t(SIDE_LABEL_KEYS[side]), children: _jsx(Input, { kind: "number", id: `dd-margin-${side}`, name: `margin-${side}`, value: meta.margins[side], onChange: (value) => patch({ margins: { ...meta.margins, [side]: value ?? "" } }), placeholder: "15" }) }, side))) }), _jsx(Field, { id: "dd-print-background", label: t("documentDesigner.settings.printBackground"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "dd-print-background", name: "printBackground", value: meta.printBackground, onChange: (printBackground) => patch({ printBackground }) }) }), _jsx(Field, { id: "dd-layout-header-footer", label: t("documentDesigner.settings.layoutHeaderFooter"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "dd-layout-header-footer", name: "layoutHeaderFooter", value: meta.layoutHeaderFooter, onChange: (layoutHeaderFooter) => patch({ layoutHeaderFooter }) }) })] }), _jsxs(SectionCard, { title: t("documentDesigner.settings.metadata"), children: [_jsx(Field, { id: "dd-meta-title", label: t("documentDesigner.settings.metaTitle"), children: _jsx(Input, { kind: "text", id: "dd-meta-title", name: "metaTitle", value: meta.title, onChange: (title) => patch({ title }) }) }), _jsx(Field, { id: "dd-meta-author", label: t("documentDesigner.settings.metaAuthor"), children: _jsx(Input, { kind: "text", id: "dd-meta-author", name: "metaAuthor", value: meta.author, onChange: (author) => patch({ author }) }) }), _jsx(Field, { id: "dd-meta-language", label: t("documentDesigner.settings.metaLanguage"), children: _jsx(Input, { kind: "text", id: "dd-meta-language", name: "metaLanguage", value: locale, onChange: () => undefined, disabled: true }) })] }), _jsx(SectionCard, { title: t("documentDesigner.settings.storage"), children: _jsx("p", { className: "text-xs text-muted-foreground", children: t("documentDesigner.settings.storageHint") }) }), _jsx(SectionCard, { title: t("documentDesigner.settings.versions"), children: _jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx("span", { children: `v${version.version}` }), _jsx("span", { className: "text-xs text-muted-foreground", children: version.modifiedAt?.slice(0, 10) ?? "" }), _jsx(StatusBadge, { tone: version.status === "active" ? "ok" : "warn", children: t(version.status === "active"
23
- ? "documentDesigner.status.active"
24
- : "documentDesigner.status.draft") })] }) })] }));
21
+ return (_jsxs("div", { "data-testid": "document-designer-settings", children: [_jsx(RailSection, { title: t("documentDesigner.settings.page"), testId: "pdf-editor-document", children: _jsxs(RailField, { children: [_jsx(Field, { id: "dd-layout", label: t("documentDesigner.settings.layout"), children: _jsx(Input, { kind: "select", id: "dd-layout", name: "layoutSlug", value: meta.layoutSlug === "" ? DEFAULT_LAYOUT_SLUG : meta.layoutSlug, onChange: (layoutSlug) => patch({ layoutSlug }), display: "dropdown", options: layouts.map((layout) => ({
22
+ value: layout.slug,
23
+ label: layoutOptionLabel(t, layout),
24
+ })) }) }), _jsx(Field, { id: "dd-file-name", label: t("documentDesigner.settings.fileName"), children: _jsx(Input, { kind: "text", id: "dd-file-name", name: "fileName", value: meta.fileName, onChange: (fileName) => patch({ fileName }), placeholder: "rechnung-{{number}}.pdf" }) }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)] gap-2", children: [_jsx(Field, { id: "dd-page-format", label: t("documentDesigner.settings.pageFormat"), children: _jsx(Input, { kind: "select", id: "dd-page-format", name: "pageFormat", value: meta.pageFormat, onChange: (pageFormat) => {
25
+ const next = PDF_PAGE_FORMATS.find((format) => format === pageFormat);
26
+ if (next !== undefined)
27
+ patch({ pageFormat: next });
28
+ }, display: "dropdown", options: PDF_PAGE_FORMATS.map((value) => ({ value, label: value })) }) }), _jsx(Field, { id: "dd-orientation", label: t("documentDesigner.settings.orientation"), children: _jsx(Input, { kind: "select", id: "dd-orientation", name: "orientation", value: meta.orientation, onChange: (orientation) => {
29
+ const next = PDF_ORIENTATIONS.find((candidate) => candidate === orientation);
30
+ if (next !== undefined)
31
+ patch({ orientation: next });
32
+ }, display: "dropdown", options: PDF_ORIENTATIONS.map((value) => ({
33
+ value,
34
+ label: t(`documentDesigner.settings.${value}`),
35
+ })) }) })] }), _jsxs("fieldset", { className: "m-0 flex min-w-0 flex-col gap-1 border-0 p-0", children: [_jsx("legend", { className: "p-0 text-sm font-medium", children: t("documentDesigner.settings.margins") }), _jsx("div", { className: "grid grid-cols-4 gap-1", children: MARGIN_SIDES.map((side) => (_jsx(Field, { id: `dd-margin-${side}`, label: t(SIDE_LABEL_KEYS[side]), hideLabel: true, children: _jsx(Input, { kind: "number", id: `dd-margin-${side}`, name: `margin-${side}`, value: meta.margins[side], onChange: (value) => patch({ margins: { ...meta.margins, [side]: value ?? "" } }), placeholder: "15" }) }, side))) })] }), _jsx(Field, { id: "dd-print-background", label: t("documentDesigner.settings.printBackground"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "dd-print-background", name: "printBackground", value: meta.printBackground, onChange: (printBackground) => patch({ printBackground }) }) }), _jsx(Field, { id: "dd-layout-header-footer", label: t("documentDesigner.settings.layoutHeaderFooter"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "dd-layout-header-footer", name: "layoutHeaderFooter", value: meta.layoutHeaderFooter, onChange: (layoutHeaderFooter) => patch({ layoutHeaderFooter }) }) })] }) }), _jsxs(RailSection, { title: t("documentDesigner.settings.metadata"), testId: "pdf-editor-metadata", children: [_jsxs(RailField, { children: [_jsx("div", { onFocusCapture: insert.markFocused("title"), "data-testid": "pdf-meta-title", children: _jsx(Field, { id: "dd-meta-title", label: t("documentDesigner.settings.metaTitle"), children: _jsx(PlaceholderLineEditor, { id: "dd-meta-title", value: meta.title, onChange: (title) => patch({ title }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, onEditorReady: insert.register("title"), "aria-label": t("documentDesigner.settings.metaTitle") }, `title:${draftKey}`) }) }), _jsx("div", { onFocusCapture: insert.markFocused("author"), "data-testid": "pdf-meta-author", children: _jsx(Field, { id: "dd-meta-author", label: t("documentDesigner.settings.metaAuthor"), children: _jsx(PlaceholderLineEditor, { id: "dd-meta-author", value: meta.author, onChange: (author) => patch({ author }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, onEditorReady: insert.register("author"), "aria-label": t("documentDesigner.settings.metaAuthor") }, `author:${draftKey}`) }) }), _jsxs("div", { className: "flex flex-col gap-1", "data-testid": "pdf-meta-language", children: [_jsx("span", { className: "text-sm font-medium", children: t("documentDesigner.settings.metaLanguage") }), _jsx(Text, { children: `${languageName(t, locale)} (${locale})` })] })] }), _jsx(RailHint, { children: t("documentDesigner.settings.metaLanguageHint") })] }), _jsx(RailSection, { title: t("documentDesigner.settings.storage"), testId: "pdf-editor-storage", children: _jsx(RailHint, { children: t("documentDesigner.settings.storageHint") }) }), _jsx(VersionsSection, { rowId: version.id, currentVersion: version.version, currentStatus: version.status })] }));
25
36
  }
@@ -3,5 +3,12 @@ import type { OverviewRowView } from "./overview-row.js";
3
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
+ export declare function draftBesideActiveSentence(t: Translate, row: {
7
+ readonly status: string;
8
+ readonly version: number;
9
+ readonly active?: {
10
+ readonly version: number;
11
+ } | null;
12
+ }): string | null;
6
13
  export declare function asRowStatus(status: string): OverviewRowView["status"];
7
14
  export {};
@@ -13,6 +13,14 @@ export function statusLabel(t, status, version) {
13
13
  ? "documentDesigner.status.activeVersion"
14
14
  : "documentDesigner.status.draftVersion", { version });
15
15
  }
16
+ export function draftBesideActiveSentence(t, row) {
17
+ if (asRowStatus(row.status) !== "draft" || row.active == null)
18
+ return null;
19
+ return t("documentDesigner.origin.draftBesideActive", {
20
+ active: row.active.version,
21
+ draft: row.version,
22
+ });
23
+ }
16
24
  export function asRowStatus(status) {
17
25
  return status === "active" || status === "archived" ? status : "draft";
18
26
  }
@@ -1,4 +1,5 @@
1
1
  import type { FieldIssue } from "@cosmicdrift/kumiko-headless";
2
+ import type { TemplateDiff } from "./layout-types.js";
2
3
  export type TemplateScope = "system" | "tenant";
3
4
  export type TemplateStatus = "draft" | "active" | "archived";
4
5
  export declare const DOCUMENT_PDF_KIND: "document-pdf";
@@ -31,12 +32,22 @@ export type TemplateDetail = {
31
32
  readonly parentTemplateId: string | null;
32
33
  readonly status: TemplateStatus;
33
34
  readonly modifiedAt: string | null;
35
+ readonly active?: ServedVersionView | null;
36
+ };
37
+ /** The version the resolver serves while a draft is open beside it. */
38
+ export type ServedVersionView = {
39
+ readonly version: number;
40
+ readonly since: string;
34
41
  };
35
42
  export type VariableType = "text" | "link" | "money" | "date" | "list";
36
43
  export type VariableInfo = {
37
44
  readonly type: VariableType;
38
45
  readonly label?: string;
39
46
  readonly required?: boolean;
47
+ readonly item?: Readonly<Record<string, {
48
+ readonly type: VariableType;
49
+ readonly label?: string;
50
+ }>>;
40
51
  };
41
52
  export type ValidationWarning = {
42
53
  readonly code: string;
@@ -82,10 +93,22 @@ export type PdfPreviewResult = {
82
93
  readonly sizeBytes: number;
83
94
  readonly durationMs: number;
84
95
  };
96
+ export type TenantOverrideRow = {
97
+ readonly tenantId: string;
98
+ readonly tenantName: string;
99
+ readonly templateId: string;
100
+ readonly locale: string;
101
+ readonly status: "active" | "draft";
102
+ readonly version: number;
103
+ readonly changedAt: string;
104
+ readonly changedBy: string | null;
105
+ };
85
106
  export type TenantOverrides = {
86
107
  readonly activeOverrides: number;
87
108
  readonly draftOverrides: number;
88
109
  readonly totalTenants: number | null;
110
+ readonly rows: readonly TenantOverrideRow[];
111
+ readonly diff: TemplateDiff | null;
89
112
  };
90
113
  export type SaveResult = {
91
114
  readonly id: string;
@@ -1,5 +1,5 @@
1
1
  import type { DocumentEditorHandle } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
2
- export type InsertField = "subject" | "previewText" | "body";
2
+ export type InsertField = "subject" | "previewText" | "body" | "title" | "author";
3
3
  export type InsertTargets = {
4
4
  readonly register: (field: InsertField) => (handle: DocumentEditorHandle) => void;
5
5
  readonly markFocused: (field: InsertField) => () => void;
@@ -11,5 +11,4 @@ export declare function useMailPersistence(args: {
11
11
  readonly title: string;
12
12
  readonly isSystemAdmin: boolean;
13
13
  readonly row: TemplateRowView | null;
14
- readonly sender: unknown;
15
14
  }): MailPersistence;
@@ -21,7 +21,7 @@ export function useMailPersistence(args) {
21
21
  title: args.title,
22
22
  content: draft.body,
23
23
  contentFormat: "html",
24
- meta: buildMeta(draft, args.sender),
24
+ meta: buildMeta(draft),
25
25
  variableSchema: parseJsonRecord(args.row?.variableSchema),
26
26
  };
27
27
  const saved = args.isSystemAdmin
@@ -0,0 +1,5 @@
1
+ export type OwnAddress = {
2
+ readonly email: string | null;
3
+ readonly profileScreenId: string | null;
4
+ };
5
+ export declare function useOwnAddress(): OwnAddress;
@@ -0,0 +1,14 @@
1
+ // @runtime client
2
+ import { useAppFeatures, useQuery } from "@cosmicdrift/kumiko-renderer";
3
+ import { PROFILE_FEATURE_NAME, PROFILE_SCREEN_ID, USER_ME_QUERY_QN } from "../constants.js";
4
+ export function useOwnAddress() {
5
+ const features = useAppFeatures();
6
+ const me = useQuery(USER_ME_QUERY_QN, {});
7
+ const email = me.data?.email;
8
+ const hasProfileScreen = features.some((feature) => feature.featureName === PROFILE_FEATURE_NAME &&
9
+ feature.screens.some((screen) => screen.id === PROFILE_SCREEN_ID));
10
+ return {
11
+ email: typeof email === "string" && email !== "" ? email : null,
12
+ profileScreenId: hasProfileScreen ? PROFILE_SCREEN_ID : null,
13
+ };
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-document-designer",
3
- "version": "0.363.0",
3
+ "version": "0.364.0",
4
4
  "description": "Document designer for Kumiko: template overview, layout editor (mail layout, PDF letterhead) and the PDF template editor.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -19,13 +19,13 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@cosmicdrift/kumiko-bundled-features": "0.363.0",
23
- "@cosmicdrift/kumiko-framework": "0.363.0",
24
- "@cosmicdrift/kumiko-headless": "0.363.0",
25
- "@cosmicdrift/kumiko-renderer": "0.363.0",
26
- "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.363.0",
27
- "@cosmicdrift/kumiko-renderer-web": "0.363.0",
28
- "@cosmicdrift/kumiko-types": "0.363.0",
22
+ "@cosmicdrift/kumiko-bundled-features": "0.364.0",
23
+ "@cosmicdrift/kumiko-framework": "0.364.0",
24
+ "@cosmicdrift/kumiko-headless": "0.364.0",
25
+ "@cosmicdrift/kumiko-renderer": "0.364.0",
26
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.364.0",
27
+ "@cosmicdrift/kumiko-renderer-web": "0.364.0",
28
+ "@cosmicdrift/kumiko-types": "0.364.0",
29
29
  "pdfjs-dist": "6.3.289",
30
30
  "react": "^19.2.6"
31
31
  },
package/src/changes.json CHANGED
@@ -1,4 +1,28 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "improvement",
5
+ "title": "Editors and overview show an open draft beside the active version",
6
+ "detail": "The editor header shows the served version (Active vN) next to the open draft (Draft vM), fed by the active field of find-by-id and layout-source; the template overview shows the draftVersion badge under the status of an active row."
7
+ },
8
+ {
9
+ "version": "0.364.0",
10
+ "type": "improvement",
11
+ "title": "Document designer editors follow the approved board",
12
+ "detail": "Mail-layout templates accept appearance meta (width, font, outer background, accent line, button style) that the mail renderer applies, and the layout-source query returns it. The layout editor uses the shared editor shell with one paper and one shared toolbar (`DocumentContentEditor` gains `toolbarHost`). Mail and PDF editors share frame, info bar, rails and footer.\nThe unused `sender` key of mail template meta is gone from the editor; stored rows that still carry it are read without it, so they stay editable.\nInvalid template meta now reports per-field messages (`templateResolver.errors.template.meta.<field>.required|invalid`) instead of the parser text; the editors show them at the field. The `tenant-overrides` query additionally returns one row per customising tenant (name, status, version, changed at/by) and, with `diffTenantId`, the diff to the system version. A language without a version shows an empty state in the mail editor instead of a disabled editor.\nThe `compileDocumentBody` option `resourceImageMaxWidthPx` caps resource images in mails (180 px). The `send-test` command takes `prefixSubject` (default true) to skip the `[TEST]` subject prefix, the template preview returns `mailLayout` (slug, scope, version of the layout used), and `RenderedMail` carries the layout ref."
13
+ },
14
+ {
15
+ "version": "0.364.0",
16
+ "type": "fix",
17
+ "title": "PDF editor keeps unset margins unset, frees old PDF previews, and shows reset and diff errors",
18
+ "detail": "Setting one margin no longer stores the other three as 15 mm, so the layout's header and footer margins still apply to them. The PDF preview no longer re-renders pages or stacks text layers on re-render and destroys the previous document. A failed reset to the system version shows an error banner in the PDF and mail editors, and a failing diff query shows an error state with retry instead of loading forever. The canvas reserves the footer margin like the renderer. Page formats, orientations and default margins now come from template-resolver (new exports PDF_ORIENTATIONS, PDF_DEFAULT_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PdfOrientation)."
19
+ },
20
+ {
21
+ "version": "0.364.0",
22
+ "type": "fix",
23
+ "title": "Layout editor shows the real number of mails or PDFs using a layout",
24
+ "detail": "The editor asks the overview for the users of one layout (kind and layoutSlug filter) and takes the count from total, so a tenant with more than 200 templates no longer sees a too low usage number."
25
+ },
2
26
  {
3
27
  "version": "0.363.0",
4
28
  "type": "improvement",
@@ -1,23 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { TemplateRowView } from "./mail-types.js";
3
- export type EditorMode = "edit" | "preview";
4
- export type HeaderActions = {
5
- readonly busy: boolean;
6
- readonly canSave: boolean;
7
- readonly canPublish: boolean;
8
- readonly onSave: () => void;
9
- readonly onPublish: () => void;
10
- };
11
- export declare function MailHeader({ title, row, locales, locale, mode, testPending, actions, onBack, onLocale, onMode, onTestMail, }: {
12
- readonly title: string;
13
- readonly row: TemplateRowView | null;
14
- readonly locales: readonly string[];
15
- readonly locale: string;
16
- readonly mode: EditorMode;
17
- readonly testPending: boolean;
18
- readonly actions: HeaderActions;
19
- readonly onBack: () => void;
20
- readonly onLocale: (locale: string) => void;
21
- readonly onMode: (mode: EditorMode) => void;
22
- readonly onTestMail: () => void;
23
- }): ReactNode;
@@ -1,16 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
- import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
- import { ModeSwitch, StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
5
- import { asRowStatus, statusLabel, statusTone } from "./status-label.js";
6
- export function MailHeader({ title, row, locales, locale, mode, testPending, actions, onBack, onLocale, onMode, onTestMail, }) {
7
- const t = useTranslation();
8
- const { Button, Heading, Link, Text } = usePrimitives();
9
- const status = row === null ? null : asRowStatus(row.status);
10
- return (_jsxs("div", { className: "flex flex-col gap-3", "data-testid": "mail-editor-header", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(Link, { href: "#", onPress: onBack, testId: "document-designer-back", children: t("documentDesigner.editor.crumb") }), _jsx(Text, { variant: "muted", children: "\u203A" }), _jsx(Heading, { variant: "page", testId: "document-designer-editor-title", children: title }), row !== null && status !== null && (_jsxs(_Fragment, { children: [_jsx(StatusBadge, { tone: statusTone(status), testId: "document-designer-editor-status", children: statusLabel(t, status, row.version) }), _jsx(StatusBadge, { tone: row.scope === "tenant" ? "accent" : "muted", testId: "document-designer-editor-origin", children: t(row.scope === "tenant"
11
- ? "documentDesigner.origin.tenant"
12
- : "documentDesigner.origin.system") })] })), _jsx("div", { className: "grow" }), _jsx(ModeSwitch, { value: locale, onChange: onLocale, ariaLabel: t("documentDesigner.mail.header.localeSwitch"), testId: "mail-locale-switch", options: locales.map((code) => ({ value: code, label: code })) }), _jsx(ModeSwitch, { value: mode, onChange: onMode, ariaLabel: t("documentDesigner.mail.header.modeSwitch"), testId: "mail-mode-switch", options: [
13
- { value: "edit", label: t("documentDesigner.mail.header.edit") },
14
- { value: "preview", label: t("documentDesigner.mail.header.preview") },
15
- ] })] }), _jsxs("div", { className: "flex items-center gap-3", "data-testid": "mail-editor-header-actions", children: [_jsx(Button, { variant: "secondary", disabled: testPending, loading: testPending, onClick: onTestMail, testId: "mail-test-mail-header", children: t("documentDesigner.mail.header.testMail") }), _jsx(Button, { variant: "secondary", disabled: !actions.canSave, loading: actions.busy, onClick: actions.onSave, testId: "mail-header-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { disabled: !actions.canPublish, onClick: actions.onPublish, testId: "mail-header-publish", children: t("documentDesigner.editor.publish") })] })] }));
16
- }
@@ -1,21 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { VariableInfo } from "./types.js";
3
- export type SystemPlaceholderRow = {
4
- readonly name: string;
5
- readonly label: string;
6
- readonly status: "used" | "layout" | "unused";
7
- };
8
- export type PlaceholderPanelProps = {
9
- readonly variables: Readonly<Record<string, VariableInfo>>;
10
- readonly used: readonly string[];
11
- readonly missingRequired: readonly string[];
12
- readonly systemRows: readonly SystemPlaceholderRow[];
13
- readonly resourceKeys: readonly string[];
14
- readonly canUpload: boolean;
15
- readonly layoutName: string;
16
- readonly onInsert: (name: string) => void;
17
- readonly onInsertResource: (key: string) => void;
18
- readonly onUpload: (file: File) => Promise<void>;
19
- };
20
- export declare function visibleSystemPlaceholders(names: readonly string[]): readonly string[];
21
- export declare function PlaceholderPanel(props: PlaceholderPanelProps): ReactNode;
@@ -1,35 +0,0 @@
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 { cn, SectionCard, UploadZone } from "@cosmicdrift/kumiko-renderer-web";
5
- const TYPE_GLYPHS = {
6
- text: "T",
7
- money: "€",
8
- date: "D",
9
- link: "↗",
10
- list: "≡",
11
- };
12
- // unsubscribeUrl belongs to mail; the PDF designer never offers it.
13
- const HIDDEN_SYSTEM_PLACEHOLDERS = new Set(["unsubscribeUrl"]);
14
- export function visibleSystemPlaceholders(names) {
15
- return names.filter((name) => !HIDDEN_SYSTEM_PLACEHOLDERS.has(name));
16
- }
17
- export function PlaceholderPanel(props) {
18
- const t = useTranslation();
19
- const { Button } = usePrimitives();
20
- const names = Object.keys(props.variables);
21
- return (_jsxs("div", { className: "flex flex-col gap-4", "data-testid": "document-designer-placeholders", children: [_jsx(SectionCard, { title: t("documentDesigner.placeholders.title"), children: _jsx("ul", { className: "m-0 flex list-none flex-col gap-1 p-0", children: names.map((name) => {
22
- const info = props.variables[name];
23
- if (info === undefined)
24
- return null;
25
- const missing = props.missingRequired.includes(name);
26
- const isUsed = props.used.includes(name);
27
- return (_jsx("li", { children: _jsxs("button", { type: "button", "data-testid": `placeholder-${name}`, onClick: () => props.onInsert(name), className: cn("flex w-full items-center gap-2 rounded-md border px-2 py-1 text-left text-sm", missing
28
- ? "border-status-bad bg-status-bad-surface"
29
- : "border-border bg-background hover:bg-muted"), children: [_jsx("span", { className: "w-4 text-center text-xs text-muted-foreground", children: TYPE_GLYPHS[info.type] }), _jsxs("span", { className: "min-w-0 flex-1 truncate", children: [info.label ?? name, info.required === true ? " *" : ""] }), _jsx("span", { className: cn("text-xs", missing ? "font-medium text-status-bad" : "text-muted-foreground"), children: missing ? t("documentDesigner.placeholders.missing") : isUsed ? "✓" : "–" })] }) }, name));
30
- }) }) }), _jsx(SectionCard, { title: t("documentDesigner.placeholders.system"), children: _jsx("ul", { className: "m-0 flex list-none flex-col gap-1 p-0", children: props.systemRows.map((row) => (_jsx("li", { children: _jsxs("button", { type: "button", "data-testid": `system-placeholder-${row.name}`, onClick: () => props.onInsert(row.name), className: "flex w-full items-center gap-2 rounded-md border border-primary/30 bg-primary/10 px-2 py-1 text-left text-sm text-primary", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: row.label }), _jsx("span", { className: "text-xs", children: row.status === "used"
31
- ? "✓"
32
- : row.status === "layout"
33
- ? t("documentDesigner.placeholders.fromLayout")
34
- : "–" })] }) }, row.name))) }) }), _jsxs(SectionCard, { title: t("documentDesigner.resources.title"), children: [props.resourceKeys.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: t("documentDesigner.resources.empty") })) : (_jsx("ul", { className: "m-0 flex list-none flex-col gap-1 p-0 font-mono text-xs", children: props.resourceKeys.map((key) => (_jsxs("li", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate", children: key }), _jsx(Button, { variant: "link", size: "sm", onClick: () => props.onInsertResource(key), ariaLabel: t("documentDesigner.resources.insertNamed", { key }), children: t("documentDesigner.resources.insert") })] }, key))) })), props.canUpload ? (_jsx(UploadZone, { multiple: false, accept: ["image/png", "image/jpeg", "image/gif", "image/webp"], title: t("documentDesigner.resources.upload"), onUpload: props.onUpload, testId: "document-designer-upload" })) : (_jsx(Button, { variant: "secondary", size: "sm", disabled: true, children: t("documentDesigner.resources.upload") })), !props.canUpload && (_jsx("p", { className: "text-xs text-muted-foreground", children: t("documentDesigner.resources.saveFirst") }))] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t("documentDesigner.placeholders.layoutHint", { layout: props.layoutName }) })] }));
35
- }