@cosmicdrift/kumiko-document-designer 0.388.0 → 0.390.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/feature.js +11 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/list-definition.d.ts +3 -1
  5. package/dist/list-definition.js +10 -3
  6. package/dist/web/client-plugin.js +6 -2
  7. package/dist/web/constants.d.ts +1 -0
  8. package/dist/web/constants.js +1 -0
  9. package/dist/web/i18n.js +1 -0
  10. package/dist/web/layout-editor-body.d.ts +3 -14
  11. package/dist/web/layout-editor-body.js +25 -172
  12. package/dist/web/layout-editor-center.d.ts +7 -0
  13. package/dist/web/layout-editor-center.js +23 -0
  14. package/dist/web/layout-editor-logic.d.ts +97 -0
  15. package/dist/web/layout-editor-logic.js +143 -0
  16. package/dist/web/layout-editor-paper.d.ts +4 -0
  17. package/dist/web/layout-editor-paper.js +24 -0
  18. package/dist/web/layout-editor.d.ts +3 -1
  19. package/dist/web/layout-editor.js +7 -3
  20. package/dist/web/mail-editor-body.d.ts +4 -16
  21. package/dist/web/mail-editor-body.js +27 -204
  22. package/dist/web/mail-editor-logic.d.ts +70 -0
  23. package/dist/web/mail-editor-logic.js +81 -0
  24. package/dist/web/mail-editor-model.d.ts +105 -0
  25. package/dist/web/mail-editor-model.js +114 -0
  26. package/dist/web/mail-editor-render.d.ts +11 -0
  27. package/dist/web/mail-editor-render.js +51 -0
  28. package/dist/web/mail-preview-render.d.ts +5 -0
  29. package/dist/web/mail-preview-render.js +22 -0
  30. package/dist/web/pdf-editor-actions.d.ts +27 -0
  31. package/dist/web/pdf-editor-actions.js +146 -0
  32. package/dist/web/pdf-editor-chrome.d.ts +6 -0
  33. package/dist/web/pdf-editor-chrome.js +60 -0
  34. package/dist/web/pdf-editor-context.d.ts +22 -0
  35. package/dist/web/pdf-editor-context.js +1 -0
  36. package/dist/web/pdf-editor-draft.d.ts +23 -0
  37. package/dist/web/pdf-editor-draft.js +33 -0
  38. package/dist/web/pdf-editor-panes.d.ts +6 -0
  39. package/dist/web/pdf-editor-panes.js +40 -0
  40. package/dist/web/pdf-editor-view.d.ts +52 -0
  41. package/dist/web/pdf-editor-view.js +89 -0
  42. package/dist/web/pdf-template-editor.d.ts +1 -1
  43. package/dist/web/pdf-template-editor.js +40 -342
  44. package/dist/web/use-layout-editor-state.d.ts +62 -0
  45. package/dist/web/use-layout-editor-state.js +155 -0
  46. package/dist/web/use-mail-editor-parts.d.ts +42 -0
  47. package/dist/web/use-mail-editor-parts.js +74 -0
  48. package/dist/web/use-mail-editor-state.d.ts +89 -0
  49. package/dist/web/use-mail-editor-state.js +55 -0
  50. package/dist/web/use-pdf-editor-state.d.ts +77 -0
  51. package/dist/web/use-pdf-editor-state.js +161 -0
  52. package/package.json +6 -6
  53. package/src/changes.json +30 -0
package/dist/feature.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { access, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { TEMPLATE_FACETS, templateColumns, templateRowActions } from "./list-definition.js";
3
- import { DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
3
+ import { DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
4
4
  import { DOCUMENT_DESIGNER_I18N } from "./web/i18n.js";
5
5
  const OVERVIEW_QUERY = "template-resolver:query:overview";
6
6
  export function createDocumentDesignerFeature(options = {}) {
@@ -20,7 +20,7 @@ export function createDocumentDesignerFeature(options = {}) {
20
20
  query: OVERVIEW_QUERY,
21
21
  columns: templateColumns({ platform: false }),
22
22
  facets: [...TEMPLATE_FACETS],
23
- rowActions: templateRowActions(),
23
+ rowActions: templateRowActions({ platform: false }),
24
24
  searchable: true,
25
25
  searchPlaceholder: i18nKey("documentDesigner.search"),
26
26
  pagination: "pages",
@@ -41,7 +41,7 @@ export function createDocumentDesignerFeature(options = {}) {
41
41
  query: OVERVIEW_QUERY,
42
42
  columns: templateColumns({ platform: true }),
43
43
  facets: [...TEMPLATE_FACETS],
44
- rowActions: templateRowActions(),
44
+ rowActions: templateRowActions({ platform: true }),
45
45
  searchable: true,
46
46
  searchPlaceholder: i18nKey("documentDesigner.search"),
47
47
  pagination: "pages",
@@ -64,6 +64,14 @@ export function createDocumentDesignerFeature(options = {}) {
64
64
  dormant: true,
65
65
  access: { roles: access.roles(...editorRoles) },
66
66
  });
67
+ r.screen({
68
+ id: PLATFORM_LAYOUT_EDITOR_SCREEN_ID,
69
+ type: "custom",
70
+ renderer: { react: { __component: PLATFORM_LAYOUT_EDITOR_SCREEN_ID } },
71
+ description: "documentDesigner.screen.layoutEditor.description",
72
+ dormant: true,
73
+ access: { roles: access.systemAdmin },
74
+ });
67
75
  r.screen({
68
76
  id: PDF_TEMPLATE_EDITOR_SCREEN_ID,
69
77
  type: "custom",
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { createDocumentDesignerFeature, type DocumentDesignerOptions } from "./feature.ts";
2
- export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, type TemplateEditorKind, } from "./web/constants.ts";
2
+ export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, type TemplateEditorKind, } from "./web/constants.ts";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { createDocumentDesignerFeature } from "./feature.js";
2
- export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
2
+ export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
@@ -3,4 +3,6 @@ export declare function templateColumns(options: {
3
3
  readonly platform: boolean;
4
4
  }): ListColumnSpec[];
5
5
  export declare const TEMPLATE_FACETS: readonly ListFacetSpec[];
6
- export declare function templateRowActions(): RowAction[];
6
+ export declare function templateRowActions(options: {
7
+ readonly platform: boolean;
8
+ }): RowAction[];
@@ -1,6 +1,6 @@
1
1
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
2
2
  import { i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
3
- import { DocumentDesignerCells, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
3
+ import { DocumentDesignerCells, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
4
4
  function cell(component) {
5
5
  return { react: { __component: component } };
6
6
  }
@@ -85,6 +85,9 @@ export const TEMPLATE_FACETS = [
85
85
  ],
86
86
  },
87
87
  ];
88
+ function isLayoutKind(kind) {
89
+ return kind === "mail-layout" || kind === "pdf-layout";
90
+ }
88
91
  const EDITABLE_KINDS = [
89
92
  "mail",
90
93
  "document-pdf",
@@ -101,9 +104,13 @@ const RESET_LOCKED_OVERRIDE_ACTION = {
101
104
  style: "danger",
102
105
  visible: { field: "canResetLockedOverride", eq: true },
103
106
  };
104
- export function templateRowActions() {
107
+ // The screen the row is opened from fixes the layout scope: the platform overview edits the system
108
+ // layout, the tenant overview always the tenant's own, even for a SystemAdmin on a system-origin row.
109
+ export function templateRowActions(options) {
105
110
  const editActions = EDITABLE_KINDS.flatMap((kind) => {
106
- const screen = TEMPLATE_EDITOR_SCREENS[kind];
111
+ const screen = options.platform && isLayoutKind(kind)
112
+ ? PLATFORM_LAYOUT_EDITOR_SCREEN_ID
113
+ : TEMPLATE_EDITOR_SCREENS[kind];
107
114
  if (screen === undefined)
108
115
  return [];
109
116
  return [
@@ -4,14 +4,17 @@ import { mergeTranslations, translationsByLocaleFromKeys, useNav, } from "@cosmi
4
4
  import { LoadingState } from "@cosmicdrift/kumiko-renderer/web";
5
5
  import { lazy, Suspense } from "react";
6
6
  import { createLocalesCell, KindCell, ModifiedCell, OriginCell, OverriddenByCell, StatusCell, TitleCell, UsedByCell, } from "./cells.js";
7
- import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, } from "./constants.js";
7
+ import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, } from "./constants.js";
8
8
  import { documentDesignerTranslations } from "./default-translations.js";
9
9
  import { DOCUMENT_DESIGNER_I18N } from "./i18n.js";
10
10
  // The editors pull tiptap, MJML preview and the page canvas; load them on first mount.
11
11
  const LazyLayoutEditor = lazy(() => import("./layout-editor.js").then((module) => ({ default: module.LayoutEditor })));
12
12
  const LazyPdfTemplateEditor = lazy(() => import("./pdf-template-editor.js").then((module) => ({ default: module.PdfTemplateEditor })));
13
13
  function LayoutEditorScreen() {
14
- return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, {}) }));
14
+ return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, { scope: "tenant" }) }));
15
+ }
16
+ function PlatformLayoutEditorScreen() {
17
+ return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, { scope: "system" }) }));
15
18
  }
16
19
  function createLazyMailEditor(appLocales) {
17
20
  const LazyMailEditor = lazy(() => import("./mail-editor.js").then((module) => ({
@@ -47,6 +50,7 @@ export function documentDesignerClient(options = {}) {
47
50
  },
48
51
  components: {
49
52
  [LAYOUT_EDITOR_SCREEN_ID]: LayoutEditorScreen,
53
+ [PLATFORM_LAYOUT_EDITOR_SCREEN_ID]: PlatformLayoutEditorScreen,
50
54
  [PDF_TEMPLATE_EDITOR_SCREEN_ID]: PdfTemplateEditorScreen,
51
55
  [MAIL_EDITOR_SCREEN_ID]: createLazyMailEditor(options.appLocales ?? DEFAULT_TEMPLATE_LOCALES),
52
56
  },
@@ -2,6 +2,7 @@ export declare const DOCUMENT_DESIGNER_FEATURE: "document-designer";
2
2
  export declare const TEMPLATES_SCREEN_ID: "templates";
3
3
  export declare const PLATFORM_TEMPLATES_SCREEN_ID: "platform-templates";
4
4
  export declare const LAYOUT_EDITOR_SCREEN_ID: "layout-editor";
5
+ export declare const PLATFORM_LAYOUT_EDITOR_SCREEN_ID: "platform-layout-editor";
5
6
  export declare const PDF_TEMPLATE_EDITOR_SCREEN_ID: "pdf-template-editor";
6
7
  export declare const MAIL_EDITOR_SCREEN_ID: "mail-editor";
7
8
  export declare const TEMPLATES_NAV_ID: "templates";
@@ -3,6 +3,7 @@ export const DOCUMENT_DESIGNER_FEATURE = "document-designer";
3
3
  export const TEMPLATES_SCREEN_ID = "templates";
4
4
  export const PLATFORM_TEMPLATES_SCREEN_ID = "platform-templates";
5
5
  export const LAYOUT_EDITOR_SCREEN_ID = "layout-editor";
6
+ export const PLATFORM_LAYOUT_EDITOR_SCREEN_ID = "platform-layout-editor";
6
7
  export const PDF_TEMPLATE_EDITOR_SCREEN_ID = "pdf-template-editor";
7
8
  export const MAIL_EDITOR_SCREEN_ID = "mail-editor";
8
9
  export const TEMPLATES_NAV_ID = "templates";
package/dist/web/i18n.js CHANGED
@@ -3,6 +3,7 @@ export const DOCUMENT_DESIGNER_I18N = {
3
3
  "screen:templates.title": { en: "Templates", de: "Vorlagen" },
4
4
  "screen:platform-templates.title": { en: "System templates", de: "System-Vorlagen" },
5
5
  "screen:layout-editor.title": { en: "Layout", de: "Layout" },
6
+ "screen:platform-layout-editor.title": { en: "System layout", de: "System-Layout" },
6
7
  "screen:pdf-template-editor.title": { en: "PDF template", de: "PDF-Vorlage" },
7
8
  "document-designer:nav.templates": { en: "Templates", de: "Vorlagen" },
8
9
  "document-designer:nav.platformTemplates": { en: "System templates", de: "System-Vorlagen" },
@@ -1,14 +1,3 @@
1
- import { type ReactNode } from "react";
2
- import type { LayoutKind, LayoutSourceView, TenantOverrideCounts } from "./layout-types.ts";
3
- import type { OverviewRowView } from "./overview-row.ts";
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;
1
+ import type { ReactNode } from "react";
2
+ import { type LayoutEditorBodyProps } from "./use-layout-editor-state.ts";
3
+ export declare function LayoutEditorBody(props: LayoutEditorBodyProps): ReactNode;
@@ -1,187 +1,40 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
3
- import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
4
- import { pluralKey } from "@cosmicdrift/kumiko-renderer/headless";
5
- import { cn, ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer/web";
6
- import { useState } from "react";
7
- import { composeLayoutHtml } from "./compose-layout.js";
8
- import { BRANDING_QUERY_QN } from "./constants.js";
2
+ import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
3
+ import { ModeSwitch } from "@cosmicdrift/kumiko-renderer/web";
9
4
  import { EditorHeader } from "./editor-header.js";
10
5
  import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
11
- import { fileUrl } from "./file-name.js";
12
- import { appearanceOf, isHexColor, metaToSave, } from "./layout-meta.js";
6
+ import { renderCenter } from "./layout-editor-center.js";
13
7
  import { AppearanceRail, BrandingRail, MjmlSourceRail, PlaceholderRail, RendererRail, UsedByRail, } from "./layout-rails.js";
14
- import { systemLabelMap } from "./mail-draft.js";
15
8
  import { OriginBar } from "./origin-bar.js";
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;
20
- const REGION_EDITOR_IDS = {
21
- header: "document-designer-region-header",
22
- signature: "document-designer-region-signature",
23
- footer: "document-designer-region-footer",
24
- };
25
- const PAPER_WIDTH_CLASS = {
26
- 560: "max-w-[560px]",
27
- 600: "max-w-[600px]",
28
- 640: "max-w-[640px]",
29
- };
30
- function issueText(t, issue) {
31
- const translated = t(issue.i18nKey, issue.params);
32
- return translated === issue.i18nKey ? issue.code : translated;
9
+ import { useLayoutEditorState, } from "./use-layout-editor-state.js";
10
+ function renderLeftRail(props, m) {
11
+ const { branding } = m;
12
+ return (_jsxs(_Fragment, { children: [_jsx(BrandingRail, { data: branding.data, unavailable: branding.error !== null || (branding.data === null && !branding.loading) }), _jsx(PlaceholderRail, { validation: m.checked }), _jsx(RendererRail, { kind: props.kind })] }));
33
13
  }
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);
14
+ function renderRightRail(props, m) {
15
+ return (_jsxs(_Fragment, { children: [m.isMail && (_jsx(AppearanceRail, { appearance: m.appearance, onChange: m.setAppearance, fromMjml: m.appearanceFromMjml })), m.isMail && !m.mjmlMode && _jsx(MjmlSourceRail, { mjmlSource: props.source.mjmlSource }), _jsx(UsedByRail, { layoutSlug: props.slug, kind: props.kind, rows: props.overviewRows })] }));
40
16
  }
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] }));
17
+ function renderKindSwitch(t, props, m) {
18
+ return (_jsx(ModeSwitch, { value: props.kind, onChange: m.requestKind, ariaLabel: t("documentDesigner.editor.kindSwitch"), testId: "document-designer-kind-switch", options: [
19
+ { value: "mail-layout", label: t("documentDesigner.editor.kind.mail") },
20
+ { value: "pdf-layout", label: t("documentDesigner.editor.kind.pdf") },
21
+ ] }));
44
22
  }
45
- export function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, usedByCount, overrides, onKindChange, onReload, }) {
23
+ export function LayoutEditorBody(props) {
24
+ const { kind, isSystemAdmin, source, overrides, onKindChange, onReload } = props;
46
25
  const t = useTranslation();
47
26
  const { Banner, Dialog } = usePrimitives();
48
- const isMail = kind === "mail-layout";
49
- const showMjmlTab = isSystemAdmin && source.mjmlSource !== null;
50
- const [tab, setTab] = useState(showMjmlTab && source.effectiveFormat === "mjml" ? "mjml" : "draft");
51
- const [header, setHeader] = useState(source.regions.header);
52
- const [signature, setSignature] = useState(source.regions.signature);
53
- const [footer, setFooter] = useState(source.regions.footer);
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);
59
- const [pendingKind, setPendingKind] = useState(null);
60
- const [saveError, setSaveError] = useState(null);
61
- const upsertTenant = useMutation(TemplateResolverHandlers.upsertTenant);
62
- const upsertSystem = useMutation(TemplateResolverHandlers.upsertSystem);
63
- const publishSystem = useMutation(TemplateResolverHandlers.publishSystem);
64
- const branding = useQuery(BRANDING_QUERY_QN, {});
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);
71
- const htmlDirty = header !== source.regions.header ||
72
- signature !== source.regions.signature ||
73
- footer !== source.regions.footer ||
74
- appearanceDirty;
75
- const dirty = mjmlMode ? mjml !== (source.mjmlSource ?? "") : htmlDirty;
76
- const content = mjmlMode
77
- ? mjml
78
- : composeLayoutHtml({ header, signature: isMail ? signature : "", footer });
79
- const debouncedContent = useDebouncedValue(content, VALIDATE_DEBOUNCE_MS);
80
- const validation = useQuery(TemplateResolverQueries.validate, {
81
- slug,
82
- kind,
83
- contentFormat: mjmlMode ? "mjml" : "html",
84
- content: debouncedContent,
85
- locale: source.locale,
86
- scope: isSystemAdmin ? "system" : "tenant",
87
- });
88
- const checked = validation.data;
89
- const shownRow = isSystemAdmin ? source.system : (source.tenant ?? source.system);
90
- const userCount = usedByCount;
91
- const placeholders = checked?.systemPlaceholders ?? [];
92
- const placeholderLabels = systemLabelMap(t, placeholders);
93
- const requestKind = (next) => {
94
- if (next === kind)
95
- return;
96
- if (dirty)
97
- setPendingKind(next);
98
- else
99
- onKindChange(next);
100
- };
101
- const save = async (publish) => {
102
- setSaveError(null);
103
- const base = {
104
- slug,
105
- kind,
106
- locale: source.locale,
107
- content,
108
- contentFormat: mjmlMode ? "mjml" : "html",
109
- meta: appearanceEditable ? metaToSave(appearance, source.meta) : isMail ? source.meta : {},
110
- };
111
- const saved = isSystemAdmin
112
- ? await upsertSystem.mutate(base)
113
- : await upsertTenant.mutate({ ...base, status: publish ? "active" : "draft" });
114
- if (!saved.isSuccess) {
115
- setSaveError(dispatcherErrorText(saved.error, t));
116
- return;
117
- }
118
- if (isSystemAdmin && publish) {
119
- const published = await publishSystem.mutate({ id: saved.data.id });
120
- if (!published.isSuccess) {
121
- setSaveError(dispatcherErrorText(published.error, t));
122
- return;
123
- }
124
- }
125
- await onReload();
126
- };
127
- const busy = upsertTenant.pending || upsertSystem.pending || publishSystem.pending;
128
- const shownStatus = shownRow === null ? null : asRowStatus(shownRow.status);
129
- const canSave = dirty && checked?.canSave === true && appearanceValid && !busy;
130
- const canPublish = checked?.canPublish === true && appearanceValid && (dirty || shownStatus === "draft") && !busy;
131
- const blockingIssue = checked === null ? undefined : [...checked.issues, ...checked.publishIssues][0];
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(pluralKey(isMail
141
- ? "documentDesigner.editor.footer.okMail"
142
- : "documentDesigner.editor.footer.okPdf", userCount), { 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 and an images block no photos.
147
- const blocks = (isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS).filter((block) => block !== "steps" && block !== "images");
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 }));
27
+ const m = useLayoutEditorState(props, t);
28
+ const { pendingKind, setPendingKind } = m;
159
29
  return (_jsxs(EditorFrame, { testId: "document-designer-layout-editor", children: [_jsx(EditorHeader, { title: t(kind === "mail-layout"
160
30
  ? "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
174
- ? "documentDesigner.editor.mjmlNotice.platform"
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) => {
31
+ : "documentDesigner.editor.title.pdf"), stored: m.shownRow, actions: {
32
+ busy: m.busy,
33
+ canSave: m.canSave,
34
+ canPublish: m.canPublish,
35
+ onSave: () => void m.save(false),
36
+ onPublish: () => void m.save(true),
37
+ }, children: renderKindSwitch(t, props, m) }), _jsx(OriginBar, { isSystemAdmin: isSystemAdmin, source: source, kind: kind, userCount: m.userCount, overrides: overrides, onReset: () => void onReload(), onResetError: m.setSaveError }), _jsx(EditorBody, { left: renderLeftRail(props, m), center: renderCenter(Banner, t, props, m), right: renderRightRail(props, m) }), _jsx(EditorFooter, { testIdPrefix: "document-designer", message: m.footerMessage, saveError: m.saveError, modifiedAt: null, dirty: m.dirty, busy: m.busy, canSave: m.canSave, canPublish: m.canPublish, onDiscard: m.discard, onSave: () => void m.save(false), onPublish: () => void m.save(true) }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
185
38
  if (!open)
186
39
  setPendingKind(null);
187
40
  }, title: t("documentDesigner.editor.dirtySwitch.title"), description: t("documentDesigner.editor.dirtySwitch.description"), confirmLabel: t("documentDesigner.editor.dirtySwitch.confirm"), variant: "danger", onConfirm: () => {
@@ -0,0 +1,7 @@
1
+ import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ import type { Translate } from "./layout-editor-logic.ts";
4
+ import type { LayoutEditorBodyProps, LayoutEditorModel } from "./use-layout-editor-state.ts";
5
+ type Banner = ReturnType<typeof usePrimitives>["Banner"];
6
+ export declare function renderCenter(Banner: Banner, t: Translate, props: LayoutEditorBodyProps, m: LayoutEditorModel): ReactNode;
7
+ export {};
@@ -0,0 +1,23 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer/web";
3
+ import { renderPaper } from "./layout-editor-paper.js";
4
+ function renderToolbarRow(t, m) {
5
+ return (_jsxs("div", { className: "flex items-center border-b bg-background", children: [_jsx("div", { ref: m.setToolbarHost, className: "min-w-0 flex-1" }), m.showMjmlTab && (_jsx("div", { className: "shrink-0 px-3 py-1", children: _jsx(ModeSwitch, { value: m.tab, onChange: m.setTab, ariaLabel: t("documentDesigner.editor.tabSwitch"), testId: "layout-mjml-tab", options: [
6
+ { value: "draft", label: t("documentDesigner.editor.tab.draft") },
7
+ { value: "mjml", label: t("documentDesigner.editor.tab.mjml") },
8
+ ] }) }))] }));
9
+ }
10
+ function renderMjmlNotice(Banner, t, props) {
11
+ return (_jsx(Banner, { variant: "info", testId: "document-designer-mjml-notice", children: t(props.isSystemAdmin
12
+ ? "documentDesigner.editor.mjmlNotice.platform"
13
+ : "documentDesigner.editor.mjmlNotice.tenant") }));
14
+ }
15
+ function renderDraftCanvas(Banner, t, props, m) {
16
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [props.source.effectiveFormat === "mjml" && renderMjmlNotice(Banner, t, props), renderPaper(t, m)] }));
17
+ }
18
+ function renderMjmlSource(t, m) {
19
+ return (_jsx(TextareaField, { id: "document-designer-mjml-source", name: "mjml", label: t("documentDesigner.editor.mjml.label"), value: m.mjml, onChange: m.setMjml, rows: 24, testId: "document-designer-mjml-source" }));
20
+ }
21
+ export function renderCenter(Banner, t, props, m) {
22
+ return (_jsxs(_Fragment, { children: [renderToolbarRow(t, m), _jsx("div", { className: "min-h-0 flex-1 overflow-auto px-8 py-6", children: m.mjmlMode ? renderMjmlSource(t, m) : renderDraftCanvas(Banner, t, props, m) })] }));
23
+ }
@@ -0,0 +1,97 @@
1
+ import { type useMutation } from "@cosmicdrift/kumiko-renderer";
2
+ import type { FooterTone } from "./editor-shell.tsx";
3
+ import { type LayoutAppearance, type LayoutWidth } from "./layout-meta.ts";
4
+ import type { BrandingView, LayoutKind, LayoutSourceView, LayoutValidation, UpsertResponse, ValidationIssue } from "./layout-types.ts";
5
+ export type EditorTab = "draft" | "mjml";
6
+ export type Region = "header" | "signature" | "footer";
7
+ export type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
8
+ export declare const REGION_EDITOR_IDS: Readonly<Record<Region, string>>;
9
+ export declare const PAPER_WIDTH_CLASS: Readonly<Record<LayoutWidth, string>>;
10
+ export type FooterMessage = {
11
+ readonly tone: FooterTone;
12
+ readonly text: string;
13
+ } | null;
14
+ export type ResourcePreviews = Readonly<Record<string, {
15
+ readonly src: string;
16
+ readonly caption: string;
17
+ }>>;
18
+ export declare function issueText(t: Translate, issue: ValidationIssue): string;
19
+ export declare function sameAppearance(a: LayoutAppearance, b: LayoutAppearance): boolean;
20
+ export declare function initialTab(showMjmlTab: boolean, source: LayoutSourceView): EditorTab;
21
+ export declare function layoutDirty(input: {
22
+ readonly mjmlMode: boolean;
23
+ readonly mjml: string;
24
+ readonly header: string;
25
+ readonly signature: string;
26
+ readonly footer: string;
27
+ readonly appearance: LayoutAppearance;
28
+ readonly storedAppearance: LayoutAppearance;
29
+ readonly appearanceEditable: boolean;
30
+ readonly source: LayoutSourceView;
31
+ }): boolean;
32
+ export declare function layoutContent(input: {
33
+ readonly mjmlMode: boolean;
34
+ readonly mjml: string;
35
+ readonly header: string;
36
+ readonly signature: string;
37
+ readonly footer: string;
38
+ readonly isMail: boolean;
39
+ }): string;
40
+ export declare function footerMessageOf(input: {
41
+ readonly t: Translate;
42
+ readonly checked: LayoutValidation | null;
43
+ readonly appearanceValid: boolean;
44
+ readonly isMail: boolean;
45
+ readonly userCount: number;
46
+ }): FooterMessage;
47
+ export declare function layoutBlocks(isMail: boolean): ("align" | "button" | "divider" | "image" | "pageBreak" | "repeat" | "table")[];
48
+ export declare function resourcePreviewsOf(branding: BrandingView | null, isMail: boolean, t: Translate): ResourcePreviews | undefined;
49
+ type UpsertMutation = ReturnType<typeof useMutation<UpsertResponse>>;
50
+ type PublishMutation = ReturnType<typeof useMutation>;
51
+ export type SaveLayoutInput = {
52
+ readonly publish: boolean;
53
+ readonly kind: LayoutKind;
54
+ readonly slug: string;
55
+ readonly source: LayoutSourceView;
56
+ readonly content: string;
57
+ readonly mjmlMode: boolean;
58
+ readonly isMail: boolean;
59
+ readonly isSystemAdmin: boolean;
60
+ readonly appearanceEditable: boolean;
61
+ readonly appearance: LayoutAppearance;
62
+ readonly t: Translate;
63
+ readonly upsertTenant: UpsertMutation;
64
+ readonly upsertSystem: UpsertMutation;
65
+ readonly publishSystem: PublishMutation;
66
+ readonly setSaveError: (message: string | null) => void;
67
+ readonly onReload: () => Promise<void>;
68
+ };
69
+ export declare function saveLayout(input: SaveLayoutInput): Promise<void>;
70
+ export declare function accentLineShown(input: {
71
+ readonly isMail: boolean;
72
+ readonly appearanceEditable: boolean;
73
+ readonly appearance: LayoutAppearance;
74
+ readonly accentColor: string;
75
+ }): boolean;
76
+ export declare function appearanceFlags(input: {
77
+ readonly isMail: boolean;
78
+ readonly mjmlMode: boolean;
79
+ readonly source: LayoutSourceView;
80
+ readonly appearance: LayoutAppearance;
81
+ }): {
82
+ appearanceFromMjml: boolean;
83
+ appearanceEditable: boolean;
84
+ appearanceValid: boolean;
85
+ };
86
+ export declare function shownRowOf(isSystemAdmin: boolean, source: LayoutSourceView): import("./layout-types.ts").LayoutSourceRow | null;
87
+ export declare function layoutSaveFlags(input: {
88
+ readonly dirty: boolean;
89
+ readonly checked: LayoutValidation | null;
90
+ readonly appearanceValid: boolean;
91
+ readonly busy: boolean;
92
+ readonly shownRow: ReturnType<typeof shownRowOf>;
93
+ }): {
94
+ canSave: boolean;
95
+ canPublish: boolean;
96
+ };
97
+ export {};