@cosmicdrift/kumiko-document-designer 0.363.0 → 0.365.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 +208 -50
- package/dist/web/cells.js +4 -3
- package/dist/web/editor-header.d.ts +37 -0
- package/dist/web/editor-header.js +43 -0
- package/dist/web/editor-locale.d.ts +1 -0
- package/dist/web/editor-locale.js +15 -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 +101 -52
- package/dist/web/layout-editor.js +14 -7
- 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 +45 -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 +60 -45
- package/dist/web/mail-editor.js +9 -14
- 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.d.ts +8 -0
- package/dist/web/mail-right-rail.js +65 -14
- package/dist/web/mail-types.d.ts +10 -1
- package/dist/web/mail-upload-modal.js +12 -2
- 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 +2 -1
- 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 +91 -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-debounced-value.d.ts +1 -0
- package/dist/web/use-debounced-value.js +11 -0
- package/dist/web/use-insert-targets.d.ts +2 -2
- package/dist/web/use-insert-targets.js +4 -1
- package/dist/web/use-mail-persistence.d.ts +2 -2
- package/dist/web/use-mail-persistence.js +4 -4
- package/dist/web/use-own-address.d.ts +5 -0
- package/dist/web/use-own-address.js +14 -0
- package/dist/web/used-by-label.d.ts +1 -0
- package/dist/web/used-by-label.js +11 -0
- package/package.json +8 -8
- package/src/changes.json +48 -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/i18n.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
export const documentDesignerTranslations = {
|
|
2
2
|
en: {
|
|
3
|
-
"documentDesigner.actions.discard": "Discard changes",
|
|
4
3
|
"documentDesigner.actions.generatePdf": "Generate PDF",
|
|
5
|
-
"documentDesigner.actions.publish": "Publish",
|
|
6
|
-
"documentDesigner.actions.save": "Save",
|
|
7
|
-
"documentDesigner.breadcrumb.platform": "Platform",
|
|
8
|
-
"documentDesigner.breadcrumb.templates": "Templates",
|
|
9
4
|
"documentDesigner.canvas.caption": 'Page 1 · {format} · The preview shows page breaks only roughly; "Generate PDF" is authoritative.',
|
|
10
5
|
"documentDesigner.canvas.fromLayout": 'from layout "{layout}"',
|
|
11
6
|
"documentDesigner.error.resourceTooLarge": "The image is too large. Please choose a smaller file.",
|
|
@@ -16,43 +11,40 @@ export const documentDesignerTranslations = {
|
|
|
16
11
|
"documentDesigner.localeDialog.discard": "Discard changes",
|
|
17
12
|
"documentDesigner.localeDialog.save": "Save and switch",
|
|
18
13
|
"documentDesigner.localeDialog.title": "Unsaved changes",
|
|
19
|
-
"documentDesigner.mode.edit": "Edit",
|
|
20
|
-
"documentDesigner.mode.label": "View",
|
|
21
|
-
"documentDesigner.mode.preview": "Preview",
|
|
22
14
|
"documentDesigner.notFound": 'Template "{slug}" was not found.',
|
|
23
|
-
"documentDesigner.origin.system": "System template",
|
|
24
|
-
"documentDesigner.origin.tenant": "Tenant customisation",
|
|
25
15
|
"documentDesigner.originBar.export": "Export as bundle",
|
|
26
16
|
"documentDesigner.originBar.noTotal": "System template for all tenants. {count} tenants have their own version and do not receive changes made here.",
|
|
27
17
|
"documentDesigner.originBar.withTotal": "System template for all tenants. {count} of {total} tenants have their own version and do not receive changes made here.",
|
|
28
18
|
"documentDesigner.originBar.showOverrides": "Show tenant customisations",
|
|
29
|
-
"documentDesigner.overrides.
|
|
30
|
-
"documentDesigner.overrides.
|
|
31
|
-
"documentDesigner.overrides.
|
|
19
|
+
"documentDesigner.overrides.action.diff": "Show difference",
|
|
20
|
+
"documentDesigner.overrides.action.open": "Open",
|
|
21
|
+
"documentDesigner.overrides.back": "Back to the list",
|
|
22
|
+
"documentDesigner.overrides.column.changed": "Changed",
|
|
23
|
+
"documentDesigner.overrides.column.status": "Status",
|
|
24
|
+
"documentDesigner.overrides.column.tenant": "Tenant",
|
|
25
|
+
"documentDesigner.overrides.column.version": "Version",
|
|
26
|
+
"documentDesigner.overrides.detail.version": "{tenant}, version {version}",
|
|
27
|
+
"documentDesigner.overrides.empty": "No tenant has customised this template. Changes here apply to everyone.",
|
|
28
|
+
"documentDesigner.overrides.status.active": "Customised",
|
|
29
|
+
"documentDesigner.overrides.status.draft": "Draft",
|
|
30
|
+
"documentDesigner.overrides.summary": "{count} tenants have their own version.",
|
|
32
31
|
"documentDesigner.overrides.title": "Tenant customisations",
|
|
33
|
-
"documentDesigner.overrides.total": "Tenants in total",
|
|
34
32
|
"documentDesigner.overrides.usedBy": "Used by",
|
|
35
33
|
"documentDesigner.overrides.usedByEmpty": "No registered notification uses this template directly.",
|
|
36
34
|
"documentDesigner.pdfUnavailable": "PDF generation is not available: {message}",
|
|
37
|
-
"documentDesigner.
|
|
35
|
+
"documentDesigner.pdfUnavailableNoReason": "PDF generation is not available.",
|
|
38
36
|
"documentDesigner.placeholders.layoutHint": 'Images travel with the PDF as data URIs. Header and footer come from the layout "{layout}" and are the same on every page.',
|
|
39
|
-
"documentDesigner.placeholders.missing": "missing",
|
|
40
|
-
"documentDesigner.placeholders.system": "System placeholders",
|
|
41
|
-
"documentDesigner.placeholders.title": "Placeholders",
|
|
42
37
|
"documentDesigner.preview.loading": "Generating PDF...",
|
|
43
38
|
"documentDesigner.preview.pageOf": "Page {page} of {pages}",
|
|
44
39
|
"documentDesigner.preview.renderFailed": "The PDF could not be displayed.",
|
|
45
40
|
"documentDesigner.preview.stale": "The preview is out of date. Generate the PDF again.",
|
|
46
41
|
"documentDesigner.resources.empty": "No images yet.",
|
|
47
|
-
"documentDesigner.resources.insert": "Insert",
|
|
48
|
-
"documentDesigner.resources.insertNamed": "Insert image {key}",
|
|
49
42
|
"documentDesigner.resources.saveFirst": "Save the template first, then you can upload images.",
|
|
50
43
|
"documentDesigner.resources.title": "Resources",
|
|
51
44
|
"documentDesigner.resources.upload": "+ Upload",
|
|
52
45
|
"documentDesigner.settings.fileName": "File name",
|
|
53
46
|
"documentDesigner.settings.landscape": "Landscape",
|
|
54
47
|
"documentDesigner.settings.layout": "Layout",
|
|
55
|
-
"documentDesigner.settings.layoutDefault": "Default",
|
|
56
48
|
"documentDesigner.settings.layoutHeaderFooter": "Header and footer from layout",
|
|
57
49
|
"documentDesigner.settings.marginBottom": "Bottom (mm)",
|
|
58
50
|
"documentDesigner.settings.marginLeft": "Left (mm)",
|
|
@@ -61,37 +53,28 @@ export const documentDesignerTranslations = {
|
|
|
61
53
|
"documentDesigner.settings.metaAuthor": "Author",
|
|
62
54
|
"documentDesigner.settings.metadata": "PDF metadata",
|
|
63
55
|
"documentDesigner.settings.metaLanguage": "Language",
|
|
56
|
+
"documentDesigner.settings.metaLanguageHint": "Follows the template language. Switch it with the language selector in the header.",
|
|
57
|
+
"documentDesigner.settings.margins": "Margins in mm (top, right, bottom, left)",
|
|
64
58
|
"documentDesigner.settings.metaTitle": "Title",
|
|
65
59
|
"documentDesigner.settings.orientation": "Orientation",
|
|
66
|
-
"documentDesigner.settings.page": "
|
|
60
|
+
"documentDesigner.settings.page": "Document",
|
|
67
61
|
"documentDesigner.settings.pageFormat": "Page format",
|
|
68
62
|
"documentDesigner.settings.portrait": "Portrait",
|
|
69
63
|
"documentDesigner.settings.printBackground": "Print backgrounds and colours",
|
|
70
64
|
"documentDesigner.settings.storage": "Storage",
|
|
71
65
|
"documentDesigner.settings.storageHint": "Every generated PDF carries the template id and version. Invoices stay verifiable even if the template changes later.",
|
|
72
|
-
"documentDesigner.settings.versions": "Versions",
|
|
73
66
|
"documentDesigner.status.active": "Active",
|
|
74
67
|
"documentDesigner.status.activeVersion": "Active v{version}",
|
|
75
68
|
"documentDesigner.status.draft": "Draft",
|
|
76
69
|
"documentDesigner.status.draftVersion": "Draft v{version}",
|
|
77
|
-
"documentDesigner.system.imprint": "Imprint",
|
|
78
|
-
"documentDesigner.system.page": "Page / Pages",
|
|
79
|
-
"documentDesigner.system.pages": "Pages",
|
|
80
|
-
"documentDesigner.system.signature": "Signature",
|
|
81
|
-
"documentDesigner.system.tenantName": "Tenant name",
|
|
82
70
|
"documentDesigner.validation.lastProbe": "Last test run: {pages} pages, {size}, {seconds} s",
|
|
83
71
|
"documentDesigner.validation.missingRequired": 'Required placeholder "{name}" is missing. Publishing is blocked.',
|
|
84
|
-
"documentDesigner.validation.ok": "All required placeholders are present.",
|
|
72
|
+
"documentDesigner.validation.ok": "All required placeholders are present. After publishing, new PDFs use this version.",
|
|
85
73
|
},
|
|
86
74
|
de: {
|
|
87
|
-
"documentDesigner.actions.discard": "Änderungen verwerfen",
|
|
88
75
|
"documentDesigner.actions.generatePdf": "PDF erzeugen",
|
|
89
|
-
"documentDesigner.actions.publish": "Veröffentlichen",
|
|
90
|
-
"documentDesigner.actions.save": "Speichern",
|
|
91
|
-
"documentDesigner.breadcrumb.platform": "Plattform",
|
|
92
|
-
"documentDesigner.breadcrumb.templates": "Vorlagen",
|
|
93
76
|
"documentDesigner.canvas.caption": 'Seite 1 · {format} · Die Vorschau zeigt Seitengrenzen nur ungefähr, verbindlich ist "PDF erzeugen".',
|
|
94
|
-
"documentDesigner.canvas.fromLayout":
|
|
77
|
+
"documentDesigner.canvas.fromLayout": "aus Layout „{layout}“",
|
|
95
78
|
"documentDesigner.error.resourceTooLarge": "Das Bild ist zu groß. Bitte eine kleinere Datei wählen.",
|
|
96
79
|
"documentDesigner.error.resourceUnsupportedImage": "Dieses Bildformat wird nicht unterstützt. Nutzen Sie PNG, JPEG, GIF oder WebP.",
|
|
97
80
|
"documentDesigner.locale": "Sprache",
|
|
@@ -100,43 +83,40 @@ export const documentDesignerTranslations = {
|
|
|
100
83
|
"documentDesigner.localeDialog.discard": "Änderungen verwerfen",
|
|
101
84
|
"documentDesigner.localeDialog.save": "Speichern und wechseln",
|
|
102
85
|
"documentDesigner.localeDialog.title": "Ungespeicherte Änderungen",
|
|
103
|
-
"documentDesigner.mode.edit": "Bearbeiten",
|
|
104
|
-
"documentDesigner.mode.label": "Ansicht",
|
|
105
|
-
"documentDesigner.mode.preview": "Vorschau",
|
|
106
86
|
"documentDesigner.notFound": 'Die Vorlage "{slug}" wurde nicht gefunden.',
|
|
107
|
-
"documentDesigner.origin.system": "System-Vorlage",
|
|
108
|
-
"documentDesigner.origin.tenant": "Mandanten-Anpassung",
|
|
109
87
|
"documentDesigner.originBar.export": "Als Bundle exportieren",
|
|
110
88
|
"documentDesigner.originBar.noTotal": "System-Vorlage für alle Mandanten. {count} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
|
|
111
89
|
"documentDesigner.originBar.withTotal": "System-Vorlage für alle Mandanten. {count} von {total} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
|
|
112
90
|
"documentDesigner.originBar.showOverrides": "Anpassungen der Mandanten anzeigen",
|
|
113
|
-
"documentDesigner.overrides.
|
|
114
|
-
"documentDesigner.overrides.
|
|
115
|
-
"documentDesigner.overrides.
|
|
91
|
+
"documentDesigner.overrides.action.diff": "Unterschied anzeigen",
|
|
92
|
+
"documentDesigner.overrides.action.open": "Öffnen",
|
|
93
|
+
"documentDesigner.overrides.back": "Zurück zur Liste",
|
|
94
|
+
"documentDesigner.overrides.column.changed": "Geändert",
|
|
95
|
+
"documentDesigner.overrides.column.status": "Status",
|
|
96
|
+
"documentDesigner.overrides.column.tenant": "Mandant",
|
|
97
|
+
"documentDesigner.overrides.column.version": "Version",
|
|
98
|
+
"documentDesigner.overrides.detail.version": "{tenant}, Version {version}",
|
|
99
|
+
"documentDesigner.overrides.empty": "Kein Mandant hat diese Vorlage angepasst. Änderungen hier gelten für alle.",
|
|
100
|
+
"documentDesigner.overrides.status.active": "Angepasst",
|
|
101
|
+
"documentDesigner.overrides.status.draft": "Entwurf",
|
|
102
|
+
"documentDesigner.overrides.summary": "{count} Mandanten haben eine eigene Fassung.",
|
|
116
103
|
"documentDesigner.overrides.title": "Anpassungen der Mandanten",
|
|
117
|
-
"documentDesigner.overrides.total": "Mandanten gesamt",
|
|
118
104
|
"documentDesigner.overrides.usedBy": "Wird verwendet von",
|
|
119
105
|
"documentDesigner.overrides.usedByEmpty": "Keine registrierte Benachrichtigung nutzt diese Vorlage direkt.",
|
|
120
106
|
"documentDesigner.pdfUnavailable": "PDF-Erzeugung nicht verfügbar: {message}",
|
|
121
|
-
"documentDesigner.
|
|
107
|
+
"documentDesigner.pdfUnavailableNoReason": "PDF-Erzeugung nicht verfügbar.",
|
|
122
108
|
"documentDesigner.placeholders.layoutHint": 'Bilder gehen im PDF als Data-URI mit. Kopf- und Fußzeile kommen aus dem Layout "{layout}" und sind auf allen Seiten gleich.',
|
|
123
|
-
"documentDesigner.placeholders.missing": "fehlt",
|
|
124
|
-
"documentDesigner.placeholders.system": "System-Platzhalter",
|
|
125
|
-
"documentDesigner.placeholders.title": "Platzhalter",
|
|
126
109
|
"documentDesigner.preview.loading": "PDF wird erzeugt ...",
|
|
127
110
|
"documentDesigner.preview.pageOf": "Seite {page} von {pages}",
|
|
128
111
|
"documentDesigner.preview.renderFailed": "Das PDF konnte nicht angezeigt werden.",
|
|
129
112
|
"documentDesigner.preview.stale": "Die Vorschau ist veraltet. Bitte das PDF neu erzeugen.",
|
|
130
113
|
"documentDesigner.resources.empty": "Noch keine Bilder.",
|
|
131
|
-
"documentDesigner.resources.insert": "Einfügen",
|
|
132
|
-
"documentDesigner.resources.insertNamed": "Bild {key} einfügen",
|
|
133
114
|
"documentDesigner.resources.saveFirst": "Speichern Sie die Vorlage zuerst, danach können Bilder hochgeladen werden.",
|
|
134
115
|
"documentDesigner.resources.title": "Ressourcen",
|
|
135
116
|
"documentDesigner.resources.upload": "+ Hochladen",
|
|
136
117
|
"documentDesigner.settings.fileName": "Dateiname",
|
|
137
118
|
"documentDesigner.settings.landscape": "Querformat",
|
|
138
119
|
"documentDesigner.settings.layout": "Layout",
|
|
139
|
-
"documentDesigner.settings.layoutDefault": "Standard",
|
|
140
120
|
"documentDesigner.settings.layoutHeaderFooter": "Kopf- und Fußzeile aus Layout",
|
|
141
121
|
"documentDesigner.settings.marginBottom": "Unten (mm)",
|
|
142
122
|
"documentDesigner.settings.marginLeft": "Links (mm)",
|
|
@@ -145,26 +125,22 @@ export const documentDesignerTranslations = {
|
|
|
145
125
|
"documentDesigner.settings.metaAuthor": "Autor",
|
|
146
126
|
"documentDesigner.settings.metadata": "Metadaten im PDF",
|
|
147
127
|
"documentDesigner.settings.metaLanguage": "Sprache",
|
|
128
|
+
"documentDesigner.settings.metaLanguageHint": "Folgt der Sprache der Vorlage. Umstellen über die Sprachauswahl im Kopf.",
|
|
129
|
+
"documentDesigner.settings.margins": "Ränder in mm (oben, rechts, unten, links)",
|
|
148
130
|
"documentDesigner.settings.metaTitle": "Titel",
|
|
149
131
|
"documentDesigner.settings.orientation": "Ausrichtung",
|
|
150
|
-
"documentDesigner.settings.page": "
|
|
132
|
+
"documentDesigner.settings.page": "Dokument",
|
|
151
133
|
"documentDesigner.settings.pageFormat": "Seitenformat",
|
|
152
134
|
"documentDesigner.settings.portrait": "Hochformat",
|
|
153
135
|
"documentDesigner.settings.printBackground": "Hintergründe und Farben drucken",
|
|
154
136
|
"documentDesigner.settings.storage": "Ablage",
|
|
155
137
|
"documentDesigner.settings.storageHint": "Jedes erzeugte PDF trägt Vorlagen-Id und Version. Rechnungen bleiben so nachweisbar, auch wenn die Vorlage später geändert wird.",
|
|
156
|
-
"documentDesigner.settings.versions": "Versionen",
|
|
157
138
|
"documentDesigner.status.active": "Aktiv",
|
|
158
139
|
"documentDesigner.status.activeVersion": "Aktiv v{version}",
|
|
159
140
|
"documentDesigner.status.draft": "Entwurf",
|
|
160
141
|
"documentDesigner.status.draftVersion": "Entwurf v{version}",
|
|
161
|
-
"documentDesigner.system.imprint": "Impressum",
|
|
162
|
-
"documentDesigner.system.page": "Seite / Seiten",
|
|
163
|
-
"documentDesigner.system.pages": "Seiten",
|
|
164
|
-
"documentDesigner.system.signature": "Signatur",
|
|
165
|
-
"documentDesigner.system.tenantName": "Mandantenname",
|
|
166
142
|
"documentDesigner.validation.lastProbe": "Letzte Probe-Erzeugung: {pages} Seiten, {size}, {seconds} s",
|
|
167
143
|
"documentDesigner.validation.missingRequired": 'Pflicht-Platzhalter "{name}" fehlt. Veröffentlichen ist gesperrt.',
|
|
168
|
-
"documentDesigner.validation.ok": "Alle Pflicht-Platzhalter vorhanden.",
|
|
144
|
+
"documentDesigner.validation.ok": "Alle Pflicht-Platzhalter vorhanden. Nach dem Veröffentlichen nutzen neue PDFs diese Fassung.",
|
|
169
145
|
},
|
|
170
146
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { LayoutKind, LayoutSourceView, TenantOverrideCounts } from "./layout-types.js";
|
|
3
3
|
import type { OverviewRowView } from "./overview-row.js";
|
|
4
|
-
export declare function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, overrides, onKindChange, onReload,
|
|
4
|
+
export declare function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, usedByCount, overrides, onKindChange, onReload, }: {
|
|
5
5
|
readonly kind: LayoutKind;
|
|
6
6
|
readonly slug: string;
|
|
7
7
|
readonly isSystemAdmin: boolean;
|
|
8
8
|
readonly source: LayoutSourceView;
|
|
9
9
|
readonly overviewRows: readonly OverviewRowView[];
|
|
10
|
+
readonly usedByCount: number;
|
|
10
11
|
readonly overrides: TenantOverrideCounts | null;
|
|
11
12
|
readonly onKindChange: (kind: LayoutKind) => void;
|
|
12
13
|
readonly onReload: () => Promise<void>;
|
|
13
|
-
readonly onBack: () => void;
|
|
14
14
|
}): ReactNode;
|
|
@@ -2,43 +2,49 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
// @runtime client
|
|
3
3
|
import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
4
|
import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { cn, ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { BRANDING_QUERY_QN } from "../constants.js";
|
|
7
8
|
import { composeLayoutHtml } from "./compose-layout.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { EditorHeader } from "./editor-header.js";
|
|
10
|
+
import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
|
|
11
|
+
import { fileUrl } from "./file-name.js";
|
|
12
|
+
import { appearanceOf, isHexColor, metaToSave, } from "./layout-meta.js";
|
|
13
|
+
import { AppearanceRail, BrandingRail, MjmlSourceRail, PlaceholderRail, RendererRail, UsedByRail, } from "./layout-rails.js";
|
|
14
|
+
import { systemLabelMap } from "./mail-draft.js";
|
|
10
15
|
import { OriginBar } from "./origin-bar.js";
|
|
11
|
-
import { asRowStatus
|
|
16
|
+
import { asRowStatus } from "./status-label.js";
|
|
17
|
+
import { useDebouncedValue } from "./use-debounced-value.js";
|
|
18
|
+
// Server-side validation compiles the whole layout (mjml), so wait for a pause in typing.
|
|
19
|
+
const VALIDATE_DEBOUNCE_MS = 400;
|
|
12
20
|
const REGION_EDITOR_IDS = {
|
|
13
21
|
header: "document-designer-region-header",
|
|
14
22
|
signature: "document-designer-region-signature",
|
|
15
23
|
footer: "document-designer-region-footer",
|
|
16
24
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
25
|
+
const PAPER_WIDTH_CLASS = {
|
|
26
|
+
560: "max-w-[560px]",
|
|
27
|
+
600: "max-w-[600px]",
|
|
28
|
+
640: "max-w-[640px]",
|
|
29
|
+
};
|
|
23
30
|
function issueText(t, issue) {
|
|
24
31
|
const translated = t(issue.i18nKey, issue.params);
|
|
25
32
|
return translated === issue.i18nKey ? issue.code : translated;
|
|
26
33
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}) }));
|
|
34
|
+
function sameAppearance(a, b) {
|
|
35
|
+
return (a.width === b.width &&
|
|
36
|
+
a.font === b.font &&
|
|
37
|
+
a.outerBackground === b.outerBackground &&
|
|
38
|
+
a.accentLine === b.accentLine &&
|
|
39
|
+
a.buttonStyle === b.buttonStyle);
|
|
40
|
+
}
|
|
41
|
+
// A dashed, labelled area of the paper; the focused one owns the shared toolbar.
|
|
42
|
+
function PaperRegion({ label, testId, children, onFocus, }) {
|
|
43
|
+
return (_jsxs("div", { className: "relative rounded-sm border border-dashed border-input p-3", "data-testid": testId, onFocusCapture: onFocus, children: [_jsx("span", { className: "absolute -top-2.5 left-2.5 bg-card px-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: label }), children] }));
|
|
38
44
|
}
|
|
39
|
-
export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, overrides, onKindChange, onReload,
|
|
45
|
+
export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, usedByCount, overrides, onKindChange, onReload, }) {
|
|
40
46
|
const t = useTranslation();
|
|
41
|
-
const { Banner,
|
|
47
|
+
const { Banner, Dialog } = usePrimitives();
|
|
42
48
|
const isMail = kind === "mail-layout";
|
|
43
49
|
const showMjmlTab = isSystemAdmin && source.mjmlSource !== null;
|
|
44
50
|
const [tab, setTab] = useState(showMjmlTab && source.effectiveFormat === "mjml" ? "mjml" : "draft");
|
|
@@ -46,32 +52,44 @@ export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRo
|
|
|
46
52
|
const [signature, setSignature] = useState(source.regions.signature);
|
|
47
53
|
const [footer, setFooter] = useState(source.regions.footer);
|
|
48
54
|
const [mjml, setMjml] = useState(source.mjmlSource ?? "");
|
|
55
|
+
const storedAppearance = appearanceOf(source.meta);
|
|
56
|
+
const [appearance, setAppearance] = useState(storedAppearance);
|
|
57
|
+
const [activeRegion, setActiveRegion] = useState("header");
|
|
58
|
+
const [toolbarHost, setToolbarHost] = useState(null);
|
|
49
59
|
const [pendingKind, setPendingKind] = useState(null);
|
|
50
60
|
const [saveError, setSaveError] = useState(null);
|
|
51
61
|
const upsertTenant = useMutation(TemplateResolverHandlers.upsertTenant);
|
|
52
62
|
const upsertSystem = useMutation(TemplateResolverHandlers.upsertSystem);
|
|
53
63
|
const publishSystem = useMutation(TemplateResolverHandlers.publishSystem);
|
|
64
|
+
const branding = useQuery(BRANDING_QUERY_QN, {});
|
|
54
65
|
const mjmlMode = tab === "mjml";
|
|
66
|
+
// An mjml layout brings its own look; the appearance fields only drive structured (html) layouts.
|
|
67
|
+
const appearanceFromMjml = source.effectiveFormat === "mjml" || mjmlMode;
|
|
68
|
+
const appearanceEditable = isMail && !appearanceFromMjml;
|
|
69
|
+
const appearanceDirty = appearanceEditable && !sameAppearance(appearance, storedAppearance);
|
|
70
|
+
const appearanceValid = !appearanceEditable || isHexColor(appearance.outerBackground);
|
|
55
71
|
const htmlDirty = header !== source.regions.header ||
|
|
56
72
|
signature !== source.regions.signature ||
|
|
57
|
-
footer !== source.regions.footer
|
|
73
|
+
footer !== source.regions.footer ||
|
|
74
|
+
appearanceDirty;
|
|
58
75
|
const dirty = mjmlMode ? mjml !== (source.mjmlSource ?? "") : htmlDirty;
|
|
59
76
|
const content = mjmlMode
|
|
60
77
|
? mjml
|
|
61
78
|
: composeLayoutHtml({ header, signature: isMail ? signature : "", footer });
|
|
62
|
-
const
|
|
79
|
+
const debouncedContent = useDebouncedValue(content, VALIDATE_DEBOUNCE_MS);
|
|
63
80
|
const validation = useQuery(TemplateResolverQueries.validate, {
|
|
64
81
|
slug,
|
|
65
82
|
kind,
|
|
66
83
|
contentFormat: mjmlMode ? "mjml" : "html",
|
|
67
|
-
content:
|
|
84
|
+
content: debouncedContent,
|
|
68
85
|
locale: source.locale,
|
|
69
86
|
scope: isSystemAdmin ? "system" : "tenant",
|
|
70
87
|
});
|
|
71
88
|
const checked = validation.data;
|
|
72
89
|
const shownRow = isSystemAdmin ? source.system : (source.tenant ?? source.system);
|
|
73
|
-
const userCount =
|
|
90
|
+
const userCount = usedByCount;
|
|
74
91
|
const placeholders = checked?.systemPlaceholders ?? [];
|
|
92
|
+
const placeholderLabels = systemLabelMap(t, placeholders);
|
|
75
93
|
const requestKind = (next) => {
|
|
76
94
|
if (next === kind)
|
|
77
95
|
return;
|
|
@@ -88,7 +106,7 @@ export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRo
|
|
|
88
106
|
locale: source.locale,
|
|
89
107
|
content,
|
|
90
108
|
contentFormat: mjmlMode ? "mjml" : "html",
|
|
91
|
-
meta: {},
|
|
109
|
+
meta: appearanceEditable ? metaToSave(appearance, source.meta) : isMail ? source.meta : {},
|
|
92
110
|
};
|
|
93
111
|
const saved = isSystemAdmin
|
|
94
112
|
? await upsertSystem.mutate(base)
|
|
@@ -108,31 +126,62 @@ export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRo
|
|
|
108
126
|
};
|
|
109
127
|
const busy = upsertTenant.pending || upsertSystem.pending || publishSystem.pending;
|
|
110
128
|
const shownStatus = shownRow === null ? null : asRowStatus(shownRow.status);
|
|
111
|
-
const canSave = dirty && checked?.canSave === true && !busy;
|
|
112
|
-
const canPublish = checked?.canPublish === true && (dirty || shownStatus === "draft") && !busy;
|
|
129
|
+
const canSave = dirty && checked?.canSave === true && appearanceValid && !busy;
|
|
130
|
+
const canPublish = checked?.canPublish === true && appearanceValid && (dirty || shownStatus === "draft") && !busy;
|
|
113
131
|
const blockingIssue = checked === null ? undefined : [...checked.issues, ...checked.publishIssues][0];
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
const footerMessage = checked === null
|
|
133
|
+
? null
|
|
134
|
+
: !appearanceValid
|
|
135
|
+
? { tone: "bad", text: t("documentDesigner.appearance.backgroundInvalid") }
|
|
136
|
+
: blockingIssue !== undefined
|
|
137
|
+
? { tone: "bad", text: issueText(t, blockingIssue) }
|
|
138
|
+
: {
|
|
139
|
+
tone: "ok",
|
|
140
|
+
text: t(isMail
|
|
141
|
+
? "documentDesigner.editor.footer.okMail"
|
|
142
|
+
: "documentDesigner.editor.footer.okPdf", { count: userCount }),
|
|
143
|
+
};
|
|
144
|
+
const accentColor = branding.data?.accentColor ?? "";
|
|
145
|
+
const showAccentLine = isMail && appearanceEditable && appearance.accentLine === "branding" && isHexColor(accentColor);
|
|
146
|
+
// A layout region has no per-mail variables, so a steps block there could never show a stage.
|
|
147
|
+
const blocks = (isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS).filter((block) => block !== "steps");
|
|
148
|
+
// The layout's "logo" resource is the branding logo, so the editor shows it instead of a chip.
|
|
149
|
+
const logoFile = branding.data?.logoFile ?? "";
|
|
150
|
+
const resourcePreviews = logoFile === ""
|
|
151
|
+
? undefined
|
|
152
|
+
: {
|
|
153
|
+
logo: {
|
|
154
|
+
src: fileUrl(logoFile),
|
|
155
|
+
caption: isMail ? t("documentDesigner.editor.logoCaption") : "",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
const regionEditor = (region, value, onChange) => (_jsx(DocumentContentEditor, { id: REGION_EDITOR_IDS[region], value: value, onChange: onChange, variables: placeholders, variableLabels: placeholderLabels, systemVariables: placeholders, readOnly: false, blocks: blocks, ...(resourcePreviews && { resourcePreviews }), chrome: "canvas", toolbarHost: activeRegion === region ? toolbarHost : null }));
|
|
159
|
+
return (_jsxs(EditorFrame, { testId: "document-designer-layout-editor", children: [_jsx(EditorHeader, { title: t(kind === "mail-layout"
|
|
160
|
+
? "documentDesigner.editor.title.mail"
|
|
161
|
+
: "documentDesigner.editor.title.pdf"), stored: shownRow, actions: {
|
|
162
|
+
busy,
|
|
163
|
+
canSave,
|
|
164
|
+
canPublish,
|
|
165
|
+
onSave: () => void save(false),
|
|
166
|
+
onPublish: () => void save(true),
|
|
167
|
+
}, children: _jsx(ModeSwitch, { value: kind, onChange: requestKind, ariaLabel: t("documentDesigner.editor.kindSwitch"), testId: "document-designer-kind-switch", options: [
|
|
168
|
+
{ value: "mail-layout", label: t("documentDesigner.editor.kind.mail") },
|
|
169
|
+
{ value: "pdf-layout", label: t("documentDesigner.editor.kind.pdf") },
|
|
170
|
+
] }) }), _jsx(OriginBar, { isSystemAdmin: isSystemAdmin, source: source, kind: kind, userCount: userCount, overrides: overrides, onReset: () => void onReload(), onResetError: setSaveError }), _jsx(EditorBody, { left: _jsxs(_Fragment, { children: [_jsx(BrandingRail, { data: branding.data, unavailable: branding.error !== null || (branding.data === null && !branding.loading) }), _jsx(PlaceholderRail, { validation: checked }), _jsx(RendererRail, { kind: kind })] }), center: _jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center border-b bg-background", children: [_jsx("div", { ref: setToolbarHost, className: "min-w-0 flex-1" }), showMjmlTab && (_jsx("div", { className: "shrink-0 px-3 py-1", children: _jsx(ModeSwitch, { value: tab, onChange: setTab, ariaLabel: t("documentDesigner.editor.tabSwitch"), testId: "layout-mjml-tab", options: [
|
|
171
|
+
{ value: "draft", label: t("documentDesigner.editor.tab.draft") },
|
|
172
|
+
{ value: "mjml", label: t("documentDesigner.editor.tab.mjml") },
|
|
173
|
+
] }) }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto px-8 py-6", children: mjmlMode ? (_jsx(TextareaField, { id: "document-designer-mjml-source", name: "mjml", label: t("documentDesigner.editor.mjml.label"), value: mjml, onChange: setMjml, rows: 24, testId: "document-designer-mjml-source" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [source.effectiveFormat === "mjml" && (_jsx(Banner, { variant: "info", testId: "document-designer-mjml-notice", children: t(isSystemAdmin
|
|
124
174
|
? "documentDesigner.editor.mjmlNotice.platform"
|
|
125
|
-
: "documentDesigner.editor.mjmlNotice.tenant") })), _jsxs("div", { className: isMail
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}, testId: "document-designer-discard", children: t("documentDesigner.editor.discard") })), _jsx(Button, { variant: "secondary", disabled: !canSave, loading: busy, onClick: () => save(false), testId: "document-designer-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { disabled: !canPublish, onClick: () => save(true), testId: "document-designer-publish", children: t("documentDesigner.editor.publish") })] }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
|
|
175
|
+
: "documentDesigner.editor.mjmlNotice.tenant") })), _jsxs("div", { className: cn("mx-auto flex w-full flex-col gap-5 bg-card px-8 py-6 shadow-md", isMail ? PAPER_WIDTH_CLASS[appearance.width] : "min-h-[842px] max-w-[595px]", showAccentLine && "border-t-4 border-t-(--layout-accent)"), ref: (element) => element?.style.setProperty("--layout-accent", accentColor), "data-testid": "document-designer-paper", children: [_jsx(PaperRegion, { label: t("documentDesigner.editor.region.header"), testId: "document-designer-region-header", onFocus: () => setActiveRegion("header"), children: regionEditor("header", header, setHeader) }), _jsxs("div", { className: "rounded-sm border-2 border-dashed border-primary bg-primary/10 px-3 py-9 text-center font-medium text-primary", "data-testid": "document-designer-region-content", children: ["\u229E ", t("documentDesigner.editor.region.content"), _jsx("small", { className: "mt-1 block text-xs font-normal opacity-85", children: t(isMail
|
|
176
|
+
? "documentDesigner.editor.slot.mail"
|
|
177
|
+
: "documentDesigner.editor.slot.pdf") })] }), isMail && (_jsx(PaperRegion, { label: t("documentDesigner.editor.region.signature"), testId: "document-designer-region-signature", onFocus: () => setActiveRegion("signature"), children: regionEditor("signature", signature, setSignature) })), _jsx("div", { className: cn(isMail && "-mx-8 -mb-6 bg-muted px-8 py-4"), children: _jsx(PaperRegion, { label: t("documentDesigner.editor.region.footer"), testId: "document-designer-region-footer", onFocus: () => setActiveRegion("footer"), children: regionEditor("footer", footer, setFooter) }) })] })] })) })] }), right: _jsxs(_Fragment, { children: [isMail && (_jsx(AppearanceRail, { appearance: appearance, onChange: setAppearance, fromMjml: appearanceFromMjml })), isMail && !mjmlMode && _jsx(MjmlSourceRail, { mjmlSource: source.mjmlSource }), _jsx(UsedByRail, { layoutSlug: slug, kind: kind, rows: overviewRows })] }) }), _jsx(EditorFooter, { testIdPrefix: "document-designer", message: footerMessage, saveError: saveError, modifiedAt: null, dirty: dirty, busy: busy, canSave: canSave, canPublish: canPublish, onDiscard: () => {
|
|
178
|
+
setHeader(source.regions.header);
|
|
179
|
+
setSignature(source.regions.signature);
|
|
180
|
+
setFooter(source.regions.footer);
|
|
181
|
+
setMjml(source.mjmlSource ?? "");
|
|
182
|
+
setAppearance(storedAppearance);
|
|
183
|
+
setSaveError(null);
|
|
184
|
+
}, onSave: () => void save(false), onPublish: () => void save(true) }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
|
|
136
185
|
if (!open)
|
|
137
186
|
setPendingKind(null);
|
|
138
187
|
}, title: t("documentDesigner.editor.dirtySwitch.title"), description: t("documentDesigner.editor.dirtySwitch.description"), confirmLabel: t("documentDesigner.editor.dirtySwitch.confirm"), variant: "danger", onConfirm: () => {
|
|
@@ -4,16 +4,20 @@ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/te
|
|
|
4
4
|
import { useNav, useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import { ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
6
|
import { useState } from "react";
|
|
7
|
-
import { DEFAULT_LAYOUT_SLUG
|
|
7
|
+
import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
|
|
8
8
|
import { LayoutEditorBody } from "./layout-editor-body.js";
|
|
9
|
+
import { layoutUserKind } from "./layout-users.js";
|
|
9
10
|
import { parseOverviewRow } from "./overview-row.js";
|
|
10
11
|
function parseLayoutKind(raw) {
|
|
11
12
|
return raw === "pdf-layout" ? "pdf-layout" : "mail-layout";
|
|
12
13
|
}
|
|
13
14
|
function LayoutEditorLoader({ kind, slug, isSystemAdmin, onKindChange, }) {
|
|
14
|
-
const nav = useNav();
|
|
15
15
|
const source = useQuery(TemplateResolverQueries.layoutSource, { kind, slug });
|
|
16
|
-
const overview = useQuery(TemplateResolverQueries.overview, {
|
|
16
|
+
const overview = useQuery(TemplateResolverQueries.overview, {
|
|
17
|
+
limit: 200,
|
|
18
|
+
layoutSlug: slug,
|
|
19
|
+
filters: [{ field: "kind", op: "in", value: [layoutUserKind(kind)] }],
|
|
20
|
+
});
|
|
17
21
|
const overrides = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind }, { enabled: isSystemAdmin });
|
|
18
22
|
if (source.error !== null) {
|
|
19
23
|
return (_jsx(ErrorState, { error: source.error, onRetry: () => void source.refetch(), testId: "document-designer-layout-error" }));
|
|
@@ -24,13 +28,16 @@ function LayoutEditorLoader({ kind, slug, isSystemAdmin, onKindChange, }) {
|
|
|
24
28
|
const data = source.data;
|
|
25
29
|
const rows = (overview.data?.rows ?? []).flatMap((row) => parseOverviewRow(row) ?? []);
|
|
26
30
|
const reload = async () => {
|
|
27
|
-
|
|
31
|
+
// refetch ignores `enabled`, and tenant-overrides is denied to everyone but the platform admin.
|
|
32
|
+
await Promise.all([
|
|
33
|
+
source.refetch(),
|
|
34
|
+
overview.refetch(),
|
|
35
|
+
...(isSystemAdmin ? [overrides.refetch()] : []),
|
|
36
|
+
]);
|
|
28
37
|
};
|
|
29
38
|
// A saved or reset layout is a new version: remount so the editors take the stored regions.
|
|
30
39
|
const bodyKey = [kind, slug, data.tenant?.id, data.tenant?.version, data.system?.version].join("|");
|
|
31
|
-
return (_jsx(LayoutEditorBody, { kind: kind, slug: slug, isSystemAdmin: isSystemAdmin, source: data, overviewRows: rows, overrides: isSystemAdmin ? overrides.data : null, onKindChange: onKindChange, onReload: reload,
|
|
32
|
-
screenId: isSystemAdmin ? PLATFORM_TEMPLATES_SCREEN_ID : TEMPLATES_SCREEN_ID,
|
|
33
|
-
}) }, bodyKey));
|
|
40
|
+
return (_jsx(LayoutEditorBody, { kind: kind, slug: slug, isSystemAdmin: isSystemAdmin, source: data, overviewRows: rows, usedByCount: overview.data?.total ?? rows.length, overrides: isSystemAdmin ? overrides.data : null, onKindChange: onKindChange, onReload: reload }, bodyKey));
|
|
34
41
|
}
|
|
35
42
|
export function LayoutEditor() {
|
|
36
43
|
const nav = useNav();
|
|
@@ -3,5 +3,7 @@ export type LayoutHtmlProps = {
|
|
|
3
3
|
readonly html: string;
|
|
4
4
|
/** Label per system placeholder (`data-variable`), shown as a chip. */
|
|
5
5
|
readonly systemLabels: Readonly<Record<string, string>>;
|
|
6
|
+
/** Display URL of an `img[data-resource]` key (the tenant logo, a linked image); no URL drops the image. */
|
|
7
|
+
readonly resourceSrc?: (key: string) => string | undefined;
|
|
6
8
|
};
|
|
7
9
|
export declare function LayoutHtml(props: LayoutHtmlProps): ReactNode;
|
package/dist/web/layout-html.js
CHANGED
|
@@ -63,8 +63,20 @@ function renderNode(node, props, path) {
|
|
|
63
63
|
return (_jsx("span", { className: "mx-0.5 inline-flex rounded border border-primary/30 bg-primary/10 px-1.5 text-xs text-primary", children: props.systemLabels[variable] ?? variable }, path));
|
|
64
64
|
}
|
|
65
65
|
const align = ALIGN_CLASSES[node.getAttribute("data-align") ?? ""];
|
|
66
|
-
const className = cn(TAG_CLASSES[tag], align);
|
|
66
|
+
const className = cn(TAG_CLASSES[tag], align, node.getAttribute("data-size") === "small" && "text-xs");
|
|
67
67
|
if (tag === "img") {
|
|
68
|
+
const resource = node.getAttribute("data-resource");
|
|
69
|
+
if (resource !== null) {
|
|
70
|
+
const resourceUrl = props.resourceSrc?.(resource);
|
|
71
|
+
if (resourceUrl === undefined)
|
|
72
|
+
return null;
|
|
73
|
+
return createElement("img", {
|
|
74
|
+
key: path,
|
|
75
|
+
className: "max-h-10 max-w-40 object-contain",
|
|
76
|
+
src: resourceUrl,
|
|
77
|
+
alt: node.getAttribute("alt") ?? resource,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
68
80
|
const src = node.getAttribute("src") ?? "";
|
|
69
81
|
if (!SAFE_IMAGE_SRC.test(src))
|
|
70
82
|
return null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const LAYOUT_WIDTHS: readonly [560, 600, 640];
|
|
2
|
+
export declare const LAYOUT_FONTS: readonly ["sans", "serif"];
|
|
3
|
+
export declare const LAYOUT_ACCENT_LINES: readonly ["branding", "none"];
|
|
4
|
+
export declare const LAYOUT_BUTTON_STYLES: readonly ["filled-rounded", "filled-square", "outline"];
|
|
5
|
+
export type LayoutWidth = (typeof LAYOUT_WIDTHS)[number];
|
|
6
|
+
export type LayoutFont = (typeof LAYOUT_FONTS)[number];
|
|
7
|
+
export type LayoutAccentLine = (typeof LAYOUT_ACCENT_LINES)[number];
|
|
8
|
+
export type LayoutButtonStyle = (typeof LAYOUT_BUTTON_STYLES)[number];
|
|
9
|
+
/** What the server stores; every field is optional and a missing one means the default. */
|
|
10
|
+
export type LayoutMeta = {
|
|
11
|
+
readonly width?: LayoutWidth;
|
|
12
|
+
readonly font?: LayoutFont;
|
|
13
|
+
readonly outerBackground?: string;
|
|
14
|
+
readonly accentLine?: LayoutAccentLine;
|
|
15
|
+
readonly buttonStyle?: LayoutButtonStyle;
|
|
16
|
+
};
|
|
17
|
+
/** The five appearance values with defaults applied; `outerBackground` stays text while it is being typed. */
|
|
18
|
+
export type LayoutAppearance = {
|
|
19
|
+
readonly width: LayoutWidth;
|
|
20
|
+
readonly font: LayoutFont;
|
|
21
|
+
readonly outerBackground: string;
|
|
22
|
+
readonly accentLine: LayoutAccentLine;
|
|
23
|
+
readonly buttonStyle: LayoutButtonStyle;
|
|
24
|
+
};
|
|
25
|
+
export declare const DEFAULT_LAYOUT_APPEARANCE: LayoutAppearance;
|
|
26
|
+
export declare function isHexColor(value: string): boolean;
|
|
27
|
+
export declare function appearanceOf(meta: LayoutMeta): LayoutAppearance;
|
|
28
|
+
/** Keeps a field that was already stored, even when it now equals the default; drops every other default. */
|
|
29
|
+
export declare function metaToSave(appearance: LayoutAppearance, stored: LayoutMeta): LayoutMeta;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Mirrors the mail-layout meta of template-resolver (template-meta.ts); the server parses it again on every write and render.
|
|
3
|
+
export const LAYOUT_WIDTHS = [560, 600, 640];
|
|
4
|
+
export const LAYOUT_FONTS = ["sans", "serif"];
|
|
5
|
+
export const LAYOUT_ACCENT_LINES = ["branding", "none"];
|
|
6
|
+
export const LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"];
|
|
7
|
+
export const DEFAULT_LAYOUT_APPEARANCE = {
|
|
8
|
+
width: 600,
|
|
9
|
+
font: "sans",
|
|
10
|
+
outerBackground: "#f5f5f5",
|
|
11
|
+
accentLine: "none",
|
|
12
|
+
buttonStyle: "filled-rounded",
|
|
13
|
+
};
|
|
14
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
15
|
+
export function isHexColor(value) {
|
|
16
|
+
return HEX_COLOR.test(value);
|
|
17
|
+
}
|
|
18
|
+
export function appearanceOf(meta) {
|
|
19
|
+
return { ...DEFAULT_LAYOUT_APPEARANCE, ...meta };
|
|
20
|
+
}
|
|
21
|
+
/** Keeps a field that was already stored, even when it now equals the default; drops every other default. */
|
|
22
|
+
export function metaToSave(appearance, stored) {
|
|
23
|
+
const defaults = DEFAULT_LAYOUT_APPEARANCE;
|
|
24
|
+
const keep = (key) => appearance[key] !== defaults[key] || key in stored;
|
|
25
|
+
return {
|
|
26
|
+
...(keep("width") && { width: appearance.width }),
|
|
27
|
+
...(keep("font") && { font: appearance.font }),
|
|
28
|
+
...(keep("outerBackground") && { outerBackground: appearance.outerBackground }),
|
|
29
|
+
...(keep("accentLine") && { accentLine: appearance.accentLine }),
|
|
30
|
+
...(keep("buttonStyle") && { buttonStyle: appearance.buttonStyle }),
|
|
31
|
+
};
|
|
32
|
+
}
|