@cosmicdrift/kumiko-samples 0.363.0 → 0.364.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +7 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +37 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +17 -5
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +17 -4
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/delivery/attachment-ref.ts +21 -0
- package/packages/bundled-features/src/delivery/changes.json +18 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +19 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +68 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +77 -13
- package/packages/bundled-features/src/template-resolver/constants.ts +12 -2
- package/packages/bundled-features/src/template-resolver/document-schema.ts +26 -4
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -1
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +31 -13
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +5 -44
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +90 -17
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/index.ts +12 -1
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +25 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +2 -2
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +87 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +159 -56
- package/packages/bundled-features/src/template-resolver/template-resources.ts +44 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +1 -0
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +63 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +12 -3
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +349 -134
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +11 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +25 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +10 -3
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +4 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +5 -3
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// never as `{{name}}` text, so the sanitizer's schema keeps them.
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
type ActionMenuItemSpec,
|
|
9
10
|
TextareaContentEditor,
|
|
10
11
|
usePrimitives,
|
|
11
12
|
useTranslation,
|
|
@@ -15,6 +16,7 @@ import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
|
15
16
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
16
17
|
import type { Editor, JSONContent } from "@tiptap/core";
|
|
17
18
|
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
|
19
|
+
import { BubbleMenu } from "@tiptap/react/menus";
|
|
18
20
|
import {
|
|
19
21
|
AlignCenter,
|
|
20
22
|
AlignJustify,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
AlignRight,
|
|
23
25
|
Bold as BoldIcon,
|
|
24
26
|
Braces,
|
|
27
|
+
ChevronDown,
|
|
25
28
|
Image as ImageIcon,
|
|
26
29
|
Italic as ItalicIcon,
|
|
27
30
|
Link as LinkIcon,
|
|
@@ -34,7 +37,8 @@ import {
|
|
|
34
37
|
Table as TableIcon,
|
|
35
38
|
Underline as UnderlineIcon,
|
|
36
39
|
} from "lucide-react";
|
|
37
|
-
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
|
40
|
+
import { Fragment, type ReactNode, useEffect, useMemo, useState } from "react";
|
|
41
|
+
import { createPortal } from "react-dom";
|
|
38
42
|
import {
|
|
39
43
|
DOCUMENT_TEXT_ALIGNMENTS,
|
|
40
44
|
type DocumentTextAlignment,
|
|
@@ -43,7 +47,10 @@ import {
|
|
|
43
47
|
isDocumentReferenceName,
|
|
44
48
|
isDocumentResourceKey,
|
|
45
49
|
} from "../document-schema.js";
|
|
46
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
createDocumentEditorExtensions,
|
|
52
|
+
DOCUMENT_BLOCK_ATTRIBUTE,
|
|
53
|
+
} from "./document-editor-extensions.js";
|
|
47
54
|
import {
|
|
48
55
|
type DocumentEditorProps,
|
|
49
56
|
type DocumentResourcePreview,
|
|
@@ -65,6 +72,9 @@ function previewsKey(previews: Readonly<Record<string, DocumentResourcePreview>>
|
|
|
65
72
|
|
|
66
73
|
type PanelKind = "link" | "placeholder" | "image" | "button" | "repeat" | "align";
|
|
67
74
|
|
|
75
|
+
// Blocks that stay in the canvas toolbar; the rest moves into the overflow menu.
|
|
76
|
+
const CANVAS_INLINE_BLOCKS: readonly DocumentToolbarBlock[] = ["image", "table"];
|
|
77
|
+
|
|
68
78
|
const BLOCK_PANEL: Partial<Record<DocumentToolbarBlock, PanelKind>> = {
|
|
69
79
|
image: "image",
|
|
70
80
|
button: "button",
|
|
@@ -137,6 +147,66 @@ type ParagraphStyle = (typeof PARAGRAPH_STYLES)[number];
|
|
|
137
147
|
|
|
138
148
|
const HEADING_LEVELS = { "1": 1, "2": 2, "3": 3 } as const;
|
|
139
149
|
|
|
150
|
+
const PLACEHOLDER_HREF = /^\{\{([^{}]+)\}\}$/;
|
|
151
|
+
|
|
152
|
+
function buttonTargetLabel(href: string, labels: Readonly<Record<string, string>>): string {
|
|
153
|
+
const name = PLACEHOLDER_HREF.exec(href)?.[1];
|
|
154
|
+
if (name === undefined) return href;
|
|
155
|
+
return Object.hasOwn(labels, name) ? (labels[name] ?? name) : name;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The bubble menu registers its plugin once per mount, so a rebuilt editor needs a fresh mount.
|
|
159
|
+
const editorMountKeys = new WeakMap<Editor, number>();
|
|
160
|
+
let lastMountKey = 0;
|
|
161
|
+
function mountKeyOf(editor: Editor): number {
|
|
162
|
+
const known = editorMountKeys.get(editor);
|
|
163
|
+
if (known !== undefined) return known;
|
|
164
|
+
lastMountKey += 1;
|
|
165
|
+
const created = lastMountKey;
|
|
166
|
+
editorMountKeys.set(editor, created);
|
|
167
|
+
return created;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Board .hb: a small bar that straddles the block's top frame line on the right; above a table it sits fully
|
|
171
|
+
// outside so the header row stays readable.
|
|
172
|
+
const BUBBLE_BAR_CLASS =
|
|
173
|
+
"flex w-max items-center gap-1 rounded border border-input bg-popover p-px text-xs [&_button]:h-5 [&_button]:px-1.5 [&_button]:text-[10px]";
|
|
174
|
+
|
|
175
|
+
function bubbleOffset(editor: Editor): (args: { rects: { floating: { height: number } } }) => {
|
|
176
|
+
mainAxis: number;
|
|
177
|
+
crossAxis: number;
|
|
178
|
+
} {
|
|
179
|
+
return ({ rects }) => ({
|
|
180
|
+
mainAxis: findRepeatTable(editor) !== null ? 2 : -rects.floating.height / 2,
|
|
181
|
+
crossAxis: -8,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const BUBBLE_MENU_PLUGIN_KEY = "documentBlockOptions";
|
|
186
|
+
|
|
187
|
+
// The options bar belongs to the block frame, not to the caret inside it.
|
|
188
|
+
function selectedBlockReference(editor: Editor): {
|
|
189
|
+
getBoundingClientRect: () => DOMRect;
|
|
190
|
+
getClientRects: () => DOMRect[];
|
|
191
|
+
contextElement: Element;
|
|
192
|
+
} | null {
|
|
193
|
+
const { node } = editor.view.domAtPos(editor.state.selection.from);
|
|
194
|
+
const start = node instanceof Element ? node : node.parentElement;
|
|
195
|
+
const block = start?.closest(`[${DOCUMENT_BLOCK_ATTRIBUTE}], table`);
|
|
196
|
+
if (block === null || block === undefined) return null;
|
|
197
|
+
return {
|
|
198
|
+
getBoundingClientRect: () => block.getBoundingClientRect(),
|
|
199
|
+
getClientRects: () => [block.getBoundingClientRect()],
|
|
200
|
+
contextElement: block,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// The bubble is first measured before React has filled it, so it sits at the wrong place until a second pass.
|
|
205
|
+
function repositionBubble(editor: Editor): void {
|
|
206
|
+
if (editor.isDestroyed) return;
|
|
207
|
+
editor.view.dispatch(editor.state.tr.setMeta(BUBBLE_MENU_PLUGIN_KEY, "updatePosition"));
|
|
208
|
+
}
|
|
209
|
+
|
|
140
210
|
function isParagraphStyle(value: string): value is ParagraphStyle {
|
|
141
211
|
return isOneOf(PARAGRAPH_STYLES, value);
|
|
142
212
|
}
|
|
@@ -266,6 +336,15 @@ function InsertPanel({ editor, editorId, kind, variables, onClose }: PanelProps)
|
|
|
266
336
|
const chain = editor.chain().focus();
|
|
267
337
|
if (!editor.state.selection.empty) chain.setLink({ href }).run();
|
|
268
338
|
else if (editor.isActive("link")) chain.extendMarkRange("link").setLink({ href }).run();
|
|
339
|
+
else if (isDocumentPlaceholderHref(href))
|
|
340
|
+
// Placeholders are never inserted as {{name}} text; the link shows the variable's value.
|
|
341
|
+
chain
|
|
342
|
+
.insertContent({
|
|
343
|
+
type: "variable",
|
|
344
|
+
attrs: { name: href.slice(2, -2) },
|
|
345
|
+
marks: [{ type: "link", attrs: { href } }],
|
|
346
|
+
})
|
|
347
|
+
.run();
|
|
269
348
|
else
|
|
270
349
|
chain
|
|
271
350
|
.insertContent({ type: "text", text: href, marks: [{ type: "link", attrs: { href } }] })
|
|
@@ -434,17 +513,22 @@ export default function DocumentTiptapEditor({
|
|
|
434
513
|
resourcePreviews = NO_PREVIEWS,
|
|
435
514
|
chrome = "default",
|
|
436
515
|
toolbar = "full",
|
|
516
|
+
renderPaper,
|
|
517
|
+
toolbarHost,
|
|
437
518
|
onEditorReady,
|
|
438
519
|
}: DocumentEditorProps): ReactNode {
|
|
439
520
|
const t = useTranslation();
|
|
440
|
-
const { Button, Field, Input } = usePrimitives();
|
|
521
|
+
const { ActionOverflowMenu, Button, Field, Input } = usePrimitives();
|
|
441
522
|
const [openPanel, setOpenPanel] = useState<PanelKind | null>(null);
|
|
523
|
+
const [canvasScroller, setCanvasScroller] = useState<HTMLElement | null>(null);
|
|
442
524
|
const [showHtml, setShowHtml] = useState(false);
|
|
443
525
|
const canvas = chrome === "canvas";
|
|
444
526
|
const compact = toolbar === "compact" && !canvas;
|
|
527
|
+
const hosted = toolbarHost !== undefined;
|
|
445
528
|
|
|
446
529
|
const placeholderLabel = t("template-resolver.documentEditor.placeholder");
|
|
447
530
|
const imageLabel = t("template-resolver.documentEditor.image");
|
|
531
|
+
const imageBlockLabel = t("template-resolver.documentEditor.imageBlock");
|
|
448
532
|
const pageBreakLabel = t("template-resolver.documentEditor.pageBreak");
|
|
449
533
|
const buttonLabel = t("template-resolver.documentEditor.button");
|
|
450
534
|
const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
|
|
@@ -457,6 +541,7 @@ export default function DocumentTiptapEditor({
|
|
|
457
541
|
image: imageLabel,
|
|
458
542
|
pageBreak: pageBreakLabel,
|
|
459
543
|
button: buttonLabel,
|
|
544
|
+
imageBlock: imageBlockLabel,
|
|
460
545
|
variableLabels: stableLabels,
|
|
461
546
|
systemVariables: stableSystem,
|
|
462
547
|
resourcePreviews: stablePreviews,
|
|
@@ -465,6 +550,7 @@ export default function DocumentTiptapEditor({
|
|
|
465
550
|
[
|
|
466
551
|
placeholderLabel,
|
|
467
552
|
imageLabel,
|
|
553
|
+
imageBlockLabel,
|
|
468
554
|
pageBreakLabel,
|
|
469
555
|
buttonLabel,
|
|
470
556
|
stableLabels,
|
|
@@ -592,6 +678,11 @@ export default function DocumentTiptapEditor({
|
|
|
592
678
|
? (variableLabels[repeatTable.lists[0] ?? ""] ?? repeatTable.lists[0])
|
|
593
679
|
: undefined;
|
|
594
680
|
|
|
681
|
+
const repeatBarLabel =
|
|
682
|
+
repeatListLabel === undefined
|
|
683
|
+
? t("template-resolver.documentEditor.repeat")
|
|
684
|
+
: t("template-resolver.documentEditor.repeatBarList", { list: repeatListLabel });
|
|
685
|
+
|
|
595
686
|
const paragraphStyleLabel = t("template-resolver.documentEditor.paragraphStyle");
|
|
596
687
|
const styleOptions = [
|
|
597
688
|
{ value: "paragraph", label: t("template-resolver.documentEditor.paragraph") },
|
|
@@ -600,17 +691,18 @@ export default function DocumentTiptapEditor({
|
|
|
600
691
|
{ value: "3", label: t("template-resolver.documentEditor.heading3") },
|
|
601
692
|
];
|
|
602
693
|
|
|
603
|
-
// Canvas chrome
|
|
694
|
+
// Canvas chrome is one flat row: unframed ghost buttons, glyph plus text for the longer names.
|
|
604
695
|
const iconButton = (
|
|
605
696
|
label: string,
|
|
606
697
|
icon: ReactNode,
|
|
607
698
|
isActive: boolean,
|
|
608
699
|
onClick: () => void,
|
|
700
|
+
text?: string,
|
|
609
701
|
): ReactNode =>
|
|
610
702
|
canvas ? (
|
|
611
703
|
<Button
|
|
612
704
|
type="button"
|
|
613
|
-
variant=
|
|
705
|
+
variant="ghost"
|
|
614
706
|
size="sm"
|
|
615
707
|
pressed={isActive}
|
|
616
708
|
disabled={readOnly}
|
|
@@ -619,6 +711,7 @@ export default function DocumentTiptapEditor({
|
|
|
619
711
|
title={label}
|
|
620
712
|
>
|
|
621
713
|
{icon}
|
|
714
|
+
{text}
|
|
622
715
|
</Button>
|
|
623
716
|
) : (
|
|
624
717
|
<Button
|
|
@@ -646,6 +739,7 @@ export default function DocumentTiptapEditor({
|
|
|
646
739
|
readonly label: string;
|
|
647
740
|
readonly icon: ReactNode;
|
|
648
741
|
readonly showIcon: boolean;
|
|
742
|
+
readonly trailingIcon?: ReactNode;
|
|
649
743
|
readonly expanded?: boolean;
|
|
650
744
|
readonly onClick: () => void;
|
|
651
745
|
}): ReactNode => {
|
|
@@ -664,112 +758,205 @@ export default function DocumentTiptapEditor({
|
|
|
664
758
|
<Button key={spec.key} {...shared} size="sm">
|
|
665
759
|
{spec.showIcon && spec.icon}
|
|
666
760
|
{spec.label}
|
|
761
|
+
{spec.trailingIcon}
|
|
667
762
|
</Button>
|
|
668
763
|
);
|
|
669
764
|
};
|
|
670
765
|
|
|
671
|
-
|
|
766
|
+
const editorContent = (
|
|
767
|
+
<EditorContent
|
|
768
|
+
editor={editor}
|
|
769
|
+
className={cn(
|
|
770
|
+
!canvas && "min-h-40 px-3 py-2 text-base outline-none md:text-sm",
|
|
771
|
+
canvas && "text-sm",
|
|
772
|
+
"[&_.ProseMirror-focused_.ProseMirror-selectednode]:ring-2 [&_.ProseMirror-focused_.ProseMirror-selectednode]:ring-primary",
|
|
773
|
+
hosted
|
|
774
|
+
? "[&_.ProseMirror]:outline-none"
|
|
775
|
+
: "[&_.ProseMirror]:min-h-40 [&_.ProseMirror]:outline-none",
|
|
776
|
+
"[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-6 [&_ol]:pl-6",
|
|
777
|
+
"[&_h1]:text-xl [&_h1]:font-bold [&_h2]:text-lg [&_h2]:font-bold [&_h3]:text-base [&_h3]:font-bold",
|
|
778
|
+
"[&_a]:underline [&_a]:text-primary",
|
|
779
|
+
"[&_strong]:font-bold [&_em]:italic [&_u]:underline",
|
|
780
|
+
canvas
|
|
781
|
+
? "[&_table]:mb-2 [&_table]:mt-6 [&_.ProseMirror>:first-child_table]:mt-0 [&_.ProseMirror>table:first-child]:mt-0 [&_td:last-child>p[data-size=small][data-align=right]]:whitespace-nowrap"
|
|
782
|
+
: "[&_table]:my-2",
|
|
783
|
+
"[&_table]:w-full [&_table]:border-collapse",
|
|
784
|
+
"[&_td]:border-b [&_td]:border-border [&_td]:px-2 [&_td]:py-1.5 [&_td>p]:m-0 [&_th>p]:m-0",
|
|
785
|
+
"[&_th]:border-b [&_th]:border-border [&_th]:px-2 [&_th]:py-1.5 [&_th]:text-left [&_th]:text-xs [&_th]:font-medium [&_th]:text-muted-foreground",
|
|
786
|
+
"[&_tr[data-each]>td]:bg-muted/50",
|
|
787
|
+
"[&_[data-align=left]]:text-left [&_[data-align=center]]:text-center [&_[data-align=right]]:text-right [&_[data-align=justify]]:text-justify",
|
|
788
|
+
"[&_p[data-size=small]]:text-xs [&_p[data-size=small]]:text-muted-foreground",
|
|
789
|
+
"[&_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",
|
|
790
|
+
"[&_hr]:my-3 [&_hr]:border-input",
|
|
791
|
+
"[&_.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)]",
|
|
792
|
+
)}
|
|
793
|
+
/>
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
// The canvas toolbar never scrolls sideways: the rarer blocks and the HTML view sit in the menu.
|
|
797
|
+
const menuBlocks =
|
|
798
|
+
canvas && ActionOverflowMenu !== undefined
|
|
799
|
+
? blocks.filter((block) => !CANVAS_INLINE_BLOCKS.includes(block))
|
|
800
|
+
: [];
|
|
801
|
+
const inlineBlocks = blocks.filter((block) => !menuBlocks.includes(block));
|
|
802
|
+
const overflowItems: readonly ActionMenuItemSpec[] = [
|
|
803
|
+
...menuBlocks.map((block) => ({
|
|
804
|
+
id: block,
|
|
805
|
+
label: blockButtons[block].label,
|
|
806
|
+
disabled: readOnly,
|
|
807
|
+
onSelect: () => runBlock(block),
|
|
808
|
+
})),
|
|
809
|
+
...(canvas && !hosted
|
|
810
|
+
? [
|
|
811
|
+
{
|
|
812
|
+
id: "html",
|
|
813
|
+
label: t("template-resolver.documentEditor.showHtml"),
|
|
814
|
+
onSelect: () => setShowHtml((current) => !current),
|
|
815
|
+
},
|
|
816
|
+
]
|
|
817
|
+
: []),
|
|
818
|
+
];
|
|
819
|
+
|
|
820
|
+
const toolbarBar = (
|
|
672
821
|
<div
|
|
673
|
-
className={cn(
|
|
822
|
+
className={cn(
|
|
823
|
+
"flex items-center border-b border-input",
|
|
824
|
+
canvas
|
|
825
|
+
? "min-h-9 flex-nowrap gap-0 overflow-hidden bg-background px-1.5 [&>*]:shrink-0 [&_button]:h-7 [&_button]:gap-1 [&_button]:px-1 [&_button]:text-xs"
|
|
826
|
+
: "flex-wrap gap-1 p-1",
|
|
827
|
+
)}
|
|
674
828
|
>
|
|
675
|
-
<div
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
name="paragraph-style"
|
|
682
|
-
value={activeState?.blockStyle ?? "paragraph"}
|
|
683
|
-
onChange={setBlockStyle}
|
|
684
|
-
options={styleOptions}
|
|
685
|
-
display="dropdown"
|
|
686
|
-
disabled={readOnly}
|
|
687
|
-
/>
|
|
688
|
-
</Field>
|
|
689
|
-
</div>
|
|
690
|
-
<Separator compact={compact} />
|
|
691
|
-
{iconButton(
|
|
692
|
-
t("kumiko.contentEditor.bold"),
|
|
693
|
-
markContent("B", <BoldIcon size={16} />, "font-bold"),
|
|
694
|
-
activeState?.bold ?? false,
|
|
695
|
-
() => editor.chain().focus().toggleBold().run(),
|
|
696
|
-
)}
|
|
697
|
-
{iconButton(
|
|
698
|
-
t("kumiko.contentEditor.italic"),
|
|
699
|
-
markContent("I", <ItalicIcon size={16} />, "italic"),
|
|
700
|
-
activeState?.italic ?? false,
|
|
701
|
-
() => editor.chain().focus().toggleItalic().run(),
|
|
702
|
-
)}
|
|
703
|
-
{iconButton(
|
|
704
|
-
t("template-resolver.documentEditor.underline"),
|
|
705
|
-
markContent("U", <UnderlineIcon size={16} />, "underline"),
|
|
706
|
-
activeState?.underline ?? false,
|
|
707
|
-
() => editor.chain().focus().toggleUnderline().run(),
|
|
829
|
+
<div
|
|
830
|
+
// kumiko-lint-ignore raw-classname the toolbar paragraph select sits flush in the editor toolbar; ComboboxInput has no flat trigger variant
|
|
831
|
+
className={cn(
|
|
832
|
+
!canvas && (compact ? "w-24" : "w-40"),
|
|
833
|
+
canvas &&
|
|
834
|
+
"w-auto shrink-0 [&_button[aria-haspopup=listbox]]:border-transparent [&_button[aria-haspopup=listbox]]:bg-transparent [&_button[aria-haspopup=listbox]]:shadow-none",
|
|
708
835
|
)}
|
|
709
|
-
|
|
710
|
-
{
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
836
|
+
>
|
|
837
|
+
<Field id={`${id}-paragraph-style`} label={paragraphStyleLabel} hideLabel>
|
|
838
|
+
<Input
|
|
839
|
+
kind="select"
|
|
840
|
+
id={`${id}-paragraph-style`}
|
|
841
|
+
name="paragraph-style"
|
|
842
|
+
value={activeState?.blockStyle ?? "paragraph"}
|
|
843
|
+
onChange={setBlockStyle}
|
|
844
|
+
options={styleOptions}
|
|
845
|
+
display="dropdown"
|
|
846
|
+
disabled={readOnly}
|
|
847
|
+
/>
|
|
848
|
+
</Field>
|
|
849
|
+
</div>
|
|
850
|
+
<Separator compact={compact || canvas} />
|
|
851
|
+
{iconButton(
|
|
852
|
+
t("kumiko.contentEditor.bold"),
|
|
853
|
+
markContent("B", <BoldIcon size={16} />, "font-bold"),
|
|
854
|
+
activeState?.bold ?? false,
|
|
855
|
+
() => editor.chain().focus().toggleBold().run(),
|
|
856
|
+
)}
|
|
857
|
+
{iconButton(
|
|
858
|
+
t("kumiko.contentEditor.italic"),
|
|
859
|
+
markContent("I", <ItalicIcon size={16} />, "italic"),
|
|
860
|
+
activeState?.italic ?? false,
|
|
861
|
+
() => editor.chain().focus().toggleItalic().run(),
|
|
862
|
+
)}
|
|
863
|
+
{iconButton(
|
|
864
|
+
t("template-resolver.documentEditor.underline"),
|
|
865
|
+
markContent("U", <UnderlineIcon size={16} />, "underline"),
|
|
866
|
+
activeState?.underline ?? false,
|
|
867
|
+
() => editor.chain().focus().toggleUnderline().run(),
|
|
868
|
+
)}
|
|
869
|
+
<Separator compact={compact || canvas} />
|
|
870
|
+
{iconButton(
|
|
871
|
+
t("kumiko.contentEditor.bulletList"),
|
|
872
|
+
<List size={16} />,
|
|
873
|
+
activeState?.bulletList ?? false,
|
|
874
|
+
() => editor.chain().focus().toggleBulletList().run(),
|
|
875
|
+
canvas ? undefined : t("template-resolver.documentEditor.list"),
|
|
876
|
+
)}
|
|
877
|
+
{iconButton(
|
|
878
|
+
t("kumiko.contentEditor.orderedList"),
|
|
879
|
+
<ListOrdered size={16} />,
|
|
880
|
+
activeState?.orderedList ?? false,
|
|
881
|
+
() => editor.chain().focus().toggleOrderedList().run(),
|
|
882
|
+
)}
|
|
883
|
+
{iconButton(
|
|
884
|
+
t("template-resolver.documentEditor.link"),
|
|
885
|
+
<LinkIcon size={16} />,
|
|
886
|
+
activeState?.link ?? false,
|
|
887
|
+
() => togglePanel("link"),
|
|
888
|
+
)}
|
|
889
|
+
<Separator compact={compact || canvas} />
|
|
890
|
+
{labelledButton({
|
|
891
|
+
label: t("template-resolver.documentEditor.insertPlaceholder"),
|
|
892
|
+
icon: <Braces size={16} />,
|
|
893
|
+
showIcon: true,
|
|
894
|
+
trailingIcon: canvas ? <ChevronDown size={12} /> : undefined,
|
|
895
|
+
expanded: openPanel === "placeholder",
|
|
896
|
+
onClick: () => togglePanel("placeholder"),
|
|
897
|
+
})}
|
|
898
|
+
{inlineBlocks.length > 0 && <Separator compact={compact || canvas} />}
|
|
899
|
+
{inlineBlocks.map((block) =>
|
|
900
|
+
canvas ? (
|
|
901
|
+
<Fragment key={block}>
|
|
902
|
+
{iconButton(blockButtons[block].label, blockButtons[block].icon, false, () =>
|
|
903
|
+
runBlock(block),
|
|
904
|
+
)}
|
|
905
|
+
</Fragment>
|
|
906
|
+
) : (
|
|
738
907
|
labelledButton({
|
|
739
908
|
key: block,
|
|
740
909
|
label: blockButtons[block].label,
|
|
741
910
|
icon: blockButtons[block].icon,
|
|
742
|
-
showIcon:
|
|
911
|
+
showIcon: true,
|
|
743
912
|
onClick: () => runBlock(block),
|
|
744
|
-
})
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
{
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
913
|
+
})
|
|
914
|
+
),
|
|
915
|
+
)}
|
|
916
|
+
{overflowItems.length > 0 && ActionOverflowMenu !== undefined && (
|
|
917
|
+
<span className="ml-auto shrink-0">
|
|
918
|
+
<ActionOverflowMenu
|
|
919
|
+
items={overflowItems}
|
|
920
|
+
label={t("template-resolver.documentEditor.more")}
|
|
921
|
+
testId={`${id}-toolbar-more`}
|
|
922
|
+
/>
|
|
923
|
+
</span>
|
|
924
|
+
)}
|
|
925
|
+
</div>
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const bubbleMenu = (
|
|
929
|
+
<BubbleMenu
|
|
930
|
+
key={mountKeyOf(editor)}
|
|
931
|
+
pluginKey={BUBBLE_MENU_PLUGIN_KEY}
|
|
932
|
+
editor={editor}
|
|
933
|
+
options={{
|
|
934
|
+
strategy: "fixed",
|
|
935
|
+
placement: "top-end",
|
|
936
|
+
offset: bubbleOffset(editor),
|
|
937
|
+
scrollTarget: canvasScroller ?? window,
|
|
938
|
+
onShow: () => requestAnimationFrame(() => repositionBubble(editor)),
|
|
939
|
+
}}
|
|
940
|
+
getReferencedVirtualElement={() => selectedBlockReference(editor)}
|
|
941
|
+
shouldShow={({ editor: e, element }) =>
|
|
942
|
+
// A leading image is selected by default; the bubble belongs to a deliberate selection.
|
|
943
|
+
(e.isFocused || element.contains(document.activeElement)) &&
|
|
944
|
+
(e.isActive("resource") || e.isActive("button") || findRepeatTable(e) !== null)
|
|
945
|
+
}
|
|
946
|
+
>
|
|
947
|
+
{selectedBlock !== null ? (
|
|
948
|
+
<div className={BUBBLE_BAR_CLASS} data-testid="document-block-options">
|
|
949
|
+
<span className="px-1.5 text-[10px] text-muted-foreground">
|
|
950
|
+
{selectedBlock.type === "resource"
|
|
951
|
+
? t("template-resolver.documentEditor.resourceKey")
|
|
952
|
+
: t("template-resolver.documentEditor.buttonTarget")}
|
|
953
|
+
{": "}
|
|
954
|
+
{selectedBlock.type === "button" ? (
|
|
955
|
+
<strong>{buttonTargetLabel(selectedBlock.detail, variableLabels)}</strong>
|
|
956
|
+
) : (
|
|
957
|
+
<code>{selectedBlock.detail}</code>
|
|
958
|
+
)}
|
|
770
959
|
</span>
|
|
771
|
-
<code className="text-xs text-muted-foreground">{selectedBlock.detail}</code>
|
|
772
|
-
<span className="flex-1" />
|
|
773
960
|
<Button
|
|
774
961
|
type="button"
|
|
775
962
|
size="sm"
|
|
@@ -781,25 +968,72 @@ export default function DocumentTiptapEditor({
|
|
|
781
968
|
{t("template-resolver.documentEditor.removeBlock")}
|
|
782
969
|
</Button>
|
|
783
970
|
</div>
|
|
971
|
+
) : (
|
|
972
|
+
repeatTable !== null && (
|
|
973
|
+
<div data-testid="document-repeat-bar" className={BUBBLE_BAR_CLASS}>
|
|
974
|
+
<span className="px-1.5 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
975
|
+
{repeatBarLabel}
|
|
976
|
+
</span>
|
|
977
|
+
<Button
|
|
978
|
+
type="button"
|
|
979
|
+
variant="secondary"
|
|
980
|
+
size="sm"
|
|
981
|
+
pressed={repeatTable.headerRepeats}
|
|
982
|
+
onClick={() => toggleRepeatHeader(editor)}
|
|
983
|
+
>
|
|
984
|
+
{t("template-resolver.documentEditor.repeatHeader")}
|
|
985
|
+
</Button>
|
|
986
|
+
</div>
|
|
987
|
+
)
|
|
784
988
|
)}
|
|
785
|
-
|
|
989
|
+
</BubbleMenu>
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const insertPanel = openPanel !== null && !readOnly && (
|
|
993
|
+
<InsertPanel
|
|
994
|
+
key={openPanel}
|
|
995
|
+
editor={editor}
|
|
996
|
+
editorId={id}
|
|
997
|
+
kind={openPanel}
|
|
998
|
+
variables={variables}
|
|
999
|
+
onClose={() => setOpenPanel(null)}
|
|
1000
|
+
/>
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
if (hosted) {
|
|
1004
|
+
return (
|
|
1005
|
+
<>
|
|
1006
|
+
{toolbarHost !== null &&
|
|
1007
|
+
createPortal(
|
|
1008
|
+
<>
|
|
1009
|
+
{toolbarBar}
|
|
1010
|
+
{insertPanel}
|
|
1011
|
+
</>,
|
|
1012
|
+
toolbarHost,
|
|
1013
|
+
)}
|
|
1014
|
+
{!readOnly && bubbleMenu}
|
|
1015
|
+
{editorContent}
|
|
1016
|
+
</>
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return (
|
|
1021
|
+
<div
|
|
1022
|
+
className={cn(
|
|
1023
|
+
canvas ? "flex min-h-0 flex-1 flex-col" : "rounded-md border border-input bg-transparent",
|
|
1024
|
+
)}
|
|
1025
|
+
>
|
|
1026
|
+
{!hosted && toolbarBar}
|
|
1027
|
+
{canvas && !readOnly && bubbleMenu}
|
|
1028
|
+
{canvas && !hosted && showHtml && (
|
|
786
1029
|
<section aria-label={t("template-resolver.documentEditor.htmlSource")}>
|
|
787
1030
|
<pre className="h-40 overflow-auto whitespace-pre-wrap break-all border-b border-input bg-muted p-2 font-mono text-xs">
|
|
788
1031
|
{activeState?.html ?? ""}
|
|
789
1032
|
</pre>
|
|
790
1033
|
</section>
|
|
791
1034
|
)}
|
|
792
|
-
{
|
|
793
|
-
|
|
794
|
-
key={openPanel}
|
|
795
|
-
editor={editor}
|
|
796
|
-
editorId={id}
|
|
797
|
-
kind={openPanel}
|
|
798
|
-
variables={variables}
|
|
799
|
-
onClose={() => setOpenPanel(null)}
|
|
800
|
-
/>
|
|
801
|
-
)}
|
|
802
|
-
{repeatTable !== null && !readOnly && (
|
|
1035
|
+
{!hosted && insertPanel}
|
|
1036
|
+
{repeatTable !== null && !readOnly && !canvas && (
|
|
803
1037
|
<div
|
|
804
1038
|
data-testid="document-repeat-bar"
|
|
805
1039
|
className="flex flex-wrap items-center gap-3 border-b border-input bg-muted/40 px-2 py-1 text-xs"
|
|
@@ -818,32 +1052,13 @@ export default function DocumentTiptapEditor({
|
|
|
818
1052
|
>
|
|
819
1053
|
{t("template-resolver.documentEditor.repeatHeader")}
|
|
820
1054
|
</Button>
|
|
821
|
-
<span className="text-muted-foreground">
|
|
822
|
-
{t("template-resolver.documentEditor.keepRowsTogether")}
|
|
823
|
-
</span>
|
|
824
1055
|
</div>
|
|
825
1056
|
)}
|
|
826
|
-
<div
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
? "w-[600px] max-w-full bg-background px-10 py-7 text-sm border border-input"
|
|
832
|
-
: "min-h-40 px-3 py-2 text-base outline-none md:text-sm",
|
|
833
|
-
"[&_.ProseMirror-selectednode]:ring-2 [&_.ProseMirror-selectednode]:ring-primary",
|
|
834
|
-
"[&_.ProseMirror]:min-h-40 [&_.ProseMirror]:outline-none",
|
|
835
|
-
"[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-6 [&_ol]:pl-6",
|
|
836
|
-
"[&_h1]:text-xl [&_h1]:font-bold [&_h2]:text-lg [&_h2]:font-bold [&_h3]:text-base [&_h3]:font-bold",
|
|
837
|
-
"[&_a]:underline [&_a]:text-primary",
|
|
838
|
-
"[&_strong]:font-bold [&_em]:italic [&_u]:underline",
|
|
839
|
-
"[&_table]:my-2 [&_table]:w-full [&_table]:border-collapse",
|
|
840
|
-
"[&_td]:border [&_td]:border-input [&_td]:p-1 [&_th]:border [&_th]:border-input [&_th]:bg-muted [&_th]:p-1 [&_th]:text-left",
|
|
841
|
-
"[&_tr[data-each]]:bg-muted",
|
|
842
|
-
"[&_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",
|
|
843
|
-
"[&_hr]:my-3 [&_hr]:border-input",
|
|
844
|
-
"[&_.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)]",
|
|
845
|
-
)}
|
|
846
|
-
/>
|
|
1057
|
+
<div
|
|
1058
|
+
ref={setCanvasScroller}
|
|
1059
|
+
className={cn(canvas && "min-h-0 flex-1 overflow-auto bg-muted px-8 py-6")}
|
|
1060
|
+
>
|
|
1061
|
+
{canvas && renderPaper !== undefined ? renderPaper(editorContent) : editorContent}
|
|
847
1062
|
</div>
|
|
848
1063
|
</div>
|
|
849
1064
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @runtime client
|
|
2
2
|
import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
3
4
|
|
|
4
5
|
export type DocumentToolbarBlock =
|
|
5
6
|
| "image"
|
|
@@ -39,6 +40,11 @@ export type DocumentEditorProps = ContentEditorProps & {
|
|
|
39
40
|
readonly chrome?: DocumentEditorChrome;
|
|
40
41
|
/** "compact" renders the placeholder and block buttons icon-only with a tooltip, for narrow columns. Default "full". */
|
|
41
42
|
readonly toolbar?: "full" | "compact";
|
|
43
|
+
/** Canvas only: wraps the editable area in a caller-owned page (letterhead, margins) instead of the default mail paper. */
|
|
44
|
+
readonly renderPaper?: (editable: ReactNode) => ReactNode;
|
|
45
|
+
/** Canvas only: renders the toolbar (and its insert panel) into this element instead of above the editor;
|
|
46
|
+
* null renders none. Lets several region editors share one toolbar that follows the focused one. */
|
|
47
|
+
readonly toolbarHost?: HTMLElement | null;
|
|
42
48
|
/** Called with the handle once the editor exists and with null when it is destroyed. */
|
|
43
49
|
readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
|
|
44
50
|
};
|
|
@@ -59,3 +65,8 @@ export const PDF_DOCUMENT_BLOCKS = [
|
|
|
59
65
|
"repeat",
|
|
60
66
|
"pageBreak",
|
|
61
67
|
] as const satisfies readonly DocumentToolbarBlock[];
|
|
68
|
+
|
|
69
|
+
/** Block set for a template-resource kind; every kind except "document-pdf" gets the mail set. */
|
|
70
|
+
export function documentBlocksForKind(kind: string | undefined): readonly DocumentToolbarBlock[] {
|
|
71
|
+
return kind === "document-pdf" ? PDF_DOCUMENT_BLOCKS : MAIL_DOCUMENT_BLOCKS;
|
|
72
|
+
}
|