@aphexcms/cms-core 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/generate-types.d.ts.map +1 -1
- package/dist/cli/generate-types.js +197 -8
- package/dist/cli/generate-types.js.map +1 -1
- package/dist/components/AdminApp.svelte +46 -137
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +237 -206
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentVersionPanel.svelte +3 -2
- package/dist/components/admin/DocumentVersionPanel.svelte.d.ts.map +1 -1
- package/dist/components/admin/MediaBrowser.svelte +22 -4
- package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
- package/dist/components/admin/ObjectModal.svelte +62 -75
- package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -1
- package/dist/components/admin/SchemaField.svelte +2 -2
- package/dist/components/admin/fields/ArrayField.svelte +9 -1
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ReferenceField.svelte +37 -8
- package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/richtext/PortableTextImageView.svelte +116 -0
- package/dist/components/admin/fields/richtext/PortableTextImageView.svelte.d.ts +12 -0
- package/dist/components/admin/fields/richtext/PortableTextImageView.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/PortableTextInlineView.svelte +111 -0
- package/dist/components/admin/fields/richtext/PortableTextInlineView.svelte.d.ts +12 -0
- package/dist/components/admin/fields/richtext/PortableTextInlineView.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/PortableTextObjectView.svelte +67 -0
- package/dist/components/admin/fields/richtext/PortableTextObjectView.svelte.d.ts +12 -0
- package/dist/components/admin/fields/richtext/PortableTextObjectView.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/RichtextField.svelte +1314 -0
- package/dist/components/admin/fields/richtext/RichtextField.svelte.d.ts +15 -0
- package/dist/components/admin/fields/richtext/RichtextField.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/custom-annotation-mark.d.ts +7 -0
- package/dist/components/admin/fields/richtext/custom-annotation-mark.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/custom-annotation-mark.js +33 -0
- package/dist/components/admin/fields/richtext/portable-text-inline-object.d.ts +11 -0
- package/dist/components/admin/fields/richtext/portable-text-inline-object.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/portable-text-inline-object.js +31 -0
- package/dist/components/admin/fields/richtext/portable-text-object-node.d.ts +11 -0
- package/dist/components/admin/fields/richtext/portable-text-object-node.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/portable-text-object-node.js +31 -0
- package/dist/components/admin/fields/richtext/portable-text-serializer.d.ts +38 -0
- package/dist/components/admin/fields/richtext/portable-text-serializer.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/portable-text-serializer.js +318 -0
- package/dist/components/admin/fields/richtext/svelte-inline-node-view.d.ts +8 -0
- package/dist/components/admin/fields/richtext/svelte-inline-node-view.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/svelte-inline-node-view.js +67 -0
- package/dist/components/admin/fields/richtext/svelte-node-view.d.ts +8 -0
- package/dist/components/admin/fields/richtext/svelte-node-view.d.ts.map +1 -0
- package/dist/components/admin/fields/richtext/svelte-node-view.js +63 -0
- package/dist/db/interfaces/document.d.ts +8 -2
- package/dist/db/interfaces/document.d.ts.map +1 -1
- package/dist/graphql/resolvers.d.ts.map +1 -1
- package/dist/graphql/resolvers.js +13 -11
- package/dist/graphql/schema.d.ts.map +1 -1
- package/dist/graphql/schema.js +7 -3
- package/dist/lib/components/admin/fields/richtext/custom-annotation-mark.d.ts +7 -0
- package/dist/lib/components/admin/fields/richtext/custom-annotation-mark.d.ts.map +1 -0
- package/dist/lib/components/admin/fields/richtext/custom-annotation-mark.js +34 -0
- package/dist/lib/components/admin/fields/richtext/custom-annotation-mark.js.map +1 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-inline-object.d.ts +11 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-inline-object.d.ts.map +1 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-inline-object.js +32 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-inline-object.js.map +1 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-object-node.d.ts +11 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-object-node.d.ts.map +1 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-object-node.js +32 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-object-node.js.map +1 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.d.ts +38 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.d.ts.map +1 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.js +319 -0
- package/dist/lib/components/admin/fields/richtext/portable-text-serializer.js.map +1 -0
- package/dist/lib/components/admin/fields/richtext/svelte-inline-node-view.d.ts +8 -0
- package/dist/lib/components/admin/fields/richtext/svelte-inline-node-view.d.ts.map +1 -0
- package/dist/lib/components/admin/fields/richtext/svelte-inline-node-view.js +68 -0
- package/dist/lib/components/admin/fields/richtext/svelte-inline-node-view.js.map +1 -0
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.d.ts +8 -0
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.d.ts.map +1 -0
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.js +64 -0
- package/dist/lib/components/admin/fields/richtext/svelte-node-view.js.map +1 -0
- package/dist/lib/db/interfaces/document.d.ts +8 -2
- package/dist/lib/db/interfaces/document.d.ts.map +1 -1
- package/dist/lib/graphql/resolvers.d.ts.map +1 -1
- package/dist/lib/graphql/resolvers.js +13 -11
- package/dist/lib/graphql/resolvers.js.map +1 -1
- package/dist/lib/graphql/schema.d.ts.map +1 -1
- package/dist/lib/graphql/schema.js +7 -3
- package/dist/lib/graphql/schema.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +7 -0
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/schema-utils/validator.d.ts.map +1 -1
- package/dist/lib/schema-utils/validator.js +5 -1
- package/dist/lib/schema-utils/validator.js.map +1 -1
- package/dist/lib/server/api/routes/assets-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/assets-by-id.js +10 -2
- package/dist/lib/server/api/routes/assets-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/assets-references.d.ts.map +1 -1
- package/dist/lib/server/api/routes/assets-references.js +6 -4
- package/dist/lib/server/api/routes/assets-references.js.map +1 -1
- package/dist/lib/types/schemas.d.ts +27 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/types/schemas.js +3 -1
- package/dist/lib/types/schemas.js.map +1 -1
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +7 -0
- package/dist/schema-utils/validator.d.ts.map +1 -1
- package/dist/schema-utils/validator.js +5 -1
- package/dist/server/api/routes/assets-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/assets-by-id.js +10 -2
- package/dist/server/api/routes/assets-references.d.ts.map +1 -1
- package/dist/server/api/routes/assets-references.js +6 -4
- package/dist/types/schemas.d.ts +27 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +3 -1
- package/package.json +6 -1
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { Editor } from '@tiptap/core';
|
|
4
|
+
import { StarterKit } from '@tiptap/starter-kit';
|
|
5
|
+
import Link from '@tiptap/extension-link';
|
|
6
|
+
import Underline from '@tiptap/extension-underline';
|
|
7
|
+
import {
|
|
8
|
+
Bold,
|
|
9
|
+
Italic,
|
|
10
|
+
Underline as UnderlineIcon,
|
|
11
|
+
Strikethrough,
|
|
12
|
+
Code,
|
|
13
|
+
List,
|
|
14
|
+
ListOrdered,
|
|
15
|
+
Link as LinkIcon,
|
|
16
|
+
Unlink,
|
|
17
|
+
Undo2,
|
|
18
|
+
Redo2,
|
|
19
|
+
Plus,
|
|
20
|
+
ChevronDown,
|
|
21
|
+
X,
|
|
22
|
+
Copy,
|
|
23
|
+
Pencil,
|
|
24
|
+
Maximize2,
|
|
25
|
+
Minimize2
|
|
26
|
+
} from '@lucide/svelte';
|
|
27
|
+
import {
|
|
28
|
+
tiptapToPortableText,
|
|
29
|
+
portableTextToTiptap,
|
|
30
|
+
type PortableTextValue
|
|
31
|
+
} from './portable-text-serializer';
|
|
32
|
+
import { PortableTextObject } from './portable-text-object-node';
|
|
33
|
+
import { PortableTextInlineObject } from './portable-text-inline-object';
|
|
34
|
+
import { createAnnotationMark } from './custom-annotation-mark';
|
|
35
|
+
import type { ArrayField as ArrayFieldType, SchemaType } from '../../../../types/schemas';
|
|
36
|
+
import { getSchemaContext } from '../../../../schema-context.svelte';
|
|
37
|
+
import { getSchemaByName } from '../../../../schema-utils/utils';
|
|
38
|
+
import ObjectModal from '../../ObjectModal.svelte';
|
|
39
|
+
import AssetBrowserModal from '../../AssetBrowserModal.svelte';
|
|
40
|
+
import * as Tooltip from '@aphexcms/ui/shadcn/tooltip';
|
|
41
|
+
import { Image as ImageIcon } from '@lucide/svelte';
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
field: ArrayFieldType;
|
|
45
|
+
value: PortableTextValue | null | undefined;
|
|
46
|
+
onUpdate: (value: PortableTextValue) => void;
|
|
47
|
+
validationClasses?: string;
|
|
48
|
+
readonly?: boolean;
|
|
49
|
+
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
50
|
+
organizationId?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
field,
|
|
55
|
+
value,
|
|
56
|
+
onUpdate,
|
|
57
|
+
validationClasses = '',
|
|
58
|
+
readonly = false,
|
|
59
|
+
onOpenReference,
|
|
60
|
+
organizationId
|
|
61
|
+
}: Props = $props();
|
|
62
|
+
|
|
63
|
+
const schemas = getSchemaContext();
|
|
64
|
+
|
|
65
|
+
let element = $state<HTMLElement>();
|
|
66
|
+
let editor = $state<Editor | null>(null);
|
|
67
|
+
let editorState = $state(0);
|
|
68
|
+
let internalVersion = 0;
|
|
69
|
+
let isSyncingFromParent = false;
|
|
70
|
+
|
|
71
|
+
let expanded = $state(false);
|
|
72
|
+
let containerBounds = $state<{ top: number; left: number; width: number; height: number } | null>(
|
|
73
|
+
null
|
|
74
|
+
);
|
|
75
|
+
let insertMenuOpen = $state(false);
|
|
76
|
+
let modalOpen = $state(false);
|
|
77
|
+
let editingSchema = $state<SchemaType | null>(null);
|
|
78
|
+
let editingValue = $state<Record<string, any>>({});
|
|
79
|
+
let editingNodeKey = $state<string | null>(null);
|
|
80
|
+
let editingIsInline = $state(false);
|
|
81
|
+
|
|
82
|
+
const blockDef = $derived(field.of.find((ref) => ref.type === 'block'));
|
|
83
|
+
const styles = $derived(
|
|
84
|
+
blockDef?.styles?.map((s) => s.value) ?? [
|
|
85
|
+
'normal',
|
|
86
|
+
'h1',
|
|
87
|
+
'h2',
|
|
88
|
+
'h3',
|
|
89
|
+
'h4',
|
|
90
|
+
'h5',
|
|
91
|
+
'h6',
|
|
92
|
+
'blockquote'
|
|
93
|
+
]
|
|
94
|
+
);
|
|
95
|
+
const decorators = $derived(
|
|
96
|
+
blockDef?.marks?.decorators?.map((d) => d.value) ?? [
|
|
97
|
+
'strong',
|
|
98
|
+
'em',
|
|
99
|
+
'underline',
|
|
100
|
+
'strike-through',
|
|
101
|
+
'code'
|
|
102
|
+
]
|
|
103
|
+
);
|
|
104
|
+
const lists = $derived(blockDef?.lists?.map((l) => l.value) ?? ['bullet', 'number']);
|
|
105
|
+
|
|
106
|
+
const customAnnotations = $derived(
|
|
107
|
+
(blockDef?.marks?.annotations ?? []).map((a) => ({
|
|
108
|
+
name: a.name,
|
|
109
|
+
title: a.title || a.name,
|
|
110
|
+
icon: a.icon,
|
|
111
|
+
fields: a.fields || []
|
|
112
|
+
}))
|
|
113
|
+
);
|
|
114
|
+
const hasAnnotations = $derived(customAnnotations.length > 0);
|
|
115
|
+
const hasLink = true;
|
|
116
|
+
|
|
117
|
+
const hasImageBlock = $derived(field.of.some((ref) => ref.type === 'image'));
|
|
118
|
+
const customTypes = $derived(
|
|
119
|
+
field.of
|
|
120
|
+
.filter((ref) => ref.type !== 'block' && ref.type !== 'image')
|
|
121
|
+
.map((t) => {
|
|
122
|
+
const registered = getSchemaByName(schemas, t.type);
|
|
123
|
+
return {
|
|
124
|
+
type: t.type,
|
|
125
|
+
title: t.title || registered?.title || t.type,
|
|
126
|
+
fields: t.fields || registered?.fields || [],
|
|
127
|
+
schema: registered
|
|
128
|
+
};
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
const hasCustomTypes = $derived(customTypes.length > 0);
|
|
132
|
+
|
|
133
|
+
const inlineTypes = $derived(
|
|
134
|
+
(blockDef?.of ?? []).map((t) => {
|
|
135
|
+
const registered = getSchemaByName(schemas, t.type);
|
|
136
|
+
return {
|
|
137
|
+
type: t.type,
|
|
138
|
+
title: t.title || registered?.title || t.type,
|
|
139
|
+
fields: t.fields || registered?.fields || [],
|
|
140
|
+
schema: registered
|
|
141
|
+
};
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
const hasInlineTypes = $derived(inlineTypes.length > 0);
|
|
145
|
+
|
|
146
|
+
const hasDecorator = (d: string) => decorators.includes(d);
|
|
147
|
+
const hasList = (l: string) => lists.includes(l);
|
|
148
|
+
const hasStyle = (s: string) => styles.includes(s);
|
|
149
|
+
|
|
150
|
+
let styleMenuOpen = $state(false);
|
|
151
|
+
|
|
152
|
+
const styleLabels: Record<string, string> = {
|
|
153
|
+
normal: 'Normal',
|
|
154
|
+
h1: 'Heading 1',
|
|
155
|
+
h2: 'Heading 2',
|
|
156
|
+
h3: 'Heading 3',
|
|
157
|
+
h4: 'Heading 4',
|
|
158
|
+
h5: 'Heading 5',
|
|
159
|
+
h6: 'Heading 6',
|
|
160
|
+
blockquote: 'Quote'
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const activeStyle = $derived.by(() => {
|
|
164
|
+
void editorState;
|
|
165
|
+
if (!editor) return 'normal';
|
|
166
|
+
for (const s of styles) {
|
|
167
|
+
if (s === 'blockquote' && editor.isActive('blockquote')) return 'blockquote';
|
|
168
|
+
if (s.startsWith('h') && editor.isActive('heading', { level: parseInt(s.slice(1), 10) }))
|
|
169
|
+
return s;
|
|
170
|
+
}
|
|
171
|
+
return 'normal';
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
function applyStyle(style: string) {
|
|
175
|
+
if (!editor) return;
|
|
176
|
+
if (style === 'normal') {
|
|
177
|
+
editor.chain().focus().setParagraph().run();
|
|
178
|
+
} else if (style === 'blockquote') {
|
|
179
|
+
editor.chain().focus().toggleBlockquote().run();
|
|
180
|
+
} else if (style.startsWith('h')) {
|
|
181
|
+
const level = parseInt(style.slice(1), 10) as 1 | 2 | 3 | 4 | 5 | 6;
|
|
182
|
+
editor.chain().focus().toggleHeading({ level }).run();
|
|
183
|
+
}
|
|
184
|
+
styleMenuOpen = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function genKey(): string {
|
|
188
|
+
return Math.random().toString(36).slice(2, 8);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleEditBlock(attrs: { _type: string; _key: string; data: Record<string, unknown> }) {
|
|
192
|
+
if (attrs._type === 'image') {
|
|
193
|
+
editingNodeKey = attrs._key;
|
|
194
|
+
showAssetBrowser = true;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const typeDef = customTypes.find((t) => t.type === attrs._type);
|
|
198
|
+
if (!typeDef) return;
|
|
199
|
+
editingSchema = {
|
|
200
|
+
type: 'object',
|
|
201
|
+
name: attrs._type,
|
|
202
|
+
title: typeDef.title,
|
|
203
|
+
fields: typeDef.fields as any
|
|
204
|
+
};
|
|
205
|
+
editingValue = { ...attrs.data };
|
|
206
|
+
editingNodeKey = attrs._key;
|
|
207
|
+
editingIsInline = false;
|
|
208
|
+
modalOpen = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleDeleteBlock(key: string) {
|
|
212
|
+
if (!editor) return;
|
|
213
|
+
const { state } = editor;
|
|
214
|
+
const { tr } = state;
|
|
215
|
+
state.doc.descendants((node, pos) => {
|
|
216
|
+
if (
|
|
217
|
+
(node.type.name === 'portableTextObject' ||
|
|
218
|
+
node.type.name === 'portableTextInlineObject') &&
|
|
219
|
+
node.attrs._key === key
|
|
220
|
+
) {
|
|
221
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
editor.view.dispatch(tr);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleInsertBlock(typeName: string) {
|
|
229
|
+
if (!editor) return;
|
|
230
|
+
const typeDef = customTypes.find((t) => t.type === typeName);
|
|
231
|
+
if (!typeDef) return;
|
|
232
|
+
const key = genKey();
|
|
233
|
+
editingSchema = {
|
|
234
|
+
type: 'object',
|
|
235
|
+
name: typeName,
|
|
236
|
+
title: typeDef.title,
|
|
237
|
+
fields: typeDef.fields as any
|
|
238
|
+
};
|
|
239
|
+
editingValue = {};
|
|
240
|
+
editingNodeKey = key;
|
|
241
|
+
editingIsInline = false;
|
|
242
|
+
modalOpen = true;
|
|
243
|
+
insertMenuOpen = false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function handleEditInline(attrs: { _type: string; _key: string; data: Record<string, unknown> }) {
|
|
247
|
+
const typeDef = inlineTypes.find((t) => t.type === attrs._type);
|
|
248
|
+
if (!typeDef) return;
|
|
249
|
+
editingSchema = {
|
|
250
|
+
type: 'object',
|
|
251
|
+
name: attrs._type,
|
|
252
|
+
title: typeDef.title,
|
|
253
|
+
fields: typeDef.fields as any
|
|
254
|
+
};
|
|
255
|
+
editingValue = { ...attrs.data };
|
|
256
|
+
editingNodeKey = attrs._key;
|
|
257
|
+
editingIsInline = true;
|
|
258
|
+
modalOpen = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function handleInsertInline(typeName: string) {
|
|
262
|
+
if (!editor) return;
|
|
263
|
+
const typeDef = inlineTypes.find((t) => t.type === typeName);
|
|
264
|
+
if (!typeDef) return;
|
|
265
|
+
const key = genKey();
|
|
266
|
+
editingSchema = {
|
|
267
|
+
type: 'object',
|
|
268
|
+
name: typeName,
|
|
269
|
+
title: typeDef.title,
|
|
270
|
+
fields: typeDef.fields as any
|
|
271
|
+
};
|
|
272
|
+
editingValue = {};
|
|
273
|
+
editingNodeKey = key;
|
|
274
|
+
editingIsInline = true;
|
|
275
|
+
modalOpen = true;
|
|
276
|
+
insertMenuOpen = false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let showAssetBrowser = $state(false);
|
|
280
|
+
|
|
281
|
+
function handleInsertImageBlock() {
|
|
282
|
+
editingNodeKey = null;
|
|
283
|
+
insertMenuOpen = false;
|
|
284
|
+
showAssetBrowser = true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function handleAssetSelected(asset: any) {
|
|
288
|
+
if (!editor || !asset) return;
|
|
289
|
+
|
|
290
|
+
const existing = editingNodeKey ? findNodeByKey(editingNodeKey) : null;
|
|
291
|
+
if (existing) {
|
|
292
|
+
const { tr } = editor.state;
|
|
293
|
+
tr.setNodeMarkup(existing.pos, undefined, {
|
|
294
|
+
_type: 'image',
|
|
295
|
+
_key: editingNodeKey,
|
|
296
|
+
data: { asset: { _type: 'reference', _ref: asset.id } }
|
|
297
|
+
});
|
|
298
|
+
editor.view.dispatch(tr);
|
|
299
|
+
} else {
|
|
300
|
+
editor
|
|
301
|
+
.chain()
|
|
302
|
+
.focus()
|
|
303
|
+
.insertContent({
|
|
304
|
+
type: 'portableTextObject',
|
|
305
|
+
attrs: {
|
|
306
|
+
_type: 'image',
|
|
307
|
+
_key: genKey(),
|
|
308
|
+
data: { asset: { _type: 'reference', _ref: asset.id } }
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
.run();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
showAssetBrowser = false;
|
|
315
|
+
editingNodeKey = null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function handleModalUpdate(updatedData: Record<string, any>) {
|
|
319
|
+
editingValue = updatedData;
|
|
320
|
+
if (!editor || !editingSchema || !editingNodeKey) return;
|
|
321
|
+
const existing = findNodeByKey(editingNodeKey);
|
|
322
|
+
if (existing) {
|
|
323
|
+
const { tr } = editor.state;
|
|
324
|
+
tr.setNodeMarkup(existing.pos, undefined, {
|
|
325
|
+
_type: editingSchema.name,
|
|
326
|
+
_key: editingNodeKey,
|
|
327
|
+
data: editingValue
|
|
328
|
+
});
|
|
329
|
+
editor.view.dispatch(tr);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function handleModalClose() {
|
|
334
|
+
if (!editor || !editingSchema || !editingNodeKey) {
|
|
335
|
+
modalOpen = false;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const existing = findNodeByKey(editingNodeKey);
|
|
339
|
+
if (existing) {
|
|
340
|
+
const { tr } = editor.state;
|
|
341
|
+
tr.setNodeMarkup(existing.pos, undefined, {
|
|
342
|
+
_type: editingSchema.name,
|
|
343
|
+
_key: editingNodeKey,
|
|
344
|
+
data: editingValue
|
|
345
|
+
});
|
|
346
|
+
editor.view.dispatch(tr);
|
|
347
|
+
} else {
|
|
348
|
+
const nodeType = editingIsInline ? 'portableTextInlineObject' : 'portableTextObject';
|
|
349
|
+
editor
|
|
350
|
+
.chain()
|
|
351
|
+
.focus()
|
|
352
|
+
.insertContent({
|
|
353
|
+
type: nodeType,
|
|
354
|
+
attrs: {
|
|
355
|
+
_type: editingSchema.name,
|
|
356
|
+
_key: editingNodeKey,
|
|
357
|
+
data: editingValue
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.run();
|
|
361
|
+
}
|
|
362
|
+
modalOpen = false;
|
|
363
|
+
editingSchema = null;
|
|
364
|
+
editingValue = {};
|
|
365
|
+
editingNodeKey = null;
|
|
366
|
+
editingIsInline = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function findNodeByKey(key: string): { pos: number; node: any } | null {
|
|
370
|
+
if (!editor) return null;
|
|
371
|
+
let found: { pos: number; node: any } | null = null;
|
|
372
|
+
editor.state.doc.descendants((node, pos) => {
|
|
373
|
+
if (
|
|
374
|
+
(node.type.name === 'portableTextObject' ||
|
|
375
|
+
node.type.name === 'portableTextInlineObject') &&
|
|
376
|
+
node.attrs._key === key
|
|
377
|
+
) {
|
|
378
|
+
found = { pos, node };
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return found;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function createEditor() {
|
|
386
|
+
editor?.destroy();
|
|
387
|
+
if (!element) return;
|
|
388
|
+
const tiptapDoc = portableTextToTiptap(value || []);
|
|
389
|
+
const starterKitConfig: Record<string, any> = {
|
|
390
|
+
heading: {
|
|
391
|
+
levels: [1, 2, 3, 4, 5, 6].filter((l) => hasStyle(`h${l}`))
|
|
392
|
+
},
|
|
393
|
+
bulletList: hasList('bullet') ? {} : false,
|
|
394
|
+
orderedList: hasList('number') ? {} : false,
|
|
395
|
+
blockquote: hasStyle('blockquote') ? {} : false,
|
|
396
|
+
bold: hasDecorator('strong') ? {} : false,
|
|
397
|
+
italic: hasDecorator('em') ? {} : false,
|
|
398
|
+
strike: hasDecorator('strike-through') ? {} : false,
|
|
399
|
+
code: hasDecorator('code') ? {} : false
|
|
400
|
+
};
|
|
401
|
+
// Prevent duplicates — StarterKit may bundle these in newer versions
|
|
402
|
+
if (hasDecorator('underline')) starterKitConfig.underline = false;
|
|
403
|
+
if (hasLink) starterKitConfig.link = false;
|
|
404
|
+
|
|
405
|
+
const extensions = [
|
|
406
|
+
StarterKit.configure(starterKitConfig),
|
|
407
|
+
...(hasDecorator('underline') ? [Underline] : []),
|
|
408
|
+
...(hasLink
|
|
409
|
+
? [
|
|
410
|
+
Link.configure({
|
|
411
|
+
openOnClick: false,
|
|
412
|
+
HTMLAttributes: { class: 'richtext-link' }
|
|
413
|
+
})
|
|
414
|
+
]
|
|
415
|
+
: []),
|
|
416
|
+
...(hasCustomTypes
|
|
417
|
+
? [
|
|
418
|
+
PortableTextObject.configure({
|
|
419
|
+
onEdit: handleEditBlock,
|
|
420
|
+
onDelete: handleDeleteBlock
|
|
421
|
+
})
|
|
422
|
+
]
|
|
423
|
+
: []),
|
|
424
|
+
...(hasInlineTypes
|
|
425
|
+
? [
|
|
426
|
+
PortableTextInlineObject.configure({
|
|
427
|
+
onEdit: handleEditInline,
|
|
428
|
+
onDelete: handleDeleteBlock
|
|
429
|
+
})
|
|
430
|
+
]
|
|
431
|
+
: []),
|
|
432
|
+
...customAnnotations.map((a) => createAnnotationMark(a.name))
|
|
433
|
+
];
|
|
434
|
+
internalVersion = 0;
|
|
435
|
+
lastSyncedVersion = 0;
|
|
436
|
+
editorJustCreated = true;
|
|
437
|
+
expanded = false;
|
|
438
|
+
editor = new Editor({
|
|
439
|
+
element,
|
|
440
|
+
editable: !readonly,
|
|
441
|
+
extensions,
|
|
442
|
+
content: tiptapDoc,
|
|
443
|
+
onUpdate: ({ editor: e }) => {
|
|
444
|
+
if (isSyncingFromParent) return;
|
|
445
|
+
internalVersion++;
|
|
446
|
+
const json = e.getJSON();
|
|
447
|
+
const pt = tiptapToPortableText(json);
|
|
448
|
+
onUpdate(pt);
|
|
449
|
+
},
|
|
450
|
+
onTransaction: ({ editor: e }) => {
|
|
451
|
+
editor = e as any;
|
|
452
|
+
queueMicrotask(() => {
|
|
453
|
+
editorState++;
|
|
454
|
+
});
|
|
455
|
+
if (linkMode === 'edit') return;
|
|
456
|
+
if (e.isActive('link') && e.state.selection.empty) {
|
|
457
|
+
showLinkPreview();
|
|
458
|
+
} else if (linkMode === 'preview') {
|
|
459
|
+
linkMode = 'closed';
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
injectCSS: false
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function handleAnnotationClick(e: MouseEvent) {
|
|
467
|
+
const target = e.target as HTMLElement;
|
|
468
|
+
const annotationEl = target.closest('[data-annotation]');
|
|
469
|
+
if (!annotationEl || !editor) return;
|
|
470
|
+
const name = annotationEl.getAttribute('data-annotation');
|
|
471
|
+
if (!name) return;
|
|
472
|
+
|
|
473
|
+
const def = customAnnotations.find((a) => a.name === name);
|
|
474
|
+
if (!def) return;
|
|
475
|
+
|
|
476
|
+
const markName = `annotation_${name}`;
|
|
477
|
+
const markType = editor.schema.marks[markName];
|
|
478
|
+
if (!markType) return;
|
|
479
|
+
|
|
480
|
+
const pos = editor.view.posAtDOM(annotationEl, 0);
|
|
481
|
+
const resolved = editor.state.doc.resolve(pos);
|
|
482
|
+
const node = resolved.parent.maybeChild(resolved.index());
|
|
483
|
+
const mark = node?.marks.find((m: any) => m.type === markType);
|
|
484
|
+
|
|
485
|
+
if (mark) {
|
|
486
|
+
annotationValue = { ...(mark.attrs.data || {}) };
|
|
487
|
+
const key = mark.attrs._key || genKey();
|
|
488
|
+
annotationKey = key;
|
|
489
|
+
const range = findMarkRange(key, markType);
|
|
490
|
+
annotationRange = range || { from: pos, to: pos + (node?.nodeSize || 0) };
|
|
491
|
+
} else {
|
|
492
|
+
annotationValue = {};
|
|
493
|
+
annotationKey = genKey();
|
|
494
|
+
annotationRange = { from: pos, to: pos };
|
|
495
|
+
}
|
|
496
|
+
annotationDef = def;
|
|
497
|
+
annotationModalOpen = true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
onMount(() => {
|
|
501
|
+
createEditor();
|
|
502
|
+
document.addEventListener('click', handleClickOutside);
|
|
503
|
+
document.addEventListener('keydown', handleKeydown);
|
|
504
|
+
element?.addEventListener('click', handleAnnotationClick);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
onDestroy(() => {
|
|
508
|
+
editor?.destroy();
|
|
509
|
+
document.removeEventListener('click', handleClickOutside);
|
|
510
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
511
|
+
element?.removeEventListener('click', handleAnnotationClick);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
function handleClickOutside(e: MouseEvent) {
|
|
515
|
+
const target = e.target as HTMLElement;
|
|
516
|
+
if (styleMenuOpen && !target.closest('.richtext-editor')) {
|
|
517
|
+
styleMenuOpen = false;
|
|
518
|
+
}
|
|
519
|
+
if (insertMenuOpen && !target.closest('.richtext-editor')) {
|
|
520
|
+
insertMenuOpen = false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
525
|
+
if (e.key === 'Escape' && expanded) {
|
|
526
|
+
expanded = false;
|
|
527
|
+
}
|
|
528
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey) && editor?.isFocused) {
|
|
529
|
+
e.preventDefault();
|
|
530
|
+
openLinkEdit();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Sync value → editor when value changes externally. Track a version
|
|
535
|
+
// counter so we can tell the difference between "parent echoed our
|
|
536
|
+
// own change back" vs "parent loaded a different document."
|
|
537
|
+
let lastSyncedVersion = 0;
|
|
538
|
+
let editorJustCreated = false;
|
|
539
|
+
$effect(() => {
|
|
540
|
+
const v = value;
|
|
541
|
+
if (!editor) return;
|
|
542
|
+
if (editorJustCreated) {
|
|
543
|
+
editorJustCreated = false;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (internalVersion !== lastSyncedVersion) {
|
|
547
|
+
lastSyncedVersion = internalVersion;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Replace content without polluting undo history
|
|
551
|
+
const tiptapDoc = portableTextToTiptap(v || []);
|
|
552
|
+
const newDoc = editor.state.schema.nodeFromJSON(tiptapDoc);
|
|
553
|
+
const tr = editor.state.tr.replaceWith(0, editor.state.doc.content.size, newDoc.content);
|
|
554
|
+
tr.setMeta('addToHistory', false);
|
|
555
|
+
isSyncingFromParent = true;
|
|
556
|
+
editor.view.dispatch(tr);
|
|
557
|
+
isSyncingFromParent = false;
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
$effect(() => {
|
|
561
|
+
if (!expanded || !element) return;
|
|
562
|
+
|
|
563
|
+
const container = element.closest('[data-document-editor]');
|
|
564
|
+
if (!container) return;
|
|
565
|
+
|
|
566
|
+
const updateBounds = () => {
|
|
567
|
+
const rect = container.getBoundingClientRect();
|
|
568
|
+
containerBounds = {
|
|
569
|
+
top: rect.top,
|
|
570
|
+
left: rect.left,
|
|
571
|
+
width: rect.width,
|
|
572
|
+
height: rect.height
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
updateBounds();
|
|
577
|
+
const observer = new ResizeObserver(updateBounds);
|
|
578
|
+
observer.observe(container);
|
|
579
|
+
|
|
580
|
+
return () => {
|
|
581
|
+
observer.disconnect();
|
|
582
|
+
containerBounds = null;
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
let linkMode = $state<'closed' | 'preview' | 'edit'>('closed');
|
|
587
|
+
let linkUrl = $state('');
|
|
588
|
+
let linkInputRef = $state<HTMLInputElement>();
|
|
589
|
+
let linkPopoverPos = $state({ top: 0, left: 0 });
|
|
590
|
+
let linkPreviewHref = $state('');
|
|
591
|
+
|
|
592
|
+
function positionLinkPopover() {
|
|
593
|
+
if (!editor || !element) return;
|
|
594
|
+
const { from, to } = editor.state.selection;
|
|
595
|
+
const start = editor.view.coordsAtPos(from);
|
|
596
|
+
const end = editor.view.coordsAtPos(to);
|
|
597
|
+
const editorRect = element.getBoundingClientRect();
|
|
598
|
+
linkPopoverPos = {
|
|
599
|
+
top: end.bottom - editorRect.top + 4,
|
|
600
|
+
left: Math.max(0, Math.min(start.left, end.left) - editorRect.left)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function openLinkEdit() {
|
|
605
|
+
if (!editor || !element) return;
|
|
606
|
+
if (editor.isActive('link')) {
|
|
607
|
+
linkUrl = editor.getAttributes('link').href || '';
|
|
608
|
+
} else {
|
|
609
|
+
linkUrl = '';
|
|
610
|
+
}
|
|
611
|
+
positionLinkPopover();
|
|
612
|
+
linkMode = 'edit';
|
|
613
|
+
queueMicrotask(() => linkInputRef?.focus());
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function showLinkPreview() {
|
|
617
|
+
if (!editor || !element) return;
|
|
618
|
+
const attrs = editor.getAttributes('link');
|
|
619
|
+
if (!attrs.href) return;
|
|
620
|
+
linkPreviewHref = attrs.href;
|
|
621
|
+
positionLinkPopover();
|
|
622
|
+
linkMode = 'preview';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function applyLink() {
|
|
626
|
+
if (!editor) return;
|
|
627
|
+
if (linkUrl.trim()) {
|
|
628
|
+
let href = linkUrl.trim();
|
|
629
|
+
if (
|
|
630
|
+
!/^https?:\/\//.test(href) &&
|
|
631
|
+
!href.startsWith('/') &&
|
|
632
|
+
!href.startsWith('#') &&
|
|
633
|
+
!href.startsWith('mailto:')
|
|
634
|
+
) {
|
|
635
|
+
href = 'https://' + href;
|
|
636
|
+
}
|
|
637
|
+
editor.chain().focus().setLink({ href }).run();
|
|
638
|
+
} else {
|
|
639
|
+
editor.chain().focus().unsetLink().run();
|
|
640
|
+
}
|
|
641
|
+
linkMode = 'closed';
|
|
642
|
+
linkUrl = '';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function removeLink() {
|
|
646
|
+
if (!editor) return;
|
|
647
|
+
editor.chain().focus().unsetLink().run();
|
|
648
|
+
linkMode = 'closed';
|
|
649
|
+
linkUrl = '';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function closeLinkPopover() {
|
|
653
|
+
linkMode = 'closed';
|
|
654
|
+
editor?.chain().focus().run();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function handleLinkKeydown(e: KeyboardEvent) {
|
|
658
|
+
if (e.key === 'Enter') {
|
|
659
|
+
e.preventDefault();
|
|
660
|
+
applyLink();
|
|
661
|
+
} else if (e.key === 'Escape') {
|
|
662
|
+
closeLinkPopover();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Annotation state
|
|
667
|
+
let annotationModalOpen = $state(false);
|
|
668
|
+
let annotationDef = $state<{ name: string; title: string; fields: any[] } | null>(null);
|
|
669
|
+
let annotationValue = $state<Record<string, any>>({});
|
|
670
|
+
let annotationKey = $state<string | null>(null);
|
|
671
|
+
let annotationRange = $state<{ from: number; to: number } | null>(null);
|
|
672
|
+
|
|
673
|
+
function openAnnotation(name: string) {
|
|
674
|
+
if (!editor) return;
|
|
675
|
+
const def = customAnnotations.find((a) => a.name === name);
|
|
676
|
+
if (!def) return;
|
|
677
|
+
|
|
678
|
+
const { from, to } = editor.state.selection;
|
|
679
|
+
const markName = `annotation_${name}`;
|
|
680
|
+
if (editor.isActive(markName)) {
|
|
681
|
+
const attrs = editor.getAttributes(markName);
|
|
682
|
+
annotationValue = { ...(attrs.data || {}) };
|
|
683
|
+
annotationKey = attrs._key || genKey();
|
|
684
|
+
} else {
|
|
685
|
+
annotationValue = {};
|
|
686
|
+
annotationKey = genKey();
|
|
687
|
+
}
|
|
688
|
+
annotationRange = { from, to };
|
|
689
|
+
annotationDef = def;
|
|
690
|
+
annotationModalOpen = true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function findMarkRange(key: string, markType: any): { from: number; to: number } | null {
|
|
694
|
+
if (!editor) return null;
|
|
695
|
+
let markFrom = -1;
|
|
696
|
+
let markTo = -1;
|
|
697
|
+
editor.state.doc.descendants((node, pos) => {
|
|
698
|
+
const mark = node.marks.find((m: any) => m.type === markType && m.attrs._key === key);
|
|
699
|
+
if (mark) {
|
|
700
|
+
if (markFrom === -1) markFrom = pos;
|
|
701
|
+
markTo = pos + node.nodeSize;
|
|
702
|
+
} else if (markFrom >= 0) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return markFrom >= 0 ? { from: markFrom, to: markTo } : null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function applyAnnotationMark() {
|
|
710
|
+
if (!editor || !annotationDef || !annotationKey) return;
|
|
711
|
+
const markName = `annotation_${annotationDef.name}`;
|
|
712
|
+
const markType = editor.schema.marks[markName];
|
|
713
|
+
if (!markType) return;
|
|
714
|
+
|
|
715
|
+
const docRange = findMarkRange(annotationKey, markType);
|
|
716
|
+
const range = docRange || annotationRange;
|
|
717
|
+
if (!range || range.from === range.to) return;
|
|
718
|
+
|
|
719
|
+
const tr = editor.state.tr.addMark(
|
|
720
|
+
range.from,
|
|
721
|
+
range.to,
|
|
722
|
+
markType.create({ _key: annotationKey, data: annotationValue })
|
|
723
|
+
);
|
|
724
|
+
editor.view.dispatch(tr);
|
|
725
|
+
|
|
726
|
+
internalVersion++;
|
|
727
|
+
const pt = tiptapToPortableText(editor.getJSON());
|
|
728
|
+
onUpdate(pt);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function handleAnnotationUpdate(updatedData: Record<string, any>) {
|
|
732
|
+
annotationValue = updatedData;
|
|
733
|
+
applyAnnotationMark();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function handleAnnotationClose() {
|
|
737
|
+
applyAnnotationMark();
|
|
738
|
+
editor?.chain().focus().run();
|
|
739
|
+
annotationModalOpen = false;
|
|
740
|
+
annotationDef = null;
|
|
741
|
+
annotationValue = {};
|
|
742
|
+
annotationKey = null;
|
|
743
|
+
annotationRange = null;
|
|
744
|
+
}
|
|
745
|
+
</script>
|
|
746
|
+
|
|
747
|
+
{#snippet toolbarBtn(
|
|
748
|
+
onclick: () => void,
|
|
749
|
+
label: string,
|
|
750
|
+
isActive: () => boolean,
|
|
751
|
+
icon: any,
|
|
752
|
+
isDisabled?: () => boolean
|
|
753
|
+
)}
|
|
754
|
+
{@const dis = editorState >= 0 && isDisabled?.() === true}
|
|
755
|
+
<Tooltip.Root delayDuration={400}>
|
|
756
|
+
<Tooltip.Trigger
|
|
757
|
+
type="button"
|
|
758
|
+
disabled={dis}
|
|
759
|
+
class="rounded p-1.5 transition-colors {dis
|
|
760
|
+
? 'text-muted-foreground/40 cursor-not-allowed'
|
|
761
|
+
: editorState >= 0 && isActive()
|
|
762
|
+
? 'bg-muted text-foreground'
|
|
763
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
|
764
|
+
onmousedown={(e: MouseEvent) => {
|
|
765
|
+
e.preventDefault();
|
|
766
|
+
if (!dis) onclick();
|
|
767
|
+
}}
|
|
768
|
+
>
|
|
769
|
+
{@const Icon = icon}
|
|
770
|
+
<Icon class="h-4 w-4" />
|
|
771
|
+
</Tooltip.Trigger>
|
|
772
|
+
<Tooltip.Content side="bottom" sideOffset={4}>{label}</Tooltip.Content>
|
|
773
|
+
</Tooltip.Root>
|
|
774
|
+
{/snippet}
|
|
775
|
+
|
|
776
|
+
<div
|
|
777
|
+
class="richtext-editor {validationClasses}"
|
|
778
|
+
class:richtext-expanded={expanded}
|
|
779
|
+
style={expanded && containerBounds
|
|
780
|
+
? `position: fixed; top: ${containerBounds.top}px; left: ${containerBounds.left}px; width: ${containerBounds.width}px; height: ${containerBounds.height}px; z-index: 50;`
|
|
781
|
+
: ''}
|
|
782
|
+
>
|
|
783
|
+
{#if editor && !readonly}
|
|
784
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
785
|
+
<Tooltip.Provider>
|
|
786
|
+
<div
|
|
787
|
+
class="border-rule bg-muted/30 flex flex-wrap items-center gap-0.5 border-b px-2 py-1.5"
|
|
788
|
+
onmousedown={(e) => e.preventDefault()}
|
|
789
|
+
>
|
|
790
|
+
{#if styles.length > 1}
|
|
791
|
+
<div class="relative">
|
|
792
|
+
<button
|
|
793
|
+
type="button"
|
|
794
|
+
class="text-muted-foreground hover:bg-muted hover:text-foreground flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors"
|
|
795
|
+
onclick={() => (styleMenuOpen = !styleMenuOpen)}
|
|
796
|
+
>
|
|
797
|
+
<span>{styleLabels[activeStyle] ?? 'Normal'}</span>
|
|
798
|
+
<ChevronDown class="h-3 w-3" />
|
|
799
|
+
</button>
|
|
800
|
+
{#if styleMenuOpen}
|
|
801
|
+
<div
|
|
802
|
+
class="bg-popover border-rule absolute top-full left-0 z-40 mt-1 min-w-[140px] rounded-md border py-1 shadow-lg"
|
|
803
|
+
>
|
|
804
|
+
{#each styles as s}
|
|
805
|
+
<button
|
|
806
|
+
type="button"
|
|
807
|
+
class="flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors {activeStyle ===
|
|
808
|
+
s
|
|
809
|
+
? 'bg-muted text-foreground font-medium'
|
|
810
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
|
811
|
+
onmousedown={(e) => {
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
applyStyle(s);
|
|
814
|
+
}}
|
|
815
|
+
>
|
|
816
|
+
{styleLabels[s] ?? s}
|
|
817
|
+
</button>
|
|
818
|
+
{/each}
|
|
819
|
+
</div>
|
|
820
|
+
{/if}
|
|
821
|
+
</div>
|
|
822
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
823
|
+
{/if}
|
|
824
|
+
|
|
825
|
+
{#if hasDecorator('strong')}
|
|
826
|
+
{@render toolbarBtn(
|
|
827
|
+
() => editor?.chain().focus().toggleBold().run(),
|
|
828
|
+
'Bold',
|
|
829
|
+
() => editor?.isActive('bold') ?? false,
|
|
830
|
+
Bold
|
|
831
|
+
)}
|
|
832
|
+
{/if}
|
|
833
|
+
{#if hasDecorator('em')}
|
|
834
|
+
{@render toolbarBtn(
|
|
835
|
+
() => editor?.chain().focus().toggleItalic().run(),
|
|
836
|
+
'Italic',
|
|
837
|
+
() => editor?.isActive('italic') ?? false,
|
|
838
|
+
Italic
|
|
839
|
+
)}
|
|
840
|
+
{/if}
|
|
841
|
+
{#if hasDecorator('underline')}
|
|
842
|
+
{@render toolbarBtn(
|
|
843
|
+
() => editor?.chain().focus().toggleUnderline().run(),
|
|
844
|
+
'Underline',
|
|
845
|
+
() => editor?.isActive('underline') ?? false,
|
|
846
|
+
UnderlineIcon
|
|
847
|
+
)}
|
|
848
|
+
{/if}
|
|
849
|
+
{#if hasDecorator('strike-through')}
|
|
850
|
+
{@render toolbarBtn(
|
|
851
|
+
() => editor?.chain().focus().toggleStrike().run(),
|
|
852
|
+
'Strikethrough',
|
|
853
|
+
() => editor?.isActive('strike') ?? false,
|
|
854
|
+
Strikethrough
|
|
855
|
+
)}
|
|
856
|
+
{/if}
|
|
857
|
+
{@render toolbarBtn(
|
|
858
|
+
() => editor?.chain().focus().toggleCodeBlock().run(),
|
|
859
|
+
'Code block',
|
|
860
|
+
() => editor?.isActive('codeBlock') ?? false,
|
|
861
|
+
Code
|
|
862
|
+
)}
|
|
863
|
+
|
|
864
|
+
{#if (hasDecorator('strong') || hasDecorator('em')) && (hasList('bullet') || hasList('number') || hasLink)}
|
|
865
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
866
|
+
{/if}
|
|
867
|
+
|
|
868
|
+
{#if hasList('bullet')}
|
|
869
|
+
{@render toolbarBtn(
|
|
870
|
+
() => editor?.chain().focus().toggleBulletList().run(),
|
|
871
|
+
'Bullet list',
|
|
872
|
+
() => editor?.isActive('bulletList') ?? false,
|
|
873
|
+
List
|
|
874
|
+
)}
|
|
875
|
+
{/if}
|
|
876
|
+
{#if hasList('number')}
|
|
877
|
+
{@render toolbarBtn(
|
|
878
|
+
() => editor?.chain().focus().toggleOrderedList().run(),
|
|
879
|
+
'Numbered list',
|
|
880
|
+
() => editor?.isActive('orderedList') ?? false,
|
|
881
|
+
ListOrdered
|
|
882
|
+
)}
|
|
883
|
+
{/if}
|
|
884
|
+
|
|
885
|
+
{#if hasLink}
|
|
886
|
+
{#if hasList('bullet') || hasList('number')}
|
|
887
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
888
|
+
{/if}
|
|
889
|
+
{@render toolbarBtn(
|
|
890
|
+
openLinkEdit,
|
|
891
|
+
(editor?.isActive('link') ?? false) ? 'Edit link' : 'Add link',
|
|
892
|
+
() => (editor?.isActive('link') ?? false) || linkMode !== 'closed',
|
|
893
|
+
LinkIcon
|
|
894
|
+
)}
|
|
895
|
+
{/if}
|
|
896
|
+
|
|
897
|
+
{#if hasImageBlock}
|
|
898
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
899
|
+
{@render toolbarBtn(handleInsertImageBlock, 'Insert image', () => false, ImageIcon)}
|
|
900
|
+
{/if}
|
|
901
|
+
|
|
902
|
+
{#if hasCustomTypes || hasInlineTypes}
|
|
903
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
904
|
+
<div class="relative">
|
|
905
|
+
<Tooltip.Root delayDuration={400}>
|
|
906
|
+
<Tooltip.Trigger
|
|
907
|
+
type="button"
|
|
908
|
+
class="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1.5 transition-colors"
|
|
909
|
+
onmousedown={(e: MouseEvent) => {
|
|
910
|
+
e.preventDefault();
|
|
911
|
+
insertMenuOpen = !insertMenuOpen;
|
|
912
|
+
}}
|
|
913
|
+
>
|
|
914
|
+
<Plus class="h-4 w-4" />
|
|
915
|
+
</Tooltip.Trigger>
|
|
916
|
+
<Tooltip.Content side="bottom" sideOffset={4}>Insert block</Tooltip.Content>
|
|
917
|
+
</Tooltip.Root>
|
|
918
|
+
{#if insertMenuOpen}
|
|
919
|
+
<div
|
|
920
|
+
class="bg-popover border-rule absolute top-full left-0 z-40 mt-1 min-w-[160px] rounded-md border py-1 shadow-lg"
|
|
921
|
+
>
|
|
922
|
+
{#if hasCustomTypes}
|
|
923
|
+
<div class="text-muted-foreground px-3 py-1 text-[10px] font-medium uppercase">
|
|
924
|
+
Blocks
|
|
925
|
+
</div>
|
|
926
|
+
{#each customTypes as ct}
|
|
927
|
+
<button
|
|
928
|
+
type="button"
|
|
929
|
+
class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
930
|
+
onclick={() => handleInsertBlock(ct.type)}
|
|
931
|
+
>
|
|
932
|
+
<span
|
|
933
|
+
class="bg-muted text-muted-foreground flex h-5 w-5 shrink-0 items-center justify-center rounded text-[10px] font-medium uppercase"
|
|
934
|
+
>{ct.type.slice(0, 2)}</span
|
|
935
|
+
>
|
|
936
|
+
<span class="capitalize">{ct.title}</span>
|
|
937
|
+
</button>
|
|
938
|
+
{/each}
|
|
939
|
+
{/if}
|
|
940
|
+
{#if hasInlineTypes}
|
|
941
|
+
{#if hasCustomTypes}
|
|
942
|
+
<div class="bg-border mx-2 my-1 h-px"></div>
|
|
943
|
+
{/if}
|
|
944
|
+
<div class="text-muted-foreground px-3 py-1 text-[10px] font-medium uppercase">
|
|
945
|
+
Inline
|
|
946
|
+
</div>
|
|
947
|
+
{#each inlineTypes as it}
|
|
948
|
+
<button
|
|
949
|
+
type="button"
|
|
950
|
+
class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
951
|
+
onclick={() => handleInsertInline(it.type)}
|
|
952
|
+
>
|
|
953
|
+
<span
|
|
954
|
+
class="bg-primary/20 text-primary flex h-5 w-5 shrink-0 items-center justify-center rounded text-[10px] font-medium uppercase"
|
|
955
|
+
>{it.type.slice(0, 2)}</span
|
|
956
|
+
>
|
|
957
|
+
<span class="capitalize">{it.title}</span>
|
|
958
|
+
</button>
|
|
959
|
+
{/each}
|
|
960
|
+
{/if}
|
|
961
|
+
</div>
|
|
962
|
+
{/if}
|
|
963
|
+
</div>
|
|
964
|
+
{/if}
|
|
965
|
+
|
|
966
|
+
{#if hasAnnotations}
|
|
967
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
968
|
+
{#each customAnnotations as ann}
|
|
969
|
+
<Tooltip.Root delayDuration={400}>
|
|
970
|
+
<Tooltip.Trigger
|
|
971
|
+
type="button"
|
|
972
|
+
class="rounded p-1.5 text-xs font-medium transition-colors {editor.isActive(
|
|
973
|
+
`annotation_${ann.name}`
|
|
974
|
+
)
|
|
975
|
+
? 'bg-muted text-foreground'
|
|
976
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
|
977
|
+
onmousedown={(e: MouseEvent) => {
|
|
978
|
+
e.preventDefault();
|
|
979
|
+
openAnnotation(ann.name);
|
|
980
|
+
}}
|
|
981
|
+
>
|
|
982
|
+
{ann.title.slice(0, 2).toUpperCase()}
|
|
983
|
+
</Tooltip.Trigger>
|
|
984
|
+
<Tooltip.Content side="bottom" sideOffset={4}>{ann.title}</Tooltip.Content>
|
|
985
|
+
</Tooltip.Root>
|
|
986
|
+
{/each}
|
|
987
|
+
{/if}
|
|
988
|
+
|
|
989
|
+
<div class="flex-1"></div>
|
|
990
|
+
|
|
991
|
+
{@render toolbarBtn(
|
|
992
|
+
() => editor?.chain().focus().undo().run(),
|
|
993
|
+
'Undo',
|
|
994
|
+
() => false,
|
|
995
|
+
Undo2,
|
|
996
|
+
() => !(editor?.can().undo() ?? false)
|
|
997
|
+
)}
|
|
998
|
+
{@render toolbarBtn(
|
|
999
|
+
() => editor?.chain().focus().redo().run(),
|
|
1000
|
+
'Redo',
|
|
1001
|
+
() => false,
|
|
1002
|
+
Redo2,
|
|
1003
|
+
() => !(editor?.can().redo() ?? false)
|
|
1004
|
+
)}
|
|
1005
|
+
|
|
1006
|
+
<div class="bg-border mx-1 h-4 w-px"></div>
|
|
1007
|
+
{@render toolbarBtn(
|
|
1008
|
+
() => (expanded = !expanded),
|
|
1009
|
+
expanded ? 'Collapse' : 'Expand',
|
|
1010
|
+
() => expanded,
|
|
1011
|
+
expanded ? Minimize2 : Maximize2
|
|
1012
|
+
)}
|
|
1013
|
+
</div>
|
|
1014
|
+
</Tooltip.Provider>
|
|
1015
|
+
{/if}
|
|
1016
|
+
|
|
1017
|
+
<div bind:this={element} class="richtext-content"></div>
|
|
1018
|
+
|
|
1019
|
+
{#if linkMode === 'preview'}
|
|
1020
|
+
<div
|
|
1021
|
+
class="border-rule bg-popover absolute z-40 flex items-center gap-1.5 rounded-md border px-2 py-1.5 shadow-lg"
|
|
1022
|
+
style="top: {linkPopoverPos.top}px; left: {linkPopoverPos.left}px;"
|
|
1023
|
+
>
|
|
1024
|
+
<a
|
|
1025
|
+
href={linkPreviewHref}
|
|
1026
|
+
target="_blank"
|
|
1027
|
+
rel="noopener noreferrer"
|
|
1028
|
+
class="text-primary max-w-[200px] truncate text-xs underline"
|
|
1029
|
+
title={linkPreviewHref}
|
|
1030
|
+
>
|
|
1031
|
+
{linkPreviewHref.replace(/^https?:\/\//, '')}
|
|
1032
|
+
</a>
|
|
1033
|
+
<div class="bg-border mx-0.5 h-3 w-px"></div>
|
|
1034
|
+
<button
|
|
1035
|
+
type="button"
|
|
1036
|
+
class="text-muted-foreground hover:text-foreground rounded p-0.5 transition-colors"
|
|
1037
|
+
onclick={() => {
|
|
1038
|
+
navigator.clipboard.writeText(linkPreviewHref);
|
|
1039
|
+
}}
|
|
1040
|
+
title="Copy URL"
|
|
1041
|
+
>
|
|
1042
|
+
<Copy class="h-3.5 w-3.5" />
|
|
1043
|
+
</button>
|
|
1044
|
+
<button
|
|
1045
|
+
type="button"
|
|
1046
|
+
class="text-muted-foreground hover:text-foreground rounded p-0.5 transition-colors"
|
|
1047
|
+
onclick={openLinkEdit}
|
|
1048
|
+
title="Edit link"
|
|
1049
|
+
>
|
|
1050
|
+
<Pencil class="h-3.5 w-3.5" />
|
|
1051
|
+
</button>
|
|
1052
|
+
<button
|
|
1053
|
+
type="button"
|
|
1054
|
+
class="text-muted-foreground hover:text-destructive rounded p-0.5 transition-colors"
|
|
1055
|
+
onclick={removeLink}
|
|
1056
|
+
title="Remove link"
|
|
1057
|
+
>
|
|
1058
|
+
<Unlink class="h-3.5 w-3.5" />
|
|
1059
|
+
</button>
|
|
1060
|
+
</div>
|
|
1061
|
+
{:else if linkMode === 'edit'}
|
|
1062
|
+
<div
|
|
1063
|
+
class="border-rule bg-popover absolute z-40 flex items-center gap-2 rounded-md border px-3 py-2 shadow-lg"
|
|
1064
|
+
style="top: {linkPopoverPos.top}px; left: {linkPopoverPos.left}px;"
|
|
1065
|
+
>
|
|
1066
|
+
<LinkIcon class="text-muted-foreground h-4 w-4 shrink-0" />
|
|
1067
|
+
<input
|
|
1068
|
+
bind:this={linkInputRef}
|
|
1069
|
+
type="text"
|
|
1070
|
+
class="text-foreground placeholder:text-muted-foreground w-52 bg-transparent text-sm outline-none"
|
|
1071
|
+
placeholder="example.com"
|
|
1072
|
+
bind:value={linkUrl}
|
|
1073
|
+
onkeydown={handleLinkKeydown}
|
|
1074
|
+
/>
|
|
1075
|
+
<button
|
|
1076
|
+
type="button"
|
|
1077
|
+
class="bg-primary text-primary-foreground rounded px-2.5 py-1 text-xs font-medium whitespace-nowrap transition-colors"
|
|
1078
|
+
onclick={applyLink}
|
|
1079
|
+
>
|
|
1080
|
+
{editor?.isActive('link') ? 'Update' : 'Apply'}
|
|
1081
|
+
</button>
|
|
1082
|
+
{#if editor?.isActive('link')}
|
|
1083
|
+
<button
|
|
1084
|
+
type="button"
|
|
1085
|
+
class="text-muted-foreground hover:text-destructive rounded p-1 transition-colors"
|
|
1086
|
+
onclick={removeLink}
|
|
1087
|
+
title="Remove link"
|
|
1088
|
+
>
|
|
1089
|
+
<Unlink class="h-3.5 w-3.5" />
|
|
1090
|
+
</button>
|
|
1091
|
+
{/if}
|
|
1092
|
+
<button
|
|
1093
|
+
type="button"
|
|
1094
|
+
class="text-muted-foreground hover:text-foreground rounded p-1 transition-colors"
|
|
1095
|
+
onclick={closeLinkPopover}
|
|
1096
|
+
title="Cancel"
|
|
1097
|
+
>
|
|
1098
|
+
<X class="h-3.5 w-3.5" />
|
|
1099
|
+
</button>
|
|
1100
|
+
</div>
|
|
1101
|
+
{/if}
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
{#if modalOpen && editingSchema}
|
|
1105
|
+
<ObjectModal
|
|
1106
|
+
open={modalOpen}
|
|
1107
|
+
schema={editingSchema}
|
|
1108
|
+
value={editingValue}
|
|
1109
|
+
onClose={handleModalClose}
|
|
1110
|
+
onUpdate={handleModalUpdate}
|
|
1111
|
+
{onOpenReference}
|
|
1112
|
+
{readonly}
|
|
1113
|
+
{organizationId}
|
|
1114
|
+
/>
|
|
1115
|
+
{/if}
|
|
1116
|
+
|
|
1117
|
+
{#if annotationModalOpen && annotationDef}
|
|
1118
|
+
<ObjectModal
|
|
1119
|
+
open={annotationModalOpen}
|
|
1120
|
+
schema={{
|
|
1121
|
+
type: 'object',
|
|
1122
|
+
name: annotationDef.name,
|
|
1123
|
+
title: annotationDef.title,
|
|
1124
|
+
fields: annotationDef.fields
|
|
1125
|
+
}}
|
|
1126
|
+
value={annotationValue}
|
|
1127
|
+
onClose={handleAnnotationClose}
|
|
1128
|
+
onUpdate={handleAnnotationUpdate}
|
|
1129
|
+
{onOpenReference}
|
|
1130
|
+
{readonly}
|
|
1131
|
+
{organizationId}
|
|
1132
|
+
/>
|
|
1133
|
+
{/if}
|
|
1134
|
+
|
|
1135
|
+
<AssetBrowserModal
|
|
1136
|
+
bind:open={showAssetBrowser}
|
|
1137
|
+
onOpenChange={(v) => (showAssetBrowser = v)}
|
|
1138
|
+
assetTypeFilter="image"
|
|
1139
|
+
onSelect={handleAssetSelected}
|
|
1140
|
+
/>
|
|
1141
|
+
|
|
1142
|
+
<style>
|
|
1143
|
+
.richtext-editor {
|
|
1144
|
+
position: relative;
|
|
1145
|
+
border: 1px solid var(--border);
|
|
1146
|
+
border-radius: var(--radius);
|
|
1147
|
+
overflow: visible;
|
|
1148
|
+
display: flex;
|
|
1149
|
+
flex-direction: column;
|
|
1150
|
+
min-height: 0;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.richtext-expanded {
|
|
1154
|
+
border: none;
|
|
1155
|
+
border-radius: 0;
|
|
1156
|
+
background: var(--background);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.richtext-content {
|
|
1160
|
+
flex: 1;
|
|
1161
|
+
display: flex;
|
|
1162
|
+
flex-direction: column;
|
|
1163
|
+
min-height: 0;
|
|
1164
|
+
overflow: auto;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.richtext-content :global(.tiptap) {
|
|
1168
|
+
padding: 0.75rem 1rem;
|
|
1169
|
+
min-height: 150px;
|
|
1170
|
+
flex: 1;
|
|
1171
|
+
outline: none;
|
|
1172
|
+
font-size: 1rem;
|
|
1173
|
+
line-height: 1.7;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.richtext-content :global(.tiptap p) {
|
|
1177
|
+
margin: 0.5em 0;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.richtext-content :global(.tiptap p:first-child) {
|
|
1181
|
+
margin-top: 0;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.richtext-content :global(.tiptap p:last-child) {
|
|
1185
|
+
margin-bottom: 0;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.richtext-content :global(.tiptap h1) {
|
|
1189
|
+
font-size: 1.875rem;
|
|
1190
|
+
font-weight: 700;
|
|
1191
|
+
margin: 0.75em 0 0.25em;
|
|
1192
|
+
line-height: 1.2;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.richtext-content :global(.tiptap h2) {
|
|
1196
|
+
font-size: 1.5rem;
|
|
1197
|
+
font-weight: 600;
|
|
1198
|
+
margin: 0.75em 0 0.25em;
|
|
1199
|
+
line-height: 1.3;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.richtext-content :global(.tiptap h3) {
|
|
1203
|
+
font-size: 1.25rem;
|
|
1204
|
+
font-weight: 600;
|
|
1205
|
+
margin: 0.75em 0 0.25em;
|
|
1206
|
+
line-height: 1.4;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.richtext-content :global(.tiptap h4) {
|
|
1210
|
+
font-size: 1.125rem;
|
|
1211
|
+
font-weight: 600;
|
|
1212
|
+
margin: 0.75em 0 0.25em;
|
|
1213
|
+
line-height: 1.4;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.richtext-content :global(.tiptap h5) {
|
|
1217
|
+
font-size: 1rem;
|
|
1218
|
+
font-weight: 600;
|
|
1219
|
+
margin: 0.75em 0 0.25em;
|
|
1220
|
+
line-height: 1.5;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.richtext-content :global(.tiptap h6) {
|
|
1224
|
+
font-size: 0.875rem;
|
|
1225
|
+
font-weight: 600;
|
|
1226
|
+
margin: 0.75em 0 0.25em;
|
|
1227
|
+
line-height: 1.5;
|
|
1228
|
+
text-transform: uppercase;
|
|
1229
|
+
letter-spacing: 0.05em;
|
|
1230
|
+
color: var(--muted-foreground);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.richtext-content :global(.tiptap blockquote) {
|
|
1234
|
+
border-left: 3px solid var(--border);
|
|
1235
|
+
padding-left: 1rem;
|
|
1236
|
+
margin: 0.75em 0;
|
|
1237
|
+
color: var(--muted-foreground);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
.richtext-content :global(.tiptap ul) {
|
|
1241
|
+
list-style: disc;
|
|
1242
|
+
padding-left: 1.5rem;
|
|
1243
|
+
margin: 0.5em 0;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.richtext-content :global(.tiptap ol) {
|
|
1247
|
+
list-style: decimal;
|
|
1248
|
+
padding-left: 1.5rem;
|
|
1249
|
+
margin: 0.5em 0;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.richtext-content :global(.tiptap li) {
|
|
1253
|
+
margin: 0.25em 0;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.richtext-content :global(.tiptap pre) {
|
|
1257
|
+
background: var(--muted);
|
|
1258
|
+
border-radius: 6px;
|
|
1259
|
+
padding: 0.75rem 1rem;
|
|
1260
|
+
margin: 0.75em 0;
|
|
1261
|
+
overflow-x: auto;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.richtext-content :global(.tiptap pre code) {
|
|
1265
|
+
background: none;
|
|
1266
|
+
padding: 0;
|
|
1267
|
+
border-radius: 0;
|
|
1268
|
+
font-size: 0.875em;
|
|
1269
|
+
font-family: var(--font-mono, monospace);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
.richtext-content :global(.tiptap code) {
|
|
1273
|
+
background: var(--muted);
|
|
1274
|
+
border-radius: 3px;
|
|
1275
|
+
padding: 0.15em 0.3em;
|
|
1276
|
+
font-size: 0.85em;
|
|
1277
|
+
font-family: var(--font-mono, monospace);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.richtext-content :global(.tiptap a),
|
|
1281
|
+
.richtext-content :global(.tiptap .richtext-link) {
|
|
1282
|
+
color: var(--primary);
|
|
1283
|
+
text-decoration: underline;
|
|
1284
|
+
cursor: pointer;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.richtext-content :global(.tiptap strong) {
|
|
1288
|
+
font-weight: 600;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.richtext-content :global(.tiptap em) {
|
|
1292
|
+
font-style: italic;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.richtext-content :global(.tiptap s) {
|
|
1296
|
+
text-decoration: line-through;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.richtext-content :global(.tiptap u) {
|
|
1300
|
+
text-decoration: underline;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.richtext-content :global(.tiptap [data-portable-text-object]) {
|
|
1304
|
+
margin: 0.5em 0;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
.richtext-content :global(.tiptap .ProseMirror-selectednode [data-portable-text-object] > div) {
|
|
1308
|
+
outline: 2px solid var(--primary);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.richtext-content :global(.tiptap [data-portable-text-inline]) {
|
|
1312
|
+
display: inline;
|
|
1313
|
+
}
|
|
1314
|
+
</style>
|