@cosmicdrift/kumiko-document-designer 0.371.1 → 0.372.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/cells.js CHANGED
@@ -1,16 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
- import { toInstant } from "@cosmicdrift/kumiko-headless";
4
+ import { EMPTY_VALUE_PLACEHOLDER, toInstant } from "@cosmicdrift/kumiko-headless";
5
5
  import { formatWhen, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
6
6
  import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
7
7
  import { parseOverviewRow } from "./overview-row.js";
8
8
  import { statusLabel, statusTone } from "./status-label.js";
9
9
  import { templateTitle, usedByLabel } from "./used-by-label.js";
10
- const EMPTY_MARK = "–";
11
10
  function withRow(row, render) {
12
11
  const parsed = parseOverviewRow(row);
13
- return parsed === null ? EMPTY_MARK : render(parsed);
12
+ return parsed === null ? EMPTY_VALUE_PLACEHOLDER : render(parsed);
14
13
  }
15
14
  export function TitleCell({ row }) {
16
15
  const t = useTranslation();
@@ -34,7 +33,7 @@ export function createLocalesCell(appLocales) {
34
33
  return function LocalesCell({ row }) {
35
34
  return withRow(row, (r) => {
36
35
  if (r.kind === "mail-layout" || r.kind === "pdf-layout")
37
- return EMPTY_MARK;
36
+ return EMPTY_VALUE_PLACEHOLDER;
38
37
  const present = new Set(r.locales);
39
38
  const shown = [...appLocales, ...r.locales.filter((l) => !appLocales.includes(l))];
40
39
  return (_jsx("span", { className: "inline-flex gap-1", children: shown.map((locale) => (_jsx("span", { "data-testid": `document-designer-locale-${locale}`, "data-present": present.has(locale), className: present.has(locale)
@@ -65,7 +64,7 @@ function formatModifiedDate(value) {
65
64
  }
66
65
  }
67
66
  export function ModifiedCell({ row }) {
68
- return withRow(row, (r) => r.modifiedAt === "" ? (EMPTY_MARK) : (_jsx("span", { className: "whitespace-nowrap", title: formatWhen(r.modifiedAt), children: formatModifiedDate(r.modifiedAt) })));
67
+ return withRow(row, (r) => r.modifiedAt === "" ? (EMPTY_VALUE_PLACEHOLDER) : (_jsx("span", { className: "whitespace-nowrap", title: formatWhen(r.modifiedAt), children: formatModifiedDate(r.modifiedAt) })));
69
68
  }
70
69
  function OverriddenByCount({ slug, kind, }) {
71
70
  const t = useTranslation();
@@ -74,7 +73,7 @@ function OverriddenByCount({ slug, kind, }) {
74
73
  kind,
75
74
  });
76
75
  if (data === null)
77
- return EMPTY_MARK;
76
+ return EMPTY_VALUE_PLACEHOLDER;
78
77
  const label = data.totalTenants === null
79
78
  ? t("documentDesigner.overriddenBy.tenants", { active: data.activeOverrides })
80
79
  : t("documentDesigner.overriddenBy.ofTotal", {
@@ -84,5 +83,5 @@ function OverriddenByCount({ slug, kind, }) {
84
83
  return _jsx("span", { className: "whitespace-nowrap", children: label });
85
84
  }
86
85
  export function OverriddenByCell({ row }) {
87
- return withRow(row, (r) => r.origin === "system" ? _jsx(OverriddenByCount, { slug: r.slug, kind: r.kind }) : EMPTY_MARK);
86
+ return withRow(row, (r) => r.origin === "system" ? (_jsx(OverriddenByCount, { slug: r.slug, kind: r.kind })) : (EMPTY_VALUE_PLACEHOLDER));
88
87
  }
package/dist/web/i18n.js CHANGED
@@ -43,6 +43,7 @@ export const documentDesignerTranslations = {
43
43
  "documentDesigner.resources.title": "Resources",
44
44
  "documentDesigner.resources.upload": "+ Upload",
45
45
  "documentDesigner.settings.fileName": "File name",
46
+ "documentDesigner.settings.fileNamePlaceholder": "invoice-{{number}}.pdf",
46
47
  "documentDesigner.settings.landscape": "Landscape",
47
48
  "documentDesigner.settings.layout": "Layout",
48
49
  "documentDesigner.settings.layoutHeaderFooter": "Header and footer from layout",
@@ -115,6 +116,7 @@ export const documentDesignerTranslations = {
115
116
  "documentDesigner.resources.title": "Ressourcen",
116
117
  "documentDesigner.resources.upload": "+ Hochladen",
117
118
  "documentDesigner.settings.fileName": "Dateiname",
119
+ "documentDesigner.settings.fileNamePlaceholder": "rechnung-{{number}}.pdf",
118
120
  "documentDesigner.settings.landscape": "Querformat",
119
121
  "documentDesigner.settings.layout": "Layout",
120
122
  "documentDesigner.settings.layoutHeaderFooter": "Kopf- und Fußzeile aus Layout",
@@ -187,6 +189,7 @@ export const documentDesignerTranslations = {
187
189
  "documentDesigner.resources.title": "Recursos",
188
190
  "documentDesigner.resources.upload": "+ Subir",
189
191
  "documentDesigner.settings.fileName": "Nombre del archivo",
192
+ "documentDesigner.settings.fileNamePlaceholder": "factura-{{number}}.pdf",
190
193
  "documentDesigner.settings.landscape": "Horizontal",
191
194
  "documentDesigner.settings.layout": "Diseño",
192
195
  "documentDesigner.settings.layoutHeaderFooter": "Encabezado y pie de página del diseño",
@@ -5,6 +5,7 @@ export declare const LAYOUT_OWNED_PLACEHOLDERS: ReadonlySet<string>;
5
5
  export declare const SYSTEM_PLACEHOLDER_TYPE: Readonly<Record<string, PlaceholderType>>;
6
6
  type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
7
7
  export declare function systemPlaceholderLabel(t: Translate, name: string): string;
8
+ export declare function labelPlaceholders(line: string, variableLabels: Readonly<Record<string, string>>): string;
8
9
  export declare function systemLabelMap(t: Translate, names: readonly string[]): Readonly<Record<string, string>>;
9
10
  export declare function parseJsonRecord(value: unknown): Readonly<Record<string, unknown>>;
10
11
  export declare function loadedMailOf(row: TemplateRowView | null): MailDraft;
@@ -15,5 +16,9 @@ export declare function uniqueResourceKey(source: string, taken: readonly string
15
16
  export declare function lastSlugSegment(slug: string): string;
16
17
  export declare function formatKilobytes(bytes: number): string;
17
18
  export declare function variableLabelMap(t: Translate, variables: Readonly<Record<string, MailVariable>>): Record<string, string>;
18
- export declare function displayValue(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, value: unknown): string;
19
+ export type SampleValueFormat = {
20
+ readonly locale: string;
21
+ readonly tenantCurrency: string | undefined;
22
+ };
23
+ export declare function displayValue(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, value: unknown, type: PlaceholderType | undefined, format: SampleValueFormat): string;
19
24
  export {};
@@ -1,3 +1,5 @@
1
+ // @runtime client
2
+ import { DOCUMENT_FALLBACK_CURRENCY, formatDocumentDate, formatDocumentMoney, formatDocumentPercent, } from "@cosmicdrift/kumiko-headless";
1
3
  import { variableLabelText } from "./used-by-label.js";
2
4
  export const EMPTY_DRAFT = {
3
5
  subject: "",
@@ -33,6 +35,13 @@ export function systemPlaceholderLabel(t, name) {
33
35
  const translated = t(key);
34
36
  return translated === key ? name : translated;
35
37
  }
38
+ // For places that show a stored {{name}} line as plain text instead of chips.
39
+ export function labelPlaceholders(line, variableLabels) {
40
+ return line.replace(/\{\{([^{}]*)\}\}/g, (_token, rawName) => {
41
+ const name = rawName.trim();
42
+ return Object.hasOwn(variableLabels, name) ? (variableLabels[name] ?? name) : name;
43
+ });
44
+ }
36
45
  // Chips in the editors show these instead of the raw system names.
37
46
  export function systemLabelMap(t, names) {
38
47
  return Object.fromEntries(names.map((name) => [name, systemPlaceholderLabel(t, name)]));
@@ -156,14 +165,19 @@ export function variableLabelMap(t, variables) {
156
165
  }
157
166
  return labels;
158
167
  }
159
- export function displayValue(t, value) {
168
+ export function displayValue(t, value, type, format) {
160
169
  if (Array.isArray(value))
161
170
  return t("documentDesigner.mail.preview.entries", { count: value.length });
162
- if (isRecord(value)) {
163
- const { amount, currency } = value;
164
- if (typeof amount === "number" && typeof currency === "string")
165
- return `${amount} ${currency}`;
166
- return JSON.stringify(value);
171
+ if (type === "money") {
172
+ if (typeof value === "number" && format.tenantCurrency === undefined)
173
+ return String(value);
174
+ return formatDocumentMoney(value, format.tenantCurrency ?? DOCUMENT_FALLBACK_CURRENCY, format.locale);
167
175
  }
176
+ if (type === "date")
177
+ return formatDocumentDate(value, format.locale);
178
+ if (type === "percent")
179
+ return formatDocumentPercent(value, format.locale);
180
+ if (isRecord(value))
181
+ return JSON.stringify(value);
168
182
  return String(value ?? "");
169
183
  }
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
4
5
  import { dispatcherErrorText, formatWhen, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
6
  import { useState } from "react";
6
7
  import { originOf } from "./editor-header.js";
@@ -18,7 +19,7 @@ function statusSentence(t, row) {
18
19
  return besideActive;
19
20
  if (row.status === "active") {
20
21
  return t("documentDesigner.mail.origin.activeSince", {
21
- when: row.modifiedAt === null ? "–" : formatWhen(row.modifiedAt),
22
+ when: row.modifiedAt === null ? EMPTY_VALUE_PLACEHOLDER : formatWhen(row.modifiedAt),
22
23
  });
23
24
  }
24
25
  return t(row.status === "archived"
@@ -1,30 +1,54 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
- import { useNav, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
3
+ import { ConfigQueries } from "@cosmicdrift/kumiko-bundled-features/config/web";
4
+ import { DOCUMENT_FALLBACK_CURRENCY } from "@cosmicdrift/kumiko-headless";
5
+ import { useNav, usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
6
+ import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
4
7
  import { useState } from "react";
5
8
  import { displayValue } from "./mail-draft.js";
6
9
  import { usePreviewRecordPicker } from "./preview-record-picker.js";
7
10
  import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
8
11
  import { variableLabelText } from "./used-by-label.js";
9
12
  const noop = () => undefined;
10
- // Check params list names as arrays; the message templates expect one string.
11
- function checkText(t, check) {
13
+ // Check params list placeholder names as arrays; the message templates expect one string, and
14
+ // the editor shows each placeholder by its label.
15
+ function checkText(t, check, validation) {
16
+ const labelOf = (name) => typeof name === "string"
17
+ ? variableLabelText(t, name, validation?.variables[name]?.label)
18
+ : String(name);
12
19
  const params = Object.fromEntries(Object.entries(check.params ?? {}).map(([name, value]) => [
13
20
  name,
14
- Array.isArray(value) ? value.join(", ") : value,
21
+ Array.isArray(value) ? value.map(labelOf).join(", ") : value,
15
22
  ]));
16
23
  const translated = t(check.i18nKey, params);
17
24
  return translated === check.i18nKey ? check.code : translated;
18
25
  }
19
- function SampleValues({ preview, validation, picked, }) {
26
+ // Same source and fallback as the renderer's readDocumentFormat, so a bare amount shows
27
+ // the currency the rendered mail prints.
28
+ function useTenantCurrency() {
29
+ const { data, error } = useQuery(ConfigQueries.values, {
30
+ keys: [TENANT_CURRENCY_CONFIG_KEY],
31
+ });
32
+ if (error)
33
+ return undefined;
34
+ if (data === null)
35
+ return undefined;
36
+ const value = data[TENANT_CURRENCY_CONFIG_KEY]?.value;
37
+ return typeof value === "string" && value !== "" ? value : DOCUMENT_FALLBACK_CURRENCY;
38
+ }
39
+ function SampleValues({ preview, validation, picked, locale, }) {
20
40
  const t = useTranslation();
41
+ const tenantCurrency = useTenantCurrency();
21
42
  const values = { ...(preview?.sampleValues ?? {}), ...(picked ?? {}) };
22
43
  const names = Object.keys(validation?.variables ?? values);
23
44
  return (_jsx(_Fragment, { children: names.map((name) => {
24
45
  const missing = validation?.missingRequired.includes(name) === true;
25
46
  return (_jsx(RailRow, { testId: `mail-sample-${name}`, label: variableLabelText(t, name, validation?.variables[name]?.label), nameHint: name, bad: missing, trailing: missing
26
47
  ? t("documentDesigner.mail.preview.missingInContent")
27
- : displayValue(t, values[name]) }, name));
48
+ : displayValue(t, values[name], validation?.variables[name]?.type, {
49
+ locale,
50
+ tenantCurrency,
51
+ }) }, name));
28
52
  }) }));
29
53
  }
30
54
  function RecordPickerLink({ slug, locale, onPick, }) {
@@ -54,5 +78,5 @@ export function PreviewSide({ preview, validation, slug, locale, own, testMail,
54
78
  const { Button, Field, Input, Text } = usePrimitives();
55
79
  const hasAttachments = preview?.attachments.some((a) => a.kind !== "inline") === true;
56
80
  const [picked, setPicked] = useState(null);
57
- return (_jsxs("div", { "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: setPicked }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsx(TestMailRecipient, { own: own }), _jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-prefix", label: t("documentDesigner.mail.testMail.prefixSubject"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-prefix", name: "mail-test-prefix", value: testMail.prefixSubject, onChange: testMail.onPrefixSubject }) }), hasAttachments ? (_jsx(Field, { id: "mail-test-attachments", label: t("documentDesigner.mail.testMail.attachments"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-attachments", name: "mail-test-attachments", value: testMail.includeAttachments, onChange: testMail.onIncludeAttachments }) })) : (_jsx(Text, { variant: "muted", testId: "mail-test-no-attachments", children: t("documentDesigner.mail.testMail.noAttachments") })), _jsx(Button, { variant: "secondary", width: "full", disabled: testMail.pending, loading: testMail.pending, onClick: testMail.onSend, testId: "mail-test-send", children: t("documentDesigner.mail.testMail.send") })] }), _jsx(RailHint, { children: t("documentDesigner.mail.testMail.hint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.preview.checks"), testId: "mail-preview-checks", children: [(preview?.checks ?? []).map((check) => (_jsx(RailRow, { testId: `mail-check-${check.code}`, symbol: check.ok ? "✓" : "!", label: checkText(t, check), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
81
+ return (_jsxs("div", { "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: setPicked }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked, locale: locale }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsx(TestMailRecipient, { own: own }), _jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-prefix", label: t("documentDesigner.mail.testMail.prefixSubject"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-prefix", name: "mail-test-prefix", value: testMail.prefixSubject, onChange: testMail.onPrefixSubject }) }), hasAttachments ? (_jsx(Field, { id: "mail-test-attachments", label: t("documentDesigner.mail.testMail.attachments"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-attachments", name: "mail-test-attachments", value: testMail.includeAttachments, onChange: testMail.onIncludeAttachments }) })) : (_jsx(Text, { variant: "muted", testId: "mail-test-no-attachments", children: t("documentDesigner.mail.testMail.noAttachments") })), _jsx(Button, { variant: "secondary", width: "full", disabled: testMail.pending, loading: testMail.pending, onClick: testMail.onSend, testId: "mail-test-send", children: t("documentDesigner.mail.testMail.send") })] }), _jsx(RailHint, { children: t("documentDesigner.mail.testMail.hint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.preview.checks"), testId: "mail-preview-checks", children: [(preview?.checks ?? []).map((check) => (_jsx(RailRow, { testId: `mail-check-${check.code}`, symbol: check.ok ? "✓" : "!", label: checkText(t, check, validation), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
58
82
  }
@@ -4,6 +4,7 @@ import { MAIL_TONES, PlaceholderLineEditor, TemplateResolverQueries, } from "@co
4
4
  import { useAppFeatures, useNav, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
5
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
6
6
  import { DEFAULT_LAYOUT_SLUG, TENANT_MAIL_SENDING_SCREEN_ID } from "../constants.js";
7
+ import { labelPlaceholders } from "./mail-draft.js";
7
8
  import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
8
9
  import { usageParts } from "./used-by-label.js";
9
10
  function useTemplateUsage(slug) {
@@ -93,7 +94,7 @@ const TONE_LABEL_KEYS = {
93
94
  };
94
95
  // The stored tone may be a placeholder that resolves per mail; the select keeps it as its own
95
96
  // option so opening the template never silently drops it.
96
- function toneOptions(t, current) {
97
+ function toneOptions(t, current, variableLabels) {
97
98
  const fixed = MAIL_TONES.map((tone) => ({ value: tone, label: t(TONE_LABEL_KEYS[tone]) }));
98
99
  return current === "" || isOneOf(MAIL_TONES, current)
99
100
  ? fixed
@@ -101,7 +102,9 @@ function toneOptions(t, current) {
101
102
  ...fixed,
102
103
  {
103
104
  value: current,
104
- label: t("documentDesigner.mail.chrome.toneFromPlaceholder", { value: current }),
105
+ label: t("documentDesigner.mail.chrome.toneFromPlaceholder", {
106
+ value: labelPlaceholders(current, variableLabels),
107
+ }),
105
108
  },
106
109
  ];
107
110
  }
@@ -110,7 +113,7 @@ export function ChromeSection({ draft, vocabulary, insert, onChange, }) {
110
113
  const { Field, Input } = usePrimitives();
111
114
  // "accent" is the renderer default, so it is stored as an empty value.
112
115
  const toneValue = draft.tone === "" ? "accent" : draft.tone;
113
- return (_jsxs(RailSection, { title: t("documentDesigner.mail.chrome.title"), testId: "mail-editor-chrome", children: [_jsx(RailField, { children: _jsx(Field, { id: "mail-header-chip", label: t("documentDesigner.mail.chrome.headerChip"), children: _jsx("div", { onFocusCapture: insert.markFocused("headerChip"), "data-testid": "mail-header-chip", children: _jsx(PlaceholderLineEditor, { id: "mail-header-chip", value: draft.headerChip, onChange: (headerChip) => onChange({ headerChip }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.headerChipPlaceholder"), onEditorReady: insert.register("headerChip"), "aria-label": t("documentDesigner.mail.chrome.headerChip") }) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-tone", label: t("documentDesigner.mail.chrome.tone"), children: _jsx(Input, { kind: "select", id: "mail-tone", name: "mail-tone", value: toneValue, display: "dropdown", onChange: (next) => onChange({ tone: next === "accent" ? "" : next }), options: toneOptions(t, draft.tone) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-footer-note", label: t("documentDesigner.mail.chrome.footerNote"), children: _jsx("div", { onFocusCapture: insert.markFocused("footerNote"), "data-testid": "mail-footer-note", children: _jsx(PlaceholderLineEditor, { id: "mail-footer-note", value: draft.footerNote, onChange: (footerNote) => onChange({ footerNote }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.footerNotePlaceholder"), onEditorReady: insert.register("footerNote"), "aria-label": t("documentDesigner.mail.chrome.footerNote") }) }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.chrome.hint") })] }));
116
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.chrome.title"), testId: "mail-editor-chrome", children: [_jsx(RailField, { children: _jsx(Field, { id: "mail-header-chip", label: t("documentDesigner.mail.chrome.headerChip"), children: _jsx("div", { onFocusCapture: insert.markFocused("headerChip"), "data-testid": "mail-header-chip", children: _jsx(PlaceholderLineEditor, { id: "mail-header-chip", value: draft.headerChip, onChange: (headerChip) => onChange({ headerChip }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.headerChipPlaceholder"), onEditorReady: insert.register("headerChip"), "aria-label": t("documentDesigner.mail.chrome.headerChip") }) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-tone", label: t("documentDesigner.mail.chrome.tone"), children: _jsx(Input, { kind: "select", id: "mail-tone", name: "mail-tone", value: toneValue, display: "dropdown", onChange: (next) => onChange({ tone: next === "accent" ? "" : next }), options: toneOptions(t, draft.tone, vocabulary.labels) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-footer-note", label: t("documentDesigner.mail.chrome.footerNote"), children: _jsx("div", { onFocusCapture: insert.markFocused("footerNote"), "data-testid": "mail-footer-note", children: _jsx(PlaceholderLineEditor, { id: "mail-footer-note", value: draft.footerNote, onChange: (footerNote) => onChange({ footerNote }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.footerNotePlaceholder"), onEditorReady: insert.register("footerNote"), "aria-label": t("documentDesigner.mail.chrome.footerNote") }) }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.chrome.hint") })] }));
114
117
  }
115
118
  export function UsageSection({ slug }) {
116
119
  const t = useTranslation();
@@ -1,4 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
2
3
  // @runtime client
3
4
  import { formatWhen, useAppFeatures, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
5
  import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
@@ -29,7 +30,7 @@ function VersionRows({ rowId, currentVersion, currentStatus, }) {
29
30
  return (_jsx(_Fragment, { children: versions.map((row) => (_jsx(RailRow, { testId: `mail-version-${row.version}`, label: t("documentDesigner.mail.versions.line", {
30
31
  version: row.version,
31
32
  when: formatWhen(row.createdAt),
32
- name: names.get(row.createdBy) ?? "–",
33
+ name: names.get(row.createdBy) ?? EMPTY_VALUE_PLACEHOLDER,
33
34
  }), trailing: row.version === currentVersion ? (_jsx(StatusBadge, { tone: statusTone(status), children: statusLabel(t, status, row.version) })) : undefined }, row.version))) }));
34
35
  }
35
36
  // Hidden when the audit feature is not mounted, so no failing query shows up as an error.
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
4
5
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
6
  import { EmptyState, ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
6
7
  import { useState } from "react";
@@ -19,7 +20,7 @@ const DIFF_TONE = {
19
20
  };
20
21
  function metaValueText(value) {
21
22
  if (value === undefined || value === null)
22
- return "–";
23
+ return EMPTY_VALUE_PLACEHOLDER;
23
24
  return typeof value === "string" ? value : JSON.stringify(value);
24
25
  }
25
26
  // "full" shows the tenant's version as it reads (everything but the removed system blocks).
package/dist/web/rail.js CHANGED
@@ -12,8 +12,13 @@ const TRAILING_TONE = {
12
12
  bad: "text-status-bad",
13
13
  muted: "text-muted-foreground",
14
14
  };
15
+ // The star rides on the last word so a wrapping label never leaves it alone on a line.
16
+ function RequiredLabel({ label }) {
17
+ const lastSpace = label.lastIndexOf(" ");
18
+ 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: " *" })] })] }));
19
+ }
15
20
  export function RailRow({ symbol, label, nameHint, 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"), title: nameHint !== undefined && nameHint !== label ? nameHint : undefined, 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 }))] }));
21
+ 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("min-w-0 grow break-words 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 truncate text-right text-xs", bad === true ? "text-status-bad" : TRAILING_TONE[trailingTone]), title: typeof trailing === "string" ? trailing : undefined, children: trailing }))] }));
17
22
  const rowClass = "flex min-h-10 min-w-0 grow items-center gap-2 px-3.5 text-left";
18
23
  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 })] }));
19
24
  }
@@ -21,7 +21,7 @@ export function SettingsPanel({ meta, onMetaChange, layouts, locale, version, vo
21
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
22
  value: layout.slug,
23
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) => {
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: t("documentDesigner.settings.fileNamePlaceholder") }) }), _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
25
  const next = PDF_PAGE_FORMATS.find((format) => format === pageFormat);
26
26
  if (next !== undefined)
27
27
  patch({ pageFormat: next });
@@ -1,3 +1,4 @@
1
+ import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
1
2
  // @runtime client
2
3
  import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
3
4
  // t() returns the key itself when no translation exists.
@@ -31,7 +32,6 @@ export function usageLabel(t, usage) {
31
32
  return usage.feature === "" ? notification : `${feature} · ${notification}`;
32
33
  }
33
34
  export function usedByLabel(t, row) {
34
- const emptyMark = "–";
35
35
  if (row.kind === "mail-layout" || row.kind === "pdf-layout") {
36
36
  if (row.slug === DEFAULT_LAYOUT_SLUG) {
37
37
  return t(row.kind === "mail-layout"
@@ -39,9 +39,9 @@ export function usedByLabel(t, row) {
39
39
  : "documentDesigner.usedBy.allPdfs");
40
40
  }
41
41
  return row.usedByCount === 0
42
- ? emptyMark
42
+ ? EMPTY_VALUE_PLACEHOLDER
43
43
  : t("documentDesigner.usedBy.templates", { count: row.usedByCount });
44
44
  }
45
45
  const first = row.usedBy[0];
46
- return first === undefined ? emptyMark : usageLabel(t, first);
46
+ return first === undefined ? EMPTY_VALUE_PLACEHOLDER : usageLabel(t, first);
47
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-document-designer",
3
- "version": "0.371.1",
3
+ "version": "0.372.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.371.1",
23
- "@cosmicdrift/kumiko-framework": "0.371.1",
24
- "@cosmicdrift/kumiko-headless": "0.371.1",
25
- "@cosmicdrift/kumiko-renderer": "0.371.1",
26
- "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.371.1",
27
- "@cosmicdrift/kumiko-renderer-web": "0.371.1",
28
- "@cosmicdrift/kumiko-types": "0.371.1",
22
+ "@cosmicdrift/kumiko-bundled-features": "0.372.0",
23
+ "@cosmicdrift/kumiko-framework": "0.372.0",
24
+ "@cosmicdrift/kumiko-headless": "0.372.0",
25
+ "@cosmicdrift/kumiko-renderer": "0.372.0",
26
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.372.0",
27
+ "@cosmicdrift/kumiko-renderer-web": "0.372.0",
28
+ "@cosmicdrift/kumiko-types": "0.372.0",
29
29
  "pdfjs-dist": "6.3.289",
30
30
  "react": "^19.2.6"
31
31
  },
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.372.0",
4
+ "type": "fix",
5
+ "title": "The designer names placeholders by their label everywhere",
6
+ "detail": "Placeholder chips in the mail subject, the header label, the footer note and the paper editor now show only the variable's label; the raw name stays available as a tooltip. The tone select reads \"From placeholder: <label>\" instead of \"{{tone}}\", and the validation notes in the preview list labels instead of names. The file-name hint in the settings is translated (de, en, es). Unknown placeholders and variables without a label still show their name."
7
+ },
8
+ {
9
+ "version": "0.372.0",
10
+ "type": "fix",
11
+ "title": "Sample values show formatted money and dates",
12
+ "detail": "The sample-values bar in the mail preview formats money, dates and percentages in the language of the selected tab and with the tenant currency, through the same helpers the mail and PDF renderers use (formatDocumentMoney, formatDocumentDate and formatDocumentPercent, now exported from kumiko-headless). An amount reads \"1.284,50 €\" instead of \"128450 EUR\". The required star in the placeholder list stays on the label's last word instead of wrapping onto its own line. config/web now exports ConfigQueries."
13
+ },
2
14
  {
3
15
  "version": "0.371.1",
4
16
  "type": "fix",