@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.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/admin-shell/feature.d.ts +3 -0
- package/dist/admin-shell/feature.js +6 -1
- package/dist/agent-tools/agent-manifest.js +2 -1
- package/dist/agent-tools/filter-ops.d.ts +1 -1
- package/dist/agent-tools/filter-ops.js +1 -3
- package/dist/agent-tools/tool-dispatch.js +2 -1
- package/dist/auth-email-password/email-templates.d.ts +4 -26
- package/dist/auth-email-password/email-templates.js +9 -101
- package/dist/auth-email-password/feature.js +24 -6
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
- package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
- package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
- package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
- package/dist/auth-email-password/index.d.ts +2 -2
- package/dist/auth-email-password/index.js +1 -1
- package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
- package/dist/auth-email-password/magic-link-mail.js +19 -14
- package/dist/auth-email-password/mail-notifications.d.ts +32 -0
- package/dist/auth-email-password/mail-notifications.js +116 -0
- package/dist/auth-email-password/signup-activation.d.ts +1 -1
- package/dist/auth-email-password/signup-activation.js +2 -3
- package/dist/auth-foundation/types.js +2 -8
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa/hmac-token-codec.js +4 -4
- package/dist/auth-mfa/mfa-challenge-token.js +3 -3
- package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
- package/dist/auth-mfa/mfa-setup-token.js +8 -8
- package/dist/billing-foundation/constants.d.ts +3 -3
- package/dist/billing-foundation/constants.js +5 -4
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
- package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
- package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
- package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +2 -2
- package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
- package/dist/billing-foundation/subscription-tier-sync.js +3 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
- package/dist/cap-overview/web/cap-usage-cell.js +5 -4
- package/dist/channel-email/email-channel.d.ts +2 -2
- package/dist/channel-email/email-channel.js +89 -26
- package/dist/channel-email/feature.js +3 -3
- package/dist/channel-email/index.d.ts +1 -1
- package/dist/channel-email/index.js +1 -1
- package/dist/channel-email/pii-guard.js +10 -0
- package/dist/channel-email/smtp-transport.js +13 -0
- package/dist/channel-email/types.d.ts +8 -0
- package/dist/config/handlers/set.write.js +1 -2
- package/dist/custom-fields/lib/value-schema.js +10 -11
- package/dist/custom-fields/wire-user-data-rights.js +7 -6
- package/dist/data-retention/index.d.ts +1 -0
- package/dist/data-retention/index.js +1 -0
- package/dist/data-retention/run-retention-cleanup.js +6 -6
- package/dist/delivery/attachment-ref.d.ts +39 -0
- package/dist/delivery/attachment-ref.js +64 -0
- package/dist/delivery/channel-context.d.ts +8 -4
- package/dist/delivery/channel-context.js +76 -2
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +1 -0
- package/dist/delivery/delivery-service.d.ts +4 -1
- package/dist/delivery/delivery-service.js +38 -32
- package/dist/delivery/feature.js +1 -1
- package/dist/delivery/index.d.ts +5 -2
- package/dist/delivery/index.js +4 -1
- package/dist/delivery/jobs.js +25 -5
- package/dist/delivery/notify-factory.js +4 -1
- package/dist/delivery/testing.d.ts +4 -1
- package/dist/delivery/types.d.ts +20 -14
- package/dist/delivery/unsubscribe.js +4 -1
- package/dist/derivatives-sharp/render.js +1 -18
- package/dist/document-ingest-foundation/pages.js +4 -4
- package/dist/feature-toggles/toggle-sync-signal.js +4 -4
- package/dist/file-foundation/index.d.ts +1 -0
- package/dist/file-foundation/index.js +1 -0
- package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
- package/dist/file-foundation/read-file-ref-content.js +49 -0
- package/dist/folders/web/folder-manager.js +3 -2
- package/dist/form-draft/db/queries/cleanup.js +4 -4
- package/dist/form-draft/db/queries/draft-count.js +2 -2
- package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
- package/dist/form-draft/release-file-refs.js +3 -2
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
- package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +7 -7
- package/dist/inbound-mail-foundation/types.js +4 -4
- package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
- package/dist/inbound-provider-imap/imap-client.js +4 -3
- package/dist/ledger/recurring.js +1 -3
- package/dist/managed-pages/branding.d.ts +2 -0
- package/dist/managed-pages/branding.js +24 -2
- package/dist/managed-pages/feature.js +1 -0
- package/dist/managed-pages/i18n.js +1 -0
- package/dist/managed-pages/screens/branding-screen.js +8 -1
- package/dist/page-render/branding.d.ts +1 -0
- package/dist/page-render/branding.js +1 -0
- package/dist/page-render/markdown.js +1 -1
- package/dist/personal-access-tokens/feature.js +1 -1
- package/dist/personal-access-tokens/handlers/list.query.js +2 -1
- package/dist/personal-access-tokens/revoke-for-user.js +2 -1
- package/dist/renderer-foundation/api.d.ts +13 -13
- package/dist/renderer-foundation/api.js +1 -1
- package/dist/renderer-foundation/constants.js +2 -4
- package/dist/renderer-foundation/feature.js +7 -7
- package/dist/renderer-foundation/index.d.ts +2 -1
- package/dist/renderer-foundation/index.js +1 -0
- package/dist/renderer-foundation/types.d.ts +92 -32
- package/dist/renderer-pdf/feature.js +7 -1
- package/dist/sessions/db/queries/cleanup.js +2 -2
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/list.query.js +2 -1
- package/dist/shared/attachment-file-name.d.ts +3 -0
- package/dist/shared/attachment-file-name.js +35 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/is-tenant-db.js +1 -1
- package/dist/shared/mail-template-body.d.ts +4 -0
- package/dist/shared/mail-template-body.js +17 -0
- package/dist/shared/sharp-input-limits.d.ts +6 -0
- package/dist/shared/sharp-input-limits.js +22 -0
- package/dist/shared/signup-handover.js +2 -2
- package/dist/tags/handlers/assign-tag.write.js +3 -1
- package/dist/tags/web/tag-chip.js +2 -0
- package/dist/template-resolver/api.d.ts +30 -2
- package/dist/template-resolver/api.js +40 -5
- package/dist/template-resolver/compile-document-body.d.ts +8 -0
- package/dist/template-resolver/compile-document-body.js +163 -0
- package/dist/template-resolver/constants.d.ts +13 -4
- package/dist/template-resolver/constants.js +30 -10
- package/dist/template-resolver/document-sanitizer.d.ts +32 -0
- package/dist/template-resolver/document-sanitizer.js +201 -0
- package/dist/template-resolver/document-schema.d.ts +21 -0
- package/dist/template-resolver/document-schema.js +296 -0
- package/dist/template-resolver/feature.d.ts +18 -0
- package/dist/template-resolver/feature.js +50 -6
- package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
- package/dist/template-resolver/handlers/bundle-import.js +147 -0
- package/dist/template-resolver/handlers/collection-set.write.js +25 -2
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
- package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
- package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
- package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
- package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
- package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
- package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
- package/dist/template-resolver/handlers/layout-source.query.js +64 -0
- package/dist/template-resolver/handlers/list.query.d.ts +4 -1
- package/dist/template-resolver/handlers/list.query.js +5 -7
- package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
- package/dist/template-resolver/handlers/overview.query.js +165 -0
- package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
- package/dist/template-resolver/handlers/preview.query.js +24 -0
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
- package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
- package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
- package/dist/template-resolver/handlers/resource-shared.js +14 -0
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
- package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
- package/dist/template-resolver/handlers/send-test.write.js +81 -0
- package/dist/template-resolver/handlers/set.write.d.ts +4 -1
- package/dist/template-resolver/handlers/set.write.js +25 -3
- package/dist/template-resolver/handlers/shared.d.ts +9 -5
- package/dist/template-resolver/handlers/shared.js +6 -2
- package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
- package/dist/template-resolver/handlers/template-gates.js +131 -0
- package/dist/template-resolver/handlers/template-write.d.ts +45 -0
- package/dist/template-resolver/handlers/template-write.js +109 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
- package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
- package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
- package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
- package/dist/template-resolver/handlers/usage.query.js +15 -0
- package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
- package/dist/template-resolver/handlers/validate.query.js +80 -0
- package/dist/template-resolver/index.d.ts +18 -1
- package/dist/template-resolver/index.js +16 -1
- package/dist/template-resolver/layout-regions.d.ts +23 -0
- package/dist/template-resolver/layout-regions.js +125 -0
- package/dist/template-resolver/linked-resources.d.ts +17 -0
- package/dist/template-resolver/linked-resources.js +75 -0
- package/dist/template-resolver/preview-display-html.d.ts +7 -0
- package/dist/template-resolver/preview-display-html.js +27 -0
- package/dist/template-resolver/print-document.d.ts +1 -0
- package/dist/template-resolver/print-document.js +55 -0
- package/dist/template-resolver/qualified-names.d.ts +30 -0
- package/dist/template-resolver/qualified-names.js +30 -0
- package/dist/template-resolver/record-mapping.d.ts +2 -0
- package/dist/template-resolver/record-mapping.js +21 -0
- package/dist/template-resolver/resource-image.d.ts +15 -0
- package/dist/template-resolver/resource-image.js +34 -0
- package/dist/template-resolver/seeding.d.ts +14 -1
- package/dist/template-resolver/seeding.js +105 -15
- package/dist/template-resolver/store-template-resource.d.ts +64 -0
- package/dist/template-resolver/store-template-resource.js +264 -0
- package/dist/template-resolver/table.d.ts +47 -8
- package/dist/template-resolver/table.js +11 -0
- package/dist/template-resolver/template-bundle.d.ts +36 -0
- package/dist/template-resolver/template-bundle.js +60 -0
- package/dist/template-resolver/template-diff.d.ts +7 -0
- package/dist/template-resolver/template-diff.js +68 -0
- package/dist/template-resolver/template-meta.d.ts +60 -0
- package/dist/template-resolver/template-meta.js +101 -0
- package/dist/template-resolver/template-preview.d.ts +86 -0
- package/dist/template-resolver/template-preview.js +374 -0
- package/dist/template-resolver/template-resources.d.ts +14 -0
- package/dist/template-resolver/template-resources.js +42 -0
- package/dist/template-resolver/template-usage.d.ts +12 -0
- package/dist/template-resolver/template-usage.js +30 -0
- package/dist/template-resolver/template-validation.d.ts +22 -0
- package/dist/template-resolver/template-validation.js +172 -0
- package/dist/template-resolver/testing.js +5 -5
- package/dist/template-resolver/user-content-table.d.ts +14 -8
- package/dist/template-resolver/web/client-plugin.d.ts +1 -0
- package/dist/template-resolver/web/client-plugin.js +9 -6
- package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
- package/dist/template-resolver/web/document-editor-extensions.js +107 -0
- package/dist/template-resolver/web/document-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-editor.js +11 -0
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
- package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
- package/dist/template-resolver/web/editor-handle.d.ts +3 -0
- package/dist/template-resolver/web/editor-handle.js +24 -0
- package/dist/template-resolver/web/i18n.js +76 -0
- package/dist/template-resolver/web/index.d.ts +4 -0
- package/dist/template-resolver/web/index.js +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
- package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
- package/dist/template-resolver/web/placeholder-text.js +34 -0
- package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
- package/dist/template-resolver/web/use-stable-by-key.js +13 -0
- package/dist/tenant/handlers/team-list.query.js +3 -2
- package/dist/tenant/seeding.js +31 -6
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
- package/dist/user/handlers/detail.query.js +2 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/email-templates.d.ts +1 -29
- package/dist/user-data-rights/email-templates.js +5 -106
- package/dist/user-data-rights/feature.js +53 -35
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
- package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
- package/dist/user-data-rights/lib/default-mailers.js +100 -48
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/mail-notifications.d.ts +13 -0
- package/dist/user-data-rights/mail-notifications.js +118 -0
- package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
- package/dist/user-profile/web/profile-screen.js +2 -1
- package/dist/waitlist/feature.js +10 -0
- package/dist/waitlist/handlers/submit.write.js +3 -4
- package/dist/waitlist/mail.d.ts +19 -6
- package/dist/waitlist/mail.js +50 -14
- package/package.json +28 -17
- package/src/admin-shell/changes.json +6 -0
- package/src/auth-email-password/changes.json +14 -0
- package/src/billing-foundation/changes.json +7 -0
- package/src/channel-email/changes.json +19 -0
- package/src/data-retention/changes.json +6 -0
- package/src/delivery/changes.json +48 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-foundation/changes.json +39 -0
- package/src/template-resolver/changes.json +118 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/waitlist/changes.json +7 -0
- package/dist/renderer-simple/feature.d.ts +0 -10
- package/dist/renderer-simple/feature.js +0 -42
- package/dist/renderer-simple/index.d.ts +0 -3
- package/dist/renderer-simple/index.js +0 -2
- package/dist/renderer-simple/mail-blocks.d.ts +0 -21
- package/dist/renderer-simple/mail-blocks.js +0 -107
- package/dist/renderer-simple/resolve-variables.d.ts +0 -7
- package/dist/renderer-simple/resolve-variables.js +0 -47
- package/dist/renderer-simple/simple-renderer.d.ts +0 -24
- package/dist/renderer-simple/simple-renderer.js +0 -200
- package/src/renderer-simple/changes.json +0 -27
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
//
|
|
4
|
+
// tiptap-backed implementation behind DocumentContentEditor's lazy() boundary —
|
|
5
|
+
// never import this file statically, tiptap must only load when a "document"
|
|
6
|
+
// collection is rendered. Variables are inserted as `variable` nodes only,
|
|
7
|
+
// never as `{{name}}` text, so the sanitizer's schema keeps them.
|
|
8
|
+
import { TextareaContentEditor, usePrimitives, useTranslation, VariableChips, } from "@cosmicdrift/kumiko-renderer";
|
|
9
|
+
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
10
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
11
|
+
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
|
12
|
+
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold as BoldIcon, Braces, Image as ImageIcon, Italic as ItalicIcon, Link as LinkIcon, List, ListOrdered, Minus, MousePointerClick, Repeat, Scissors, Table as TableIcon, Underline as UnderlineIcon, } from "lucide-react";
|
|
13
|
+
import { useEffect, useMemo, useState } from "react";
|
|
14
|
+
import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "../document-schema.js";
|
|
15
|
+
import { createDocumentEditorExtensions } from "./document-editor-extensions.js";
|
|
16
|
+
import { MAIL_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.js";
|
|
17
|
+
import { createEditorHandle } from "./editor-handle.js";
|
|
18
|
+
import { useStableByKey } from "./use-stable-by-key.js";
|
|
19
|
+
const NO_LABELS = {};
|
|
20
|
+
const NO_NAMES = [];
|
|
21
|
+
const NO_PREVIEWS = {};
|
|
22
|
+
function previewsKey(previews) {
|
|
23
|
+
return Object.entries(previews)
|
|
24
|
+
.map(([key, { src, caption }]) => `${key}|${src.length}|${src.slice(-24)}|${caption ?? ""}`)
|
|
25
|
+
.join("\n");
|
|
26
|
+
}
|
|
27
|
+
const BLOCK_PANEL = {
|
|
28
|
+
image: "image",
|
|
29
|
+
button: "button",
|
|
30
|
+
repeat: "repeat",
|
|
31
|
+
align: "align",
|
|
32
|
+
};
|
|
33
|
+
const ALIGNMENT_ICONS = {
|
|
34
|
+
left: _jsx(AlignLeft, { size: 16 }),
|
|
35
|
+
center: _jsx(AlignCenter, { size: 16 }),
|
|
36
|
+
right: _jsx(AlignRight, { size: 16 }),
|
|
37
|
+
justify: _jsx(AlignJustify, { size: 16 }),
|
|
38
|
+
};
|
|
39
|
+
const ALIGNMENT_LABEL_KEYS = {
|
|
40
|
+
left: "template-resolver.documentEditor.alignLeft",
|
|
41
|
+
center: "template-resolver.documentEditor.alignCenter",
|
|
42
|
+
right: "template-resolver.documentEditor.alignRight",
|
|
43
|
+
justify: "template-resolver.documentEditor.alignJustify",
|
|
44
|
+
};
|
|
45
|
+
// print-document moves the leading rows flagged repeatHeader into thead, so the
|
|
46
|
+
// toggle reads and writes the table's first row.
|
|
47
|
+
function findRepeatTable(editor) {
|
|
48
|
+
const { $from } = editor.state.selection;
|
|
49
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
50
|
+
const node = $from.node(depth);
|
|
51
|
+
if (node.type.name !== "table")
|
|
52
|
+
continue;
|
|
53
|
+
const lists = [];
|
|
54
|
+
node.forEach((row) => {
|
|
55
|
+
const each = row.attrs["each"];
|
|
56
|
+
if (typeof each === "string" && each !== "" && !lists.includes(each))
|
|
57
|
+
lists.push(each);
|
|
58
|
+
});
|
|
59
|
+
if (lists.length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
lists,
|
|
63
|
+
headerRepeats: node.firstChild?.attrs["repeatHeader"] === true,
|
|
64
|
+
firstRowPos: $from.start(depth),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function toggleRepeatHeader(editor) {
|
|
70
|
+
const found = findRepeatTable(editor);
|
|
71
|
+
if (found === null)
|
|
72
|
+
return;
|
|
73
|
+
editor
|
|
74
|
+
.chain()
|
|
75
|
+
.focus()
|
|
76
|
+
.command(({ tr }) => {
|
|
77
|
+
const row = tr.doc.nodeAt(found.firstRowPos);
|
|
78
|
+
if (row === null)
|
|
79
|
+
return false;
|
|
80
|
+
tr.setNodeMarkup(found.firstRowPos, undefined, {
|
|
81
|
+
...row.attrs,
|
|
82
|
+
repeatHeader: !found.headerRepeats,
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
})
|
|
86
|
+
.run();
|
|
87
|
+
}
|
|
88
|
+
const PARAGRAPH_STYLES = ["paragraph", "1", "2", "3"];
|
|
89
|
+
const HEADING_LEVELS = { "1": 1, "2": 2, "3": 3 };
|
|
90
|
+
function isParagraphStyle(value) {
|
|
91
|
+
return isOneOf(PARAGRAPH_STYLES, value);
|
|
92
|
+
}
|
|
93
|
+
function emptyParagraph() {
|
|
94
|
+
return { type: "paragraph" };
|
|
95
|
+
}
|
|
96
|
+
function repeatTableContent(listName) {
|
|
97
|
+
const headerRow = {
|
|
98
|
+
type: "tableRow",
|
|
99
|
+
content: [
|
|
100
|
+
{ type: "tableHeader", content: [emptyParagraph()] },
|
|
101
|
+
{ type: "tableHeader", content: [emptyParagraph()] },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
const repeatRow = {
|
|
105
|
+
type: "tableRow",
|
|
106
|
+
attrs: { each: listName },
|
|
107
|
+
content: [
|
|
108
|
+
{ type: "tableCell", content: [emptyParagraph()] },
|
|
109
|
+
{ type: "tableCell", content: [emptyParagraph()] },
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
return { type: "table", content: [headerRow, repeatRow] };
|
|
113
|
+
}
|
|
114
|
+
function Separator({ compact = false }) {
|
|
115
|
+
return (_jsx("span", { "aria-hidden": "true", className: cn("h-5 w-px bg-border", compact ? "mx-0.5" : "mx-2") }));
|
|
116
|
+
}
|
|
117
|
+
function PanelActions({ onInsert, onRemoveLink, onCancel, }) {
|
|
118
|
+
const { Button } = usePrimitives();
|
|
119
|
+
const t = useTranslation();
|
|
120
|
+
return (_jsxs("div", { className: "flex gap-2", children: [onInsert && (_jsx(Button, { type: "button", size: "sm", onClick: onInsert, children: t("template-resolver.documentEditor.insert") })), onRemoveLink && (_jsx(Button, { type: "button", size: "sm", variant: "danger-ghost", onClick: onRemoveLink, children: t("template-resolver.documentEditor.removeLink") })), _jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: onCancel, children: t("template-resolver.documentEditor.cancel") })] }));
|
|
121
|
+
}
|
|
122
|
+
function NameSelect({ id, label, names, value, onChange }) {
|
|
123
|
+
const { Field, Input } = usePrimitives();
|
|
124
|
+
return (_jsx(Field, { id: id, label: label, children: _jsx(Input, { kind: "select", id: id, name: id, value: value, onChange: onChange, display: "dropdown", options: names.map((name) => ({ value: name, label: name })) }) }));
|
|
125
|
+
}
|
|
126
|
+
function InsertPanel({ editor, editorId, kind, variables, onClose }) {
|
|
127
|
+
const { Button, Field, Input } = usePrimitives();
|
|
128
|
+
const t = useTranslation();
|
|
129
|
+
const [href, setHref] = useState(() => {
|
|
130
|
+
const current = editor.getAttributes("link")["href"];
|
|
131
|
+
return typeof current === "string" ? current : "";
|
|
132
|
+
});
|
|
133
|
+
const [resource, setResource] = useState("");
|
|
134
|
+
const [alt, setAlt] = useState("");
|
|
135
|
+
const [buttonText, setButtonText] = useState(() => t("template-resolver.documentEditor.buttonDefaultText"));
|
|
136
|
+
const [chosenName, setChosenName] = useState("");
|
|
137
|
+
const [error, setError] = useState(null);
|
|
138
|
+
const referenceNames = variables.filter(isDocumentReferenceName);
|
|
139
|
+
const buttonTargets = referenceNames.filter((name) => isDocumentPlaceholderHref(`{{${name}}}`));
|
|
140
|
+
const selectable = kind === "button" ? buttonTargets : referenceNames;
|
|
141
|
+
const selectedName = selectable.includes(chosenName) ? chosenName : (selectable[0] ?? "");
|
|
142
|
+
const insertAndClose = (content) => {
|
|
143
|
+
editor.chain().focus().insertContent(content).run();
|
|
144
|
+
onClose();
|
|
145
|
+
};
|
|
146
|
+
const submitLink = () => {
|
|
147
|
+
if (!isAllowedDocumentLinkHref(href)) {
|
|
148
|
+
setError(t("template-resolver.documentEditor.invalidLink"));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const chain = editor.chain().focus();
|
|
152
|
+
if (!editor.state.selection.empty)
|
|
153
|
+
chain.setLink({ href }).run();
|
|
154
|
+
else if (editor.isActive("link"))
|
|
155
|
+
chain.extendMarkRange("link").setLink({ href }).run();
|
|
156
|
+
else
|
|
157
|
+
chain
|
|
158
|
+
.insertContent({ type: "text", text: href, marks: [{ type: "link", attrs: { href } }] })
|
|
159
|
+
.run();
|
|
160
|
+
onClose();
|
|
161
|
+
};
|
|
162
|
+
const submitImage = () => {
|
|
163
|
+
if (!isDocumentResourceKey(resource)) {
|
|
164
|
+
setError(t("template-resolver.documentEditor.invalidResourceKey"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (/[<>]/.test(alt)) {
|
|
168
|
+
setError(t("template-resolver.documentEditor.invalidImageAlt"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
insertAndClose({ type: "resource", attrs: { resource, alt: alt || null } });
|
|
172
|
+
};
|
|
173
|
+
const needsVariables = kind === "button" || kind === "repeat" || kind === "placeholder";
|
|
174
|
+
const noVariables = needsVariables && selectable.length === 0;
|
|
175
|
+
const idPrefix = `${editorId}-panel`;
|
|
176
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 border-b border-input bg-muted/40 p-2", children: [noVariables && (_jsx("p", { className: "text-sm text-muted-foreground", children: t("template-resolver.documentEditor.noVariables") })), kind === "link" && (_jsx(Field, { id: `${idPrefix}-href`, label: t("template-resolver.documentEditor.linkHref"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-href`, name: "href", value: href, onChange: setHref, placeholder: t("template-resolver.documentEditor.linkHrefPlaceholder") }) })), kind === "placeholder" && !noVariables && (_jsx("div", { className: "flex flex-wrap gap-1", children: _jsx(VariableChips, { variables: selectable, onInsert: (name) => insertAndClose({ type: "variable", attrs: { name } }) }) })), kind === "align" && (_jsx("div", { className: "flex flex-wrap gap-1", children: DOCUMENT_TEXT_ALIGNMENTS.map((alignment) => (_jsxs(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => {
|
|
177
|
+
editor
|
|
178
|
+
.chain()
|
|
179
|
+
.focus()
|
|
180
|
+
.updateAttributes("paragraph", { textAlign: alignment })
|
|
181
|
+
.updateAttributes("heading", { textAlign: alignment })
|
|
182
|
+
.run();
|
|
183
|
+
onClose();
|
|
184
|
+
}, children: [ALIGNMENT_ICONS[alignment], t(ALIGNMENT_LABEL_KEYS[alignment])] }, alignment))) })), kind === "image" && (_jsxs(_Fragment, { children: [_jsx(Field, { id: `${idPrefix}-resource`, label: t("template-resolver.documentEditor.resourceKey"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-resource`, name: "resource", value: resource, onChange: setResource }) }), _jsx(Field, { id: `${idPrefix}-alt`, label: t("template-resolver.documentEditor.imageAlt"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-alt`, name: "alt", value: alt, onChange: setAlt }) })] })), kind === "button" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-target`, label: t("template-resolver.documentEditor.buttonTarget"), names: selectable, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-text`, label: t("template-resolver.documentEditor.buttonText"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-text`, name: "text", value: buttonText, onChange: setButtonText }) })] })), kind === "repeat" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-list`, label: t("template-resolver.documentEditor.repeatList"), names: selectable, value: selectedName, onChange: setChosenName })), error !== null && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsx("div", { className: "flex flex-wrap gap-2", children: _jsx(PanelActions, { onCancel: onClose, ...(kind === "link" && { onInsert: submitLink }), ...(kind === "link" &&
|
|
185
|
+
editor.isActive("link") && {
|
|
186
|
+
onRemoveLink: () => {
|
|
187
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
188
|
+
onClose();
|
|
189
|
+
},
|
|
190
|
+
}), ...(kind === "image" && { onInsert: submitImage }), ...(kind === "button" &&
|
|
191
|
+
!noVariables && {
|
|
192
|
+
onInsert: () => insertAndClose({
|
|
193
|
+
type: "button",
|
|
194
|
+
attrs: { href: `{{${selectedName}}}` },
|
|
195
|
+
content: buttonText ? [{ type: "text", text: buttonText }] : [],
|
|
196
|
+
}),
|
|
197
|
+
}), ...(kind === "repeat" &&
|
|
198
|
+
!noVariables && { onInsert: () => insertAndClose(repeatTableContent(selectedName)) }) }) })] }));
|
|
199
|
+
}
|
|
200
|
+
export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks = MAIL_DOCUMENT_BLOCKS, variableLabels = NO_LABELS, systemVariables = NO_NAMES, resourcePreviews = NO_PREVIEWS, chrome = "default", toolbar = "full", onEditorReady, }) {
|
|
201
|
+
const t = useTranslation();
|
|
202
|
+
const { Button, Field, Input } = usePrimitives();
|
|
203
|
+
const [openPanel, setOpenPanel] = useState(null);
|
|
204
|
+
const [showHtml, setShowHtml] = useState(false);
|
|
205
|
+
const canvas = chrome === "canvas";
|
|
206
|
+
const compact = toolbar === "compact" && !canvas;
|
|
207
|
+
const placeholderLabel = t("template-resolver.documentEditor.placeholder");
|
|
208
|
+
const imageLabel = t("template-resolver.documentEditor.image");
|
|
209
|
+
const pageBreakLabel = t("template-resolver.documentEditor.pageBreak");
|
|
210
|
+
const buttonLabel = t("template-resolver.documentEditor.button");
|
|
211
|
+
const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
|
|
212
|
+
const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
|
|
213
|
+
const stablePreviews = useStableByKey(resourcePreviews, previewsKey(resourcePreviews));
|
|
214
|
+
const extensions = useMemo(() => createDocumentEditorExtensions({
|
|
215
|
+
placeholder: placeholderLabel,
|
|
216
|
+
image: imageLabel,
|
|
217
|
+
pageBreak: pageBreakLabel,
|
|
218
|
+
button: buttonLabel,
|
|
219
|
+
variableLabels: stableLabels,
|
|
220
|
+
systemVariables: stableSystem,
|
|
221
|
+
resourcePreviews: stablePreviews,
|
|
222
|
+
blockLabels: canvas,
|
|
223
|
+
}), [
|
|
224
|
+
placeholderLabel,
|
|
225
|
+
imageLabel,
|
|
226
|
+
pageBreakLabel,
|
|
227
|
+
buttonLabel,
|
|
228
|
+
stableLabels,
|
|
229
|
+
stableSystem,
|
|
230
|
+
stablePreviews,
|
|
231
|
+
canvas,
|
|
232
|
+
]);
|
|
233
|
+
// The node views are built from labels and previews, so a changed set
|
|
234
|
+
// rebuilds the editor; its content is re-read from `value`.
|
|
235
|
+
const editor = useEditor({
|
|
236
|
+
extensions,
|
|
237
|
+
content: value,
|
|
238
|
+
editable: !readOnly,
|
|
239
|
+
// Suspense (DocumentContentEditor's lazy boundary) speculatively mounts
|
|
240
|
+
// this component before committing it; immediate render would create an
|
|
241
|
+
// editor instance during that throwaway pass.
|
|
242
|
+
immediatelyRender: false,
|
|
243
|
+
editorProps: { attributes: { id } },
|
|
244
|
+
onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
|
|
245
|
+
onUpdate: ({ editor: e }) => onChange(e.getHTML()),
|
|
246
|
+
}, [extensions]);
|
|
247
|
+
// @tiptap/react v3 does not re-render on selection-only transactions, so
|
|
248
|
+
// active states must come from a subscription.
|
|
249
|
+
const activeState = useEditorState({
|
|
250
|
+
editor,
|
|
251
|
+
selector: ({ editor: e }) => e === null
|
|
252
|
+
? null
|
|
253
|
+
: {
|
|
254
|
+
blockStyle: e.isActive("heading", { level: 1 })
|
|
255
|
+
? "1"
|
|
256
|
+
: e.isActive("heading", { level: 2 })
|
|
257
|
+
? "2"
|
|
258
|
+
: e.isActive("heading", { level: 3 })
|
|
259
|
+
? "3"
|
|
260
|
+
: "paragraph",
|
|
261
|
+
bold: e.isActive("bold"),
|
|
262
|
+
italic: e.isActive("italic"),
|
|
263
|
+
underline: e.isActive("underline"),
|
|
264
|
+
selectedBlock: e.isActive("resource")
|
|
265
|
+
? {
|
|
266
|
+
type: "resource",
|
|
267
|
+
detail: String(e.getAttributes("resource")["resource"]),
|
|
268
|
+
}
|
|
269
|
+
: e.isActive("button")
|
|
270
|
+
? { type: "button", detail: String(e.getAttributes("button")["href"]) }
|
|
271
|
+
: null,
|
|
272
|
+
html: showHtml ? e.getHTML() : "",
|
|
273
|
+
bulletList: e.isActive("bulletList"),
|
|
274
|
+
orderedList: e.isActive("orderedList"),
|
|
275
|
+
link: e.isActive("link"),
|
|
276
|
+
repeatTable: findRepeatTable(e),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
// The entry loads asynchronously, so `value` arrives after useEditor read
|
|
280
|
+
// its initial content; the guard keeps in-flight typing from being clobbered.
|
|
281
|
+
// kumiko-lint-ignore no-raw-hooks DOM integration: syncs the controlled value into the imperative tiptap editor
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (editor && value !== editor.getHTML()) {
|
|
284
|
+
editor.commands.setContent(value, { emitUpdate: false });
|
|
285
|
+
}
|
|
286
|
+
}, [value, editor]);
|
|
287
|
+
// kumiko-lint-ignore no-raw-hooks DOM integration: tiptap exposes editability only imperatively via setEditable
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
editor?.setEditable(!readOnly);
|
|
290
|
+
}, [readOnly, editor]);
|
|
291
|
+
if (!editor)
|
|
292
|
+
return (_jsx(TextareaContentEditor, { id: id, value: value, onChange: onChange, variables: variables, readOnly: readOnly }));
|
|
293
|
+
const togglePanel = (kind) => setOpenPanel((current) => (current === kind ? null : kind));
|
|
294
|
+
const setBlockStyle = (style) => {
|
|
295
|
+
if (!isParagraphStyle(style))
|
|
296
|
+
return;
|
|
297
|
+
const chain = editor.chain().focus();
|
|
298
|
+
if (style === "paragraph")
|
|
299
|
+
chain.setParagraph().run();
|
|
300
|
+
else
|
|
301
|
+
chain.setHeading({ level: HEADING_LEVELS[style] }).run();
|
|
302
|
+
};
|
|
303
|
+
const runBlock = (block) => {
|
|
304
|
+
const panel = BLOCK_PANEL[block];
|
|
305
|
+
if (panel) {
|
|
306
|
+
togglePanel(panel);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const chain = editor.chain().focus();
|
|
310
|
+
if (block === "table")
|
|
311
|
+
chain.insertTable({ rows: 3, cols: 2, withHeaderRow: true }).run();
|
|
312
|
+
else if (block === "divider")
|
|
313
|
+
chain.insertContent({ type: "divider" }).run();
|
|
314
|
+
else if (block === "pageBreak")
|
|
315
|
+
chain.insertContent({ type: "pageBreak" }).run();
|
|
316
|
+
};
|
|
317
|
+
const blockButtons = {
|
|
318
|
+
image: { label: imageLabel, icon: _jsx(ImageIcon, { size: 16 }) },
|
|
319
|
+
table: { label: t("template-resolver.documentEditor.table"), icon: _jsx(TableIcon, { size: 16 }) },
|
|
320
|
+
button: {
|
|
321
|
+
label: t("template-resolver.documentEditor.button"),
|
|
322
|
+
icon: _jsx(MousePointerClick, { size: 16 }),
|
|
323
|
+
},
|
|
324
|
+
divider: { label: t("template-resolver.documentEditor.divider"), icon: _jsx(Minus, { size: 16 }) },
|
|
325
|
+
repeat: { label: t("template-resolver.documentEditor.repeat"), icon: _jsx(Repeat, { size: 16 }) },
|
|
326
|
+
pageBreak: { label: pageBreakLabel, icon: _jsx(Scissors, { size: 16 }) },
|
|
327
|
+
align: { label: t("template-resolver.documentEditor.align"), icon: _jsx(AlignLeft, { size: 16 }) },
|
|
328
|
+
};
|
|
329
|
+
const repeatTable = activeState?.repeatTable ?? null;
|
|
330
|
+
const repeatListLabel = repeatTable !== null && repeatTable.lists.length === 1
|
|
331
|
+
? (variableLabels[repeatTable.lists[0] ?? ""] ?? repeatTable.lists[0])
|
|
332
|
+
: undefined;
|
|
333
|
+
const paragraphStyleLabel = t("template-resolver.documentEditor.paragraphStyle");
|
|
334
|
+
const styleOptions = [
|
|
335
|
+
{ value: "paragraph", label: t("template-resolver.documentEditor.paragraph") },
|
|
336
|
+
{ value: "1", label: t("kumiko.contentEditor.heading1") },
|
|
337
|
+
{ value: "2", label: t("kumiko.contentEditor.heading2") },
|
|
338
|
+
{ value: "3", label: t("template-resolver.documentEditor.heading3") },
|
|
339
|
+
];
|
|
340
|
+
// Canvas chrome shows plain text buttons instead of icons.
|
|
341
|
+
const iconButton = (label, icon, isActive, onClick) => canvas ? (_jsx(Button, { type: "button", variant: isActive ? "secondary" : "ghost", size: "sm", pressed: isActive, disabled: readOnly, onClick: onClick, ariaLabel: label, title: label, children: icon })) : (_jsx(Button, { type: "button", variant: isActive ? "primary" : "secondary", size: "icon", pressed: isActive, disabled: readOnly, onClick: onClick, ariaLabel: label, title: label, children: icon }));
|
|
342
|
+
const markContent = (letter, icon, className) => canvas ? _jsx("span", { className: className, children: letter }) : icon;
|
|
343
|
+
const selectedBlock = activeState?.selectedBlock ?? null;
|
|
344
|
+
const flatVariant = canvas ? "ghost" : "secondary";
|
|
345
|
+
const labelledButton = (spec) => {
|
|
346
|
+
const shared = {
|
|
347
|
+
type: "button",
|
|
348
|
+
variant: flatVariant,
|
|
349
|
+
disabled: readOnly,
|
|
350
|
+
onClick: spec.onClick,
|
|
351
|
+
...(spec.expanded !== undefined && { expanded: spec.expanded }),
|
|
352
|
+
};
|
|
353
|
+
return compact ? (_jsx(Button, { ...shared, size: "icon", ariaLabel: spec.label, title: spec.label, children: spec.icon }, spec.key)) : (_jsxs(Button, { ...shared, size: "sm", children: [spec.showIcon && spec.icon, spec.label] }, spec.key));
|
|
354
|
+
};
|
|
355
|
+
return (_jsxs("div", { className: cn("rounded-md border border-input", canvas ? "bg-background" : "bg-transparent"), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-1 border-b border-input p-1", children: [_jsx("div", { className: compact ? "w-24" : "w-40", children: _jsx(Field, { id: `${id}-paragraph-style`, label: paragraphStyleLabel, hideLabel: true, children: _jsx(Input, { kind: "select", id: `${id}-paragraph-style`, name: "paragraph-style", value: activeState?.blockStyle ?? "paragraph", onChange: setBlockStyle, options: styleOptions, display: "dropdown", disabled: readOnly }) }) }), _jsx(Separator, { compact: compact }), iconButton(t("kumiko.contentEditor.bold"), markContent("B", _jsx(BoldIcon, { size: 16 }), "font-bold"), activeState?.bold ?? false, () => editor.chain().focus().toggleBold().run()), iconButton(t("kumiko.contentEditor.italic"), markContent("I", _jsx(ItalicIcon, { size: 16 }), "italic"), activeState?.italic ?? false, () => editor.chain().focus().toggleItalic().run()), iconButton(t("template-resolver.documentEditor.underline"), markContent("U", _jsx(UnderlineIcon, { size: 16 }), "underline"), activeState?.underline ?? false, () => editor.chain().focus().toggleUnderline().run()), _jsx(Separator, { compact: compact }), iconButton(t("kumiko.contentEditor.bulletList"), _jsx(List, { size: 16 }), activeState?.bulletList ?? false, () => editor.chain().focus().toggleBulletList().run()), iconButton(t("kumiko.contentEditor.orderedList"), _jsx(ListOrdered, { size: 16 }), activeState?.orderedList ?? false, () => editor.chain().focus().toggleOrderedList().run()), iconButton(t("template-resolver.documentEditor.link"), _jsx(LinkIcon, { size: 16 }), activeState?.link ?? false, () => togglePanel("link")), _jsx(Separator, { compact: compact }), labelledButton({
|
|
356
|
+
label: t("template-resolver.documentEditor.insertPlaceholder"),
|
|
357
|
+
icon: _jsx(Braces, { size: 16 }),
|
|
358
|
+
showIcon: true,
|
|
359
|
+
expanded: openPanel === "placeholder",
|
|
360
|
+
onClick: () => togglePanel("placeholder"),
|
|
361
|
+
}), blocks.length > 0 && _jsx(Separator, { compact: compact }), blocks.map((block) => labelledButton({
|
|
362
|
+
key: block,
|
|
363
|
+
label: blockButtons[block].label,
|
|
364
|
+
icon: blockButtons[block].icon,
|
|
365
|
+
showIcon: !canvas,
|
|
366
|
+
onClick: () => runBlock(block),
|
|
367
|
+
})), canvas && (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1" }), _jsx(Button, { type: "button", variant: flatVariant, size: "sm", expanded: showHtml, onClick: () => {
|
|
368
|
+
setShowHtml((current) => !current);
|
|
369
|
+
}, children: t("template-resolver.documentEditor.showHtml") })] }))] }), canvas && selectedBlock !== null && !readOnly && (_jsxs("div", { className: "flex items-center gap-2 border-b border-input bg-muted/40 px-2 py-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t("template-resolver.documentEditor.selectedBlock") }), _jsx("span", { className: "font-medium", children: selectedBlock.type === "resource" ? imageLabel : buttonLabel }), _jsx("code", { className: "text-xs text-muted-foreground", children: selectedBlock.detail }), _jsx("span", { className: "flex-1" }), _jsx(Button, { type: "button", size: "sm", variant: "danger-ghost", onClick: () => {
|
|
370
|
+
editor.chain().focus().deleteNode(selectedBlock.type).run();
|
|
371
|
+
}, children: t("template-resolver.documentEditor.removeBlock") })] })), canvas && showHtml && (_jsx("section", { "aria-label": t("template-resolver.documentEditor.htmlSource"), children: _jsx("pre", { className: "h-40 overflow-auto whitespace-pre-wrap break-all border-b border-input bg-muted p-2 font-mono text-xs", children: activeState?.html ?? "" }) })), openPanel !== null && !readOnly && (_jsx(InsertPanel, { editor: editor, editorId: id, kind: openPanel, variables: variables, onClose: () => setOpenPanel(null) }, openPanel)), repeatTable !== null && !readOnly && (_jsxs("div", { "data-testid": "document-repeat-bar", className: "flex flex-wrap items-center gap-3 border-b border-input bg-muted/40 px-2 py-1 text-xs", children: [_jsx("span", { className: "font-medium", children: repeatListLabel === undefined
|
|
372
|
+
? t("template-resolver.documentEditor.repeat")
|
|
373
|
+
: t("template-resolver.documentEditor.repeatBarList", { list: repeatListLabel }) }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", pressed: repeatTable.headerRepeats, onClick: () => toggleRepeatHeader(editor), children: t("template-resolver.documentEditor.repeatHeader") }), _jsx("span", { className: "text-muted-foreground", children: t("template-resolver.documentEditor.keepRowsTogether") })] })), _jsx("div", { className: cn(canvas && "flex justify-center bg-muted p-6"), children: _jsx(EditorContent, { editor: editor, className: cn(canvas
|
|
374
|
+
? "w-[600px] max-w-full bg-background px-10 py-7 text-sm border border-input"
|
|
375
|
+
: "min-h-40 px-3 py-2 text-base outline-none md:text-sm", "[&_.ProseMirror-selectednode]:ring-2 [&_.ProseMirror-selectednode]:ring-primary", "[&_.ProseMirror]:min-h-40 [&_.ProseMirror]:outline-none", "[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-6 [&_ol]:pl-6", "[&_h1]:text-xl [&_h1]:font-bold [&_h2]:text-lg [&_h2]:font-bold [&_h3]:text-base [&_h3]:font-bold", "[&_a]:underline [&_a]:text-primary", "[&_strong]:font-bold [&_em]:italic [&_u]:underline", "[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse", "[&_td]:border [&_td]:border-input [&_td]:p-1 [&_th]:border [&_th]:border-input [&_th]:bg-muted [&_th]:p-1 [&_th]:text-left", "[&_tr[data-each]]:bg-muted", "[&_a[data-button]]:inline-block [&_a[data-button]]:rounded-full [&_a[data-button]]:bg-primary [&_a[data-button]]:px-4 [&_a[data-button]]:py-1.5 [&_a[data-button]]:text-primary-foreground [&_a[data-button]]:no-underline", "[&_hr]:my-3 [&_hr]:border-input", "[&_.is-editor-empty]:before:pointer-events-none [&_.is-editor-empty]:before:float-left [&_.is-editor-empty]:before:h-0 [&_.is-editor-empty]:before:text-muted-foreground [&_.is-editor-empty]:before:content-[attr(data-placeholder)]") }) })] }));
|
|
376
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
export type DocumentToolbarBlock = "image" | "table" | "button" | "divider" | "repeat" | "pageBreak" | "align";
|
|
3
|
+
/** Imperative access for an outside panel (placeholder list, resource list). */
|
|
4
|
+
export type DocumentEditorHandle = {
|
|
5
|
+
readonly insertPlaceholder: (name: string) => void;
|
|
6
|
+
readonly insertResource: (key: string, alt?: string) => void;
|
|
7
|
+
readonly focus: () => void;
|
|
8
|
+
};
|
|
9
|
+
export type DocumentResourcePreview = {
|
|
10
|
+
/** Shown as the image source in the editor only; never written to the document. */
|
|
11
|
+
readonly src: string;
|
|
12
|
+
readonly caption?: string;
|
|
13
|
+
};
|
|
14
|
+
/** "default" keeps the compact bordered editor; "canvas" renders a flat text
|
|
15
|
+
* toolbar and a paper-like page with block labels. */
|
|
16
|
+
export type DocumentEditorChrome = "default" | "canvas";
|
|
17
|
+
export type DocumentEditorProps = ContentEditorProps & {
|
|
18
|
+
readonly blocks?: readonly DocumentToolbarBlock[];
|
|
19
|
+
/** Display label per variable name, as inserted (list item fields use the
|
|
20
|
+
* dotted form, e.g. "lines.description"). A chip shows label and name. */
|
|
21
|
+
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
22
|
+
/** Variable names rendered as system chips (a different tint). */
|
|
23
|
+
readonly systemVariables?: readonly string[];
|
|
24
|
+
/** Image previews by resource key; resources without one show a key placeholder. */
|
|
25
|
+
readonly resourcePreviews?: Readonly<Record<string, DocumentResourcePreview>>;
|
|
26
|
+
readonly chrome?: DocumentEditorChrome;
|
|
27
|
+
/** "compact" renders the placeholder and block buttons icon-only with a tooltip, for narrow columns. Default "full". */
|
|
28
|
+
readonly toolbar?: "full" | "compact";
|
|
29
|
+
/** Called with the handle once the editor exists and with null when it is destroyed. */
|
|
30
|
+
readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
|
|
31
|
+
};
|
|
32
|
+
export declare const MAIL_DOCUMENT_BLOCKS: readonly ["image", "table", "button", "divider", "repeat"];
|
|
33
|
+
export declare const PDF_DOCUMENT_BLOCKS: readonly ["align", "image", "table", "divider", "repeat", "pageBreak"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isDocumentReferenceName, isDocumentResourceKey } from "../document-schema.js";
|
|
2
|
+
export function createEditorHandle(editor) {
|
|
3
|
+
return {
|
|
4
|
+
insertPlaceholder: (name) => {
|
|
5
|
+
// skip: an invalid variable name is not insertable
|
|
6
|
+
if (!isDocumentReferenceName(name))
|
|
7
|
+
return;
|
|
8
|
+
editor.chain().focus().insertContent({ type: "variable", attrs: { name } }).run();
|
|
9
|
+
},
|
|
10
|
+
insertResource: (key, alt) => {
|
|
11
|
+
// skip: an invalid resource key is not insertable
|
|
12
|
+
if (!isDocumentResourceKey(key))
|
|
13
|
+
return;
|
|
14
|
+
editor
|
|
15
|
+
.chain()
|
|
16
|
+
.focus()
|
|
17
|
+
.insertContent({ type: "resource", attrs: { resource: key, alt: alt ?? null } })
|
|
18
|
+
.run();
|
|
19
|
+
},
|
|
20
|
+
focus: () => {
|
|
21
|
+
editor.commands.focus();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -6,12 +6,88 @@
|
|
|
6
6
|
// Keys follow `template-resolver.editor.<slug>`.
|
|
7
7
|
export const defaultTranslations = {
|
|
8
8
|
en: {
|
|
9
|
+
"template-resolver.documentEditor.button": "Button",
|
|
10
|
+
"template-resolver.documentEditor.buttonDefaultText": "Confirm",
|
|
11
|
+
"template-resolver.documentEditor.buttonTarget": "Button target",
|
|
12
|
+
"template-resolver.documentEditor.buttonText": "Button text",
|
|
13
|
+
"template-resolver.documentEditor.cancel": "Cancel",
|
|
14
|
+
"template-resolver.documentEditor.divider": "Divider",
|
|
15
|
+
"template-resolver.documentEditor.heading3": "Heading 3",
|
|
16
|
+
"template-resolver.documentEditor.image": "Image",
|
|
17
|
+
"template-resolver.documentEditor.imageAlt": "Alt text (optional)",
|
|
18
|
+
"template-resolver.documentEditor.insert": "Insert",
|
|
19
|
+
"template-resolver.documentEditor.insertPlaceholder": "Placeholder",
|
|
20
|
+
"template-resolver.documentEditor.invalidImageAlt": "The alt text must not contain < or >.",
|
|
21
|
+
"template-resolver.documentEditor.invalidLink": "This link target is not allowed. Use https, http, mailto, tel or a placeholder.",
|
|
22
|
+
"template-resolver.documentEditor.invalidResourceKey": "This resource key is not valid.",
|
|
23
|
+
"template-resolver.documentEditor.link": "Link",
|
|
24
|
+
"template-resolver.documentEditor.linkHref": "Link target",
|
|
25
|
+
"template-resolver.documentEditor.linkHrefPlaceholder": "https://",
|
|
26
|
+
"template-resolver.documentEditor.noVariables": "No matching variables are available here.",
|
|
27
|
+
"template-resolver.documentEditor.pageBreak": "Page break",
|
|
28
|
+
"template-resolver.documentEditor.align": "Align",
|
|
29
|
+
"template-resolver.documentEditor.alignLeft": "Left",
|
|
30
|
+
"template-resolver.documentEditor.alignCenter": "Centered",
|
|
31
|
+
"template-resolver.documentEditor.alignRight": "Right",
|
|
32
|
+
"template-resolver.documentEditor.alignJustify": "Justified",
|
|
33
|
+
"template-resolver.documentEditor.repeatBarList": "Repeat · {list}",
|
|
34
|
+
"template-resolver.documentEditor.repeatHeader": "Repeat header row",
|
|
35
|
+
"template-resolver.documentEditor.keepRowsTogether": "Keep rows together",
|
|
36
|
+
"template-resolver.documentEditor.paragraph": "Paragraph",
|
|
37
|
+
"template-resolver.documentEditor.paragraphStyle": "Paragraph style",
|
|
38
|
+
"template-resolver.documentEditor.placeholder": "Write your text…",
|
|
39
|
+
"template-resolver.documentEditor.removeLink": "Remove link",
|
|
40
|
+
"template-resolver.documentEditor.repeat": "Repeat",
|
|
41
|
+
"template-resolver.documentEditor.repeatList": "List to repeat",
|
|
42
|
+
"template-resolver.documentEditor.resourceKey": "Resource key",
|
|
43
|
+
"template-resolver.documentEditor.table": "Table",
|
|
9
44
|
"template-resolver.editor.titleLabel": "Title",
|
|
10
45
|
"template-resolver.editor.contentLabel": "Content",
|
|
11
46
|
"template-resolver.editor.save": "Save",
|
|
12
47
|
"template-resolver.editor.saving": "Saving…",
|
|
13
48
|
"template-resolver.editor.created": "Created.",
|
|
14
49
|
"template-resolver.editor.saved": "Saved.",
|
|
50
|
+
"templateResolver.errors.document.buttonHrefNotPlaceholder": "The button target “{detail}” must be a placeholder.",
|
|
51
|
+
"templateResolver.errors.document.disallowedLinkHref": "Link target “{detail}” is not allowed; use https, http, mailto, tel or a placeholder.",
|
|
52
|
+
"templateResolver.errors.document.documentRequiresHtmlFormat": "This template kind requires the html format, got “{detail}”.",
|
|
53
|
+
"templateResolver.errors.document.layoutRequiresMjmlFormat": "Mail layouts require the mjml format, got “{detail}”.",
|
|
54
|
+
"templateResolver.errors.document.layoutInvalidStructure": "The layout may only contain the header, content, signature and footer regions: {detail}.",
|
|
55
|
+
"templateResolver.errors.document.layoutMissingContentSlot": "The layout needs its content slot: {detail}.",
|
|
56
|
+
"templateResolver.errors.document.layoutSourceSystemOnly": "Only system layouts may use {detail} source; use the structured html layout.",
|
|
57
|
+
"templateResolver.errors.template.targetExists": "A template for locale “{detail}” already exists; overwrite it explicitly to replace it.",
|
|
58
|
+
"templateResolver.warnings.resourceNotUploaded": "Image not uploaded: {detail}",
|
|
59
|
+
"templateResolver.checks.resourcesResolved": "All images are available",
|
|
60
|
+
"templateResolver.checks.textPresent": "A plain-text version is present",
|
|
61
|
+
"templateResolver.checks.sizeUnderCap": "The mail is within the 10 MiB size limit",
|
|
62
|
+
"templateResolver.checks.requiredPlaceholders": "All required placeholders are used",
|
|
63
|
+
"templateResolver.errors.preview.renderFailed": "The preview could not be rendered.",
|
|
64
|
+
"templateResolver.errors.testMail.noChannel": "No email channel is set up, so no test mail can be sent.",
|
|
65
|
+
"templateResolver.errors.testMail.noAddress": "Your account has no email address to send the test mail to.",
|
|
66
|
+
"templateResolver.errors.testMail.failed": "The test mail could not be sent.",
|
|
67
|
+
"templateResolver.errors.resource.tooLarge": "The image is too large.",
|
|
68
|
+
"templateResolver.errors.resource.unsupportedImage": "This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
|
|
69
|
+
"templateResolver.errors.template.systemResourcesNeedSystemTenant": "Resource files can only be imported as system templates by a user of the system tenant.",
|
|
70
|
+
"templateResolver.errors.collection.kindNotAllowed": "“{detail}” templates are written through their own editor, not through a collection.",
|
|
71
|
+
"templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
|
|
72
|
+
"templateResolver.errors.template.invalidMeta": "Invalid template settings: {detail}",
|
|
73
|
+
"templateResolver.errors.template.unknownPlaceholders": "Unknown placeholders: {detail}",
|
|
74
|
+
"templateResolver.errors.template.missingRequiredPlaceholders": "Required placeholders are not used: {detail}",
|
|
75
|
+
"templateResolver.errors.document.embeddedContent": "<{detail}> elements are not allowed in template bodies.",
|
|
76
|
+
"templateResolver.errors.document.unsafeAttributeValue": "The attribute value “{detail}” must not contain < or >.",
|
|
77
|
+
"templateResolver.errors.document.eventHandlerAttribute": "Inline event handler “{detail}” is not allowed.",
|
|
78
|
+
"templateResolver.errors.document.externalImageSource": "Images must reference an uploaded resource; image source “{detail}” is not allowed.",
|
|
79
|
+
"templateResolver.errors.document.imageWithoutResource": "Images must reference an uploaded resource.",
|
|
80
|
+
"templateResolver.errors.document.invalidReferenceName": "“{detail}” is not a valid name.",
|
|
81
|
+
"templateResolver.errors.document.scriptElement": "<script> elements are not allowed in template bodies.",
|
|
82
|
+
"templateResolver.errors.attachment.unsupportedType": "Attachments must be a PDF, PNG or JPEG file.",
|
|
83
|
+
"templateResolver.errors.attachment.limit": "A template can have at most {max} attachments.",
|
|
84
|
+
"templateResolver.errors.attachment.invalid": "The attachment needs a template, a key and a valid file name.",
|
|
85
|
+
"templateResolver.errors.attachment.pdfTemplateMissing": "There is no PDF template “{slug}” for this tenant or the system.",
|
|
86
|
+
"template-resolver.documentEditor.htmlSource": "HTML source",
|
|
87
|
+
"template-resolver.documentEditor.removeBlock": "Remove block",
|
|
88
|
+
"template-resolver.documentEditor.selectedBlock": "Selected block",
|
|
89
|
+
"template-resolver.documentEditor.showHtml": "HTML",
|
|
90
|
+
"template-resolver.documentEditor.underline": "Underline",
|
|
15
91
|
"template-resolver.editor.saveFailed": "Save failed.",
|
|
16
92
|
"template-resolver.editor.networkError": "Network error while saving.",
|
|
17
93
|
"template-resolver.editor.loading": "Loading current version…",
|
|
@@ -1 +1,5 @@
|
|
|
1
|
+
export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "../qualified-names.js";
|
|
1
2
|
export { textBlocksClient } from "./client-plugin.js";
|
|
3
|
+
export { DocumentContentEditor } from "./document-editor.js";
|
|
4
|
+
export { type DocumentEditorChrome, type DocumentEditorHandle, type DocumentEditorProps, type DocumentResourcePreview, type DocumentToolbarBlock, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.js";
|
|
5
|
+
export { PlaceholderLineEditor, type PlaceholderLineEditorProps, } from "./placeholder-line-editor-lazy.js";
|
|
@@ -4,4 +4,8 @@
|
|
|
4
4
|
// — the server side (defineFeature) lives in
|
|
5
5
|
// `@cosmicdrift/kumiko-bundled-features/template-resolver` and has no React or
|
|
6
6
|
// DOM deps.
|
|
7
|
+
export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "../qualified-names.js";
|
|
7
8
|
export { textBlocksClient } from "./client-plugin.js";
|
|
9
|
+
export { DocumentContentEditor } from "./document-editor.js";
|
|
10
|
+
export { MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.js";
|
|
11
|
+
export { PlaceholderLineEditor, } from "./placeholder-line-editor-lazy.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PlaceholderLineEditorProps } from "./placeholder-line-editor.js";
|
|
3
|
+
export type { PlaceholderLineEditorProps } from "./placeholder-line-editor.js";
|
|
4
|
+
export declare function PlaceholderLineEditor(props: PlaceholderLineEditorProps): ReactNode;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
//
|
|
4
|
+
// Lazy boundary for the placeholder line editor: tiptap loads with the chunk,
|
|
5
|
+
// the fallback is a plain text input holding the same `{{name}}` text.
|
|
6
|
+
// Never import placeholder-line-editor statically here.
|
|
7
|
+
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
8
|
+
import { lazy, Suspense } from "react";
|
|
9
|
+
const LazyPlaceholderLineEditor = lazy(() => import("./placeholder-line-editor.js"));
|
|
10
|
+
function PlainLineFallback({ id, value, onChange, readOnly = false, "aria-label": ariaLabel, }) {
|
|
11
|
+
const { Field, Input } = usePrimitives();
|
|
12
|
+
return (_jsx(Field, { id: id, label: ariaLabel, hideLabel: true, children: _jsx(Input, { kind: "text", id: id, name: id, value: value, onChange: onChange, disabled: readOnly }) }));
|
|
13
|
+
}
|
|
14
|
+
export function PlaceholderLineEditor(props) {
|
|
15
|
+
return (_jsx(Suspense, { fallback: _jsx(PlainLineFallback, { ...props }), children: _jsx(LazyPlaceholderLineEditor, { ...props }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { DocumentEditorHandle } from "./document-toolbar-blocks.js";
|
|
3
|
+
export type PlaceholderLineEditorProps = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
/** Plain text with `{{name}}` placeholders. */
|
|
6
|
+
readonly value: string;
|
|
7
|
+
readonly onChange: (value: string) => void;
|
|
8
|
+
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
9
|
+
/** Placeholders shown as system chips (a different tint). */
|
|
10
|
+
readonly systemVariables?: readonly string[];
|
|
11
|
+
readonly readOnly?: boolean;
|
|
12
|
+
readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
|
|
13
|
+
readonly "aria-label": string;
|
|
14
|
+
};
|
|
15
|
+
export default function PlaceholderLineEditor({ id, value, onChange, variableLabels, systemVariables, readOnly, onEditorReady, "aria-label": ariaLabel, }: PlaceholderLineEditorProps): ReactNode;
|