@open-mercato/documents 0.10.1-develop.7808.1.8128cefd28 → 0.10.1-develop.7809.1.ea24461aeb
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/contentSecurityPolicy.js +23 -0
- package/dist/contentSecurityPolicy.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js +3 -1
- package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js +3 -1
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js +3 -0
- package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js.map +2 -2
- package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js +3 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js.map +2 -2
- package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js +3 -0
- package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js.map +2 -2
- package/dist/modules/documents/backend/documents/components/TemplatePreview.js +3 -0
- package/dist/modules/documents/backend/documents/components/TemplatePreview.js.map +2 -2
- package/package.json +6 -6
- package/src/contentSecurityPolicy.ts +21 -0
- package/src/modules/documents/backend/documents/[id]/DocumentCanvas.tsx +3 -1
- package/src/modules/documents/backend/documents/[id]/DocumentEditorSurface.tsx +3 -1
- package/src/modules/documents/backend/documents/[id]/VersionPreviewDialog.tsx +3 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentEditor.ts +3 -0
- package/src/modules/documents/backend/documents/components/TemplateBodyEditor.tsx +3 -0
- package/src/modules/documents/backend/documents/components/TemplatePreview.tsx +3 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { resolveDocumentsCollaborationEndpoint } from "./modules/documents/lib/collabEndpoint.js";
|
|
3
|
+
let warnedAboutExcludedEndpoint = false;
|
|
4
|
+
function resolveDocumentsCollaborationCspOrigins() {
|
|
5
|
+
const endpoint = resolveDocumentsCollaborationEndpoint();
|
|
6
|
+
if (!endpoint) return [];
|
|
7
|
+
const url = new URL(endpoint);
|
|
8
|
+
if (url.username || url.password || !/^(?:[a-z0-9.-]+|\[[0-9a-f:]+\])$/i.test(url.hostname)) {
|
|
9
|
+
if (!warnedAboutExcludedEndpoint) {
|
|
10
|
+
warnedAboutExcludedEndpoint = true;
|
|
11
|
+
createLogger("documents:csp").warn(
|
|
12
|
+
"[internal] Collaboration endpoint excluded from CSP; use a credential-free DNS or IPv6 URL",
|
|
13
|
+
{ configuration: "NEXT_PUBLIC_DOCUMENTS_COLLAB_URL" }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return [url.origin];
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
resolveDocumentsCollaborationCspOrigins
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=contentSecurityPolicy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/contentSecurityPolicy.ts"],
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveDocumentsCollaborationEndpoint } from './modules/documents/lib/collabEndpoint'\n\nlet warnedAboutExcludedEndpoint = false\n\nexport function resolveDocumentsCollaborationCspOrigins(): string[] {\n const endpoint = resolveDocumentsCollaborationEndpoint()\n if (!endpoint) return []\n const url = new URL(endpoint)\n if (url.username || url.password || !/^(?:[a-z0-9.-]+|\\[[0-9a-f:]+\\])$/i.test(url.hostname)) {\n if (!warnedAboutExcludedEndpoint) {\n warnedAboutExcludedEndpoint = true\n createLogger('documents:csp').warn(\n '[internal] Collaboration endpoint excluded from CSP; use a credential-free DNS or IPv6 URL',\n { configuration: 'NEXT_PUBLIC_DOCUMENTS_COLLAB_URL' },\n )\n }\n return []\n }\n return [url.origin]\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,6CAA6C;AAEtD,IAAI,8BAA8B;AAE3B,SAAS,0CAAoD;AAClE,QAAM,WAAW,sCAAsC;AACvD,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,QAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,MAAI,IAAI,YAAY,IAAI,YAAY,CAAC,oCAAoC,KAAK,IAAI,QAAQ,GAAG;AAC3F,QAAI,CAAC,6BAA6B;AAChC,oCAA8B;AAC9B,mBAAa,eAAe,EAAE;AAAA,QAC5B;AAAA,QACA,EAAE,eAAe,mCAAmC;AAAA,MACtD;AAAA,IACF;AACA,WAAO,CAAC;AAAA,EACV;AACA,SAAO,CAAC,IAAI,MAAM;AACpB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -14,6 +14,7 @@ import { OutlinePane } from "./OutlinePane.js";
|
|
|
14
14
|
import { captureCommentAnchor } from "./CommentAnchorNavigation.js";
|
|
15
15
|
import { DOCUMENT_EDITOR_CONTENT_CLASS } from "./editorTypes.js";
|
|
16
16
|
import { DOCUMENT_PAGINATION_STYLES } from "./documentPagination.js";
|
|
17
|
+
import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
|
|
17
18
|
const EMPTY_BUBBLE_MENU_STATE = {
|
|
18
19
|
bold: false,
|
|
19
20
|
italic: false,
|
|
@@ -30,6 +31,7 @@ function DocumentCanvas({
|
|
|
30
31
|
onComment
|
|
31
32
|
}) {
|
|
32
33
|
const t = useT();
|
|
34
|
+
const nonce = useContentSecurityPolicyNonce();
|
|
33
35
|
const editorState = useEditorState({
|
|
34
36
|
editor,
|
|
35
37
|
selector: ({ editor: current }) => current ? {
|
|
@@ -50,7 +52,7 @@ function DocumentCanvas({
|
|
|
50
52
|
[onComment, readOnly]
|
|
51
53
|
);
|
|
52
54
|
return /* @__PURE__ */ jsxs("div", { className: "om-doc-canvas max-h-[calc(100dvh-var(--topbar-height,0px)-12rem)] min-h-96 overflow-auto bg-muted p-0 sm:px-4 sm:py-8 md:px-8 md:py-10", children: [
|
|
53
|
-
/* @__PURE__ */ jsx("style", { children: DOCUMENT_PAGINATION_STYLES }),
|
|
55
|
+
/* @__PURE__ */ jsx("style", { nonce, children: DOCUMENT_PAGINATION_STYLES }),
|
|
54
56
|
notice ? /* @__PURE__ */ jsx(Alert, { status: "information", style: "lighter", className: "mx-auto mb-4 max-w-3xl", children: /* @__PURE__ */ jsx(AlertDescription, { children: notice }) }) : null,
|
|
55
57
|
/* @__PURE__ */ jsxs("div", { className: "mx-auto flex w-fit min-w-full flex-col gap-4 lg:flex-row lg:items-start", children: [
|
|
56
58
|
outlineOpen ? /* @__PURE__ */ jsx(OutlinePane, { editor }) : null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/DocumentCanvas.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { Editor } from '@tiptap/core'\nimport { EditorContent, useEditorState } from '@tiptap/react'\nimport { BubbleMenu } from '@tiptap/react/menus'\nimport { Bold, Italic, Link2, MessageSquare, Underline } from 'lucide-react'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { OutlinePane } from './OutlinePane'\nimport { captureCommentAnchor, type CommentAnchor } from './CommentAnchorNavigation'\nimport { DOCUMENT_EDITOR_CONTENT_CLASS } from './editorTypes'\nimport { DOCUMENT_PAGINATION_STYLES } from './documentPagination'\n\ntype TitleModel = {\n value: string\n setValue: (value: string) => void\n saving: boolean\n commit: () => Promise<void>\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void\n}\n\nconst EMPTY_BUBBLE_MENU_STATE = {\n bold: false,\n italic: false,\n underline: false,\n link: false,\n}\n\nexport function DocumentCanvas({\n editor,\n title,\n readOnly,\n outlineOpen,\n notice,\n onOpenLink,\n onComment,\n}: {\n editor: Editor | null\n title: TitleModel\n readOnly: boolean\n outlineOpen: boolean\n notice: string | null\n onOpenLink: () => void\n onComment?: (anchor: CommentAnchor) => void\n}) {\n const t = useT()\n const editorState = useEditorState({\n editor,\n selector: ({ editor: current }) => current ? {\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n underline: current.isActive('underline'),\n link: current.isActive('link'),\n } : EMPTY_BUBBLE_MENU_STATE,\n }) ?? EMPTY_BUBBLE_MENU_STATE\n const keepSelection = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault(), [])\n const comment = React.useCallback(() => {\n if (!editor || !onComment) return\n const anchor = captureCommentAnchor(editor)\n if (anchor) onComment(anchor)\n }, [editor, onComment])\n const shouldShow = React.useCallback(\n ({ from, to }: { from: number; to: number }) => from !== to && (!readOnly || Boolean(onComment)),\n [onComment, readOnly],\n )\n\n return (\n <div className=\"om-doc-canvas max-h-[calc(100dvh-var(--topbar-height,0px)-12rem)] min-h-96 overflow-auto bg-muted p-0 sm:px-4 sm:py-8 md:px-8 md:py-10\">\n <style>{DOCUMENT_PAGINATION_STYLES}</style>\n {notice ? <Alert status=\"information\" style=\"lighter\" className=\"mx-auto mb-4 max-w-3xl\"><AlertDescription>{notice}</AlertDescription></Alert> : null}\n <div className=\"mx-auto flex w-fit min-w-full flex-col gap-4 lg:flex-row lg:items-start\">\n {outlineOpen ? <OutlinePane editor={editor} /> : null}\n <article className=\"om-doc-paper mx-auto flex-none bg-card sm:rounded-sm sm:shadow-lg\">\n {readOnly ? (\n <h1 className=\"mb-6 text-xl font-semibold leading-tight text-foreground sm:mb-8 sm:text-3xl\">{title.value}</h1>\n ) : (\n <Input\n value={title.value}\n onChange={(event) => title.setValue(event.target.value)}\n onBlur={() => void title.commit()}\n onKeyDown={title.onKeyDown}\n maxLength={512}\n disabled={title.saving}\n aria-label={t('documents.editor.title.ariaLabel')}\n className=\"mb-6 h-auto border-0 bg-transparent px-0 py-0 text-xl font-semibold leading-tight text-foreground shadow-none focus-visible:shadow-focus sm:mb-8 sm:text-3xl\"\n />\n )}\n {editor ? (\n <BubbleMenu editor={editor} shouldShow={shouldShow} className=\"z-popover flex items-center gap-1 rounded-md border border-border bg-card p-1 shadow-md\">\n {!readOnly ? (\n <>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.bold ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.bold')} aria-pressed={editorState.bold} onMouseDown={keepSelection} onClick={() => editor.chain().focus().toggleBold().run()}><Bold /></IconButton>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.italic ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.italic')} aria-pressed={editorState.italic} onMouseDown={keepSelection} onClick={() => editor.chain().focus().toggleItalic().run()}><Italic /></IconButton>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.underline ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.underline')} aria-pressed={editorState.underline} onMouseDown={keepSelection} onClick={() => editor.chain().focus().toggleMark('underline').run()}><Underline /></IconButton>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.link ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.link')} aria-pressed={editorState.link} onMouseDown={keepSelection} onClick={onOpenLink}><Link2 /></IconButton>\n </>\n ) : null}\n {onComment ? <Button type=\"button\" size=\"sm\" variant=\"ghost\" onMouseDown={keepSelection} onClick={comment}><MessageSquare />{t('documents.editor.toolbar.comment')}</Button> : null}\n </BubbleMenu>\n ) : null}\n <EditorContent className={cn(DOCUMENT_EDITOR_CONTENT_CLASS, readOnly && 'select-text')} editor={editor} />\n </article>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { Editor } from '@tiptap/core'\nimport { EditorContent, useEditorState } from '@tiptap/react'\nimport { BubbleMenu } from '@tiptap/react/menus'\nimport { Bold, Italic, Link2, MessageSquare, Underline } from 'lucide-react'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { OutlinePane } from './OutlinePane'\nimport { captureCommentAnchor, type CommentAnchor } from './CommentAnchorNavigation'\nimport { DOCUMENT_EDITOR_CONTENT_CLASS } from './editorTypes'\nimport { DOCUMENT_PAGINATION_STYLES } from './documentPagination'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\n\ntype TitleModel = {\n value: string\n setValue: (value: string) => void\n saving: boolean\n commit: () => Promise<void>\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void\n}\n\nconst EMPTY_BUBBLE_MENU_STATE = {\n bold: false,\n italic: false,\n underline: false,\n link: false,\n}\n\nexport function DocumentCanvas({\n editor,\n title,\n readOnly,\n outlineOpen,\n notice,\n onOpenLink,\n onComment,\n}: {\n editor: Editor | null\n title: TitleModel\n readOnly: boolean\n outlineOpen: boolean\n notice: string | null\n onOpenLink: () => void\n onComment?: (anchor: CommentAnchor) => void\n}) {\n const t = useT()\n const nonce = useContentSecurityPolicyNonce()\n const editorState = useEditorState({\n editor,\n selector: ({ editor: current }) => current ? {\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n underline: current.isActive('underline'),\n link: current.isActive('link'),\n } : EMPTY_BUBBLE_MENU_STATE,\n }) ?? EMPTY_BUBBLE_MENU_STATE\n const keepSelection = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault(), [])\n const comment = React.useCallback(() => {\n if (!editor || !onComment) return\n const anchor = captureCommentAnchor(editor)\n if (anchor) onComment(anchor)\n }, [editor, onComment])\n const shouldShow = React.useCallback(\n ({ from, to }: { from: number; to: number }) => from !== to && (!readOnly || Boolean(onComment)),\n [onComment, readOnly],\n )\n\n return (\n <div className=\"om-doc-canvas max-h-[calc(100dvh-var(--topbar-height,0px)-12rem)] min-h-96 overflow-auto bg-muted p-0 sm:px-4 sm:py-8 md:px-8 md:py-10\">\n <style nonce={nonce}>{DOCUMENT_PAGINATION_STYLES}</style>\n {notice ? <Alert status=\"information\" style=\"lighter\" className=\"mx-auto mb-4 max-w-3xl\"><AlertDescription>{notice}</AlertDescription></Alert> : null}\n <div className=\"mx-auto flex w-fit min-w-full flex-col gap-4 lg:flex-row lg:items-start\">\n {outlineOpen ? <OutlinePane editor={editor} /> : null}\n <article className=\"om-doc-paper mx-auto flex-none bg-card sm:rounded-sm sm:shadow-lg\">\n {readOnly ? (\n <h1 className=\"mb-6 text-xl font-semibold leading-tight text-foreground sm:mb-8 sm:text-3xl\">{title.value}</h1>\n ) : (\n <Input\n value={title.value}\n onChange={(event) => title.setValue(event.target.value)}\n onBlur={() => void title.commit()}\n onKeyDown={title.onKeyDown}\n maxLength={512}\n disabled={title.saving}\n aria-label={t('documents.editor.title.ariaLabel')}\n className=\"mb-6 h-auto border-0 bg-transparent px-0 py-0 text-xl font-semibold leading-tight text-foreground shadow-none focus-visible:shadow-focus sm:mb-8 sm:text-3xl\"\n />\n )}\n {editor ? (\n <BubbleMenu editor={editor} shouldShow={shouldShow} className=\"z-popover flex items-center gap-1 rounded-md border border-border bg-card p-1 shadow-md\">\n {!readOnly ? (\n <>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.bold ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.bold')} aria-pressed={editorState.bold} onMouseDown={keepSelection} onClick={() => editor.chain().focus().toggleBold().run()}><Bold /></IconButton>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.italic ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.italic')} aria-pressed={editorState.italic} onMouseDown={keepSelection} onClick={() => editor.chain().focus().toggleItalic().run()}><Italic /></IconButton>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.underline ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.underline')} aria-pressed={editorState.underline} onMouseDown={keepSelection} onClick={() => editor.chain().focus().toggleMark('underline').run()}><Underline /></IconButton>\n <IconButton type=\"button\" size=\"sm\" variant={editorState.link ? 'outline' : 'ghost'} aria-label={t('documents.editor.toolbar.link')} aria-pressed={editorState.link} onMouseDown={keepSelection} onClick={onOpenLink}><Link2 /></IconButton>\n </>\n ) : null}\n {onComment ? <Button type=\"button\" size=\"sm\" variant=\"ghost\" onMouseDown={keepSelection} onClick={comment}><MessageSquare />{t('documents.editor.toolbar.comment')}</Button> : null}\n </BubbleMenu>\n ) : null}\n <EditorContent className={cn(DOCUMENT_EDITOR_CONTENT_CLASS, readOnly && 'select-text')} editor={editor} />\n </article>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
|
|
4
5
|
import { Link2, Unlink } from "lucide-react";
|
|
5
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
7
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
@@ -20,6 +21,7 @@ import { useDocumentTitle } from "./useDocumentTitle.js";
|
|
|
20
21
|
import { useEditorInsertions } from "./useEditorInsertions.js";
|
|
21
22
|
function DocumentEditorSurface(input) {
|
|
22
23
|
const t = useT();
|
|
24
|
+
const nonce = useContentSecurityPolicyNonce();
|
|
23
25
|
const [outlineOpen, setOutlineOpen] = React.useState(false);
|
|
24
26
|
const [suggestionRange, setSuggestionRange] = React.useState(null);
|
|
25
27
|
const modeChangeInFlightRef = React.useRef(false);
|
|
@@ -82,7 +84,7 @@ function DocumentEditorSurface(input) {
|
|
|
82
84
|
}
|
|
83
85
|
}, [fallbackPersistence.saveNow, input.mode, input.onModeChange, input.transport]);
|
|
84
86
|
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", input.transport === "collab" && "om-doc-collab", input.mode === "preview" && "om-doc-preview"), children: [
|
|
85
|
-
/* @__PURE__ */ jsx("style", { children: EDITOR_PRESENCE_STYLES }),
|
|
87
|
+
/* @__PURE__ */ jsx("style", { nonce, children: EDITOR_PRESENCE_STYLES }),
|
|
86
88
|
/* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border border-border bg-muted shadow-sm", children: [
|
|
87
89
|
/* @__PURE__ */ jsxs("div", { className: "sticky top-0 z-sticky border-b border-border bg-card/95", children: [
|
|
88
90
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-between gap-2 px-3 py-2", children: /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/DocumentEditorSurface.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Link2, Unlink } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { EntityPicker } from '../components/EntityPicker'\nimport { DocumentCanvas } from './DocumentCanvas'\nimport { EditorStatusPresence } from './EditorStatusPresence'\nimport { EditorToolbar } from './EditorToolbar'\nimport { RecordFieldsDialog } from './RecordFieldsDialog'\nimport { EDITOR_PRESENCE_STYLES } from './editorPresenceStyles'\nimport { canCreateSelectionComment, type CollabResources, type ConnectionStatus, type DocumentEditorIslandProps, type EditorMode, type PresenceUser } from './editorTypes'\nimport { useDocumentEditor } from './useDocumentEditor'\nimport { useFallbackContentPersistence } from './useFallbackContentPersistence'\nimport { useDocumentTitle } from './useDocumentTitle'\nimport { useEditorInsertions } from './useEditorInsertions'\n\ntype Props = DocumentEditorIslandProps & {\n transport: 'collab' | 'fallback'\n collabResources?: CollabResources\n connectionStatus: ConnectionStatus\n presenceUsers: PresenceUser[]\n mode: EditorMode\n onModeChange: (mode: EditorMode) => void\n}\n\nexport function DocumentEditorSurface(input: Props) {\n const t = useT()\n const [outlineOpen, setOutlineOpen] = React.useState(false)\n const [suggestionRange, setSuggestionRange] = React.useState<{ from: number; to: number } | null>(null)\n const modeChangeInFlightRef = React.useRef(false)\n const effectiveReadOnly = input.readOnly || input.mode === 'preview'\n const openSuggestion = React.useCallback((range: { from: number; to: number }) => setSuggestionRange(range), [])\n const closeSuggestion = React.useCallback(() => setSuggestionRange(null), [])\n const fallbackPersistence = useFallbackContentPersistence({\n documentId: input.documentId,\n initialUpdatedAt: input.contentUpdatedAt ?? null,\n enabled: input.transport === 'fallback' && !input.readOnly,\n onConflictRefresh: input.onContentConflict,\n })\n const editorModel = useDocumentEditor({\n documentId: input.documentId,\n initialContentHtml: input.initialContentHtml,\n editorMode: input.transport,\n collabResources: input.collabResources,\n readOnly: effectiveReadOnly,\n onEditorReady: input.onEditorReady,\n onUpdate: fallbackPersistence.onEditorUpdate,\n onEntitySuggestion: openSuggestion,\n onSuggestionClose: closeSuggestion,\n })\n const title = useDocumentTitle({\n documentId: input.documentId,\n title: input.title,\n updatedAt: input.documentUpdatedAt ?? null,\n readOnly: effectiveReadOnly,\n onTitleChange: input.onTitleChange,\n })\n const insertions = useEditorInsertions({\n documentId: input.documentId,\n editorRef: editorModel.editorRef,\n disabled: effectiveReadOnly,\n suggestionRange,\n setSuggestionRange,\n })\n React.useEffect(() => {\n if (suggestionRange && !effectiveReadOnly) insertions.setPickerOpen(true)\n }, [effectiveReadOnly, insertions.setPickerOpen, suggestionRange])\n\n const status = input.transport === 'fallback' ? 'offline' : input.connectionStatus\n const notice = input.transport === 'fallback'\n ? t(input.readOnly ? 'documents.editor.realtime.readOnlyFallback' : 'documents.editor.realtime.singleUserFallback')\n : input.readOnly\n ? t('documents.editor.readOnly')\n : input.mode === 'preview'\n ? t('documents.editor.mode.previewNotice')\n : null\n const selectionCommentHandler = canCreateSelectionComment({\n permissionReadOnly: input.readOnly,\n mode: input.mode,\n transport: input.transport,\n hasCommentHandler: Boolean(input.onComment),\n }) ? input.onComment : undefined\n const handleModeChange = React.useCallback(async (nextMode: EditorMode) => {\n if (nextMode === input.mode || modeChangeInFlightRef.current) return\n if (input.transport !== 'fallback' || nextMode !== 'preview') {\n input.onModeChange(nextMode)\n return\n }\n\n modeChangeInFlightRef.current = true\n try {\n if (await fallbackPersistence.saveNow()) input.onModeChange(nextMode)\n } finally {\n modeChangeInFlightRef.current = false\n }\n }, [fallbackPersistence.saveNow, input.mode, input.onModeChange, input.transport])\n\n return (\n <div className={cn('space-y-3', input.transport === 'collab' && 'om-doc-collab', input.mode === 'preview' && 'om-doc-preview')}>\n <style>{EDITOR_PRESENCE_STYLES}</style>\n <div className=\"overflow-hidden rounded-lg border border-border bg-muted shadow-sm\">\n <div className=\"sticky top-0 z-sticky border-b border-border bg-card/95\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 px-3 py-2\">\n <EditorStatusPresence\n status={status}\n users={input.presenceUsers}\n counts={editorModel.counts}\n mode={input.mode}\n canEdit={!input.readOnly}\n onModeChange={(nextMode) => { void handleModeChange(nextMode) }}\n fallbackSave={input.transport === 'fallback' && !input.readOnly ? {\n status: fallbackPersistence.status,\n onSave: fallbackPersistence.saveNow,\n } : undefined}\n />\n </div>\n {input.mode === 'edit' ? (\n <EditorToolbar\n editor={editorModel.editor}\n disabled={effectiveReadOnly || !editorModel.editor}\n outlineOpen={outlineOpen}\n uploading={insertions.uploading}\n onToggleOutline={() => setOutlineOpen((value) => !value)}\n onOpenEntityPicker={insertions.openEntityPicker}\n onOpenLink={insertions.openLink}\n onImage={() => insertions.fileInputRef.current?.click()}\n />\n ) : null}\n {insertions.linkOpen && !effectiveReadOnly ? (\n <div className=\"flex flex-col gap-3 border-t border-border bg-muted/30 p-3 md:flex-row md:items-end\" onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); insertions.applyLink() }\n if (event.key === 'Escape') { event.preventDefault(); insertions.setLinkOpen(false) }\n }}>\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Label htmlFor={`document-link-${input.documentId}`}>{t('documents.editor.link.url')}</Label>\n <Input id={`document-link-${input.documentId}`} type=\"url\" value={insertions.linkHref} onChange={(event) => insertions.setLinkHref(event.target.value)} placeholder={t('documents.editor.link.placeholder')} />\n </div>\n <Button type=\"button\" onClick={insertions.applyLink}><Link2 />{t('documents.editor.link.apply')}</Button>\n <Button type=\"button\" variant=\"outline\" onClick={insertions.removeLink}><Unlink />{t('documents.editor.link.remove')}</Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => insertions.setLinkOpen(false)}>{t('documents.actions.cancel')}</Button>\n </div>\n ) : null}\n </div>\n <DocumentCanvas editor={editorModel.editor} title={title} readOnly={effectiveReadOnly} outlineOpen={outlineOpen} notice={notice} onOpenLink={insertions.openLink} onComment={selectionCommentHandler} />\n </div>\n <EntityPicker open={insertions.entityPickerOpen} onOpenChange={insertions.setPickerOpen} onPick={(pick) => { void insertions.handleEntityPick(pick) }} />\n <RecordFieldsDialog\n documentId={input.documentId}\n linkId={insertions.recordFieldsLinkId}\n editor={editorModel.editor}\n canInsert={!effectiveReadOnly && Boolean(editorModel.editor)}\n onOpenChange={(open) => { if (!open) insertions.closeRecordFields() }}\n />\n <input ref={insertions.fileInputRef} type=\"file\" accept=\"image/*\" className=\"hidden\" aria-label={t('documents.editor.imageInput')} onChange={(event) => { void insertions.handleFiles(event.target.files) }} />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\nimport { Link2, Unlink } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { EntityPicker } from '../components/EntityPicker'\nimport { DocumentCanvas } from './DocumentCanvas'\nimport { EditorStatusPresence } from './EditorStatusPresence'\nimport { EditorToolbar } from './EditorToolbar'\nimport { RecordFieldsDialog } from './RecordFieldsDialog'\nimport { EDITOR_PRESENCE_STYLES } from './editorPresenceStyles'\nimport { canCreateSelectionComment, type CollabResources, type ConnectionStatus, type DocumentEditorIslandProps, type EditorMode, type PresenceUser } from './editorTypes'\nimport { useDocumentEditor } from './useDocumentEditor'\nimport { useFallbackContentPersistence } from './useFallbackContentPersistence'\nimport { useDocumentTitle } from './useDocumentTitle'\nimport { useEditorInsertions } from './useEditorInsertions'\n\ntype Props = DocumentEditorIslandProps & {\n transport: 'collab' | 'fallback'\n collabResources?: CollabResources\n connectionStatus: ConnectionStatus\n presenceUsers: PresenceUser[]\n mode: EditorMode\n onModeChange: (mode: EditorMode) => void\n}\n\nexport function DocumentEditorSurface(input: Props) {\n const t = useT()\n const nonce = useContentSecurityPolicyNonce()\n const [outlineOpen, setOutlineOpen] = React.useState(false)\n const [suggestionRange, setSuggestionRange] = React.useState<{ from: number; to: number } | null>(null)\n const modeChangeInFlightRef = React.useRef(false)\n const effectiveReadOnly = input.readOnly || input.mode === 'preview'\n const openSuggestion = React.useCallback((range: { from: number; to: number }) => setSuggestionRange(range), [])\n const closeSuggestion = React.useCallback(() => setSuggestionRange(null), [])\n const fallbackPersistence = useFallbackContentPersistence({\n documentId: input.documentId,\n initialUpdatedAt: input.contentUpdatedAt ?? null,\n enabled: input.transport === 'fallback' && !input.readOnly,\n onConflictRefresh: input.onContentConflict,\n })\n const editorModel = useDocumentEditor({\n documentId: input.documentId,\n initialContentHtml: input.initialContentHtml,\n editorMode: input.transport,\n collabResources: input.collabResources,\n readOnly: effectiveReadOnly,\n onEditorReady: input.onEditorReady,\n onUpdate: fallbackPersistence.onEditorUpdate,\n onEntitySuggestion: openSuggestion,\n onSuggestionClose: closeSuggestion,\n })\n const title = useDocumentTitle({\n documentId: input.documentId,\n title: input.title,\n updatedAt: input.documentUpdatedAt ?? null,\n readOnly: effectiveReadOnly,\n onTitleChange: input.onTitleChange,\n })\n const insertions = useEditorInsertions({\n documentId: input.documentId,\n editorRef: editorModel.editorRef,\n disabled: effectiveReadOnly,\n suggestionRange,\n setSuggestionRange,\n })\n React.useEffect(() => {\n if (suggestionRange && !effectiveReadOnly) insertions.setPickerOpen(true)\n }, [effectiveReadOnly, insertions.setPickerOpen, suggestionRange])\n\n const status = input.transport === 'fallback' ? 'offline' : input.connectionStatus\n const notice = input.transport === 'fallback'\n ? t(input.readOnly ? 'documents.editor.realtime.readOnlyFallback' : 'documents.editor.realtime.singleUserFallback')\n : input.readOnly\n ? t('documents.editor.readOnly')\n : input.mode === 'preview'\n ? t('documents.editor.mode.previewNotice')\n : null\n const selectionCommentHandler = canCreateSelectionComment({\n permissionReadOnly: input.readOnly,\n mode: input.mode,\n transport: input.transport,\n hasCommentHandler: Boolean(input.onComment),\n }) ? input.onComment : undefined\n const handleModeChange = React.useCallback(async (nextMode: EditorMode) => {\n if (nextMode === input.mode || modeChangeInFlightRef.current) return\n if (input.transport !== 'fallback' || nextMode !== 'preview') {\n input.onModeChange(nextMode)\n return\n }\n\n modeChangeInFlightRef.current = true\n try {\n if (await fallbackPersistence.saveNow()) input.onModeChange(nextMode)\n } finally {\n modeChangeInFlightRef.current = false\n }\n }, [fallbackPersistence.saveNow, input.mode, input.onModeChange, input.transport])\n\n return (\n <div className={cn('space-y-3', input.transport === 'collab' && 'om-doc-collab', input.mode === 'preview' && 'om-doc-preview')}>\n <style nonce={nonce}>{EDITOR_PRESENCE_STYLES}</style>\n <div className=\"overflow-hidden rounded-lg border border-border bg-muted shadow-sm\">\n <div className=\"sticky top-0 z-sticky border-b border-border bg-card/95\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 px-3 py-2\">\n <EditorStatusPresence\n status={status}\n users={input.presenceUsers}\n counts={editorModel.counts}\n mode={input.mode}\n canEdit={!input.readOnly}\n onModeChange={(nextMode) => { void handleModeChange(nextMode) }}\n fallbackSave={input.transport === 'fallback' && !input.readOnly ? {\n status: fallbackPersistence.status,\n onSave: fallbackPersistence.saveNow,\n } : undefined}\n />\n </div>\n {input.mode === 'edit' ? (\n <EditorToolbar\n editor={editorModel.editor}\n disabled={effectiveReadOnly || !editorModel.editor}\n outlineOpen={outlineOpen}\n uploading={insertions.uploading}\n onToggleOutline={() => setOutlineOpen((value) => !value)}\n onOpenEntityPicker={insertions.openEntityPicker}\n onOpenLink={insertions.openLink}\n onImage={() => insertions.fileInputRef.current?.click()}\n />\n ) : null}\n {insertions.linkOpen && !effectiveReadOnly ? (\n <div className=\"flex flex-col gap-3 border-t border-border bg-muted/30 p-3 md:flex-row md:items-end\" onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); insertions.applyLink() }\n if (event.key === 'Escape') { event.preventDefault(); insertions.setLinkOpen(false) }\n }}>\n <div className=\"min-w-0 flex-1 space-y-2\">\n <Label htmlFor={`document-link-${input.documentId}`}>{t('documents.editor.link.url')}</Label>\n <Input id={`document-link-${input.documentId}`} type=\"url\" value={insertions.linkHref} onChange={(event) => insertions.setLinkHref(event.target.value)} placeholder={t('documents.editor.link.placeholder')} />\n </div>\n <Button type=\"button\" onClick={insertions.applyLink}><Link2 />{t('documents.editor.link.apply')}</Button>\n <Button type=\"button\" variant=\"outline\" onClick={insertions.removeLink}><Unlink />{t('documents.editor.link.remove')}</Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => insertions.setLinkOpen(false)}>{t('documents.actions.cancel')}</Button>\n </div>\n ) : null}\n </div>\n <DocumentCanvas editor={editorModel.editor} title={title} readOnly={effectiveReadOnly} outlineOpen={outlineOpen} notice={notice} onOpenLink={insertions.openLink} onComment={selectionCommentHandler} />\n </div>\n <EntityPicker open={insertions.entityPickerOpen} onOpenChange={insertions.setPickerOpen} onPick={(pick) => { void insertions.handleEntityPick(pick) }} />\n <RecordFieldsDialog\n documentId={input.documentId}\n linkId={insertions.recordFieldsLinkId}\n editor={editorModel.editor}\n canInsert={!effectiveReadOnly && Boolean(editorModel.editor)}\n onOpenChange={(open) => { if (!open) insertions.closeRecordFields() }}\n />\n <input ref={insertions.fileInputRef} type=\"file\" accept=\"image/*\" className=\"hidden\" aria-label={t('documents.editor.imageInput')} onChange={(event) => { void insertions.handleFiles(event.target.files) }} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA0GM,cAkCQ,YAlCR;AAxGN,YAAY,WAAW;AACvB,SAAS,qCAAqC;AAC9C,SAAS,OAAO,cAAc;AAC9B,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,iCAAkJ;AAC3J,SAAS,yBAAyB;AAClC,SAAS,qCAAqC;AAC9C,SAAS,wBAAwB;AACjC,SAAS,2BAA2B;AAW7B,SAAS,sBAAsB,OAAc;AAClD,QAAM,IAAI,KAAK;AACf,QAAM,QAAQ,8BAA8B;AAC5C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAC1D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAA8C,IAAI;AACtG,QAAM,wBAAwB,MAAM,OAAO,KAAK;AAChD,QAAM,oBAAoB,MAAM,YAAY,MAAM,SAAS;AAC3D,QAAM,iBAAiB,MAAM,YAAY,CAAC,UAAwC,mBAAmB,KAAK,GAAG,CAAC,CAAC;AAC/G,QAAM,kBAAkB,MAAM,YAAY,MAAM,mBAAmB,IAAI,GAAG,CAAC,CAAC;AAC5E,QAAM,sBAAsB,8BAA8B;AAAA,IACxD,YAAY,MAAM;AAAA,IAClB,kBAAkB,MAAM,oBAAoB;AAAA,IAC5C,SAAS,MAAM,cAAc,cAAc,CAAC,MAAM;AAAA,IAClD,mBAAmB,MAAM;AAAA,EAC3B,CAAC;AACD,QAAM,cAAc,kBAAkB;AAAA,IACpC,YAAY,MAAM;AAAA,IAClB,oBAAoB,MAAM;AAAA,IAC1B,YAAY,MAAM;AAAA,IAClB,iBAAiB,MAAM;AAAA,IACvB,UAAU;AAAA,IACV,eAAe,MAAM;AAAA,IACrB,UAAU,oBAAoB;AAAA,IAC9B,oBAAoB;AAAA,IACpB,mBAAmB;AAAA,EACrB,CAAC;AACD,QAAM,QAAQ,iBAAiB;AAAA,IAC7B,YAAY,MAAM;AAAA,IAClB,OAAO,MAAM;AAAA,IACb,WAAW,MAAM,qBAAqB;AAAA,IACtC,UAAU;AAAA,IACV,eAAe,MAAM;AAAA,EACvB,CAAC;AACD,QAAM,aAAa,oBAAoB;AAAA,IACrC,YAAY,MAAM;AAAA,IAClB,WAAW,YAAY;AAAA,IACvB,UAAU;AAAA,IACV;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,UAAU,MAAM;AACpB,QAAI,mBAAmB,CAAC,kBAAmB,YAAW,cAAc,IAAI;AAAA,EAC1E,GAAG,CAAC,mBAAmB,WAAW,eAAe,eAAe,CAAC;AAEjE,QAAM,SAAS,MAAM,cAAc,aAAa,YAAY,MAAM;AAClE,QAAM,SAAS,MAAM,cAAc,aAC/B,EAAE,MAAM,WAAW,+CAA+C,8CAA8C,IAChH,MAAM,WACJ,EAAE,2BAA2B,IAC7B,MAAM,SAAS,YACb,EAAE,qCAAqC,IACvC;AACR,QAAM,0BAA0B,0BAA0B;AAAA,IACxD,oBAAoB,MAAM;AAAA,IAC1B,MAAM,MAAM;AAAA,IACZ,WAAW,MAAM;AAAA,IACjB,mBAAmB,QAAQ,MAAM,SAAS;AAAA,EAC5C,CAAC,IAAI,MAAM,YAAY;AACvB,QAAM,mBAAmB,MAAM,YAAY,OAAO,aAAyB;AACzE,QAAI,aAAa,MAAM,QAAQ,sBAAsB,QAAS;AAC9D,QAAI,MAAM,cAAc,cAAc,aAAa,WAAW;AAC5D,YAAM,aAAa,QAAQ;AAC3B;AAAA,IACF;AAEA,0BAAsB,UAAU;AAChC,QAAI;AACF,UAAI,MAAM,oBAAoB,QAAQ,EAAG,OAAM,aAAa,QAAQ;AAAA,IACtE,UAAE;AACA,4BAAsB,UAAU;AAAA,IAClC;AAAA,EACF,GAAG,CAAC,oBAAoB,SAAS,MAAM,MAAM,MAAM,cAAc,MAAM,SAAS,CAAC;AAEjF,SACE,qBAAC,SAAI,WAAW,GAAG,aAAa,MAAM,cAAc,YAAY,iBAAiB,MAAM,SAAS,aAAa,gBAAgB,GAC3H;AAAA,wBAAC,WAAM,OAAe,kCAAuB;AAAA,IAC7C,qBAAC,SAAI,WAAU,sEACb;AAAA,2BAAC,SAAI,WAAU,2DACb;AAAA,4BAAC,SAAI,WAAU,+DACb;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,OAAO,MAAM;AAAA,YACb,QAAQ,YAAY;AAAA,YACpB,MAAM,MAAM;AAAA,YACZ,SAAS,CAAC,MAAM;AAAA,YAChB,cAAc,CAAC,aAAa;AAAE,mBAAK,iBAAiB,QAAQ;AAAA,YAAE;AAAA,YAC9D,cAAc,MAAM,cAAc,cAAc,CAAC,MAAM,WAAW;AAAA,cAChE,QAAQ,oBAAoB;AAAA,cAC5B,QAAQ,oBAAoB;AAAA,YAC9B,IAAI;AAAA;AAAA,QACN,GACF;AAAA,QACC,MAAM,SAAS,SACd;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,YAAY;AAAA,YACpB,UAAU,qBAAqB,CAAC,YAAY;AAAA,YAC5C;AAAA,YACA,WAAW,WAAW;AAAA,YACtB,iBAAiB,MAAM,eAAe,CAAC,UAAU,CAAC,KAAK;AAAA,YACvD,oBAAoB,WAAW;AAAA,YAC/B,YAAY,WAAW;AAAA,YACvB,SAAS,MAAM,WAAW,aAAa,SAAS,MAAM;AAAA;AAAA,QACxD,IACE;AAAA,QACH,WAAW,YAAY,CAAC,oBACvB,qBAAC,SAAI,WAAU,uFAAsF,WAAW,CAAC,UAAU;AACzH,eAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAAE,kBAAM,eAAe;AAAG,uBAAW,UAAU;AAAA,UAAE;AAChH,cAAI,MAAM,QAAQ,UAAU;AAAE,kBAAM,eAAe;AAAG,uBAAW,YAAY,KAAK;AAAA,UAAE;AAAA,QACtF,GACE;AAAA,+BAAC,SAAI,WAAU,4BACb;AAAA,gCAAC,SAAM,SAAS,iBAAiB,MAAM,UAAU,IAAK,YAAE,2BAA2B,GAAE;AAAA,YACrF,oBAAC,SAAM,IAAI,iBAAiB,MAAM,UAAU,IAAI,MAAK,OAAM,OAAO,WAAW,UAAU,UAAU,CAAC,UAAU,WAAW,YAAY,MAAM,OAAO,KAAK,GAAG,aAAa,EAAE,mCAAmC,GAAG;AAAA,aAC/M;AAAA,UACA,qBAAC,UAAO,MAAK,UAAS,SAAS,WAAW,WAAW;AAAA,gCAAC,SAAM;AAAA,YAAG,EAAE,6BAA6B;AAAA,aAAE;AAAA,UAChG,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,WAAW,YAAY;AAAA,gCAAC,UAAO;AAAA,YAAG,EAAE,8BAA8B;AAAA,aAAE;AAAA,UACrH,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,SAAS,MAAM,WAAW,YAAY,KAAK,GAAI,YAAE,0BAA0B,GAAE;AAAA,WACrH,IACE;AAAA,SACN;AAAA,MACA,oBAAC,kBAAe,QAAQ,YAAY,QAAQ,OAAc,UAAU,mBAAmB,aAA0B,QAAgB,YAAY,WAAW,UAAU,WAAW,yBAAyB;AAAA,OACxM;AAAA,IACA,oBAAC,gBAAa,MAAM,WAAW,kBAAkB,cAAc,WAAW,eAAe,QAAQ,CAAC,SAAS;AAAE,WAAK,WAAW,iBAAiB,IAAI;AAAA,IAAE,GAAG;AAAA,IACvJ;AAAA,MAAC;AAAA;AAAA,QACC,YAAY,MAAM;AAAA,QAClB,QAAQ,WAAW;AAAA,QACnB,QAAQ,YAAY;AAAA,QACpB,WAAW,CAAC,qBAAqB,QAAQ,YAAY,MAAM;AAAA,QAC3D,cAAc,CAAC,SAAS;AAAE,cAAI,CAAC,KAAM,YAAW,kBAAkB;AAAA,QAAE;AAAA;AAAA,IACtE;AAAA,IACA,oBAAC,WAAM,KAAK,WAAW,cAAc,MAAK,QAAO,QAAO,WAAU,WAAU,UAAS,cAAY,EAAE,6BAA6B,GAAG,UAAU,CAAC,UAAU;AAAE,WAAK,WAAW,YAAY,MAAM,OAAO,KAAK;AAAA,IAAE,GAAG;AAAA,KAC/M;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
|
|
4
5
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
5
6
|
import { RotateCcw } from "lucide-react";
|
|
6
7
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
@@ -41,7 +42,9 @@ function normalizeVersionPreview(payload, fallbackCreatorLabel) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
function ReadOnlyVersion({ contentHtml, fallbackLabel }) {
|
|
45
|
+
const nonce = useContentSecurityPolicyNonce();
|
|
44
46
|
const editor = useEditor({
|
|
47
|
+
injectNonce: nonce,
|
|
45
48
|
extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),
|
|
46
49
|
content: contentHtml,
|
|
47
50
|
editable: false,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/VersionPreviewDialog.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { RotateCcw } from 'lucide-react'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { getDocumentEditorExtensions } from '../../../lib/editorConfig'\nimport { firstSafeDocumentsDisplayLabel } from '../../../lib/displayLabels'\nimport { sanitizeDocumentVersionLabel } from '../../../lib/versionLabels'\nimport { readRecord, readString } from '../documentUi'\nimport { DOCUMENT_EDITOR_CONTENT_CLASS } from './editorTypes'\n\nexport type VersionPreview = {\n id: string\n label: string | null\n creatorLabel: string\n createdAt: string\n contentHtml: string\n}\n\ntype VersionPreviewDialogProps = {\n documentId: string\n versionId: string | null\n canRestore: boolean\n isRestoring: boolean\n onOpenChange: (open: boolean) => void\n onRestore: (version: VersionPreview) => void | Promise<void>\n}\n\nexport function normalizeVersionPreview(\n payload: unknown,\n fallbackCreatorLabel?: string,\n): VersionPreview | null {\n const root = readRecord(payload)\n const record = readRecord(root?.item) ?? readRecord(root?.data) ?? root\n if (!record) return null\n const id = readString(record, 'id')\n const creatorLabel = firstSafeDocumentsDisplayLabel(\n readString(record, 'creatorLabel', 'creator_label'),\n fallbackCreatorLabel,\n )\n const createdAt = readString(record, 'createdAt', 'created_at')\n if (!id || !creatorLabel || !createdAt) return null\n return {\n id,\n creatorLabel,\n createdAt,\n label: sanitizeDocumentVersionLabel(readString(record, 'label')),\n contentHtml: readString(record, 'contentHtml', 'content_html') ?? '',\n }\n}\n\nfunction ReadOnlyVersion({ contentHtml, fallbackLabel }: { contentHtml: string; fallbackLabel: string }) {\n const editor = useEditor({\n extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),\n content: contentHtml,\n editable: false,\n editorProps: { attributes: { class: 'min-h-64 text-base leading-7 text-foreground focus-visible:outline-none' } },\n }, [contentHtml])\n React.useEffect(() => { editor?.commands.setContent(contentHtml) }, [contentHtml, editor])\n // Share the live editor's typography so tables keep their grid lines and\n // headings/lists read the same in the preview as on the canvas.\n return <EditorContent editor={editor} className={cn(DOCUMENT_EDITOR_CONTENT_CLASS, 'rounded-md border border-border bg-card p-4')} />\n}\n\nexport function VersionPreviewDialog({\n documentId,\n versionId,\n canRestore,\n isRestoring,\n onOpenChange,\n onRestore,\n}: VersionPreviewDialogProps) {\n const t = useT()\n const [preview, setPreview] = React.useState<VersionPreview | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(false)\n const [loadAttempt, setLoadAttempt] = React.useState(0)\n const [confirmingRestore, setConfirmingRestore] = React.useState(false)\n const cancelRestoreRef = React.useRef<HTMLButtonElement>(null)\n\n React.useEffect(() => {\n setConfirmingRestore(false)\n if (!versionId) { setPreview(null); setError(null); return }\n let active = true\n setLoading(true)\n setError(null)\n void apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}/versions/${encodeURIComponent(versionId)}`)\n .then((call) => {\n if (!active) return\n const next = call.ok\n ? normalizeVersionPreview(call.result, t('documents.users.unknown'))\n : null\n if (!next) setError(t('documents.versions.preview.error'))\n setPreview(next)\n })\n .catch(() => { if (active) setError(t('documents.versions.preview.error')) })\n .finally(() => { if (active) setLoading(false) })\n return () => { active = false }\n }, [documentId, loadAttempt, t, versionId])\n\n React.useEffect(() => {\n if (confirmingRestore) cancelRestoreRef.current?.focus()\n }, [confirmingRestore])\n\n const requestRestore = React.useCallback(() => {\n if (!canRestore || !preview || isRestoring) return\n setConfirmingRestore(true)\n }, [canRestore, isRestoring, preview])\n\n const confirmRestore = React.useCallback(() => {\n if (!canRestore || !preview || isRestoring) return\n void onRestore(preview)\n }, [canRestore, isRestoring, onRestore, preview])\n\n const handleOpenChange = React.useCallback((open: boolean) => {\n if (!open) setConfirmingRestore(false)\n onOpenChange(open)\n }, [onOpenChange])\n\n return (\n <Dialog open={versionId !== null} onOpenChange={handleOpenChange}>\n <DialogContent size=\"xl\" onKeyDown={(event) => {\n if (event.key === 'Escape') {\n if (confirmingRestore) {\n event.preventDefault()\n event.stopPropagation()\n if (!isRestoring) setConfirmingRestore(false)\n } else {\n onOpenChange(false)\n }\n }\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && canRestore && preview && !isRestoring) {\n event.preventDefault()\n if (confirmingRestore) confirmRestore()\n else requestRestore()\n }\n }}>\n <DialogHeader>\n <DialogTitle>\n {confirmingRestore\n ? t('documents.versions.restore.confirmTitle')\n : preview?.label ?? t('documents.versions.preview.title')}\n </DialogTitle>\n <DialogDescription aria-live={confirmingRestore ? 'assertive' : undefined}>\n {confirmingRestore\n ? t('documents.versions.restore.confirmBody')\n : preview\n ? t('documents.versions.preview.description', { creator: preview.creatorLabel, date: new Date(preview.createdAt).toLocaleString() })\n : t('documents.versions.preview.loading')}\n </DialogDescription>\n </DialogHeader>\n {!confirmingRestore && loading ? <LoadingMessage label={t('documents.versions.preview.loading')} /> : null}\n {!confirmingRestore && error ? (\n <ErrorMessage\n label={error}\n action={<Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setLoadAttempt((current) => current + 1)}>{t('documents.actions.retry')}</Button>}\n />\n ) : null}\n {!confirmingRestore && preview && !loading && !error ? <ReadOnlyVersion contentHtml={preview.contentHtml} fallbackLabel={t('documents.editor.entityRef.fallbackLabel')} /> : null}\n <DialogFooter>\n {confirmingRestore ? (\n <>\n <Button ref={cancelRestoreRef} type=\"button\" variant=\"outline\" onClick={() => setConfirmingRestore(false)} disabled={isRestoring}>\n {t('documents.actions.cancel')}\n </Button>\n <Button type=\"button\" onClick={confirmRestore} disabled={isRestoring}>\n <RotateCcw />{t('documents.versions.actions.restore')}\n </Button>\n </>\n ) : (\n <>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>{t('documents.actions.close')}</Button>\n {canRestore && preview ? (\n <Button type=\"button\" onClick={requestRestore} disabled={isRestoring}>\n <RotateCcw />{t('documents.versions.actions.restore')}\n </Button>\n ) : null}\n </>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { RotateCcw } from 'lucide-react'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { getDocumentEditorExtensions } from '../../../lib/editorConfig'\nimport { firstSafeDocumentsDisplayLabel } from '../../../lib/displayLabels'\nimport { sanitizeDocumentVersionLabel } from '../../../lib/versionLabels'\nimport { readRecord, readString } from '../documentUi'\nimport { DOCUMENT_EDITOR_CONTENT_CLASS } from './editorTypes'\n\nexport type VersionPreview = {\n id: string\n label: string | null\n creatorLabel: string\n createdAt: string\n contentHtml: string\n}\n\ntype VersionPreviewDialogProps = {\n documentId: string\n versionId: string | null\n canRestore: boolean\n isRestoring: boolean\n onOpenChange: (open: boolean) => void\n onRestore: (version: VersionPreview) => void | Promise<void>\n}\n\nexport function normalizeVersionPreview(\n payload: unknown,\n fallbackCreatorLabel?: string,\n): VersionPreview | null {\n const root = readRecord(payload)\n const record = readRecord(root?.item) ?? readRecord(root?.data) ?? root\n if (!record) return null\n const id = readString(record, 'id')\n const creatorLabel = firstSafeDocumentsDisplayLabel(\n readString(record, 'creatorLabel', 'creator_label'),\n fallbackCreatorLabel,\n )\n const createdAt = readString(record, 'createdAt', 'created_at')\n if (!id || !creatorLabel || !createdAt) return null\n return {\n id,\n creatorLabel,\n createdAt,\n label: sanitizeDocumentVersionLabel(readString(record, 'label')),\n contentHtml: readString(record, 'contentHtml', 'content_html') ?? '',\n }\n}\n\nfunction ReadOnlyVersion({ contentHtml, fallbackLabel }: { contentHtml: string; fallbackLabel: string }) {\n const nonce = useContentSecurityPolicyNonce()\n const editor = useEditor({\n injectNonce: nonce,\n extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),\n content: contentHtml,\n editable: false,\n editorProps: { attributes: { class: 'min-h-64 text-base leading-7 text-foreground focus-visible:outline-none' } },\n }, [contentHtml])\n React.useEffect(() => { editor?.commands.setContent(contentHtml) }, [contentHtml, editor])\n // Share the live editor's typography so tables keep their grid lines and\n // headings/lists read the same in the preview as on the canvas.\n return <EditorContent editor={editor} className={cn(DOCUMENT_EDITOR_CONTENT_CLASS, 'rounded-md border border-border bg-card p-4')} />\n}\n\nexport function VersionPreviewDialog({\n documentId,\n versionId,\n canRestore,\n isRestoring,\n onOpenChange,\n onRestore,\n}: VersionPreviewDialogProps) {\n const t = useT()\n const [preview, setPreview] = React.useState<VersionPreview | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(false)\n const [loadAttempt, setLoadAttempt] = React.useState(0)\n const [confirmingRestore, setConfirmingRestore] = React.useState(false)\n const cancelRestoreRef = React.useRef<HTMLButtonElement>(null)\n\n React.useEffect(() => {\n setConfirmingRestore(false)\n if (!versionId) { setPreview(null); setError(null); return }\n let active = true\n setLoading(true)\n setError(null)\n void apiCall<unknown>(`/api/documents/${encodeURIComponent(documentId)}/versions/${encodeURIComponent(versionId)}`)\n .then((call) => {\n if (!active) return\n const next = call.ok\n ? normalizeVersionPreview(call.result, t('documents.users.unknown'))\n : null\n if (!next) setError(t('documents.versions.preview.error'))\n setPreview(next)\n })\n .catch(() => { if (active) setError(t('documents.versions.preview.error')) })\n .finally(() => { if (active) setLoading(false) })\n return () => { active = false }\n }, [documentId, loadAttempt, t, versionId])\n\n React.useEffect(() => {\n if (confirmingRestore) cancelRestoreRef.current?.focus()\n }, [confirmingRestore])\n\n const requestRestore = React.useCallback(() => {\n if (!canRestore || !preview || isRestoring) return\n setConfirmingRestore(true)\n }, [canRestore, isRestoring, preview])\n\n const confirmRestore = React.useCallback(() => {\n if (!canRestore || !preview || isRestoring) return\n void onRestore(preview)\n }, [canRestore, isRestoring, onRestore, preview])\n\n const handleOpenChange = React.useCallback((open: boolean) => {\n if (!open) setConfirmingRestore(false)\n onOpenChange(open)\n }, [onOpenChange])\n\n return (\n <Dialog open={versionId !== null} onOpenChange={handleOpenChange}>\n <DialogContent size=\"xl\" onKeyDown={(event) => {\n if (event.key === 'Escape') {\n if (confirmingRestore) {\n event.preventDefault()\n event.stopPropagation()\n if (!isRestoring) setConfirmingRestore(false)\n } else {\n onOpenChange(false)\n }\n }\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter' && canRestore && preview && !isRestoring) {\n event.preventDefault()\n if (confirmingRestore) confirmRestore()\n else requestRestore()\n }\n }}>\n <DialogHeader>\n <DialogTitle>\n {confirmingRestore\n ? t('documents.versions.restore.confirmTitle')\n : preview?.label ?? t('documents.versions.preview.title')}\n </DialogTitle>\n <DialogDescription aria-live={confirmingRestore ? 'assertive' : undefined}>\n {confirmingRestore\n ? t('documents.versions.restore.confirmBody')\n : preview\n ? t('documents.versions.preview.description', { creator: preview.creatorLabel, date: new Date(preview.createdAt).toLocaleString() })\n : t('documents.versions.preview.loading')}\n </DialogDescription>\n </DialogHeader>\n {!confirmingRestore && loading ? <LoadingMessage label={t('documents.versions.preview.loading')} /> : null}\n {!confirmingRestore && error ? (\n <ErrorMessage\n label={error}\n action={<Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setLoadAttempt((current) => current + 1)}>{t('documents.actions.retry')}</Button>}\n />\n ) : null}\n {!confirmingRestore && preview && !loading && !error ? <ReadOnlyVersion contentHtml={preview.contentHtml} fallbackLabel={t('documents.editor.entityRef.fallbackLabel')} /> : null}\n <DialogFooter>\n {confirmingRestore ? (\n <>\n <Button ref={cancelRestoreRef} type=\"button\" variant=\"outline\" onClick={() => setConfirmingRestore(false)} disabled={isRestoring}>\n {t('documents.actions.cancel')}\n </Button>\n <Button type=\"button\" onClick={confirmRestore} disabled={isRestoring}>\n <RotateCcw />{t('documents.versions.actions.restore')}\n </Button>\n </>\n ) : (\n <>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>{t('documents.actions.close')}</Button>\n {canRestore && preview ? (\n <Button type=\"button\" onClick={requestRestore} disabled={isRestoring}>\n <RotateCcw />{t('documents.versions.actions.restore')}\n </Button>\n ) : null}\n </>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
import { useEditor } from "@tiptap/react";
|
|
4
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
@@ -41,6 +42,7 @@ function documentCounts(editor) {
|
|
|
41
42
|
}
|
|
42
43
|
function useDocumentEditor(input) {
|
|
43
44
|
const t = useT();
|
|
45
|
+
const nonce = useContentSecurityPolicyNonce();
|
|
44
46
|
const editorRef = React.useRef(null);
|
|
45
47
|
const countTimerRef = React.useRef(null);
|
|
46
48
|
const readOnlyRef = React.useRef(input.readOnly);
|
|
@@ -82,6 +84,7 @@ function useDocumentEditor(input) {
|
|
|
82
84
|
}), ...getClientEditorExtras(), DocumentPagination, ...suggestions];
|
|
83
85
|
}, [input.collabResources, input.editorMode, t]);
|
|
84
86
|
const editor = useEditor({
|
|
87
|
+
injectNonce: nonce,
|
|
85
88
|
extensions,
|
|
86
89
|
content: input.editorMode === "fallback" ? input.initialContentHtml : void 0,
|
|
87
90
|
editable: !input.readOnly,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/%5Bid%5D/useDocumentEditor.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { Editor } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { useEditor } from '@tiptap/react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n activateEntityRefFromKeyboardEvent,\n activateEntityRefFromPointerEvent,\n getClientEditorExtras,\n getCollaborativeEditorExtensions,\n getDocumentEditorExtensions,\n} from '../../../lib/editorConfig'\nimport { createEntitySuggestionExtension } from '../../../lib/entitySuggestion'\nimport { DocumentPagination } from './documentPagination'\nimport type { CollabResources, EditorSelectionRange, EditorWordCount } from './editorTypes'\n\ntype UseDocumentEditorInput = {\n documentId: string\n initialContentHtml: string\n editorMode: 'collab' | 'fallback'\n collabResources?: CollabResources\n readOnly: boolean\n onEditorReady?: (editor: Editor | null) => void\n onUpdate?: (editor: Editor) => void\n onEntitySuggestion: (range: EditorSelectionRange) => void\n onSuggestionClose: () => void\n}\n\nexport const DOCUMENT_COUNT_UPDATE_DELAY_MS = 150\n\nexport function countDocumentWordsAndCharacters(doc: ProseMirrorNode): EditorWordCount {\n let words = 0\n let characters = 0\n let firstBlock = true\n let insideWord = false\n\n doc.nodesBetween(0, doc.content.size, (node) => {\n const nodeText = node.isText ? node.text ?? '' : node.isLeaf ? ' ' : ''\n if (node.isBlock && ((node.isLeaf && nodeText.length > 0) || node.isTextblock)) {\n if (firstBlock) firstBlock = false\n else insideWord = false\n }\n characters += nodeText.length\n for (let index = 0; index < nodeText.length; index += 1) {\n if (nodeText[index] === ' ') {\n insideWord = false\n } else if (!insideWord) {\n words += 1\n insideWord = true\n }\n }\n })\n return { words, characters }\n}\n\nfunction documentCounts(editor: Editor | null): EditorWordCount {\n if (!editor) return { words: 0, characters: 0 }\n return countDocumentWordsAndCharacters(editor.state.doc)\n}\n\nexport function useDocumentEditor(input: UseDocumentEditorInput) {\n const t = useT()\n const editorRef = React.useRef<Editor | null>(null)\n const countTimerRef = React.useRef<number | null>(null)\n const readOnlyRef = React.useRef(input.readOnly)\n const onEntitySuggestionRef = React.useRef(input.onEntitySuggestion)\n const onSuggestionCloseRef = React.useRef(input.onSuggestionClose)\n const [counts, setCounts] = React.useState<EditorWordCount>({ words: 0, characters: 0 })\n readOnlyRef.current = input.readOnly\n onEntitySuggestionRef.current = input.onEntitySuggestion\n onSuggestionCloseRef.current = input.onSuggestionClose\n\n const applyCounts = React.useCallback((updated: Editor | null) => {\n const next = documentCounts(updated)\n React.startTransition(() => {\n setCounts((current) => current.words === next.words && current.characters === next.characters ? current : next)\n })\n }, [])\n const scheduleCounts = React.useCallback((updated: Editor) => {\n if (countTimerRef.current !== null) window.clearTimeout(countTimerRef.current)\n countTimerRef.current = window.setTimeout(() => {\n countTimerRef.current = null\n applyCounts(updated)\n }, DOCUMENT_COUNT_UPDATE_DELAY_MS)\n }, [applyCounts])\n\n const extensions = React.useMemo(() => {\n const suggestions = [createEntitySuggestionExtension({\n onTrigger: ({ range }) => {\n if (!readOnlyRef.current) onEntitySuggestionRef.current(range)\n },\n onClose: () => onSuggestionCloseRef.current(),\n })]\n return input.editorMode === 'collab' && input.collabResources\n ? [...getCollaborativeEditorExtensions({\n ydoc: input.collabResources.ydoc,\n provider: input.collabResources.provider,\n user: input.collabResources.user,\n placeholder: t('documents.editor.placeholder'),\n fallbackUserLabel: t('documents.users.unknown'),\n fallbackEntityRefLabel: t('documents.editor.entityRef.fallbackLabel'),\n }), ...getClientEditorExtras(), DocumentPagination, ...suggestions]\n : [...getDocumentEditorExtensions({\n entityRefFallbackLabel: t('documents.editor.entityRef.fallbackLabel'),\n }), ...getClientEditorExtras(), DocumentPagination, ...suggestions]\n }, [input.collabResources, input.editorMode, t])\n\n const editor = useEditor({\n extensions,\n content: input.editorMode === 'fallback' ? input.initialContentHtml : undefined,\n editable: !input.readOnly,\n editorProps: {\n attributes: {\n class: 'min-h-96 text-base leading-7 text-foreground focus-visible:outline-none',\n role: 'textbox',\n 'aria-label': t('documents.editor.content.ariaLabel'),\n 'aria-multiline': 'true',\n },\n handleClick(_view, _position, event) {\n return activateEntityRefFromPointerEvent(\n event,\n (href) => { window.open(href, '_blank', 'noopener') },\n )\n },\n handleKeyDown(_view, event) {\n return activateEntityRefFromKeyboardEvent(\n event,\n (href) => { window.open(href, '_blank', 'noopener') },\n )\n },\n },\n onCreate: ({ editor: created }) => { editorRef.current = created; applyCounts(created); input.onEditorReady?.(created) },\n onDestroy: () => {\n if (countTimerRef.current !== null) window.clearTimeout(countTimerRef.current)\n countTimerRef.current = null\n editorRef.current = null\n input.onEditorReady?.(null)\n },\n onUpdate: ({ editor: updated }) => {\n editorRef.current = updated\n scheduleCounts(updated)\n input.onUpdate?.(updated)\n },\n }, [input.documentId, input.editorMode, extensions])\n\n React.useEffect(() => { editorRef.current = editor; editor?.setEditable(!input.readOnly) }, [editor, input.readOnly])\n React.useEffect(() => {\n if (input.readOnly) onSuggestionCloseRef.current()\n }, [input.readOnly])\n React.useEffect(() => () => {\n if (countTimerRef.current !== null) window.clearTimeout(countTimerRef.current)\n }, [])\n React.useEffect(() => () => { input.onEditorReady?.(null) }, [input.onEditorReady])\n return { editor, editorRef, counts }\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\n\nimport * as React from 'react'\nimport type { Editor } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { useEditor } from '@tiptap/react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n activateEntityRefFromKeyboardEvent,\n activateEntityRefFromPointerEvent,\n getClientEditorExtras,\n getCollaborativeEditorExtensions,\n getDocumentEditorExtensions,\n} from '../../../lib/editorConfig'\nimport { createEntitySuggestionExtension } from '../../../lib/entitySuggestion'\nimport { DocumentPagination } from './documentPagination'\nimport type { CollabResources, EditorSelectionRange, EditorWordCount } from './editorTypes'\n\ntype UseDocumentEditorInput = {\n documentId: string\n initialContentHtml: string\n editorMode: 'collab' | 'fallback'\n collabResources?: CollabResources\n readOnly: boolean\n onEditorReady?: (editor: Editor | null) => void\n onUpdate?: (editor: Editor) => void\n onEntitySuggestion: (range: EditorSelectionRange) => void\n onSuggestionClose: () => void\n}\n\nexport const DOCUMENT_COUNT_UPDATE_DELAY_MS = 150\n\nexport function countDocumentWordsAndCharacters(doc: ProseMirrorNode): EditorWordCount {\n let words = 0\n let characters = 0\n let firstBlock = true\n let insideWord = false\n\n doc.nodesBetween(0, doc.content.size, (node) => {\n const nodeText = node.isText ? node.text ?? '' : node.isLeaf ? ' ' : ''\n if (node.isBlock && ((node.isLeaf && nodeText.length > 0) || node.isTextblock)) {\n if (firstBlock) firstBlock = false\n else insideWord = false\n }\n characters += nodeText.length\n for (let index = 0; index < nodeText.length; index += 1) {\n if (nodeText[index] === ' ') {\n insideWord = false\n } else if (!insideWord) {\n words += 1\n insideWord = true\n }\n }\n })\n return { words, characters }\n}\n\nfunction documentCounts(editor: Editor | null): EditorWordCount {\n if (!editor) return { words: 0, characters: 0 }\n return countDocumentWordsAndCharacters(editor.state.doc)\n}\n\nexport function useDocumentEditor(input: UseDocumentEditorInput) {\n const t = useT()\n const nonce = useContentSecurityPolicyNonce()\n const editorRef = React.useRef<Editor | null>(null)\n const countTimerRef = React.useRef<number | null>(null)\n const readOnlyRef = React.useRef(input.readOnly)\n const onEntitySuggestionRef = React.useRef(input.onEntitySuggestion)\n const onSuggestionCloseRef = React.useRef(input.onSuggestionClose)\n const [counts, setCounts] = React.useState<EditorWordCount>({ words: 0, characters: 0 })\n readOnlyRef.current = input.readOnly\n onEntitySuggestionRef.current = input.onEntitySuggestion\n onSuggestionCloseRef.current = input.onSuggestionClose\n\n const applyCounts = React.useCallback((updated: Editor | null) => {\n const next = documentCounts(updated)\n React.startTransition(() => {\n setCounts((current) => current.words === next.words && current.characters === next.characters ? current : next)\n })\n }, [])\n const scheduleCounts = React.useCallback((updated: Editor) => {\n if (countTimerRef.current !== null) window.clearTimeout(countTimerRef.current)\n countTimerRef.current = window.setTimeout(() => {\n countTimerRef.current = null\n applyCounts(updated)\n }, DOCUMENT_COUNT_UPDATE_DELAY_MS)\n }, [applyCounts])\n\n const extensions = React.useMemo(() => {\n const suggestions = [createEntitySuggestionExtension({\n onTrigger: ({ range }) => {\n if (!readOnlyRef.current) onEntitySuggestionRef.current(range)\n },\n onClose: () => onSuggestionCloseRef.current(),\n })]\n return input.editorMode === 'collab' && input.collabResources\n ? [...getCollaborativeEditorExtensions({\n ydoc: input.collabResources.ydoc,\n provider: input.collabResources.provider,\n user: input.collabResources.user,\n placeholder: t('documents.editor.placeholder'),\n fallbackUserLabel: t('documents.users.unknown'),\n fallbackEntityRefLabel: t('documents.editor.entityRef.fallbackLabel'),\n }), ...getClientEditorExtras(), DocumentPagination, ...suggestions]\n : [...getDocumentEditorExtensions({\n entityRefFallbackLabel: t('documents.editor.entityRef.fallbackLabel'),\n }), ...getClientEditorExtras(), DocumentPagination, ...suggestions]\n }, [input.collabResources, input.editorMode, t])\n\n const editor = useEditor({\n injectNonce: nonce,\n extensions,\n content: input.editorMode === 'fallback' ? input.initialContentHtml : undefined,\n editable: !input.readOnly,\n editorProps: {\n attributes: {\n class: 'min-h-96 text-base leading-7 text-foreground focus-visible:outline-none',\n role: 'textbox',\n 'aria-label': t('documents.editor.content.ariaLabel'),\n 'aria-multiline': 'true',\n },\n handleClick(_view, _position, event) {\n return activateEntityRefFromPointerEvent(\n event,\n (href) => { window.open(href, '_blank', 'noopener') },\n )\n },\n handleKeyDown(_view, event) {\n return activateEntityRefFromKeyboardEvent(\n event,\n (href) => { window.open(href, '_blank', 'noopener') },\n )\n },\n },\n onCreate: ({ editor: created }) => { editorRef.current = created; applyCounts(created); input.onEditorReady?.(created) },\n onDestroy: () => {\n if (countTimerRef.current !== null) window.clearTimeout(countTimerRef.current)\n countTimerRef.current = null\n editorRef.current = null\n input.onEditorReady?.(null)\n },\n onUpdate: ({ editor: updated }) => {\n editorRef.current = updated\n scheduleCounts(updated)\n input.onUpdate?.(updated)\n },\n }, [input.documentId, input.editorMode, extensions])\n\n React.useEffect(() => { editorRef.current = editor; editor?.setEditable(!input.readOnly) }, [editor, input.readOnly])\n React.useEffect(() => {\n if (input.readOnly) onSuggestionCloseRef.current()\n }, [input.readOnly])\n React.useEffect(() => () => {\n if (countTimerRef.current !== null) window.clearTimeout(countTimerRef.current)\n }, [])\n React.useEffect(() => () => { input.onEditorReady?.(null) }, [input.onEditorReady])\n return { editor, editorRef, counts }\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAS,qCAAqC;AAE9C,YAAY,WAAW;AAGvB,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uCAAuC;AAChD,SAAS,0BAA0B;AAe5B,MAAM,iCAAiC;AAEvC,SAAS,gCAAgC,KAAuC;AACrF,MAAI,QAAQ;AACZ,MAAI,aAAa;AACjB,MAAI,aAAa;AACjB,MAAI,aAAa;AAEjB,MAAI,aAAa,GAAG,IAAI,QAAQ,MAAM,CAAC,SAAS;AAC9C,UAAM,WAAW,KAAK,SAAS,KAAK,QAAQ,KAAK,KAAK,SAAS,MAAM;AACrE,QAAI,KAAK,YAAa,KAAK,UAAU,SAAS,SAAS,KAAM,KAAK,cAAc;AAC9E,UAAI,WAAY,cAAa;AAAA,UACxB,cAAa;AAAA,IACpB;AACA,kBAAc,SAAS;AACvB,aAAS,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS,GAAG;AACvD,UAAI,SAAS,KAAK,MAAM,KAAK;AAC3B,qBAAa;AAAA,MACf,WAAW,CAAC,YAAY;AACtB,iBAAS;AACT,qBAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO,EAAE,OAAO,WAAW;AAC7B;AAEA,SAAS,eAAe,QAAwC;AAC9D,MAAI,CAAC,OAAQ,QAAO,EAAE,OAAO,GAAG,YAAY,EAAE;AAC9C,SAAO,gCAAgC,OAAO,MAAM,GAAG;AACzD;AAEO,SAAS,kBAAkB,OAA+B;AAC/D,QAAM,IAAI,KAAK;AACf,QAAM,QAAQ,8BAA8B;AAC5C,QAAM,YAAY,MAAM,OAAsB,IAAI;AAClD,QAAM,gBAAgB,MAAM,OAAsB,IAAI;AACtD,QAAM,cAAc,MAAM,OAAO,MAAM,QAAQ;AAC/C,QAAM,wBAAwB,MAAM,OAAO,MAAM,kBAAkB;AACnE,QAAM,uBAAuB,MAAM,OAAO,MAAM,iBAAiB;AACjE,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA0B,EAAE,OAAO,GAAG,YAAY,EAAE,CAAC;AACvF,cAAY,UAAU,MAAM;AAC5B,wBAAsB,UAAU,MAAM;AACtC,uBAAqB,UAAU,MAAM;AAErC,QAAM,cAAc,MAAM,YAAY,CAAC,YAA2B;AAChE,UAAM,OAAO,eAAe,OAAO;AACnC,UAAM,gBAAgB,MAAM;AAC1B,gBAAU,CAAC,YAAY,QAAQ,UAAU,KAAK,SAAS,QAAQ,eAAe,KAAK,aAAa,UAAU,IAAI;AAAA,IAChH,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AACL,QAAM,iBAAiB,MAAM,YAAY,CAAC,YAAoB;AAC5D,QAAI,cAAc,YAAY,KAAM,QAAO,aAAa,cAAc,OAAO;AAC7E,kBAAc,UAAU,OAAO,WAAW,MAAM;AAC9C,oBAAc,UAAU;AACxB,kBAAY,OAAO;AAAA,IACrB,GAAG,8BAA8B;AAAA,EACnC,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,aAAa,MAAM,QAAQ,MAAM;AACrC,UAAM,cAAc,CAAC,gCAAgC;AAAA,MACnD,WAAW,CAAC,EAAE,MAAM,MAAM;AACxB,YAAI,CAAC,YAAY,QAAS,uBAAsB,QAAQ,KAAK;AAAA,MAC/D;AAAA,MACA,SAAS,MAAM,qBAAqB,QAAQ;AAAA,IAC9C,CAAC,CAAC;AACF,WAAO,MAAM,eAAe,YAAY,MAAM,kBAC1C,CAAC,GAAG,iCAAiC;AAAA,MACrC,MAAM,MAAM,gBAAgB;AAAA,MAC5B,UAAU,MAAM,gBAAgB;AAAA,MAChC,MAAM,MAAM,gBAAgB;AAAA,MAC5B,aAAa,EAAE,8BAA8B;AAAA,MAC7C,mBAAmB,EAAE,yBAAyB;AAAA,MAC9C,wBAAwB,EAAE,0CAA0C;AAAA,IACtE,CAAC,GAAG,GAAG,sBAAsB,GAAG,oBAAoB,GAAG,WAAW,IAChE,CAAC,GAAG,4BAA4B;AAAA,MAChC,wBAAwB,EAAE,0CAA0C;AAAA,IACtE,CAAC,GAAG,GAAG,sBAAsB,GAAG,oBAAoB,GAAG,WAAW;AAAA,EACtE,GAAG,CAAC,MAAM,iBAAiB,MAAM,YAAY,CAAC,CAAC;AAE/C,QAAM,SAAS,UAAU;AAAA,IACvB,aAAa;AAAA,IACb;AAAA,IACA,SAAS,MAAM,eAAe,aAAa,MAAM,qBAAqB;AAAA,IACtE,UAAU,CAAC,MAAM;AAAA,IACjB,aAAa;AAAA,MACX,YAAY;AAAA,QACV,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc,EAAE,oCAAoC;AAAA,QACpD,kBAAkB;AAAA,MACpB;AAAA,MACA,YAAY,OAAO,WAAW,OAAO;AACnC,eAAO;AAAA,UACL;AAAA,UACA,CAAC,SAAS;AAAE,mBAAO,KAAK,MAAM,UAAU,UAAU;AAAA,UAAE;AAAA,QACtD;AAAA,MACF;AAAA,MACA,cAAc,OAAO,OAAO;AAC1B,eAAO;AAAA,UACL;AAAA,UACA,CAAC,SAAS;AAAE,mBAAO,KAAK,MAAM,UAAU,UAAU;AAAA,UAAE;AAAA,QACtD;AAAA,MACF;AAAA,IACF;AAAA,IACA,UAAU,CAAC,EAAE,QAAQ,QAAQ,MAAM;AAAE,gBAAU,UAAU;AAAS,kBAAY,OAAO;AAAG,YAAM,gBAAgB,OAAO;AAAA,IAAE;AAAA,IACvH,WAAW,MAAM;AACf,UAAI,cAAc,YAAY,KAAM,QAAO,aAAa,cAAc,OAAO;AAC7E,oBAAc,UAAU;AACxB,gBAAU,UAAU;AACpB,YAAM,gBAAgB,IAAI;AAAA,IAC5B;AAAA,IACA,UAAU,CAAC,EAAE,QAAQ,QAAQ,MAAM;AACjC,gBAAU,UAAU;AACpB,qBAAe,OAAO;AACtB,YAAM,WAAW,OAAO;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,MAAM,YAAY,MAAM,YAAY,UAAU,CAAC;AAEnD,QAAM,UAAU,MAAM;AAAE,cAAU,UAAU;AAAQ,YAAQ,YAAY,CAAC,MAAM,QAAQ;AAAA,EAAE,GAAG,CAAC,QAAQ,MAAM,QAAQ,CAAC;AACpH,QAAM,UAAU,MAAM;AACpB,QAAI,MAAM,SAAU,sBAAqB,QAAQ;AAAA,EACnD,GAAG,CAAC,MAAM,QAAQ,CAAC;AACnB,QAAM,UAAU,MAAM,MAAM;AAC1B,QAAI,cAAc,YAAY,KAAM,QAAO,aAAa,cAAc,OAAO;AAAA,EAC/E,GAAG,CAAC,CAAC;AACL,QAAM,UAAU,MAAM,MAAM;AAAE,UAAM,gBAAgB,IAAI;AAAA,EAAE,GAAG,CAAC,MAAM,aAAa,CAAC;AAClF,SAAO,EAAE,QAAQ,WAAW,OAAO;AACrC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
|
|
4
5
|
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
|
5
6
|
import { Bold, Heading1, Heading2, Italic, List, ListOrdered } from "lucide-react";
|
|
6
7
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
@@ -23,9 +24,11 @@ function EditorButton({ editor, label, active, children, onClick }) {
|
|
|
23
24
|
}
|
|
24
25
|
function TemplateBodyEditor({ bodyHtml, tokenOptions, onChange }) {
|
|
25
26
|
const t = useT();
|
|
27
|
+
const nonce = useContentSecurityPolicyNonce();
|
|
26
28
|
const editorLabelId = React.useId();
|
|
27
29
|
const [token, setToken] = React.useState();
|
|
28
30
|
const editor = useEditor({
|
|
31
|
+
injectNonce: nonce,
|
|
29
32
|
extensions: getDocumentEditorExtensions({
|
|
30
33
|
entityRefFallbackLabel: t("documents.editor.entityRef.fallbackLabel")
|
|
31
34
|
}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/components/TemplateBodyEditor.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { Editor } from '@tiptap/core'\nimport { EditorContent, useEditor, useEditorState } from '@tiptap/react'\nimport { Bold, Heading1, Heading2, Italic, List, ListOrdered } from 'lucide-react'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getDocumentEditorExtensions } from '../../../lib/editorConfig'\n\nconst EMPTY_TEMPLATE_EDITOR_STATE = {\n bold: false,\n italic: false,\n heading1: false,\n heading2: false,\n bulletList: false,\n orderedList: false,\n}\n\nfunction EditorButton({ editor, label, active, children, onClick }: {\n editor: Editor | null\n label: string\n active: boolean\n children: React.ReactNode\n onClick: (editor: Editor) => void\n}) {\n return <IconButton type=\"button\" variant={active ? 'outline' : 'ghost'} aria-label={label} aria-pressed={active} title={label} disabled={!editor} onMouseDown={(event) => event.preventDefault()} onClick={() => { if (editor) onClick(editor) }}>{children}</IconButton>\n}\n\nexport function TemplateBodyEditor({ bodyHtml, tokenOptions, onChange }: {\n bodyHtml: string\n tokenOptions: string[]\n onChange: (html: string) => void\n}) {\n const t = useT()\n const editorLabelId = React.useId()\n const [token, setToken] = React.useState<string | undefined>()\n const editor = useEditor({\n extensions: getDocumentEditorExtensions({\n entityRefFallbackLabel: t('documents.editor.entityRef.fallbackLabel'),\n }),\n content: bodyHtml,\n editable: true,\n editorProps: {\n attributes: {\n class: 'min-h-80 text-base leading-7 text-foreground focus-visible:outline-none',\n role: 'textbox',\n 'aria-labelledby': editorLabelId,\n 'aria-multiline': 'true',\n },\n },\n onUpdate: ({ editor: updated }) => onChange(updated.getHTML()),\n }, [])\n React.useEffect(() => {\n if (editor && editor.getHTML() !== bodyHtml) editor.commands.setContent(bodyHtml)\n }, [bodyHtml, editor])\n const editorState = useEditorState({\n editor,\n selector: ({ editor: current }) => current ? {\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n heading1: current.isActive('heading', { level: 1 }),\n heading2: current.isActive('heading', { level: 2 }),\n bulletList: current.isActive('bulletList'),\n orderedList: current.isActive('orderedList'),\n } : EMPTY_TEMPLATE_EDITOR_STATE,\n }) ?? EMPTY_TEMPLATE_EDITOR_STATE\n return (\n <div className=\"space-y-3\">\n <Label id={editorLabelId}>{t('documents.templates.fields.body')}</Label>\n <div className=\"rounded-md border border-border bg-card\">\n <div className=\"flex flex-wrap items-center gap-1 border-b border-border p-2\">\n <EditorButton editor={editor} label={t('documents.editor.toolbar.bold')} active={editorState.bold} onClick={(current) => { current.chain().focus().toggleBold().run() }}><Bold /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.italic')} active={editorState.italic} onClick={(current) => { current.chain().focus().toggleItalic().run() }}><Italic /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.heading1')} active={editorState.heading1} onClick={(current) => { current.chain().focus().toggleHeading({ level: 1 }).run() }}><Heading1 /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.heading2')} active={editorState.heading2} onClick={(current) => { current.chain().focus().toggleHeading({ level: 2 }).run() }}><Heading2 /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.bulletList')} active={editorState.bulletList} onClick={(current) => { current.chain().focus().toggleBulletList().run() }}><List /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.orderedList')} active={editorState.orderedList} onClick={(current) => { current.chain().focus().toggleOrderedList().run() }}><ListOrdered /></EditorButton>\n <div className=\"min-w-52\">\n <Select value={token} onValueChange={(value) => {\n setToken(value)\n editor?.chain().focus().insertContent(value).run()\n window.setTimeout(() => setToken(undefined), 0)\n }}>\n <SelectTrigger aria-label={t('documents.templates.editor.insertField')}><SelectValue placeholder={t('documents.templates.editor.insertField')} /></SelectTrigger>\n <SelectContent>{tokenOptions.map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>\n </Select>\n </div>\n </div>\n <EditorContent editor={editor} className=\"max-w-none p-4 text-foreground [&_.ProseMirror]:min-h-80 [&_.ProseMirror]:focus-visible:outline-none\" />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\nimport type { Editor } from '@tiptap/core'\nimport { EditorContent, useEditor, useEditorState } from '@tiptap/react'\nimport { Bold, Heading1, Heading2, Italic, List, ListOrdered } from 'lucide-react'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getDocumentEditorExtensions } from '../../../lib/editorConfig'\n\nconst EMPTY_TEMPLATE_EDITOR_STATE = {\n bold: false,\n italic: false,\n heading1: false,\n heading2: false,\n bulletList: false,\n orderedList: false,\n}\n\nfunction EditorButton({ editor, label, active, children, onClick }: {\n editor: Editor | null\n label: string\n active: boolean\n children: React.ReactNode\n onClick: (editor: Editor) => void\n}) {\n return <IconButton type=\"button\" variant={active ? 'outline' : 'ghost'} aria-label={label} aria-pressed={active} title={label} disabled={!editor} onMouseDown={(event) => event.preventDefault()} onClick={() => { if (editor) onClick(editor) }}>{children}</IconButton>\n}\n\nexport function TemplateBodyEditor({ bodyHtml, tokenOptions, onChange }: {\n bodyHtml: string\n tokenOptions: string[]\n onChange: (html: string) => void\n}) {\n const t = useT()\n const nonce = useContentSecurityPolicyNonce()\n const editorLabelId = React.useId()\n const [token, setToken] = React.useState<string | undefined>()\n const editor = useEditor({\n injectNonce: nonce,\n extensions: getDocumentEditorExtensions({\n entityRefFallbackLabel: t('documents.editor.entityRef.fallbackLabel'),\n }),\n content: bodyHtml,\n editable: true,\n editorProps: {\n attributes: {\n class: 'min-h-80 text-base leading-7 text-foreground focus-visible:outline-none',\n role: 'textbox',\n 'aria-labelledby': editorLabelId,\n 'aria-multiline': 'true',\n },\n },\n onUpdate: ({ editor: updated }) => onChange(updated.getHTML()),\n }, [])\n React.useEffect(() => {\n if (editor && editor.getHTML() !== bodyHtml) editor.commands.setContent(bodyHtml)\n }, [bodyHtml, editor])\n const editorState = useEditorState({\n editor,\n selector: ({ editor: current }) => current ? {\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n heading1: current.isActive('heading', { level: 1 }),\n heading2: current.isActive('heading', { level: 2 }),\n bulletList: current.isActive('bulletList'),\n orderedList: current.isActive('orderedList'),\n } : EMPTY_TEMPLATE_EDITOR_STATE,\n }) ?? EMPTY_TEMPLATE_EDITOR_STATE\n return (\n <div className=\"space-y-3\">\n <Label id={editorLabelId}>{t('documents.templates.fields.body')}</Label>\n <div className=\"rounded-md border border-border bg-card\">\n <div className=\"flex flex-wrap items-center gap-1 border-b border-border p-2\">\n <EditorButton editor={editor} label={t('documents.editor.toolbar.bold')} active={editorState.bold} onClick={(current) => { current.chain().focus().toggleBold().run() }}><Bold /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.italic')} active={editorState.italic} onClick={(current) => { current.chain().focus().toggleItalic().run() }}><Italic /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.heading1')} active={editorState.heading1} onClick={(current) => { current.chain().focus().toggleHeading({ level: 1 }).run() }}><Heading1 /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.heading2')} active={editorState.heading2} onClick={(current) => { current.chain().focus().toggleHeading({ level: 2 }).run() }}><Heading2 /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.bulletList')} active={editorState.bulletList} onClick={(current) => { current.chain().focus().toggleBulletList().run() }}><List /></EditorButton>\n <EditorButton editor={editor} label={t('documents.editor.toolbar.orderedList')} active={editorState.orderedList} onClick={(current) => { current.chain().focus().toggleOrderedList().run() }}><ListOrdered /></EditorButton>\n <div className=\"min-w-52\">\n <Select value={token} onValueChange={(value) => {\n setToken(value)\n editor?.chain().focus().insertContent(value).run()\n window.setTimeout(() => setToken(undefined), 0)\n }}>\n <SelectTrigger aria-label={t('documents.templates.editor.insertField')}><SelectValue placeholder={t('documents.templates.editor.insertField')} /></SelectTrigger>\n <SelectContent>{tokenOptions.map((value) => <SelectItem key={value} value={value}>{value}</SelectItem>)}</SelectContent>\n </Select>\n </div>\n </div>\n <EditorContent editor={editor} className=\"max-w-none p-4 text-foreground [&_.ProseMirror]:min-h-80 [&_.ProseMirror]:focus-visible:outline-none\" />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useContentSecurityPolicyNonce } from "@open-mercato/ui/theme/ContentSecurityPolicyProvider";
|
|
4
5
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
5
6
|
import { Alert, AlertDescription, AlertTitle } from "@open-mercato/ui/primitives/alert";
|
|
6
7
|
import { LoadingMessage } from "@open-mercato/ui/backend/detail";
|
|
7
8
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
9
|
import { getDocumentEditorExtensions } from "../../../lib/editorConfig.js";
|
|
9
10
|
function PreviewSurface({ html, fallbackLabel }) {
|
|
11
|
+
const nonce = useContentSecurityPolicyNonce();
|
|
10
12
|
const editor = useEditor({
|
|
13
|
+
injectNonce: nonce,
|
|
11
14
|
extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),
|
|
12
15
|
content: html,
|
|
13
16
|
editable: false,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/documents/backend/documents/components/TemplatePreview.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getDocumentEditorExtensions } from '../../../lib/editorConfig'\nimport type { TemplatePreviewResult } from './templateUi'\n\nfunction PreviewSurface({ html, fallbackLabel }: { html: string; fallbackLabel: string }) {\n const editor = useEditor({\n extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),\n content: html,\n editable: false,\n editorProps: { attributes: { class: 'min-h-48 text-base leading-7 text-foreground focus-visible:outline-none' } },\n }, [fallbackLabel])\n React.useEffect(() => { editor?.commands.setContent(html) }, [editor, html])\n return <EditorContent editor={editor} className=\"max-w-none rounded-md border border-border bg-card p-4\" />\n}\n\nexport function TemplatePreview({ preview, isLoading }: { preview: TemplatePreviewResult | null; isLoading: boolean }) {\n const t = useT()\n if (isLoading) return <LoadingMessage label={t('documents.templates.preview.loading')} />\n if (!preview) {\n return (\n <div className=\"rounded-md border border-dashed border-border p-4 text-sm text-muted-foreground\">\n {t('documents.templates.preview.empty')}\n </div>\n )\n }\n return (\n <div className=\"space-y-3\">\n {preview.unresolvedTokens.length > 0 ? (\n <Alert status=\"warning\">\n <AlertTitle>{t('documents.templates.preview.unresolvedTitle')}</AlertTitle>\n <AlertDescription>{t('documents.templates.preview.unresolvedBody', { tokens: preview.unresolvedTokens.join(', ') })}</AlertDescription>\n </Alert>\n ) : null}\n <PreviewSurface html={preview.contentHtml} fallbackLabel={t('documents.editor.entityRef.fallbackLabel')} />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getDocumentEditorExtensions } from '../../../lib/editorConfig'\nimport type { TemplatePreviewResult } from './templateUi'\n\nfunction PreviewSurface({ html, fallbackLabel }: { html: string; fallbackLabel: string }) {\n const nonce = useContentSecurityPolicyNonce()\n const editor = useEditor({\n injectNonce: nonce,\n extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),\n content: html,\n editable: false,\n editorProps: { attributes: { class: 'min-h-48 text-base leading-7 text-foreground focus-visible:outline-none' } },\n }, [fallbackLabel])\n React.useEffect(() => { editor?.commands.setContent(html) }, [editor, html])\n return <EditorContent editor={editor} className=\"max-w-none rounded-md border border-border bg-card p-4\" />\n}\n\nexport function TemplatePreview({ preview, isLoading }: { preview: TemplatePreviewResult | null; isLoading: boolean }) {\n const t = useT()\n if (isLoading) return <LoadingMessage label={t('documents.templates.preview.loading')} />\n if (!preview) {\n return (\n <div className=\"rounded-md border border-dashed border-border p-4 text-sm text-muted-foreground\">\n {t('documents.templates.preview.empty')}\n </div>\n )\n }\n return (\n <div className=\"space-y-3\">\n {preview.unresolvedTokens.length > 0 ? (\n <Alert status=\"warning\">\n <AlertTitle>{t('documents.templates.preview.unresolvedTitle')}</AlertTitle>\n <AlertDescription>{t('documents.templates.preview.unresolvedBody', { tokens: preview.unresolvedTokens.join(', ') })}</AlertDescription>\n </Alert>\n ) : null}\n <PreviewSurface html={preview.contentHtml} fallbackLabel={t('documents.editor.entityRef.fallbackLabel')} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAqBS,cAgBD,YAhBC;AAnBT,YAAY,WAAW;AACvB,SAAS,qCAAqC;AAC9C,SAAS,eAAe,iBAAiB;AACzC,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,sBAAsB;AAC/B,SAAS,YAAY;AACrB,SAAS,mCAAmC;AAG5C,SAAS,eAAe,EAAE,MAAM,cAAc,GAA4C;AACxF,QAAM,QAAQ,8BAA8B;AAC5C,QAAM,SAAS,UAAU;AAAA,IACvB,aAAa;AAAA,IACb,YAAY,4BAA4B,EAAE,wBAAwB,cAAc,CAAC;AAAA,IACjF,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa,EAAE,YAAY,EAAE,OAAO,0EAA0E,EAAE;AAAA,EAClH,GAAG,CAAC,aAAa,CAAC;AAClB,QAAM,UAAU,MAAM;AAAE,YAAQ,SAAS,WAAW,IAAI;AAAA,EAAE,GAAG,CAAC,QAAQ,IAAI,CAAC;AAC3E,SAAO,oBAAC,iBAAc,QAAgB,WAAU,0DAAyD;AAC3G;AAEO,SAAS,gBAAgB,EAAE,SAAS,UAAU,GAAkE;AACrH,QAAM,IAAI,KAAK;AACf,MAAI,UAAW,QAAO,oBAAC,kBAAe,OAAO,EAAE,qCAAqC,GAAG;AACvF,MAAI,CAAC,SAAS;AACZ,WACE,oBAAC,SAAI,WAAU,mFACZ,YAAE,mCAAmC,GACxC;AAAA,EAEJ;AACA,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,YAAQ,iBAAiB,SAAS,IACjC,qBAAC,SAAM,QAAO,WACZ;AAAA,0BAAC,cAAY,YAAE,6CAA6C,GAAE;AAAA,MAC9D,oBAAC,oBAAkB,YAAE,8CAA8C,EAAE,QAAQ,QAAQ,iBAAiB,KAAK,IAAI,EAAE,CAAC,GAAE;AAAA,OACtH,IACE;AAAA,IACJ,oBAAC,kBAAe,MAAM,QAAQ,aAAa,eAAe,EAAE,0CAA0C,GAAG;AAAA,KAC3G;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/documents",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7809.1.ea24461aeb",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"@hocuspocus/server": "^4.4.0",
|
|
76
76
|
"@hocuspocus/transformer": "^4.3.0",
|
|
77
77
|
"@mikro-orm/core": "^7.1.14",
|
|
78
|
-
"@open-mercato/events": "0.10.1-develop.
|
|
79
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
78
|
+
"@open-mercato/events": "0.10.1-develop.7809.1.ea24461aeb",
|
|
79
|
+
"@open-mercato/ui": "0.10.1-develop.7809.1.ea24461aeb",
|
|
80
80
|
"@tiptap/core": "^3.32.2",
|
|
81
81
|
"@tiptap/extension-bubble-menu": "^3.32.2",
|
|
82
82
|
"@tiptap/extension-collaboration": "^3.32.2",
|
|
@@ -113,13 +113,13 @@
|
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
115
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
116
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
116
|
+
"@open-mercato/shared": "0.10.1-develop.7809.1.ea24461aeb",
|
|
117
117
|
"react": "^19.0.0",
|
|
118
118
|
"react-dom": "^19.0.0"
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
|
-
"@open-mercato/core": "0.10.1-develop.
|
|
122
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
121
|
+
"@open-mercato/core": "0.10.1-develop.7809.1.ea24461aeb",
|
|
122
|
+
"@open-mercato/shared": "0.10.1-develop.7809.1.ea24461aeb",
|
|
123
123
|
"@types/jest": "^30.0.0",
|
|
124
124
|
"@types/pg": "^8.23.1",
|
|
125
125
|
"@types/react": "^19.2.18",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
2
|
+
import { resolveDocumentsCollaborationEndpoint } from './modules/documents/lib/collabEndpoint'
|
|
3
|
+
|
|
4
|
+
let warnedAboutExcludedEndpoint = false
|
|
5
|
+
|
|
6
|
+
export function resolveDocumentsCollaborationCspOrigins(): string[] {
|
|
7
|
+
const endpoint = resolveDocumentsCollaborationEndpoint()
|
|
8
|
+
if (!endpoint) return []
|
|
9
|
+
const url = new URL(endpoint)
|
|
10
|
+
if (url.username || url.password || !/^(?:[a-z0-9.-]+|\[[0-9a-f:]+\])$/i.test(url.hostname)) {
|
|
11
|
+
if (!warnedAboutExcludedEndpoint) {
|
|
12
|
+
warnedAboutExcludedEndpoint = true
|
|
13
|
+
createLogger('documents:csp').warn(
|
|
14
|
+
'[internal] Collaboration endpoint excluded from CSP; use a credential-free DNS or IPv6 URL',
|
|
15
|
+
{ configuration: 'NEXT_PUBLIC_DOCUMENTS_COLLAB_URL' },
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
return [url.origin]
|
|
21
|
+
}
|
|
@@ -15,6 +15,7 @@ import { OutlinePane } from './OutlinePane'
|
|
|
15
15
|
import { captureCommentAnchor, type CommentAnchor } from './CommentAnchorNavigation'
|
|
16
16
|
import { DOCUMENT_EDITOR_CONTENT_CLASS } from './editorTypes'
|
|
17
17
|
import { DOCUMENT_PAGINATION_STYLES } from './documentPagination'
|
|
18
|
+
import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
|
|
18
19
|
|
|
19
20
|
type TitleModel = {
|
|
20
21
|
value: string
|
|
@@ -49,6 +50,7 @@ export function DocumentCanvas({
|
|
|
49
50
|
onComment?: (anchor: CommentAnchor) => void
|
|
50
51
|
}) {
|
|
51
52
|
const t = useT()
|
|
53
|
+
const nonce = useContentSecurityPolicyNonce()
|
|
52
54
|
const editorState = useEditorState({
|
|
53
55
|
editor,
|
|
54
56
|
selector: ({ editor: current }) => current ? {
|
|
@@ -71,7 +73,7 @@ export function DocumentCanvas({
|
|
|
71
73
|
|
|
72
74
|
return (
|
|
73
75
|
<div className="om-doc-canvas max-h-[calc(100dvh-var(--topbar-height,0px)-12rem)] min-h-96 overflow-auto bg-muted p-0 sm:px-4 sm:py-8 md:px-8 md:py-10">
|
|
74
|
-
<style>{DOCUMENT_PAGINATION_STYLES}</style>
|
|
76
|
+
<style nonce={nonce}>{DOCUMENT_PAGINATION_STYLES}</style>
|
|
75
77
|
{notice ? <Alert status="information" style="lighter" className="mx-auto mb-4 max-w-3xl"><AlertDescription>{notice}</AlertDescription></Alert> : null}
|
|
76
78
|
<div className="mx-auto flex w-fit min-w-full flex-col gap-4 lg:flex-row lg:items-start">
|
|
77
79
|
{outlineOpen ? <OutlinePane editor={editor} /> : null}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
|
|
4
5
|
import { Link2, Unlink } from 'lucide-react'
|
|
5
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
7
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -30,6 +31,7 @@ type Props = DocumentEditorIslandProps & {
|
|
|
30
31
|
|
|
31
32
|
export function DocumentEditorSurface(input: Props) {
|
|
32
33
|
const t = useT()
|
|
34
|
+
const nonce = useContentSecurityPolicyNonce()
|
|
33
35
|
const [outlineOpen, setOutlineOpen] = React.useState(false)
|
|
34
36
|
const [suggestionRange, setSuggestionRange] = React.useState<{ from: number; to: number } | null>(null)
|
|
35
37
|
const modeChangeInFlightRef = React.useRef(false)
|
|
@@ -102,7 +104,7 @@ export function DocumentEditorSurface(input: Props) {
|
|
|
102
104
|
|
|
103
105
|
return (
|
|
104
106
|
<div className={cn('space-y-3', input.transport === 'collab' && 'om-doc-collab', input.mode === 'preview' && 'om-doc-preview')}>
|
|
105
|
-
<style>{EDITOR_PRESENCE_STYLES}</style>
|
|
107
|
+
<style nonce={nonce}>{EDITOR_PRESENCE_STYLES}</style>
|
|
106
108
|
<div className="overflow-hidden rounded-lg border border-border bg-muted shadow-sm">
|
|
107
109
|
<div className="sticky top-0 z-sticky border-b border-border bg-card/95">
|
|
108
110
|
<div className="flex flex-wrap items-center justify-between gap-2 px-3 py-2">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
|
|
4
5
|
import { EditorContent, useEditor } from '@tiptap/react'
|
|
5
6
|
import { RotateCcw } from 'lucide-react'
|
|
6
7
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -63,7 +64,9 @@ export function normalizeVersionPreview(
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function ReadOnlyVersion({ contentHtml, fallbackLabel }: { contentHtml: string; fallbackLabel: string }) {
|
|
67
|
+
const nonce = useContentSecurityPolicyNonce()
|
|
66
68
|
const editor = useEditor({
|
|
69
|
+
injectNonce: nonce,
|
|
67
70
|
extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),
|
|
68
71
|
content: contentHtml,
|
|
69
72
|
editable: false,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client"
|
|
2
|
+
import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
|
|
2
3
|
|
|
3
4
|
import * as React from 'react'
|
|
4
5
|
import type { Editor } from '@tiptap/core'
|
|
@@ -62,6 +63,7 @@ function documentCounts(editor: Editor | null): EditorWordCount {
|
|
|
62
63
|
|
|
63
64
|
export function useDocumentEditor(input: UseDocumentEditorInput) {
|
|
64
65
|
const t = useT()
|
|
66
|
+
const nonce = useContentSecurityPolicyNonce()
|
|
65
67
|
const editorRef = React.useRef<Editor | null>(null)
|
|
66
68
|
const countTimerRef = React.useRef<number | null>(null)
|
|
67
69
|
const readOnlyRef = React.useRef(input.readOnly)
|
|
@@ -108,6 +110,7 @@ export function useDocumentEditor(input: UseDocumentEditorInput) {
|
|
|
108
110
|
}, [input.collabResources, input.editorMode, t])
|
|
109
111
|
|
|
110
112
|
const editor = useEditor({
|
|
113
|
+
injectNonce: nonce,
|
|
111
114
|
extensions,
|
|
112
115
|
content: input.editorMode === 'fallback' ? input.initialContentHtml : undefined,
|
|
113
116
|
editable: !input.readOnly,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
|
|
4
5
|
import type { Editor } from '@tiptap/core'
|
|
5
6
|
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
|
|
6
7
|
import { Bold, Heading1, Heading2, Italic, List, ListOrdered } from 'lucide-react'
|
|
@@ -35,9 +36,11 @@ export function TemplateBodyEditor({ bodyHtml, tokenOptions, onChange }: {
|
|
|
35
36
|
onChange: (html: string) => void
|
|
36
37
|
}) {
|
|
37
38
|
const t = useT()
|
|
39
|
+
const nonce = useContentSecurityPolicyNonce()
|
|
38
40
|
const editorLabelId = React.useId()
|
|
39
41
|
const [token, setToken] = React.useState<string | undefined>()
|
|
40
42
|
const editor = useEditor({
|
|
43
|
+
injectNonce: nonce,
|
|
41
44
|
extensions: getDocumentEditorExtensions({
|
|
42
45
|
entityRefFallbackLabel: t('documents.editor.entityRef.fallbackLabel'),
|
|
43
46
|
}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { useContentSecurityPolicyNonce } from '@open-mercato/ui/theme/ContentSecurityPolicyProvider'
|
|
4
5
|
import { EditorContent, useEditor } from '@tiptap/react'
|
|
5
6
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
6
7
|
import { LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
@@ -9,7 +10,9 @@ import { getDocumentEditorExtensions } from '../../../lib/editorConfig'
|
|
|
9
10
|
import type { TemplatePreviewResult } from './templateUi'
|
|
10
11
|
|
|
11
12
|
function PreviewSurface({ html, fallbackLabel }: { html: string; fallbackLabel: string }) {
|
|
13
|
+
const nonce = useContentSecurityPolicyNonce()
|
|
12
14
|
const editor = useEditor({
|
|
15
|
+
injectNonce: nonce,
|
|
13
16
|
extensions: getDocumentEditorExtensions({ entityRefFallbackLabel: fallbackLabel }),
|
|
14
17
|
content: html,
|
|
15
18
|
editable: false,
|