@cosmicdrift/kumiko-document-designer 0.0.0-stage → 0.364.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +57 -0
- package/README.md +37 -2
- package/dist/constants.d.ts +27 -0
- package/dist/constants.js +37 -0
- package/dist/feature.d.ts +2 -0
- package/dist/feature.js +85 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +764 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/list-definition.d.ts +6 -0
- package/dist/list-definition.js +109 -0
- package/dist/web/cells.d.ts +10 -0
- package/dist/web/cells.js +87 -0
- package/dist/web/client-plugin.d.ts +12 -0
- package/dist/web/client-plugin.js +43 -0
- package/dist/web/compose-layout.d.ts +6 -0
- package/dist/web/compose-layout.js +13 -0
- package/dist/web/editor-header.d.ts +37 -0
- package/dist/web/editor-header.js +43 -0
- package/dist/web/editor-shell.d.ts +40 -0
- package/dist/web/editor-shell.js +36 -0
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.js +24 -0
- package/dist/web/field-issue-text.d.ts +4 -0
- package/dist/web/field-issue-text.js +12 -0
- package/dist/web/file-name.d.ts +2 -0
- package/dist/web/file-name.js +17 -0
- package/dist/web/i18n.d.ts +2 -0
- package/dist/web/i18n.js +146 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +7 -0
- package/dist/web/layout-editor-body.d.ts +14 -0
- package/dist/web/layout-editor-body.js +187 -0
- package/dist/web/layout-editor.d.ts +2 -0
- package/dist/web/layout-editor.js +43 -0
- package/dist/web/layout-html.d.ts +9 -0
- package/dist/web/layout-html.js +93 -0
- package/dist/web/layout-meta.d.ts +29 -0
- package/dist/web/layout-meta.js +32 -0
- package/dist/web/layout-rails.d.ts +28 -0
- package/dist/web/layout-rails.js +100 -0
- package/dist/web/layout-types.d.ts +66 -0
- package/dist/web/layout-types.js +1 -0
- package/dist/web/layout-users.d.ts +4 -0
- package/dist/web/layout-users.js +11 -0
- package/dist/web/mail-dialogs.d.ts +24 -0
- package/dist/web/mail-dialogs.js +15 -0
- package/dist/web/mail-draft.d.ts +19 -0
- package/dist/web/mail-draft.js +154 -0
- package/dist/web/mail-edit-center.d.ts +19 -0
- package/dist/web/mail-edit-center.js +12 -0
- package/dist/web/mail-editor-body.d.ts +17 -0
- package/dist/web/mail-editor-body.js +209 -0
- package/dist/web/mail-editor.d.ts +2 -0
- package/dist/web/mail-editor.js +68 -0
- package/dist/web/mail-footer.d.ts +10 -0
- package/dist/web/mail-footer.js +43 -0
- package/dist/web/mail-left-rail.d.ts +23 -0
- package/dist/web/mail-left-rail.js +56 -0
- 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 +15 -0
- package/dist/web/mail-origin-bar.js +60 -0
- package/dist/web/mail-pdf-link-modal.d.ts +11 -0
- package/dist/web/mail-pdf-link-modal.js +54 -0
- package/dist/web/mail-preview-side.d.ts +19 -0
- package/dist/web/mail-preview-side.js +57 -0
- package/dist/web/mail-preview.d.ts +24 -0
- package/dist/web/mail-preview.js +54 -0
- package/dist/web/mail-right-rail.d.ts +19 -0
- package/dist/web/mail-right-rail.js +57 -0
- package/dist/web/mail-types.d.ts +117 -0
- package/dist/web/mail-types.js +1 -0
- package/dist/web/mail-upload-modal.d.ts +16 -0
- package/dist/web/mail-upload-modal.js +56 -0
- package/dist/web/mail-versions.d.ts +6 -0
- package/dist/web/mail-versions.js +43 -0
- package/dist/web/origin-bar.d.ts +22 -0
- package/dist/web/origin-bar.js +104 -0
- package/dist/web/overrides-drawer.d.ts +10 -0
- package/dist/web/overrides-drawer.js +95 -0
- package/dist/web/overview-row.d.ts +23 -0
- package/dist/web/overview-row.js +48 -0
- package/dist/web/page-canvas.d.ts +19 -0
- package/dist/web/page-canvas.js +48 -0
- 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 +21 -0
- package/dist/web/pdf-meta.js +95 -0
- package/dist/web/pdf-preview.d.ts +4 -0
- package/dist/web/pdf-preview.js +112 -0
- package/dist/web/pdf-template-editor.d.ts +8 -0
- package/dist/web/pdf-template-editor.js +340 -0
- package/dist/web/preview-record-picker.d.ts +12 -0
- package/dist/web/preview-record-picker.js +7 -0
- package/dist/web/rail.d.ts +30 -0
- package/dist/web/rail.js +22 -0
- package/dist/web/relative-time.d.ts +1 -0
- package/dist/web/relative-time.js +18 -0
- package/dist/web/settings-panel.d.ts +23 -0
- package/dist/web/settings-panel.js +36 -0
- package/dist/web/status-label.d.ts +14 -0
- package/dist/web/status-label.js +26 -0
- package/dist/web/template-rows.d.ts +9 -0
- package/dist/web/template-rows.js +34 -0
- package/dist/web/types.d.ts +126 -0
- package/dist/web/types.js +3 -0
- package/dist/web/use-insert-targets.d.ts +9 -0
- package/dist/web/use-insert-targets.js +17 -0
- package/dist/web/use-mail-persistence.d.ts +14 -0
- package/dist/web/use-mail-persistence.js +49 -0
- package/dist/web/use-overview-rows.d.ts +7 -0
- package/dist/web/use-overview-rows.js +19 -0
- package/dist/web/use-own-address.d.ts +5 -0
- package/dist/web/use-own-address.js +14 -0
- package/dist/web/use-settled-value.d.ts +2 -0
- package/dist/web/use-settled-value.js +18 -0
- package/dist/web/use-test-mail.d.ts +15 -0
- package/dist/web/use-test-mail.js +27 -0
- package/dist/web/used-by-label.d.ts +9 -0
- package/dist/web/used-by-label.js +32 -0
- package/package.json +63 -4
- package/src/changes.json +44 -0
package/dist/web/i18n.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const documentDesignerTranslations = {
|
|
2
|
+
en: {
|
|
3
|
+
"documentDesigner.actions.generatePdf": "Generate PDF",
|
|
4
|
+
"documentDesigner.canvas.caption": 'Page 1 · {format} · The preview shows page breaks only roughly; "Generate PDF" is authoritative.',
|
|
5
|
+
"documentDesigner.canvas.fromLayout": 'from layout "{layout}"',
|
|
6
|
+
"documentDesigner.error.resourceTooLarge": "The image is too large. Please choose a smaller file.",
|
|
7
|
+
"documentDesigner.error.resourceUnsupportedImage": "This image format is not supported. Use PNG, JPEG, GIF or WebP.",
|
|
8
|
+
"documentDesigner.locale": "Language",
|
|
9
|
+
"documentDesigner.localeDialog.cancel": "Cancel",
|
|
10
|
+
"documentDesigner.localeDialog.description": "The changes of this language have not been saved yet.",
|
|
11
|
+
"documentDesigner.localeDialog.discard": "Discard changes",
|
|
12
|
+
"documentDesigner.localeDialog.save": "Save and switch",
|
|
13
|
+
"documentDesigner.localeDialog.title": "Unsaved changes",
|
|
14
|
+
"documentDesigner.notFound": 'Template "{slug}" was not found.',
|
|
15
|
+
"documentDesigner.originBar.export": "Export as bundle",
|
|
16
|
+
"documentDesigner.originBar.noTotal": "System template for all tenants. {count} tenants have their own version and do not receive changes made here.",
|
|
17
|
+
"documentDesigner.originBar.withTotal": "System template for all tenants. {count} of {total} tenants have their own version and do not receive changes made here.",
|
|
18
|
+
"documentDesigner.originBar.showOverrides": "Show tenant customisations",
|
|
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.",
|
|
31
|
+
"documentDesigner.overrides.title": "Tenant customisations",
|
|
32
|
+
"documentDesigner.overrides.usedBy": "Used by",
|
|
33
|
+
"documentDesigner.overrides.usedByEmpty": "No registered notification uses this template directly.",
|
|
34
|
+
"documentDesigner.pdfUnavailable": "PDF generation is not available: {message}",
|
|
35
|
+
"documentDesigner.pdfUnavailableNoReason": "PDF generation is not available.",
|
|
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.',
|
|
37
|
+
"documentDesigner.preview.loading": "Generating PDF...",
|
|
38
|
+
"documentDesigner.preview.pageOf": "Page {page} of {pages}",
|
|
39
|
+
"documentDesigner.preview.renderFailed": "The PDF could not be displayed.",
|
|
40
|
+
"documentDesigner.preview.stale": "The preview is out of date. Generate the PDF again.",
|
|
41
|
+
"documentDesigner.resources.empty": "No images yet.",
|
|
42
|
+
"documentDesigner.resources.saveFirst": "Save the template first, then you can upload images.",
|
|
43
|
+
"documentDesigner.resources.title": "Resources",
|
|
44
|
+
"documentDesigner.resources.upload": "+ Upload",
|
|
45
|
+
"documentDesigner.settings.fileName": "File name",
|
|
46
|
+
"documentDesigner.settings.landscape": "Landscape",
|
|
47
|
+
"documentDesigner.settings.layout": "Layout",
|
|
48
|
+
"documentDesigner.settings.layoutHeaderFooter": "Header and footer from layout",
|
|
49
|
+
"documentDesigner.settings.marginBottom": "Bottom (mm)",
|
|
50
|
+
"documentDesigner.settings.marginLeft": "Left (mm)",
|
|
51
|
+
"documentDesigner.settings.marginRight": "Right (mm)",
|
|
52
|
+
"documentDesigner.settings.marginTop": "Top (mm)",
|
|
53
|
+
"documentDesigner.settings.metaAuthor": "Author",
|
|
54
|
+
"documentDesigner.settings.metadata": "PDF metadata",
|
|
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)",
|
|
58
|
+
"documentDesigner.settings.metaTitle": "Title",
|
|
59
|
+
"documentDesigner.settings.orientation": "Orientation",
|
|
60
|
+
"documentDesigner.settings.page": "Document",
|
|
61
|
+
"documentDesigner.settings.pageFormat": "Page format",
|
|
62
|
+
"documentDesigner.settings.portrait": "Portrait",
|
|
63
|
+
"documentDesigner.settings.printBackground": "Print backgrounds and colours",
|
|
64
|
+
"documentDesigner.settings.storage": "Storage",
|
|
65
|
+
"documentDesigner.settings.storageHint": "Every generated PDF carries the template id and version. Invoices stay verifiable even if the template changes later.",
|
|
66
|
+
"documentDesigner.status.active": "Active",
|
|
67
|
+
"documentDesigner.status.activeVersion": "Active v{version}",
|
|
68
|
+
"documentDesigner.status.draft": "Draft",
|
|
69
|
+
"documentDesigner.status.draftVersion": "Draft v{version}",
|
|
70
|
+
"documentDesigner.validation.lastProbe": "Last test run: {pages} pages, {size}, {seconds} s",
|
|
71
|
+
"documentDesigner.validation.missingRequired": 'Required placeholder "{name}" is missing. Publishing is blocked.',
|
|
72
|
+
"documentDesigner.validation.ok": "All required placeholders are present. After publishing, new PDFs use this version.",
|
|
73
|
+
},
|
|
74
|
+
de: {
|
|
75
|
+
"documentDesigner.actions.generatePdf": "PDF erzeugen",
|
|
76
|
+
"documentDesigner.canvas.caption": 'Seite 1 · {format} · Die Vorschau zeigt Seitengrenzen nur ungefähr, verbindlich ist "PDF erzeugen".',
|
|
77
|
+
"documentDesigner.canvas.fromLayout": "aus Layout „{layout}“",
|
|
78
|
+
"documentDesigner.error.resourceTooLarge": "Das Bild ist zu groß. Bitte eine kleinere Datei wählen.",
|
|
79
|
+
"documentDesigner.error.resourceUnsupportedImage": "Dieses Bildformat wird nicht unterstützt. Nutzen Sie PNG, JPEG, GIF oder WebP.",
|
|
80
|
+
"documentDesigner.locale": "Sprache",
|
|
81
|
+
"documentDesigner.localeDialog.cancel": "Abbrechen",
|
|
82
|
+
"documentDesigner.localeDialog.description": "Die Änderungen in dieser Sprache sind noch nicht gespeichert.",
|
|
83
|
+
"documentDesigner.localeDialog.discard": "Änderungen verwerfen",
|
|
84
|
+
"documentDesigner.localeDialog.save": "Speichern und wechseln",
|
|
85
|
+
"documentDesigner.localeDialog.title": "Ungespeicherte Änderungen",
|
|
86
|
+
"documentDesigner.notFound": 'Die Vorlage "{slug}" wurde nicht gefunden.',
|
|
87
|
+
"documentDesigner.originBar.export": "Als Bundle exportieren",
|
|
88
|
+
"documentDesigner.originBar.noTotal": "System-Vorlage für alle Mandanten. {count} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
|
|
89
|
+
"documentDesigner.originBar.withTotal": "System-Vorlage für alle Mandanten. {count} von {total} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
|
|
90
|
+
"documentDesigner.originBar.showOverrides": "Anpassungen der Mandanten anzeigen",
|
|
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.",
|
|
103
|
+
"documentDesigner.overrides.title": "Anpassungen der Mandanten",
|
|
104
|
+
"documentDesigner.overrides.usedBy": "Wird verwendet von",
|
|
105
|
+
"documentDesigner.overrides.usedByEmpty": "Keine registrierte Benachrichtigung nutzt diese Vorlage direkt.",
|
|
106
|
+
"documentDesigner.pdfUnavailable": "PDF-Erzeugung nicht verfügbar: {message}",
|
|
107
|
+
"documentDesigner.pdfUnavailableNoReason": "PDF-Erzeugung nicht verfügbar.",
|
|
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.',
|
|
109
|
+
"documentDesigner.preview.loading": "PDF wird erzeugt ...",
|
|
110
|
+
"documentDesigner.preview.pageOf": "Seite {page} von {pages}",
|
|
111
|
+
"documentDesigner.preview.renderFailed": "Das PDF konnte nicht angezeigt werden.",
|
|
112
|
+
"documentDesigner.preview.stale": "Die Vorschau ist veraltet. Bitte das PDF neu erzeugen.",
|
|
113
|
+
"documentDesigner.resources.empty": "Noch keine Bilder.",
|
|
114
|
+
"documentDesigner.resources.saveFirst": "Speichern Sie die Vorlage zuerst, danach können Bilder hochgeladen werden.",
|
|
115
|
+
"documentDesigner.resources.title": "Ressourcen",
|
|
116
|
+
"documentDesigner.resources.upload": "+ Hochladen",
|
|
117
|
+
"documentDesigner.settings.fileName": "Dateiname",
|
|
118
|
+
"documentDesigner.settings.landscape": "Querformat",
|
|
119
|
+
"documentDesigner.settings.layout": "Layout",
|
|
120
|
+
"documentDesigner.settings.layoutHeaderFooter": "Kopf- und Fußzeile aus Layout",
|
|
121
|
+
"documentDesigner.settings.marginBottom": "Unten (mm)",
|
|
122
|
+
"documentDesigner.settings.marginLeft": "Links (mm)",
|
|
123
|
+
"documentDesigner.settings.marginRight": "Rechts (mm)",
|
|
124
|
+
"documentDesigner.settings.marginTop": "Oben (mm)",
|
|
125
|
+
"documentDesigner.settings.metaAuthor": "Autor",
|
|
126
|
+
"documentDesigner.settings.metadata": "Metadaten im PDF",
|
|
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)",
|
|
130
|
+
"documentDesigner.settings.metaTitle": "Titel",
|
|
131
|
+
"documentDesigner.settings.orientation": "Ausrichtung",
|
|
132
|
+
"documentDesigner.settings.page": "Dokument",
|
|
133
|
+
"documentDesigner.settings.pageFormat": "Seitenformat",
|
|
134
|
+
"documentDesigner.settings.portrait": "Hochformat",
|
|
135
|
+
"documentDesigner.settings.printBackground": "Hintergründe und Farben drucken",
|
|
136
|
+
"documentDesigner.settings.storage": "Ablage",
|
|
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.",
|
|
138
|
+
"documentDesigner.status.active": "Aktiv",
|
|
139
|
+
"documentDesigner.status.activeVersion": "Aktiv v{version}",
|
|
140
|
+
"documentDesigner.status.draft": "Entwurf",
|
|
141
|
+
"documentDesigner.status.draftVersion": "Entwurf v{version}",
|
|
142
|
+
"documentDesigner.validation.lastProbe": "Letzte Probe-Erzeugung: {pages} Seiten, {size}, {seconds} s",
|
|
143
|
+
"documentDesigner.validation.missingRequired": 'Pflicht-Platzhalter "{name}" fehlt. Veröffentlichen ist gesperrt.',
|
|
144
|
+
"documentDesigner.validation.ok": "Alle Pflicht-Platzhalter vorhanden. Nach dem Veröffentlichen nutzen neue PDFs diese Fassung.",
|
|
145
|
+
},
|
|
146
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type DocumentDesignerClientOptions, documentDesignerClient, PdfTemplateEditorScreen, } from "./client-plugin.js";
|
|
2
|
+
export { composeLayoutHtml, type LayoutRegionValues } from "./compose-layout.js";
|
|
3
|
+
export { documentDesignerTranslations } from "./i18n.js";
|
|
4
|
+
export { PdfTemplateEditor, type PdfTemplateEditorProps } from "./pdf-template-editor.js";
|
|
5
|
+
export { type PreviewRecordPicker, type PreviewRecordPickerProps, PreviewRecordPickerProvider, } from "./preview-record-picker.js";
|
|
6
|
+
export { usageLabel, usedByLabel } from "./used-by-label.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
export { documentDesignerClient, PdfTemplateEditorScreen, } from "./client-plugin.js";
|
|
3
|
+
export { composeLayoutHtml } from "./compose-layout.js";
|
|
4
|
+
export { documentDesignerTranslations } from "./i18n.js";
|
|
5
|
+
export { PdfTemplateEditor } from "./pdf-template-editor.js";
|
|
6
|
+
export { PreviewRecordPickerProvider, } from "./preview-record-picker.js";
|
|
7
|
+
export { usageLabel, usedByLabel } from "./used-by-label.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { LayoutKind, LayoutSourceView, TenantOverrideCounts } from "./layout-types.js";
|
|
3
|
+
import type { OverviewRowView } from "./overview-row.js";
|
|
4
|
+
export declare function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, usedByCount, overrides, onKindChange, onReload, }: {
|
|
5
|
+
readonly kind: LayoutKind;
|
|
6
|
+
readonly slug: string;
|
|
7
|
+
readonly isSystemAdmin: boolean;
|
|
8
|
+
readonly source: LayoutSourceView;
|
|
9
|
+
readonly overviewRows: readonly OverviewRowView[];
|
|
10
|
+
readonly usedByCount: number;
|
|
11
|
+
readonly overrides: TenantOverrideCounts | null;
|
|
12
|
+
readonly onKindChange: (kind: LayoutKind) => void;
|
|
13
|
+
readonly onReload: () => Promise<void>;
|
|
14
|
+
}): ReactNode;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { cn, ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
|
+
import { useDeferredValue, useState } from "react";
|
|
7
|
+
import { BRANDING_QUERY_QN } from "../constants.js";
|
|
8
|
+
import { composeLayoutHtml } from "./compose-layout.js";
|
|
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";
|
|
15
|
+
import { OriginBar } from "./origin-bar.js";
|
|
16
|
+
import { asRowStatus } from "./status-label.js";
|
|
17
|
+
const REGION_EDITOR_IDS = {
|
|
18
|
+
header: "document-designer-region-header",
|
|
19
|
+
signature: "document-designer-region-signature",
|
|
20
|
+
footer: "document-designer-region-footer",
|
|
21
|
+
};
|
|
22
|
+
const PAPER_WIDTH_CLASS = {
|
|
23
|
+
560: "max-w-[560px]",
|
|
24
|
+
600: "max-w-[600px]",
|
|
25
|
+
640: "max-w-[640px]",
|
|
26
|
+
};
|
|
27
|
+
function issueText(t, issue) {
|
|
28
|
+
const translated = t(issue.i18nKey, issue.params);
|
|
29
|
+
return translated === issue.i18nKey ? issue.code : translated;
|
|
30
|
+
}
|
|
31
|
+
function sameAppearance(a, b) {
|
|
32
|
+
return (a.width === b.width &&
|
|
33
|
+
a.font === b.font &&
|
|
34
|
+
a.outerBackground === b.outerBackground &&
|
|
35
|
+
a.accentLine === b.accentLine &&
|
|
36
|
+
a.buttonStyle === b.buttonStyle);
|
|
37
|
+
}
|
|
38
|
+
// A dashed, labelled area of the paper; the focused one owns the shared toolbar.
|
|
39
|
+
function PaperRegion({ label, testId, children, onFocus, }) {
|
|
40
|
+
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] }));
|
|
41
|
+
}
|
|
42
|
+
export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, usedByCount, overrides, onKindChange, onReload, }) {
|
|
43
|
+
const t = useTranslation();
|
|
44
|
+
const { Banner, Dialog } = usePrimitives();
|
|
45
|
+
const isMail = kind === "mail-layout";
|
|
46
|
+
const showMjmlTab = isSystemAdmin && source.mjmlSource !== null;
|
|
47
|
+
const [tab, setTab] = useState(showMjmlTab && source.effectiveFormat === "mjml" ? "mjml" : "draft");
|
|
48
|
+
const [header, setHeader] = useState(source.regions.header);
|
|
49
|
+
const [signature, setSignature] = useState(source.regions.signature);
|
|
50
|
+
const [footer, setFooter] = useState(source.regions.footer);
|
|
51
|
+
const [mjml, setMjml] = useState(source.mjmlSource ?? "");
|
|
52
|
+
const storedAppearance = appearanceOf(source.meta);
|
|
53
|
+
const [appearance, setAppearance] = useState(storedAppearance);
|
|
54
|
+
const [activeRegion, setActiveRegion] = useState("header");
|
|
55
|
+
const [toolbarHost, setToolbarHost] = useState(null);
|
|
56
|
+
const [pendingKind, setPendingKind] = useState(null);
|
|
57
|
+
const [saveError, setSaveError] = useState(null);
|
|
58
|
+
const upsertTenant = useMutation(TemplateResolverHandlers.upsertTenant);
|
|
59
|
+
const upsertSystem = useMutation(TemplateResolverHandlers.upsertSystem);
|
|
60
|
+
const publishSystem = useMutation(TemplateResolverHandlers.publishSystem);
|
|
61
|
+
const branding = useQuery(BRANDING_QUERY_QN, {});
|
|
62
|
+
const mjmlMode = tab === "mjml";
|
|
63
|
+
// An mjml layout brings its own look; the appearance fields only drive structured (html) layouts.
|
|
64
|
+
const appearanceFromMjml = source.effectiveFormat === "mjml" || mjmlMode;
|
|
65
|
+
const appearanceEditable = isMail && !appearanceFromMjml;
|
|
66
|
+
const appearanceDirty = appearanceEditable && !sameAppearance(appearance, storedAppearance);
|
|
67
|
+
const appearanceValid = !appearanceEditable || isHexColor(appearance.outerBackground);
|
|
68
|
+
const htmlDirty = header !== source.regions.header ||
|
|
69
|
+
signature !== source.regions.signature ||
|
|
70
|
+
footer !== source.regions.footer ||
|
|
71
|
+
appearanceDirty;
|
|
72
|
+
const dirty = mjmlMode ? mjml !== (source.mjmlSource ?? "") : htmlDirty;
|
|
73
|
+
const content = mjmlMode
|
|
74
|
+
? mjml
|
|
75
|
+
: composeLayoutHtml({ header, signature: isMail ? signature : "", footer });
|
|
76
|
+
const deferredContent = useDeferredValue(content);
|
|
77
|
+
const validation = useQuery(TemplateResolverQueries.validate, {
|
|
78
|
+
slug,
|
|
79
|
+
kind,
|
|
80
|
+
contentFormat: mjmlMode ? "mjml" : "html",
|
|
81
|
+
content: deferredContent,
|
|
82
|
+
locale: source.locale,
|
|
83
|
+
scope: isSystemAdmin ? "system" : "tenant",
|
|
84
|
+
});
|
|
85
|
+
const checked = validation.data;
|
|
86
|
+
const shownRow = isSystemAdmin ? source.system : (source.tenant ?? source.system);
|
|
87
|
+
const userCount = usedByCount;
|
|
88
|
+
const placeholders = checked?.systemPlaceholders ?? [];
|
|
89
|
+
const placeholderLabels = systemLabelMap(t, placeholders);
|
|
90
|
+
const requestKind = (next) => {
|
|
91
|
+
if (next === kind)
|
|
92
|
+
return;
|
|
93
|
+
if (dirty)
|
|
94
|
+
setPendingKind(next);
|
|
95
|
+
else
|
|
96
|
+
onKindChange(next);
|
|
97
|
+
};
|
|
98
|
+
const save = async (publish) => {
|
|
99
|
+
setSaveError(null);
|
|
100
|
+
const base = {
|
|
101
|
+
slug,
|
|
102
|
+
kind,
|
|
103
|
+
locale: source.locale,
|
|
104
|
+
content,
|
|
105
|
+
contentFormat: mjmlMode ? "mjml" : "html",
|
|
106
|
+
meta: appearanceEditable ? metaToSave(appearance, source.meta) : isMail ? source.meta : {},
|
|
107
|
+
};
|
|
108
|
+
const saved = isSystemAdmin
|
|
109
|
+
? await upsertSystem.mutate(base)
|
|
110
|
+
: await upsertTenant.mutate({ ...base, status: publish ? "active" : "draft" });
|
|
111
|
+
if (!saved.isSuccess) {
|
|
112
|
+
setSaveError(dispatcherErrorText(saved.error, t));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (isSystemAdmin && publish) {
|
|
116
|
+
const published = await publishSystem.mutate({ id: saved.data.id });
|
|
117
|
+
if (!published.isSuccess) {
|
|
118
|
+
setSaveError(dispatcherErrorText(published.error, t));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await onReload();
|
|
123
|
+
};
|
|
124
|
+
const busy = upsertTenant.pending || upsertSystem.pending || publishSystem.pending;
|
|
125
|
+
const shownStatus = shownRow === null ? null : asRowStatus(shownRow.status);
|
|
126
|
+
const canSave = dirty && checked?.canSave === true && appearanceValid && !busy;
|
|
127
|
+
const canPublish = checked?.canPublish === true && appearanceValid && (dirty || shownStatus === "draft") && !busy;
|
|
128
|
+
const blockingIssue = checked === null ? undefined : [...checked.issues, ...checked.publishIssues][0];
|
|
129
|
+
const footerMessage = checked === null
|
|
130
|
+
? null
|
|
131
|
+
: !appearanceValid
|
|
132
|
+
? { tone: "bad", text: t("documentDesigner.appearance.backgroundInvalid") }
|
|
133
|
+
: blockingIssue !== undefined
|
|
134
|
+
? { tone: "bad", text: issueText(t, blockingIssue) }
|
|
135
|
+
: {
|
|
136
|
+
tone: "ok",
|
|
137
|
+
text: t(isMail
|
|
138
|
+
? "documentDesigner.editor.footer.okMail"
|
|
139
|
+
: "documentDesigner.editor.footer.okPdf", { count: userCount }),
|
|
140
|
+
};
|
|
141
|
+
const accentColor = branding.data?.accentColor ?? "";
|
|
142
|
+
const showAccentLine = isMail && appearanceEditable && appearance.accentLine === "branding" && isHexColor(accentColor);
|
|
143
|
+
const blocks = isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS;
|
|
144
|
+
// The layout's "logo" resource is the branding logo, so the editor shows it instead of a chip.
|
|
145
|
+
const logoFile = branding.data?.logoFile ?? "";
|
|
146
|
+
const resourcePreviews = logoFile === ""
|
|
147
|
+
? undefined
|
|
148
|
+
: {
|
|
149
|
+
logo: {
|
|
150
|
+
src: fileUrl(logoFile),
|
|
151
|
+
caption: isMail ? t("documentDesigner.editor.logoCaption") : "",
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
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 }));
|
|
155
|
+
return (_jsxs(EditorFrame, { testId: "document-designer-layout-editor", children: [_jsx(EditorHeader, { title: t(kind === "mail-layout"
|
|
156
|
+
? "documentDesigner.editor.title.mail"
|
|
157
|
+
: "documentDesigner.editor.title.pdf"), stored: shownRow, actions: {
|
|
158
|
+
busy,
|
|
159
|
+
canSave,
|
|
160
|
+
canPublish,
|
|
161
|
+
onSave: () => void save(false),
|
|
162
|
+
onPublish: () => void save(true),
|
|
163
|
+
}, children: _jsx(ModeSwitch, { value: kind, onChange: requestKind, ariaLabel: t("documentDesigner.editor.kindSwitch"), testId: "document-designer-kind-switch", options: [
|
|
164
|
+
{ value: "mail-layout", label: t("documentDesigner.editor.kind.mail") },
|
|
165
|
+
{ value: "pdf-layout", label: t("documentDesigner.editor.kind.pdf") },
|
|
166
|
+
] }) }), _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: [
|
|
167
|
+
{ value: "draft", label: t("documentDesigner.editor.tab.draft") },
|
|
168
|
+
{ value: "mjml", label: t("documentDesigner.editor.tab.mjml") },
|
|
169
|
+
] }) }))] }), _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
|
|
170
|
+
? "documentDesigner.editor.mjmlNotice.platform"
|
|
171
|
+
: "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
|
|
172
|
+
? "documentDesigner.editor.slot.mail"
|
|
173
|
+
: "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: () => {
|
|
174
|
+
setHeader(source.regions.header);
|
|
175
|
+
setSignature(source.regions.signature);
|
|
176
|
+
setFooter(source.regions.footer);
|
|
177
|
+
setMjml(source.mjmlSource ?? "");
|
|
178
|
+
setAppearance(storedAppearance);
|
|
179
|
+
setSaveError(null);
|
|
180
|
+
}, onSave: () => void save(false), onPublish: () => void save(true) }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
|
|
181
|
+
if (!open)
|
|
182
|
+
setPendingKind(null);
|
|
183
|
+
}, title: t("documentDesigner.editor.dirtySwitch.title"), description: t("documentDesigner.editor.dirtySwitch.description"), confirmLabel: t("documentDesigner.editor.dirtySwitch.confirm"), variant: "danger", onConfirm: () => {
|
|
184
|
+
if (pendingKind !== null)
|
|
185
|
+
onKindChange(pendingKind);
|
|
186
|
+
} })] }));
|
|
187
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { useNav, useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
|
|
8
|
+
import { LayoutEditorBody } from "./layout-editor-body.js";
|
|
9
|
+
import { layoutUserKind } from "./layout-users.js";
|
|
10
|
+
import { parseOverviewRow } from "./overview-row.js";
|
|
11
|
+
function parseLayoutKind(raw) {
|
|
12
|
+
return raw === "pdf-layout" ? "pdf-layout" : "mail-layout";
|
|
13
|
+
}
|
|
14
|
+
function LayoutEditorLoader({ kind, slug, isSystemAdmin, onKindChange, }) {
|
|
15
|
+
const source = useQuery(TemplateResolverQueries.layoutSource, { kind, slug });
|
|
16
|
+
const overview = useQuery(TemplateResolverQueries.overview, {
|
|
17
|
+
limit: 200,
|
|
18
|
+
layoutSlug: slug,
|
|
19
|
+
filters: [{ field: "kind", op: "in", value: [layoutUserKind(kind)] }],
|
|
20
|
+
});
|
|
21
|
+
const overrides = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind }, { enabled: isSystemAdmin });
|
|
22
|
+
if (source.error !== null) {
|
|
23
|
+
return (_jsx(ErrorState, { error: source.error, onRetry: () => void source.refetch(), testId: "document-designer-layout-error" }));
|
|
24
|
+
}
|
|
25
|
+
if (source.data === null) {
|
|
26
|
+
return _jsx(LoadingState, {});
|
|
27
|
+
}
|
|
28
|
+
const data = source.data;
|
|
29
|
+
const rows = (overview.data?.rows ?? []).flatMap((row) => parseOverviewRow(row) ?? []);
|
|
30
|
+
const reload = async () => {
|
|
31
|
+
await Promise.all([source.refetch(), overview.refetch(), overrides.refetch()]);
|
|
32
|
+
};
|
|
33
|
+
// A saved or reset layout is a new version: remount so the editors take the stored regions.
|
|
34
|
+
const bodyKey = [kind, slug, data.tenant?.id, data.tenant?.version, data.system?.version].join("|");
|
|
35
|
+
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));
|
|
36
|
+
}
|
|
37
|
+
export function LayoutEditor() {
|
|
38
|
+
const nav = useNav();
|
|
39
|
+
const roles = useUserRoles();
|
|
40
|
+
const [kind, setKind] = useState(() => parseLayoutKind(nav.searchParams["kind"]));
|
|
41
|
+
const slug = nav.searchParams["slug"] ?? DEFAULT_LAYOUT_SLUG;
|
|
42
|
+
return (_jsx(LayoutEditorLoader, { kind: kind, slug: slug, isSystemAdmin: roles?.includes("SystemAdmin") === true, onKindChange: setKind }, kind));
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type LayoutHtmlProps = {
|
|
3
|
+
readonly html: string;
|
|
4
|
+
/** Label per system placeholder (`data-variable`), shown as a chip. */
|
|
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;
|
|
8
|
+
};
|
|
9
|
+
export declare function LayoutHtml(props: LayoutHtmlProps): ReactNode;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
4
|
+
import { createElement, useMemo } from "react";
|
|
5
|
+
// Layout regions arrive server-sanitised, but the preview still renders them
|
|
6
|
+
// through a whitelist instead of innerHTML so a sanitiser gap can never run
|
|
7
|
+
// markup in the admin session. Unknown elements are unwrapped, scripts dropped.
|
|
8
|
+
const TAG_CLASSES = {
|
|
9
|
+
div: "",
|
|
10
|
+
p: "m-0",
|
|
11
|
+
span: "",
|
|
12
|
+
strong: "font-semibold",
|
|
13
|
+
b: "font-semibold",
|
|
14
|
+
em: "italic",
|
|
15
|
+
i: "italic",
|
|
16
|
+
u: "underline",
|
|
17
|
+
small: "text-xs",
|
|
18
|
+
h1: "m-0 text-lg font-semibold",
|
|
19
|
+
h2: "m-0 text-base font-semibold",
|
|
20
|
+
h3: "m-0 text-sm font-semibold",
|
|
21
|
+
table: "w-full border-collapse",
|
|
22
|
+
thead: "",
|
|
23
|
+
tbody: "",
|
|
24
|
+
tr: "",
|
|
25
|
+
td: "align-top",
|
|
26
|
+
th: "align-top text-left font-semibold",
|
|
27
|
+
ul: "m-0 list-disc pl-5",
|
|
28
|
+
ol: "m-0 list-decimal pl-5",
|
|
29
|
+
li: "",
|
|
30
|
+
a: "underline",
|
|
31
|
+
hr: "my-1 border-border",
|
|
32
|
+
br: "",
|
|
33
|
+
img: "max-h-12 max-w-full object-contain",
|
|
34
|
+
};
|
|
35
|
+
const VOID_TAGS = new Set(["br", "hr", "img"]);
|
|
36
|
+
const DROPPED_TAGS = new Set(["script", "style", "iframe", "object", "embed"]);
|
|
37
|
+
const SAFE_IMAGE_SRC = /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/=]+$/;
|
|
38
|
+
const ALIGN_CLASSES = {
|
|
39
|
+
left: "text-left",
|
|
40
|
+
center: "text-center",
|
|
41
|
+
right: "text-right",
|
|
42
|
+
justify: "text-justify",
|
|
43
|
+
};
|
|
44
|
+
const ELEMENT_NODE = 1;
|
|
45
|
+
const TEXT_NODE = 3;
|
|
46
|
+
function renderChildren(node, props, path) {
|
|
47
|
+
return Array.from(node.childNodes).map((child, index) => renderNode(child, props, `${path}.${index}`));
|
|
48
|
+
}
|
|
49
|
+
function renderNode(node, props, path) {
|
|
50
|
+
if (node.nodeType === TEXT_NODE)
|
|
51
|
+
return node.textContent;
|
|
52
|
+
if (node.nodeType !== ELEMENT_NODE)
|
|
53
|
+
return null;
|
|
54
|
+
const tag = node.nodeName.toLowerCase();
|
|
55
|
+
if (DROPPED_TAGS.has(tag))
|
|
56
|
+
return null;
|
|
57
|
+
if (!(node instanceof Element))
|
|
58
|
+
return null;
|
|
59
|
+
if (!(tag in TAG_CLASSES))
|
|
60
|
+
return _jsx("span", { children: renderChildren(node, props, path) }, path);
|
|
61
|
+
const variable = node.getAttribute("data-variable");
|
|
62
|
+
if (variable !== null) {
|
|
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
|
+
}
|
|
65
|
+
const align = ALIGN_CLASSES[node.getAttribute("data-align") ?? ""];
|
|
66
|
+
const className = cn(TAG_CLASSES[tag], align, node.getAttribute("data-size") === "small" && "text-xs");
|
|
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
|
+
}
|
|
80
|
+
const src = node.getAttribute("src") ?? "";
|
|
81
|
+
if (!SAFE_IMAGE_SRC.test(src))
|
|
82
|
+
return null;
|
|
83
|
+
return createElement("img", { key: path, className, src, alt: node.getAttribute("alt") ?? "" });
|
|
84
|
+
}
|
|
85
|
+
if (VOID_TAGS.has(tag))
|
|
86
|
+
return createElement(tag, { key: path, className });
|
|
87
|
+
return createElement(tag, { key: path, className }, ...renderChildren(node, props, path));
|
|
88
|
+
}
|
|
89
|
+
export function LayoutHtml(props) {
|
|
90
|
+
const { html } = props;
|
|
91
|
+
const body = useMemo(() => new DOMParser().parseFromString(html, "text/html").body, [html]);
|
|
92
|
+
return _jsx(_Fragment, { children: renderChildren(body, props, "r") });
|
|
93
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type LayoutAppearance } from "./layout-meta.js";
|
|
3
|
+
import type { BrandingView, LayoutKind, LayoutValidation } from "./layout-types.js";
|
|
4
|
+
import type { OverviewRowView } from "./overview-row.js";
|
|
5
|
+
export declare function BrandingRail({ data, unavailable, }: {
|
|
6
|
+
readonly data: BrandingView | null;
|
|
7
|
+
readonly unavailable: boolean;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
export declare function PlaceholderRail({ validation, }: {
|
|
10
|
+
readonly validation: LayoutValidation | null;
|
|
11
|
+
}): ReactNode;
|
|
12
|
+
export declare function RendererRail({ kind }: {
|
|
13
|
+
readonly kind: LayoutKind;
|
|
14
|
+
}): ReactNode;
|
|
15
|
+
export declare function AppearanceRail({ appearance, onChange, fromMjml, }: {
|
|
16
|
+
readonly appearance: LayoutAppearance;
|
|
17
|
+
readonly onChange: (next: LayoutAppearance) => void;
|
|
18
|
+
/** An mjml layout brings its own look, so the controls would have no effect. */
|
|
19
|
+
readonly fromMjml: boolean;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
export declare function UsedByRail({ layoutSlug, kind, rows, }: {
|
|
22
|
+
readonly layoutSlug: string;
|
|
23
|
+
readonly kind: LayoutKind;
|
|
24
|
+
readonly rows: readonly OverviewRowView[];
|
|
25
|
+
}): ReactNode;
|
|
26
|
+
export declare function MjmlSourceRail({ mjmlSource }: {
|
|
27
|
+
readonly mjmlSource: string | null;
|
|
28
|
+
}): ReactNode;
|