@intinyagroup/rich-text 0.1.1-alpha.7 → 0.1.1
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.
|
@@ -1,46 +1,92 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount, onDestroy } from
|
|
3
|
-
import { Editor, Node, mergeAttributes } from
|
|
4
|
-
import StarterKit from
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
2
|
+
import { onMount, onDestroy } from "svelte";
|
|
3
|
+
import { Editor, Mark, Node, mergeAttributes } from "@tiptap/core";
|
|
4
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
5
|
+
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|
6
|
+
import { common, createLowlight } from "lowlight";
|
|
7
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
8
|
+
import Underline from "@tiptap/extension-underline";
|
|
9
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
10
|
+
import Link from "@tiptap/extension-link";
|
|
11
|
+
import Image from "@tiptap/extension-image";
|
|
12
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
13
|
+
import { Table } from "@tiptap/extension-table";
|
|
14
|
+
import TableRow from "@tiptap/extension-table-row";
|
|
15
|
+
import TableCell from "@tiptap/extension-table-cell";
|
|
16
|
+
import TableHeader from "@tiptap/extension-table-header";
|
|
17
|
+
import Typography from "@tiptap/extension-typography";
|
|
18
|
+
import BubbleMenu from "@tiptap/extension-bubble-menu";
|
|
19
|
+
import TaskList from "@tiptap/extension-task-list";
|
|
20
|
+
import TaskItem from "@tiptap/extension-task-item";
|
|
17
21
|
import {
|
|
18
|
-
Bold,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
Bold,
|
|
23
|
+
Italic,
|
|
24
|
+
Underline as UnderlineIcon,
|
|
25
|
+
Strikethrough,
|
|
26
|
+
Highlighter,
|
|
27
|
+
AlignLeft,
|
|
28
|
+
AlignCenter,
|
|
29
|
+
AlignRight,
|
|
30
|
+
AlignJustify,
|
|
31
|
+
List,
|
|
32
|
+
ListOrdered,
|
|
33
|
+
Quote,
|
|
34
|
+
Code,
|
|
35
|
+
Minus,
|
|
36
|
+
Link as LinkIcon,
|
|
37
|
+
Image as ImageIcon,
|
|
38
|
+
Undo,
|
|
39
|
+
Redo,
|
|
40
|
+
Heading1,
|
|
41
|
+
Heading2,
|
|
42
|
+
Heading3,
|
|
43
|
+
TableIcon,
|
|
44
|
+
Plus,
|
|
45
|
+
Trash2,
|
|
22
46
|
Video,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
CheckSquare,
|
|
48
|
+
Info,
|
|
49
|
+
AlertTriangle,
|
|
50
|
+
Sparkles,
|
|
51
|
+
HelpCircle,
|
|
52
|
+
FileText,
|
|
53
|
+
Search,
|
|
54
|
+
Download,
|
|
55
|
+
Unlink,
|
|
56
|
+
} from "lucide-svelte";
|
|
57
|
+
import { Button, Input, Separator } from "@intinyagroup/ui";
|
|
58
|
+
import { cn } from "@intinyagroup/grid-core/utils";
|
|
59
|
+
|
|
60
|
+
const lowlight = createLowlight(common);
|
|
26
61
|
|
|
27
62
|
let {
|
|
28
|
-
content =
|
|
29
|
-
placeholder =
|
|
63
|
+
content = "",
|
|
64
|
+
placeholder = "Start writing...",
|
|
30
65
|
editable = true,
|
|
66
|
+
mode = "classic",
|
|
31
67
|
height = 400,
|
|
32
68
|
class: className,
|
|
33
69
|
onUpdate,
|
|
34
70
|
onImageUpload,
|
|
71
|
+
onOpenSubPage,
|
|
72
|
+
onExportHtml,
|
|
73
|
+
onExportMarkdown,
|
|
35
74
|
}: {
|
|
36
75
|
content?: string;
|
|
37
76
|
placeholder?: string;
|
|
38
77
|
editable?: boolean;
|
|
78
|
+
/** Toolbar mode: 'classic' (fixed top toolbar), 'bubble' (Notion-style floating toolbar only), or 'none' */
|
|
79
|
+
mode?: "classic" | "bubble" | "none";
|
|
39
80
|
height?: number;
|
|
40
81
|
class?: string;
|
|
41
82
|
onUpdate?: (html: string) => void;
|
|
42
83
|
/** Called when paste/drop provides an image file. Return a URL to insert. */
|
|
43
84
|
onImageUpload?: (file: File) => Promise<string>;
|
|
85
|
+
onOpenSubPage?: (page: { id: string; title: string }) => void;
|
|
86
|
+
/** Called by HTML export action with current document HTML. */
|
|
87
|
+
onExportHtml?: (html: string) => void;
|
|
88
|
+
/** Called by Markdown export action with best-effort CommonMark output. */
|
|
89
|
+
onExportMarkdown?: (markdown: string) => void;
|
|
44
90
|
} = $props();
|
|
45
91
|
|
|
46
92
|
let editorEl: HTMLDivElement | null = null;
|
|
@@ -48,7 +94,146 @@
|
|
|
48
94
|
let editor: Editor | null = null;
|
|
49
95
|
let isActive = $state<Record<string, boolean>>({});
|
|
50
96
|
let uploadingCount = $state(0);
|
|
97
|
+
let showFindReplace = $state(false);
|
|
98
|
+
let findQuery = $state("");
|
|
99
|
+
let replaceQuery = $state("");
|
|
100
|
+
let findCount = $state(0);
|
|
101
|
+
|
|
102
|
+
const TextColor = Mark.create({
|
|
103
|
+
name: "textColor",
|
|
104
|
+
addAttributes() {
|
|
105
|
+
return { color: { default: null } };
|
|
106
|
+
},
|
|
107
|
+
parseHTML() {
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
tag: "span[data-text-color]",
|
|
111
|
+
getAttrs: (element) => ({
|
|
112
|
+
color:
|
|
113
|
+
(element as HTMLElement).style.color ||
|
|
114
|
+
element.getAttribute("color") ||
|
|
115
|
+
null,
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
},
|
|
120
|
+
renderHTML({ HTMLAttributes }) {
|
|
121
|
+
const color = HTMLAttributes.color;
|
|
122
|
+
return [
|
|
123
|
+
"span",
|
|
124
|
+
mergeAttributes({
|
|
125
|
+
"data-text-color": "",
|
|
126
|
+
style: color ? `color: ${color}` : undefined,
|
|
127
|
+
}),
|
|
128
|
+
0,
|
|
129
|
+
];
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Slash commands state
|
|
134
|
+
let showSlashMenu = $state(false);
|
|
135
|
+
let slashSearch = $state("");
|
|
136
|
+
let slashIndex = $state(0);
|
|
137
|
+
let slashMenuPos = $state({ top: 0, left: 0 });
|
|
138
|
+
|
|
139
|
+
let textColor = $state("#1f2937");
|
|
140
|
+
let highlightColor = $state("#fef08a");
|
|
141
|
+
let searchMatches: Array<{ from: number; to: number }> = [];
|
|
51
142
|
|
|
143
|
+
const slashCommands = [
|
|
144
|
+
{
|
|
145
|
+
title: "Sub-page",
|
|
146
|
+
desc: "Embed a sub-page inside this page",
|
|
147
|
+
icon: FileText,
|
|
148
|
+
action: () => insertSubPage(),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
title: "Heading 1",
|
|
152
|
+
desc: "Big section heading",
|
|
153
|
+
icon: Heading1,
|
|
154
|
+
action: () => setHeading(1),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: "Heading 2",
|
|
158
|
+
desc: "Medium section heading",
|
|
159
|
+
icon: Heading2,
|
|
160
|
+
action: () => setHeading(2),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
title: "Heading 3",
|
|
164
|
+
desc: "Small subsection heading",
|
|
165
|
+
icon: Heading3,
|
|
166
|
+
action: () => setHeading(3),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
title: "To-do list",
|
|
170
|
+
desc: "Track tasks with a checklist",
|
|
171
|
+
icon: CheckSquare,
|
|
172
|
+
action: () => editor?.chain().focus().toggleTaskList().run(),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
title: "Bullet list",
|
|
176
|
+
desc: "Create a bulleted list",
|
|
177
|
+
icon: List,
|
|
178
|
+
action: () => toggleBulletList(),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
title: "Numbered list",
|
|
182
|
+
desc: "Create a numbered list",
|
|
183
|
+
icon: ListOrdered,
|
|
184
|
+
action: () => toggleOrderedList(),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: "Quote",
|
|
188
|
+
desc: "Capture a quote or blockquote",
|
|
189
|
+
icon: Quote,
|
|
190
|
+
action: () => toggleBlockquote(),
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
title: "Code block",
|
|
194
|
+
desc: "Code snippet with syntax highlight",
|
|
195
|
+
icon: Code,
|
|
196
|
+
action: () => toggleCodeBlock(),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
title: "Callout Info",
|
|
200
|
+
desc: "Informational highlight block",
|
|
201
|
+
icon: Info,
|
|
202
|
+
action: () => insertCallout("info"),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
title: "Callout Warning",
|
|
206
|
+
desc: "Warning or caution block",
|
|
207
|
+
icon: AlertTriangle,
|
|
208
|
+
action: () => insertCallout("warning"),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
title: "Callout Tip",
|
|
212
|
+
desc: "Tip or recommendation block",
|
|
213
|
+
icon: Sparkles,
|
|
214
|
+
action: () => insertCallout("tip"),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
title: "Table",
|
|
218
|
+
desc: "Insert 3x3 table grid",
|
|
219
|
+
icon: TableIcon,
|
|
220
|
+
action: () => insertTable(),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
title: "Divider",
|
|
224
|
+
desc: "Visually divide sections",
|
|
225
|
+
icon: Minus,
|
|
226
|
+
action: () => insertHorizontalRule(),
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const slashFilteredCommands = $derived(
|
|
231
|
+
slashCommands.filter(
|
|
232
|
+
(c) =>
|
|
233
|
+
c.title.toLowerCase().includes(slashSearch.toLowerCase()) ||
|
|
234
|
+
c.desc.toLowerCase().includes(slashSearch.toLowerCase()),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
52
237
|
// ---------------------------------------------------------------------------
|
|
53
238
|
// Custom Extensions
|
|
54
239
|
// ---------------------------------------------------------------------------
|
|
@@ -59,16 +244,19 @@
|
|
|
59
244
|
return {
|
|
60
245
|
...this.parent?.(),
|
|
61
246
|
href: { default: null },
|
|
62
|
-
alt: { default:
|
|
63
|
-
title: { default:
|
|
64
|
-
target: { default:
|
|
247
|
+
alt: { default: "" },
|
|
248
|
+
title: { default: "" },
|
|
249
|
+
target: { default: "_blank" },
|
|
65
250
|
};
|
|
66
251
|
},
|
|
67
252
|
renderHTML({ HTMLAttributes }) {
|
|
68
253
|
const { href, target, alt, title, ...rest } = HTMLAttributes;
|
|
69
|
-
const img = [
|
|
254
|
+
const img = [
|
|
255
|
+
"img",
|
|
256
|
+
mergeAttributes(this.options.HTMLAttributes, rest, { alt, title }),
|
|
257
|
+
];
|
|
70
258
|
if (href) {
|
|
71
|
-
return [
|
|
259
|
+
return ["a", { href, target, class: "image-link" }, img];
|
|
72
260
|
}
|
|
73
261
|
return img;
|
|
74
262
|
},
|
|
@@ -76,8 +264,8 @@
|
|
|
76
264
|
|
|
77
265
|
/** YouTube iframe embed node */
|
|
78
266
|
const CustomYoutube = Node.create({
|
|
79
|
-
name:
|
|
80
|
-
group:
|
|
267
|
+
name: "youtube",
|
|
268
|
+
group: "block",
|
|
81
269
|
atom: true,
|
|
82
270
|
addAttributes() {
|
|
83
271
|
return { src: { default: null } };
|
|
@@ -90,14 +278,14 @@
|
|
|
90
278
|
},
|
|
91
279
|
renderHTML({ HTMLAttributes }) {
|
|
92
280
|
return [
|
|
93
|
-
|
|
94
|
-
{ class:
|
|
281
|
+
"div",
|
|
282
|
+
{ class: "video-wrapper" },
|
|
95
283
|
[
|
|
96
|
-
|
|
284
|
+
"iframe",
|
|
97
285
|
mergeAttributes(HTMLAttributes, {
|
|
98
|
-
width:
|
|
99
|
-
height:
|
|
100
|
-
allowfullscreen:
|
|
286
|
+
width: "100%",
|
|
287
|
+
height: "315",
|
|
288
|
+
allowfullscreen: "true",
|
|
101
289
|
}),
|
|
102
290
|
],
|
|
103
291
|
];
|
|
@@ -116,8 +304,8 @@
|
|
|
116
304
|
reader.onload = (e) => {
|
|
117
305
|
const img = new Image();
|
|
118
306
|
img.onload = () => {
|
|
119
|
-
const canvas = document.createElement(
|
|
120
|
-
const ctx = canvas.getContext(
|
|
307
|
+
const canvas = document.createElement("canvas");
|
|
308
|
+
const ctx = canvas.getContext("2d");
|
|
121
309
|
if (!ctx) {
|
|
122
310
|
resolve(file);
|
|
123
311
|
return;
|
|
@@ -143,13 +331,13 @@
|
|
|
143
331
|
canvas.toBlob(
|
|
144
332
|
(blob) => {
|
|
145
333
|
if (blob) {
|
|
146
|
-
resolve(new File([blob], file.name, { type:
|
|
334
|
+
resolve(new File([blob], file.name, { type: "image/jpeg" }));
|
|
147
335
|
} else {
|
|
148
336
|
resolve(file);
|
|
149
337
|
}
|
|
150
338
|
},
|
|
151
|
-
|
|
152
|
-
0.85
|
|
339
|
+
"image/jpeg",
|
|
340
|
+
0.85,
|
|
153
341
|
);
|
|
154
342
|
};
|
|
155
343
|
img.src = e.target?.result as string;
|
|
@@ -172,34 +360,95 @@
|
|
|
172
360
|
src = await onImageUpload(compressed);
|
|
173
361
|
} else {
|
|
174
362
|
// Fallback: inline data URL (no server upload)
|
|
175
|
-
src = await
|
|
176
|
-
const reader = new FileReader();
|
|
177
|
-
reader.onload = (ev) => resolve(ev.target?.result as string);
|
|
178
|
-
reader.readAsDataURL(file);
|
|
179
|
-
});
|
|
363
|
+
src = await fileToDataUrl(file);
|
|
180
364
|
}
|
|
181
365
|
editor.chain().focus().setImage({ src }).run();
|
|
182
|
-
} catch (err) {
|
|
183
|
-
console.error('Failed to upload image:', err);
|
|
184
366
|
} finally {
|
|
185
367
|
uploadingCount--;
|
|
186
368
|
}
|
|
187
369
|
}
|
|
188
370
|
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
371
|
+
function handleFiles(files: File[]) {
|
|
372
|
+
for (const file of files) {
|
|
373
|
+
if (file.type.startsWith("image/")) {
|
|
374
|
+
uploadAndInsertImage(file);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
192
377
|
}
|
|
193
378
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
379
|
+
/** SubPage block extension (Page-in-page card) */
|
|
380
|
+
const SubPage = Node.create({
|
|
381
|
+
name: "subpage",
|
|
382
|
+
group: "block",
|
|
383
|
+
atom: true,
|
|
384
|
+
addAttributes() {
|
|
385
|
+
return {
|
|
386
|
+
id: { default: () => `page-${Date.now()}` },
|
|
387
|
+
title: { default: "Untitled Sub-page" },
|
|
388
|
+
icon: { default: "📄" },
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
parseHTML() {
|
|
392
|
+
return [{ tag: 'div[data-type="subpage"]' }];
|
|
393
|
+
},
|
|
394
|
+
renderHTML({ HTMLAttributes }) {
|
|
395
|
+
return [
|
|
396
|
+
"div",
|
|
397
|
+
mergeAttributes(HTMLAttributes, {
|
|
398
|
+
"data-type": "subpage",
|
|
399
|
+
class:
|
|
400
|
+
"subpage-block my-2 flex items-center gap-2.5 px-3 py-2 rounded-lg border border-[var(--ui-border)] bg-[var(--ui-card)] hover:bg-[var(--ui-secondary)]/50 transition-colors cursor-pointer shadow-xs group",
|
|
401
|
+
"data-page-id": HTMLAttributes.id,
|
|
402
|
+
"data-page-title": HTMLAttributes.title,
|
|
403
|
+
}),
|
|
404
|
+
[
|
|
405
|
+
"span",
|
|
406
|
+
{ class: "text-base shrink-0 select-none" },
|
|
407
|
+
HTMLAttributes.icon || "📄",
|
|
408
|
+
],
|
|
409
|
+
[
|
|
410
|
+
"span",
|
|
411
|
+
{
|
|
412
|
+
class:
|
|
413
|
+
"text-sm font-semibold text-[var(--ui-foreground)] underline-offset-4 group-hover:underline truncate",
|
|
414
|
+
},
|
|
415
|
+
HTMLAttributes.title || "Untitled Sub-page",
|
|
416
|
+
],
|
|
417
|
+
];
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
/** Callout block extension with type icon & colored border */
|
|
422
|
+
const Callout = Node.create({
|
|
423
|
+
name: "callout",
|
|
424
|
+
group: "block",
|
|
425
|
+
content: "block+",
|
|
426
|
+
defining: true,
|
|
427
|
+
addAttributes() {
|
|
428
|
+
return {
|
|
429
|
+
type: { default: "info" }, // info | warning | tip
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
parseHTML() {
|
|
433
|
+
return [{ tag: 'div[data-type="callout"]' }];
|
|
434
|
+
},
|
|
435
|
+
renderHTML({ HTMLAttributes }) {
|
|
436
|
+
return [
|
|
437
|
+
"div",
|
|
438
|
+
mergeAttributes(HTMLAttributes, {
|
|
439
|
+
"data-type": "callout",
|
|
440
|
+
class: `callout-box callout-${HTMLAttributes.type || "info"} my-3 p-3.5 rounded-xl border flex gap-3`,
|
|
441
|
+
}),
|
|
442
|
+
0,
|
|
443
|
+
];
|
|
444
|
+
},
|
|
445
|
+
});
|
|
197
446
|
|
|
198
447
|
function addYoutube() {
|
|
199
|
-
const url = window.prompt(
|
|
448
|
+
const url = window.prompt("Enter YouTube URL:");
|
|
200
449
|
if (!url || !editor) return;
|
|
201
450
|
const match = url.match(
|
|
202
|
-
/^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|shorts\/)([^#&?]*)
|
|
451
|
+
/^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|shorts\/)([^#&?]*).*/,
|
|
203
452
|
);
|
|
204
453
|
const id = match && match[2].length === 11 ? match[2] : null;
|
|
205
454
|
if (id) {
|
|
@@ -207,7 +456,7 @@
|
|
|
207
456
|
.chain()
|
|
208
457
|
.focus()
|
|
209
458
|
.insertContent({
|
|
210
|
-
type:
|
|
459
|
+
type: "youtube",
|
|
211
460
|
attrs: { src: `https://www.youtube.com/embed/${id}` },
|
|
212
461
|
})
|
|
213
462
|
.run();
|
|
@@ -220,66 +469,257 @@
|
|
|
220
469
|
|
|
221
470
|
function getActiveStates(e: Editor): Record<string, boolean> {
|
|
222
471
|
return {
|
|
223
|
-
bold: e.isActive(
|
|
224
|
-
italic: e.isActive(
|
|
225
|
-
underline: e.isActive(
|
|
226
|
-
strike: e.isActive(
|
|
227
|
-
highlight: e.isActive(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
472
|
+
bold: e.isActive("bold"),
|
|
473
|
+
italic: e.isActive("italic"),
|
|
474
|
+
underline: e.isActive("underline"),
|
|
475
|
+
strike: e.isActive("strike"),
|
|
476
|
+
highlight: e.isActive("highlight"),
|
|
477
|
+
textColor: e.isActive("textColor"),
|
|
478
|
+
h1: e.isActive("heading", { level: 1 }),
|
|
479
|
+
h2: e.isActive("heading", { level: 2 }),
|
|
480
|
+
h3: e.isActive("heading", { level: 3 }),
|
|
481
|
+
bulletList: e.isActive("bulletList"),
|
|
482
|
+
orderedList: e.isActive("orderedList"),
|
|
483
|
+
blockquote: e.isActive("blockquote"),
|
|
484
|
+
codeBlock: e.isActive("codeBlock"),
|
|
485
|
+
alignLeft: e.isActive({ textAlign: "left" }),
|
|
486
|
+
alignCenter: e.isActive({ textAlign: "center" }),
|
|
487
|
+
alignRight: e.isActive({ textAlign: "right" }),
|
|
488
|
+
alignJustify: e.isActive({ textAlign: "justify" }),
|
|
489
|
+
link: e.isActive("link"),
|
|
240
490
|
};
|
|
241
491
|
}
|
|
242
492
|
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
function toggleStrike() { editor?.chain().focus().toggleStrike().run(); }
|
|
247
|
-
function setHeading(level: 1 | 2 | 3) { editor?.chain().focus().toggleHeading({ level }).run(); }
|
|
248
|
-
function toggleBulletList() { editor?.chain().focus().toggleBulletList().run(); }
|
|
249
|
-
function toggleOrderedList() { editor?.chain().focus().toggleOrderedList().run(); }
|
|
250
|
-
function toggleBlockquote() { editor?.chain().focus().toggleBlockquote().run(); }
|
|
251
|
-
function toggleCodeBlock() { editor?.chain().focus().toggleCodeBlock().run(); }
|
|
252
|
-
function setAlign(align: 'left' | 'center' | 'right' | 'justify') {
|
|
253
|
-
editor?.chain().focus().setTextAlign(align).run();
|
|
493
|
+
function setTextColor(color: string) {
|
|
494
|
+
textColor = color;
|
|
495
|
+
editor?.chain().focus().setMark("textColor", { color }).run();
|
|
254
496
|
}
|
|
255
|
-
function
|
|
256
|
-
|
|
257
|
-
|
|
497
|
+
function toggleTextColor() {
|
|
498
|
+
if (editor?.isActive("textColor"))
|
|
499
|
+
editor.chain().focus().unsetMark("textColor").run();
|
|
500
|
+
else setTextColor(textColor);
|
|
501
|
+
}
|
|
502
|
+
function toggleHighlight(color = highlightColor) {
|
|
503
|
+
highlightColor = color;
|
|
504
|
+
editor?.chain().focus().toggleHighlight({ color }).run();
|
|
505
|
+
}
|
|
506
|
+
function updateSearchMatches() {
|
|
507
|
+
searchMatches = [];
|
|
508
|
+
if (!editor || !findQuery) {
|
|
509
|
+
findCount = 0;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const query = findQuery.toLocaleLowerCase();
|
|
513
|
+
editor.state.doc.descendants((node, pos) => {
|
|
514
|
+
if (!node.isText || !node.text) return;
|
|
515
|
+
let index = 0;
|
|
516
|
+
const text = node.text.toLocaleLowerCase();
|
|
517
|
+
while ((index = text.indexOf(query, index)) !== -1) {
|
|
518
|
+
searchMatches.push({
|
|
519
|
+
from: pos + 1 + index,
|
|
520
|
+
to: pos + 1 + index + query.length,
|
|
521
|
+
});
|
|
522
|
+
index += query.length;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
findCount = searchMatches.length;
|
|
526
|
+
}
|
|
527
|
+
function findNext() {
|
|
528
|
+
updateSearchMatches();
|
|
529
|
+
if (!editor || !searchMatches.length) return;
|
|
530
|
+
const current = editor.state.selection.from;
|
|
531
|
+
const next =
|
|
532
|
+
searchMatches.find((match) => match.from > current) ?? searchMatches[0];
|
|
533
|
+
editor.chain().focus().setTextSelection(next).run();
|
|
534
|
+
}
|
|
535
|
+
function replaceCurrent() {
|
|
536
|
+
if (!editor || !findQuery) return;
|
|
537
|
+
const current = editor.state.selection;
|
|
538
|
+
if (
|
|
539
|
+
current.from !== current.to &&
|
|
540
|
+
editor.state.doc.textBetween(current.from, current.to) === findQuery
|
|
541
|
+
) {
|
|
542
|
+
editor
|
|
543
|
+
.chain()
|
|
544
|
+
.focus()
|
|
545
|
+
.insertContentAt({ from: current.from, to: current.to }, replaceQuery)
|
|
546
|
+
.run();
|
|
547
|
+
} else findNext();
|
|
548
|
+
updateSearchMatches();
|
|
549
|
+
}
|
|
550
|
+
function replaceAll() {
|
|
551
|
+
updateSearchMatches();
|
|
552
|
+
if (!editor || !searchMatches.length) return;
|
|
553
|
+
const transaction = editor.state.tr;
|
|
554
|
+
for (const match of [...searchMatches].reverse())
|
|
555
|
+
transaction.insertText(replaceQuery, match.from, match.to);
|
|
556
|
+
editor.view.dispatch(transaction);
|
|
557
|
+
updateSearchMatches();
|
|
558
|
+
}
|
|
559
|
+
function markdownFromHtml(html: string) {
|
|
560
|
+
return html
|
|
561
|
+
.replace(/<br\s*\/?>(\n)?/gi, "\n")
|
|
562
|
+
.replace(
|
|
563
|
+
/<h([1-6])>(.*?)<\/h\1>/gi,
|
|
564
|
+
(_m, level, text) => `${"#".repeat(Number(level))} ${text}\n\n`,
|
|
565
|
+
)
|
|
566
|
+
.replace(/<strong>(.*?)<\/strong>/gi, "**$1**")
|
|
567
|
+
.replace(/<em>(.*?)<\/em>/gi, "_$1_")
|
|
568
|
+
.replace(/<u>(.*?)<\/u>/gi, "__$1__")
|
|
569
|
+
.replace(/<s>(.*?)<\/s>/gi, "~~$1~~")
|
|
570
|
+
.replace(/<a[^>]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)")
|
|
571
|
+
.replace(/<li>(.*?)<\/li>/gi, "- $1\n")
|
|
572
|
+
.replace(/<p>(.*?)<\/p>/gi, "$1\n\n")
|
|
573
|
+
.replace(/<hr\s*\/?>(\n)?/gi, "---\n\n")
|
|
574
|
+
.replace(/<[^>]+>/g, "")
|
|
575
|
+
.replace(/ /g, " ")
|
|
576
|
+
.replace(/&/g, "&")
|
|
577
|
+
.replace(/</g, "<")
|
|
578
|
+
.replace(/>/g, ">")
|
|
579
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
580
|
+
.trim();
|
|
581
|
+
}
|
|
582
|
+
function downloadText(filename: string, content: string, type: string) {
|
|
583
|
+
const url = URL.createObjectURL(new Blob([content], { type }));
|
|
584
|
+
const link = document.createElement("a");
|
|
585
|
+
link.href = url;
|
|
586
|
+
link.download = filename;
|
|
587
|
+
link.click();
|
|
588
|
+
URL.revokeObjectURL(url);
|
|
589
|
+
}
|
|
590
|
+
function exportHtml() {
|
|
591
|
+
if (!editor) return;
|
|
592
|
+
const html = editor.getHTML();
|
|
593
|
+
onExportHtml?.(html);
|
|
594
|
+
downloadText("document.html", html, "text/html;charset=utf-8");
|
|
595
|
+
}
|
|
596
|
+
function exportMarkdown() {
|
|
597
|
+
if (!editor) return;
|
|
598
|
+
const markdown = markdownFromHtml(editor.getHTML());
|
|
599
|
+
onExportMarkdown?.(markdown);
|
|
600
|
+
downloadText("document.md", markdown, "text/markdown;charset=utf-8");
|
|
601
|
+
}
|
|
602
|
+
function toggleBold() {
|
|
603
|
+
editor?.chain().focus().toggleBold().run();
|
|
604
|
+
}
|
|
605
|
+
function toggleItalic() {
|
|
606
|
+
editor?.chain().focus().toggleItalic().run();
|
|
607
|
+
}
|
|
608
|
+
function toggleUnderline() {
|
|
609
|
+
editor?.chain().focus().toggleUnderline().run();
|
|
610
|
+
}
|
|
611
|
+
function toggleStrike() {
|
|
612
|
+
editor?.chain().focus().toggleStrike().run();
|
|
613
|
+
}
|
|
614
|
+
function setHeading(level: 1 | 2 | 3) {
|
|
615
|
+
editor?.chain().focus().toggleHeading({ level }).run();
|
|
616
|
+
}
|
|
617
|
+
function toggleBulletList() {
|
|
618
|
+
editor?.chain().focus().toggleBulletList().run();
|
|
619
|
+
}
|
|
620
|
+
function toggleOrderedList() {
|
|
621
|
+
editor?.chain().focus().toggleOrderedList().run();
|
|
622
|
+
}
|
|
623
|
+
function toggleBlockquote() {
|
|
624
|
+
editor?.chain().focus().toggleBlockquote().run();
|
|
625
|
+
}
|
|
626
|
+
function toggleCodeBlock() {
|
|
627
|
+
editor?.chain().focus().toggleCodeBlock().run();
|
|
628
|
+
}
|
|
629
|
+
function setAlign(align: "left" | "center" | "right" | "justify") {
|
|
630
|
+
editor?.chain().focus().setTextAlign(align).run();
|
|
258
631
|
}
|
|
259
632
|
function setImage() {
|
|
260
|
-
const url = window.prompt(
|
|
633
|
+
const url = window.prompt("Enter image URL:");
|
|
261
634
|
if (url) editor?.chain().focus().setImage({ src: url }).run();
|
|
262
635
|
}
|
|
263
|
-
function insertHorizontalRule() {
|
|
636
|
+
function insertHorizontalRule() {
|
|
637
|
+
editor?.chain().focus().setHorizontalRule().run();
|
|
638
|
+
}
|
|
264
639
|
function insertTable() {
|
|
265
|
-
editor
|
|
640
|
+
editor
|
|
641
|
+
?.chain()
|
|
642
|
+
.focus()
|
|
643
|
+
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
644
|
+
.run();
|
|
645
|
+
}
|
|
646
|
+
function insertCallout(type: "info" | "warning" | "tip") {
|
|
647
|
+
editor
|
|
648
|
+
?.chain()
|
|
649
|
+
.focus()
|
|
650
|
+
.insertContent({
|
|
651
|
+
type: "callout",
|
|
652
|
+
attrs: { type },
|
|
653
|
+
content: [
|
|
654
|
+
{ type: "paragraph", text: "Tulis catatan penting di sini..." },
|
|
655
|
+
],
|
|
656
|
+
})
|
|
657
|
+
.run();
|
|
266
658
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
659
|
+
function insertSubPage() {
|
|
660
|
+
const title = window.prompt("Enter Sub-page title:") || "Untitled Sub-page";
|
|
661
|
+
editor
|
|
662
|
+
?.chain()
|
|
663
|
+
.focus()
|
|
664
|
+
.insertContent({
|
|
665
|
+
type: "subpage",
|
|
666
|
+
attrs: { id: `page-${Date.now()}`, title, icon: "📄" },
|
|
667
|
+
})
|
|
668
|
+
.run();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function executeSlashCommand(cmd: (typeof slashCommands)[0]) {
|
|
672
|
+
if (!editor) return;
|
|
673
|
+
const { from } = editor.state.selection;
|
|
674
|
+
const deleteFrom = from - (slashSearch.length + 1);
|
|
675
|
+
editor
|
|
676
|
+
.chain()
|
|
677
|
+
.focus()
|
|
678
|
+
.deleteRange({ from: Math.max(0, deleteFrom), to: from })
|
|
679
|
+
.run();
|
|
680
|
+
cmd.action();
|
|
681
|
+
showSlashMenu = false;
|
|
682
|
+
}
|
|
683
|
+
function setLink() {
|
|
684
|
+
if (!editor) return;
|
|
685
|
+
if (editor.isActive("link")) {
|
|
686
|
+
const current = editor.getAttributes("link").href || "";
|
|
687
|
+
const url = window.prompt("Edit URL:", current);
|
|
688
|
+
if (url === null) return;
|
|
689
|
+
if (!url.trim()) editor.chain().focus().unsetLink().run();
|
|
690
|
+
else editor.chain().focus().setLink({ href: url.trim() }).run();
|
|
691
|
+
return;
|
|
272
692
|
}
|
|
693
|
+
const url = window.prompt("Enter URL:");
|
|
694
|
+
if (url?.trim()) editor.chain().focus().setLink({ href: url.trim() }).run();
|
|
695
|
+
}
|
|
696
|
+
function addColumnBefore() {
|
|
697
|
+
editor?.chain().focus().addColumnBefore().run();
|
|
698
|
+
}
|
|
699
|
+
function addColumnAfter() {
|
|
700
|
+
editor?.chain().focus().addColumnAfter().run();
|
|
701
|
+
}
|
|
702
|
+
function deleteColumn() {
|
|
703
|
+
editor?.chain().focus().deleteColumn().run();
|
|
704
|
+
}
|
|
705
|
+
function addRowBefore() {
|
|
706
|
+
editor?.chain().focus().addRowBefore().run();
|
|
707
|
+
}
|
|
708
|
+
function addRowAfter() {
|
|
709
|
+
editor?.chain().focus().addRowAfter().run();
|
|
710
|
+
}
|
|
711
|
+
function deleteRow() {
|
|
712
|
+
editor?.chain().focus().deleteRow().run();
|
|
713
|
+
}
|
|
714
|
+
function deleteTable() {
|
|
715
|
+
editor?.chain().focus().deleteTable().run();
|
|
716
|
+
}
|
|
717
|
+
function undo() {
|
|
718
|
+
editor?.chain().focus().undo().run();
|
|
719
|
+
}
|
|
720
|
+
function redo() {
|
|
721
|
+
editor?.chain().focus().redo().run();
|
|
273
722
|
}
|
|
274
|
-
function addColumnBefore() { editor?.chain().focus().addColumnBefore().run(); }
|
|
275
|
-
function addColumnAfter() { editor?.chain().focus().addColumnAfter().run(); }
|
|
276
|
-
function deleteColumn() { editor?.chain().focus().deleteColumn().run(); }
|
|
277
|
-
function addRowBefore() { editor?.chain().focus().addRowBefore().run(); }
|
|
278
|
-
function addRowAfter() { editor?.chain().focus().addRowAfter().run(); }
|
|
279
|
-
function deleteRow() { editor?.chain().focus().deleteRow().run(); }
|
|
280
|
-
function deleteTable() { editor?.chain().focus().deleteTable().run(); }
|
|
281
|
-
function undo() { editor?.chain().focus().undo().run(); }
|
|
282
|
-
function redo() { editor?.chain().focus().redo().run(); }
|
|
283
723
|
|
|
284
724
|
// ---------------------------------------------------------------------------
|
|
285
725
|
// Lifecycle
|
|
@@ -291,15 +731,17 @@
|
|
|
291
731
|
editor = new Editor({
|
|
292
732
|
element: editorEl,
|
|
293
733
|
extensions: [
|
|
294
|
-
StarterKit,
|
|
734
|
+
StarterKit.configure({ codeBlock: false }),
|
|
735
|
+
CodeBlockLowlight.configure({ lowlight }),
|
|
295
736
|
Placeholder.configure({ placeholder }),
|
|
296
737
|
Underline,
|
|
297
|
-
|
|
738
|
+
TextColor,
|
|
739
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
298
740
|
Link.configure({ openOnClick: false }),
|
|
299
741
|
CustomImage.configure({
|
|
300
742
|
inline: false,
|
|
301
743
|
HTMLAttributes: {
|
|
302
|
-
class:
|
|
744
|
+
class: "rounded-lg mx-auto block max-w-full h-auto cursor-pointer",
|
|
303
745
|
},
|
|
304
746
|
}),
|
|
305
747
|
Highlight.configure({ multicolor: true }),
|
|
@@ -310,13 +752,17 @@
|
|
|
310
752
|
TableHeader,
|
|
311
753
|
BubbleMenu.configure({
|
|
312
754
|
element: bubbleMenuEl!,
|
|
313
|
-
pluginKey:
|
|
755
|
+
pluginKey: "bubbleMenu",
|
|
314
756
|
shouldShow: ({ editor: e }) =>
|
|
315
757
|
e.isEditable &&
|
|
316
|
-
!e.isActive(
|
|
758
|
+
!e.isActive("table") &&
|
|
317
759
|
e.view.state.selection.content().size > 0,
|
|
318
760
|
}),
|
|
319
761
|
CustomYoutube,
|
|
762
|
+
Callout,
|
|
763
|
+
SubPage,
|
|
764
|
+
TaskList,
|
|
765
|
+
TaskItem.configure({ nested: true }),
|
|
320
766
|
],
|
|
321
767
|
content,
|
|
322
768
|
editable,
|
|
@@ -324,13 +770,51 @@
|
|
|
324
770
|
const html = e.getHTML();
|
|
325
771
|
isActive = getActiveStates(e);
|
|
326
772
|
onUpdate?.(html);
|
|
773
|
+
|
|
774
|
+
// Detect slash command trigger
|
|
775
|
+
const { state } = e;
|
|
776
|
+
const { from } = state.selection;
|
|
777
|
+
const textBefore = state.doc.textBetween(
|
|
778
|
+
Math.max(0, from - 20),
|
|
779
|
+
from,
|
|
780
|
+
"\n",
|
|
781
|
+
"\0",
|
|
782
|
+
);
|
|
783
|
+
const slashMatch = textBefore.match(/\/([a-zA-Z0-9]*)$/);
|
|
784
|
+
|
|
785
|
+
if (slashMatch) {
|
|
786
|
+
slashSearch = slashMatch[1];
|
|
787
|
+
slashIndex = 0;
|
|
788
|
+
try {
|
|
789
|
+
const coords = e.view.coordsAtPos(from);
|
|
790
|
+
const parentRect = editorEl?.getBoundingClientRect();
|
|
791
|
+
if (parentRect) {
|
|
792
|
+
slashMenuPos = {
|
|
793
|
+
top: coords.bottom - parentRect.top + 8,
|
|
794
|
+
left: Math.max(
|
|
795
|
+
16,
|
|
796
|
+
Math.min(
|
|
797
|
+
coords.left - parentRect.left,
|
|
798
|
+
parentRect.width - 260,
|
|
799
|
+
),
|
|
800
|
+
),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
} catch {
|
|
804
|
+
slashMenuPos = { top: 60, left: 24 };
|
|
805
|
+
}
|
|
806
|
+
showSlashMenu = true;
|
|
807
|
+
} else {
|
|
808
|
+
showSlashMenu = false;
|
|
809
|
+
}
|
|
327
810
|
},
|
|
328
811
|
onSelectionUpdate: ({ editor: e }) => {
|
|
329
812
|
isActive = getActiveStates(e);
|
|
330
813
|
},
|
|
331
814
|
editorProps: {
|
|
332
815
|
attributes: {
|
|
333
|
-
class:
|
|
816
|
+
class:
|
|
817
|
+
"rich-text-content prose prose-sm max-w-none focus:outline-none",
|
|
334
818
|
},
|
|
335
819
|
handleDOMEvents: {
|
|
336
820
|
paste: (_view, event) => {
|
|
@@ -338,7 +822,7 @@
|
|
|
338
822
|
if (!items) return false;
|
|
339
823
|
const imageFiles: File[] = [];
|
|
340
824
|
for (let i = 0; i < items.length; i++) {
|
|
341
|
-
if (items[i].type.startsWith(
|
|
825
|
+
if (items[i].type.startsWith("image/")) {
|
|
342
826
|
const file = items[i].getAsFile();
|
|
343
827
|
if (file) imageFiles.push(file);
|
|
344
828
|
}
|
|
@@ -354,7 +838,7 @@
|
|
|
354
838
|
const files = event.dataTransfer?.files;
|
|
355
839
|
if (!files?.length) return false;
|
|
356
840
|
const imageFiles = Array.from(files).filter((f) =>
|
|
357
|
-
f.type.startsWith(
|
|
841
|
+
f.type.startsWith("image/"),
|
|
358
842
|
);
|
|
359
843
|
if (imageFiles.length) {
|
|
360
844
|
event.preventDefault();
|
|
@@ -363,6 +847,46 @@
|
|
|
363
847
|
}
|
|
364
848
|
return false;
|
|
365
849
|
},
|
|
850
|
+
click: (_view, event) => {
|
|
851
|
+
const target = (event.target as HTMLElement)?.closest(
|
|
852
|
+
".subpage-block",
|
|
853
|
+
) as HTMLElement | null;
|
|
854
|
+
if (target) {
|
|
855
|
+
const id = target.getAttribute("data-page-id") || "";
|
|
856
|
+
const title = target.getAttribute("data-page-title") || "";
|
|
857
|
+
onOpenSubPage?.({ id, title });
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
return false;
|
|
861
|
+
},
|
|
862
|
+
keydown: (_view, event) => {
|
|
863
|
+
if (showSlashMenu) {
|
|
864
|
+
if (event.key === "ArrowDown") {
|
|
865
|
+
event.preventDefault();
|
|
866
|
+
slashIndex = (slashIndex + 1) % slashFilteredCommands.length;
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
if (event.key === "ArrowUp") {
|
|
870
|
+
event.preventDefault();
|
|
871
|
+
slashIndex =
|
|
872
|
+
(slashIndex - 1 + slashFilteredCommands.length) %
|
|
873
|
+
slashFilteredCommands.length;
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
if (event.key === "Enter") {
|
|
877
|
+
event.preventDefault();
|
|
878
|
+
const cmd = slashFilteredCommands[slashIndex];
|
|
879
|
+
if (cmd) executeSlashCommand(cmd);
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
if (event.key === "Escape") {
|
|
883
|
+
event.preventDefault();
|
|
884
|
+
showSlashMenu = false;
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return false;
|
|
889
|
+
},
|
|
366
890
|
},
|
|
367
891
|
},
|
|
368
892
|
});
|
|
@@ -375,243 +899,519 @@
|
|
|
375
899
|
|
|
376
900
|
<div
|
|
377
901
|
class={cn(
|
|
378
|
-
|
|
379
|
-
className
|
|
902
|
+
"rich-text-editor group rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] shadow-sm overflow-hidden transition-[border-color,box-shadow] focus-within:border-[var(--ui-ring)] focus-within:ring-2 focus-within:ring-[var(--ui-ring)]/15",
|
|
903
|
+
className,
|
|
380
904
|
)}
|
|
381
905
|
>
|
|
382
|
-
<!-- Toolbar -->
|
|
383
|
-
{#if editable}
|
|
906
|
+
<!-- Fixed Top Toolbar (only shown in 'classic' mode) -->
|
|
907
|
+
{#if editable && mode === "classic"}
|
|
384
908
|
<div
|
|
385
|
-
class="flex
|
|
909
|
+
class="rich-text-toolbar sticky top-0 z-20 flex min-h-12 items-center gap-1 overflow-x-auto border-b border-[var(--ui-border)] bg-[var(--ui-card)]/95 px-2.5 py-2 backdrop-blur-md sm:px-3"
|
|
910
|
+
role="toolbar"
|
|
911
|
+
aria-label="Text formatting"
|
|
386
912
|
>
|
|
387
|
-
<
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
class={cn('size-8 p-0', isActive.h2 && 'bg-[var(--ui-primary)]/10')}
|
|
408
|
-
onclick={() => setHeading(2)}
|
|
409
|
-
>
|
|
410
|
-
<Heading2 class="size-4" />
|
|
411
|
-
</Button>
|
|
412
|
-
<Button
|
|
413
|
-
variant="ghost"
|
|
414
|
-
size="sm"
|
|
415
|
-
class={cn('size-8 p-0', isActive.h3 && 'bg-[var(--ui-primary)]/10')}
|
|
416
|
-
onclick={() => setHeading(3)}
|
|
417
|
-
>
|
|
418
|
-
<Heading3 class="size-4" />
|
|
419
|
-
</Button>
|
|
913
|
+
<div class="flex items-center gap-0.5">
|
|
914
|
+
<Button
|
|
915
|
+
variant="ghost"
|
|
916
|
+
size="sm"
|
|
917
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
918
|
+
onclick={undo}
|
|
919
|
+
title="Undo (Ctrl+Z)"
|
|
920
|
+
>
|
|
921
|
+
<Undo class="size-3.5" />
|
|
922
|
+
</Button>
|
|
923
|
+
<Button
|
|
924
|
+
variant="ghost"
|
|
925
|
+
size="sm"
|
|
926
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
927
|
+
onclick={redo}
|
|
928
|
+
title="Redo (Ctrl+Y)"
|
|
929
|
+
>
|
|
930
|
+
<Redo class="size-3.5" />
|
|
931
|
+
</Button>
|
|
932
|
+
</div>
|
|
420
933
|
|
|
421
|
-
<Separator orientation="vertical" class="h-
|
|
934
|
+
<Separator orientation="vertical" class="h-4 mx-1.5 opacity-60" />
|
|
422
935
|
|
|
423
|
-
<
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
936
|
+
<div class="flex items-center gap-0.5">
|
|
937
|
+
<Button
|
|
938
|
+
variant="ghost"
|
|
939
|
+
size="sm"
|
|
940
|
+
class={cn(
|
|
941
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
942
|
+
isActive.h1
|
|
943
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
944
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
945
|
+
)}
|
|
946
|
+
onclick={() => setHeading(1)}
|
|
947
|
+
title="Heading 1"
|
|
948
|
+
>
|
|
949
|
+
<Heading1 class="size-3.5" />
|
|
950
|
+
</Button>
|
|
951
|
+
<Button
|
|
952
|
+
variant="ghost"
|
|
953
|
+
size="sm"
|
|
954
|
+
class={cn(
|
|
955
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
956
|
+
isActive.h2
|
|
957
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
958
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
959
|
+
)}
|
|
960
|
+
onclick={() => setHeading(2)}
|
|
961
|
+
title="Heading 2"
|
|
962
|
+
>
|
|
963
|
+
<Heading2 class="size-3.5" />
|
|
964
|
+
</Button>
|
|
965
|
+
<Button
|
|
966
|
+
variant="ghost"
|
|
967
|
+
size="sm"
|
|
968
|
+
class={cn(
|
|
969
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
970
|
+
isActive.h3
|
|
971
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
972
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
973
|
+
)}
|
|
974
|
+
onclick={() => setHeading(3)}
|
|
975
|
+
title="Heading 3"
|
|
976
|
+
>
|
|
977
|
+
<Heading3 class="size-3.5" />
|
|
978
|
+
</Button>
|
|
979
|
+
</div>
|
|
466
980
|
|
|
467
|
-
<Separator orientation="vertical" class="h-
|
|
981
|
+
<Separator orientation="vertical" class="h-4 mx-1.5 opacity-60" />
|
|
468
982
|
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
983
|
+
<div class="flex items-center gap-0.5">
|
|
984
|
+
<Button
|
|
985
|
+
variant="ghost"
|
|
986
|
+
size="sm"
|
|
987
|
+
class={cn(
|
|
988
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
989
|
+
isActive.bold
|
|
990
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
991
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
992
|
+
)}
|
|
993
|
+
onclick={toggleBold}
|
|
994
|
+
title="Bold (Ctrl+B)"
|
|
995
|
+
>
|
|
996
|
+
<Bold class="size-3.5" />
|
|
997
|
+
</Button>
|
|
998
|
+
<Button
|
|
999
|
+
variant="ghost"
|
|
1000
|
+
size="sm"
|
|
1001
|
+
class={cn(
|
|
1002
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1003
|
+
isActive.italic
|
|
1004
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1005
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1006
|
+
)}
|
|
1007
|
+
onclick={toggleItalic}
|
|
1008
|
+
title="Italic (Ctrl+I)"
|
|
1009
|
+
>
|
|
1010
|
+
<Italic class="size-3.5" />
|
|
1011
|
+
</Button>
|
|
1012
|
+
<Button
|
|
1013
|
+
variant="ghost"
|
|
1014
|
+
size="sm"
|
|
1015
|
+
class={cn(
|
|
1016
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1017
|
+
isActive.underline
|
|
1018
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1019
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1020
|
+
)}
|
|
1021
|
+
onclick={toggleUnderline}
|
|
1022
|
+
title="Underline (Ctrl+U)"
|
|
1023
|
+
>
|
|
1024
|
+
<UnderlineIcon class="size-3.5" />
|
|
1025
|
+
</Button>
|
|
1026
|
+
<Button
|
|
1027
|
+
variant="ghost"
|
|
1028
|
+
size="sm"
|
|
1029
|
+
class={cn(
|
|
1030
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1031
|
+
isActive.strike
|
|
1032
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1033
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1034
|
+
)}
|
|
1035
|
+
onclick={toggleStrike}
|
|
1036
|
+
title="Strikethrough"
|
|
1037
|
+
>
|
|
1038
|
+
<Strikethrough class="size-3.5" />
|
|
1039
|
+
</Button>
|
|
1040
|
+
<Button
|
|
1041
|
+
variant="ghost"
|
|
1042
|
+
size="sm"
|
|
1043
|
+
class={cn(
|
|
1044
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1045
|
+
isActive.highlight
|
|
1046
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1047
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1048
|
+
)}
|
|
1049
|
+
onclick={() => toggleHighlight("#fef08a")}
|
|
1050
|
+
title="Highlight text"
|
|
1051
|
+
>
|
|
1052
|
+
<Highlighter class="size-3.5" />
|
|
1053
|
+
</Button>
|
|
1054
|
+
<label
|
|
1055
|
+
class="relative flex size-7.5 cursor-pointer items-center justify-center rounded-md text-[var(--ui-muted-foreground)] hover:bg-[var(--ui-secondary)] hover:text-[var(--ui-foreground)] focus-within:ring-2 focus-within:ring-[var(--ui-ring)]"
|
|
1056
|
+
title="Text color"
|
|
1057
|
+
>
|
|
1058
|
+
<span class="text-xs font-bold" style="color: {textColor}">A</span>
|
|
1059
|
+
<input
|
|
1060
|
+
class="absolute inset-0 cursor-pointer opacity-0"
|
|
1061
|
+
type="color"
|
|
1062
|
+
value={textColor}
|
|
1063
|
+
aria-label="Text color"
|
|
1064
|
+
onchange={(event) =>
|
|
1065
|
+
setTextColor((event.currentTarget as HTMLInputElement).value)}
|
|
1066
|
+
/>
|
|
1067
|
+
</label>
|
|
1068
|
+
<label
|
|
1069
|
+
class="relative flex size-7.5 cursor-pointer items-center justify-center rounded-md text-[var(--ui-muted-foreground)] hover:bg-[var(--ui-secondary)] hover:text-[var(--ui-foreground)] hover:text-[var(--ui-foreground)] focus-within:ring-2 focus-within:ring-[var(--ui-ring)]"
|
|
1070
|
+
title="Highlight color"
|
|
1071
|
+
>
|
|
1072
|
+
<Highlighter class="size-3.5" style="color: {highlightColor}" />
|
|
1073
|
+
<input
|
|
1074
|
+
class="absolute inset-0 cursor-pointer opacity-0"
|
|
1075
|
+
type="color"
|
|
1076
|
+
value={highlightColor}
|
|
1077
|
+
aria-label="Highlight color"
|
|
1078
|
+
onchange={(event) =>
|
|
1079
|
+
toggleHighlight((event.currentTarget as HTMLInputElement).value)}
|
|
1080
|
+
/>
|
|
1081
|
+
</label>
|
|
1082
|
+
<Button
|
|
1083
|
+
variant="ghost"
|
|
1084
|
+
size="sm"
|
|
1085
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1086
|
+
onclick={toggleTextColor}
|
|
1087
|
+
title="Toggle text color"
|
|
1088
|
+
>
|
|
1089
|
+
<span class="text-xs font-bold">A</span>
|
|
1090
|
+
</Button>
|
|
1091
|
+
</div>
|
|
513
1092
|
|
|
514
|
-
<Separator orientation="vertical" class="h-
|
|
1093
|
+
<Separator orientation="vertical" class="h-4 mx-1.5 opacity-60" />
|
|
1094
|
+
<div class="flex items-center gap-0.5">
|
|
1095
|
+
<Button
|
|
1096
|
+
variant="ghost"
|
|
1097
|
+
size="sm"
|
|
1098
|
+
class={cn(
|
|
1099
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1100
|
+
isActive.bulletList
|
|
1101
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1102
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1103
|
+
)}
|
|
1104
|
+
onclick={toggleBulletList}
|
|
1105
|
+
title="Bullet list"
|
|
1106
|
+
>
|
|
1107
|
+
<List class="size-3.5" />
|
|
1108
|
+
</Button>
|
|
1109
|
+
<Button
|
|
1110
|
+
variant="ghost"
|
|
1111
|
+
size="sm"
|
|
1112
|
+
class={cn(
|
|
1113
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1114
|
+
isActive.orderedList
|
|
1115
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1116
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1117
|
+
)}
|
|
1118
|
+
onclick={toggleOrderedList}
|
|
1119
|
+
title="Numbered list"
|
|
1120
|
+
>
|
|
1121
|
+
<ListOrdered class="size-3.5" />
|
|
1122
|
+
</Button>
|
|
1123
|
+
<Button
|
|
1124
|
+
variant="ghost"
|
|
1125
|
+
size="sm"
|
|
1126
|
+
class={cn(
|
|
1127
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1128
|
+
isActive.blockquote
|
|
1129
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1130
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1131
|
+
)}
|
|
1132
|
+
onclick={toggleBlockquote}
|
|
1133
|
+
title="Quote"
|
|
1134
|
+
>
|
|
1135
|
+
<Quote class="size-3.5" />
|
|
1136
|
+
</Button>
|
|
1137
|
+
<Button
|
|
1138
|
+
variant="ghost"
|
|
1139
|
+
size="sm"
|
|
1140
|
+
class={cn(
|
|
1141
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1142
|
+
isActive.codeBlock
|
|
1143
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1144
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1145
|
+
)}
|
|
1146
|
+
onclick={toggleCodeBlock}
|
|
1147
|
+
title="Code block"
|
|
1148
|
+
>
|
|
1149
|
+
<Code class="size-3.5" />
|
|
1150
|
+
</Button>
|
|
1151
|
+
</div>
|
|
515
1152
|
|
|
516
|
-
<
|
|
517
|
-
variant="ghost"
|
|
518
|
-
size="sm"
|
|
519
|
-
class={cn(
|
|
520
|
-
'size-8 p-0',
|
|
521
|
-
isActive.alignLeft && 'bg-[var(--ui-primary)]/10'
|
|
522
|
-
)}
|
|
523
|
-
onclick={() => setAlign('left')}
|
|
524
|
-
>
|
|
525
|
-
<AlignLeft class="size-4" />
|
|
526
|
-
</Button>
|
|
527
|
-
<Button
|
|
528
|
-
variant="ghost"
|
|
529
|
-
size="sm"
|
|
530
|
-
class={cn(
|
|
531
|
-
'size-8 p-0',
|
|
532
|
-
isActive.alignCenter && 'bg-[var(--ui-primary)]/10'
|
|
533
|
-
)}
|
|
534
|
-
onclick={() => setAlign('center')}
|
|
535
|
-
>
|
|
536
|
-
<AlignCenter class="size-4" />
|
|
537
|
-
</Button>
|
|
538
|
-
<Button
|
|
539
|
-
variant="ghost"
|
|
540
|
-
size="sm"
|
|
541
|
-
class={cn(
|
|
542
|
-
'size-8 p-0',
|
|
543
|
-
isActive.alignRight && 'bg-[var(--ui-primary)]/10'
|
|
544
|
-
)}
|
|
545
|
-
onclick={() => setAlign('right')}
|
|
546
|
-
>
|
|
547
|
-
<AlignRight class="size-4" />
|
|
548
|
-
</Button>
|
|
549
|
-
<Button
|
|
550
|
-
variant="ghost"
|
|
551
|
-
size="sm"
|
|
552
|
-
class={cn(
|
|
553
|
-
'size-8 p-0',
|
|
554
|
-
isActive.alignJustify && 'bg-[var(--ui-primary)]/10'
|
|
555
|
-
)}
|
|
556
|
-
onclick={() => setAlign('justify')}
|
|
557
|
-
>
|
|
558
|
-
<AlignJustify class="size-4" />
|
|
559
|
-
</Button>
|
|
1153
|
+
<Separator orientation="vertical" class="h-4 mx-1.5 opacity-60" />
|
|
560
1154
|
|
|
561
|
-
<
|
|
1155
|
+
<div class="flex items-center gap-0.5">
|
|
1156
|
+
<Button
|
|
1157
|
+
variant="ghost"
|
|
1158
|
+
size="sm"
|
|
1159
|
+
class={cn(
|
|
1160
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1161
|
+
isActive.alignLeft
|
|
1162
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1163
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1164
|
+
)}
|
|
1165
|
+
onclick={() => setAlign("left")}
|
|
1166
|
+
title="Align left"
|
|
1167
|
+
>
|
|
1168
|
+
<AlignLeft class="size-3.5" />
|
|
1169
|
+
</Button>
|
|
1170
|
+
<Button
|
|
1171
|
+
variant="ghost"
|
|
1172
|
+
size="sm"
|
|
1173
|
+
class={cn(
|
|
1174
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1175
|
+
isActive.alignCenter
|
|
1176
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1177
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1178
|
+
)}
|
|
1179
|
+
onclick={() => setAlign("center")}
|
|
1180
|
+
title="Align center"
|
|
1181
|
+
>
|
|
1182
|
+
<AlignCenter class="size-3.5" />
|
|
1183
|
+
</Button>
|
|
1184
|
+
<Button
|
|
1185
|
+
variant="ghost"
|
|
1186
|
+
size="sm"
|
|
1187
|
+
class={cn(
|
|
1188
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1189
|
+
isActive.alignRight
|
|
1190
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1191
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1192
|
+
)}
|
|
1193
|
+
onclick={() => setAlign("right")}
|
|
1194
|
+
title="Align right"
|
|
1195
|
+
>
|
|
1196
|
+
<AlignRight class="size-3.5" />
|
|
1197
|
+
</Button>
|
|
1198
|
+
<Button
|
|
1199
|
+
variant="ghost"
|
|
1200
|
+
size="sm"
|
|
1201
|
+
class={cn(
|
|
1202
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1203
|
+
isActive.alignJustify
|
|
1204
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1205
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1206
|
+
)}
|
|
1207
|
+
onclick={() => setAlign("justify")}
|
|
1208
|
+
title="Align justify"
|
|
1209
|
+
>
|
|
1210
|
+
<AlignJustify class="size-3.5" />
|
|
1211
|
+
</Button>
|
|
1212
|
+
</div>
|
|
562
1213
|
|
|
563
|
-
<
|
|
564
|
-
variant="ghost"
|
|
565
|
-
size="sm"
|
|
566
|
-
class={cn('size-8 p-0', isActive.link && 'bg-[var(--ui-primary)]/10')}
|
|
567
|
-
onclick={setLink}
|
|
568
|
-
>
|
|
569
|
-
<LinkIcon class="size-4" />
|
|
570
|
-
</Button>
|
|
571
|
-
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={setImage}>
|
|
572
|
-
<ImageIcon class="size-4" />
|
|
573
|
-
</Button>
|
|
574
|
-
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={insertTable}>
|
|
575
|
-
<TableIcon class="size-4" />
|
|
576
|
-
</Button>
|
|
577
|
-
<Button
|
|
578
|
-
variant="ghost"
|
|
579
|
-
size="sm"
|
|
580
|
-
class="size-8 p-0"
|
|
581
|
-
onclick={addYoutube}
|
|
582
|
-
title="Embed YouTube video"
|
|
583
|
-
>
|
|
584
|
-
<Video class="size-4" />
|
|
585
|
-
</Button>
|
|
586
|
-
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={insertHorizontalRule}>
|
|
587
|
-
<Minus class="size-4" />
|
|
588
|
-
</Button>
|
|
1214
|
+
<Separator orientation="vertical" class="h-4 mx-1.5 opacity-60" />
|
|
589
1215
|
|
|
1216
|
+
<div class="flex items-center gap-0.5">
|
|
1217
|
+
<Button
|
|
1218
|
+
variant="ghost"
|
|
1219
|
+
size="sm"
|
|
1220
|
+
class={cn(
|
|
1221
|
+
"size-7.5 p-0 rounded-md transition-colors",
|
|
1222
|
+
isActive.link
|
|
1223
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1224
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1225
|
+
)}
|
|
1226
|
+
onclick={setLink}
|
|
1227
|
+
title="Insert link"
|
|
1228
|
+
>
|
|
1229
|
+
<LinkIcon class="size-3.5" />
|
|
1230
|
+
</Button>
|
|
1231
|
+
{#if isActive.link}
|
|
1232
|
+
<Button
|
|
1233
|
+
variant="ghost"
|
|
1234
|
+
size="sm"
|
|
1235
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1236
|
+
onclick={() => editor?.chain().focus().unsetLink().run()}
|
|
1237
|
+
title="Remove link"
|
|
1238
|
+
>
|
|
1239
|
+
<Unlink class="size-3.5" />
|
|
1240
|
+
</Button>
|
|
1241
|
+
{/if}
|
|
1242
|
+
<Button
|
|
1243
|
+
variant="ghost"
|
|
1244
|
+
size="sm"
|
|
1245
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1246
|
+
onclick={() => (showFindReplace = !showFindReplace)}
|
|
1247
|
+
title="Find and replace"
|
|
1248
|
+
>
|
|
1249
|
+
<Search class="size-3.5" />
|
|
1250
|
+
</Button>
|
|
1251
|
+
<Button
|
|
1252
|
+
variant="ghost"
|
|
1253
|
+
size="sm"
|
|
1254
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1255
|
+
onclick={exportHtml}
|
|
1256
|
+
title="Export HTML"
|
|
1257
|
+
>
|
|
1258
|
+
<Download class="size-3.5" /><span class="sr-only">Export HTML</span>
|
|
1259
|
+
</Button>
|
|
1260
|
+
<Button
|
|
1261
|
+
variant="ghost"
|
|
1262
|
+
size="sm"
|
|
1263
|
+
class="hidden size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)] sm:inline-flex"
|
|
1264
|
+
onclick={exportMarkdown}
|
|
1265
|
+
title="Export Markdown"
|
|
1266
|
+
>
|
|
1267
|
+
<FileText class="size-3.5" /><span class="sr-only"
|
|
1268
|
+
>Export Markdown</span
|
|
1269
|
+
>
|
|
1270
|
+
</Button>
|
|
1271
|
+
<Button
|
|
1272
|
+
variant="ghost"
|
|
1273
|
+
size="sm"
|
|
1274
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1275
|
+
onclick={setImage}
|
|
1276
|
+
title="Insert image"
|
|
1277
|
+
>
|
|
1278
|
+
<ImageIcon class="size-3.5" />
|
|
1279
|
+
</Button>
|
|
1280
|
+
<Button
|
|
1281
|
+
variant="ghost"
|
|
1282
|
+
size="sm"
|
|
1283
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1284
|
+
onclick={insertTable}
|
|
1285
|
+
title="Insert table"
|
|
1286
|
+
>
|
|
1287
|
+
<TableIcon class="size-3.5" />
|
|
1288
|
+
</Button>
|
|
1289
|
+
<Button
|
|
1290
|
+
variant="ghost"
|
|
1291
|
+
size="sm"
|
|
1292
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1293
|
+
onclick={addYoutube}
|
|
1294
|
+
title="Embed YouTube video"
|
|
1295
|
+
>
|
|
1296
|
+
<Video class="size-3.5" />
|
|
1297
|
+
</Button>
|
|
1298
|
+
<Button
|
|
1299
|
+
variant="ghost"
|
|
1300
|
+
size="sm"
|
|
1301
|
+
class="size-7.5 p-0 rounded-md text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]"
|
|
1302
|
+
onclick={insertHorizontalRule}
|
|
1303
|
+
title="Divider line"
|
|
1304
|
+
>
|
|
1305
|
+
<Minus class="size-3.5" />
|
|
1306
|
+
</Button>
|
|
1307
|
+
</div>
|
|
590
1308
|
{#if uploadingCount > 0}
|
|
591
|
-
<span
|
|
592
|
-
|
|
1309
|
+
<span
|
|
1310
|
+
class="ml-auto text-xs text-[var(--ui-muted-foreground)] font-medium"
|
|
1311
|
+
>
|
|
1312
|
+
Uploading {uploadingCount} image{uploadingCount > 1 ? "s" : ""}...
|
|
593
1313
|
</span>
|
|
594
1314
|
{/if}
|
|
595
1315
|
</div>
|
|
596
1316
|
{/if}
|
|
597
|
-
|
|
598
|
-
|
|
1317
|
+
{#if editable && mode === "classic" && showFindReplace}
|
|
1318
|
+
<div
|
|
1319
|
+
class="flex flex-wrap items-center gap-2 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/30 px-3 py-2"
|
|
1320
|
+
role="search"
|
|
1321
|
+
aria-label="Find and replace"
|
|
1322
|
+
>
|
|
1323
|
+
<Input
|
|
1324
|
+
class="h-8 min-w-32 flex-1 sm:max-w-56"
|
|
1325
|
+
bind:value={findQuery}
|
|
1326
|
+
placeholder="Find text"
|
|
1327
|
+
aria-label="Find text"
|
|
1328
|
+
oninput={updateSearchMatches}
|
|
1329
|
+
onkeydown={(event) => event.key === "Enter" && findNext()}
|
|
1330
|
+
/>
|
|
1331
|
+
<Input
|
|
1332
|
+
class="h-8 min-w-32 flex-1 sm:max-w-56"
|
|
1333
|
+
bind:value={replaceQuery}
|
|
1334
|
+
placeholder="Replace with"
|
|
1335
|
+
aria-label="Replace with"
|
|
1336
|
+
/>
|
|
1337
|
+
<span
|
|
1338
|
+
class="text-xs tabular-nums text-[var(--ui-muted-foreground)]"
|
|
1339
|
+
aria-live="polite">{findCount} match{findCount === 1 ? "" : "es"}</span
|
|
1340
|
+
>
|
|
1341
|
+
<Button variant="outline" size="sm" onclick={findNext}>Find next</Button>
|
|
1342
|
+
<Button variant="outline" size="sm" onclick={replaceCurrent}
|
|
1343
|
+
>Replace</Button
|
|
1344
|
+
>
|
|
1345
|
+
<Button variant="outline" size="sm" onclick={replaceAll}
|
|
1346
|
+
>Replace all</Button
|
|
1347
|
+
>
|
|
1348
|
+
</div>
|
|
1349
|
+
{/if}
|
|
599
1350
|
<div
|
|
600
1351
|
bind:this={editorEl}
|
|
601
|
-
class="prose prose-sm max-w-none p-
|
|
1352
|
+
class="rich-text-content prose prose-sm max-w-none p-5 sm:p-7 focus:outline-none"
|
|
602
1353
|
style="min-height: {height}px;"
|
|
603
1354
|
></div>
|
|
604
1355
|
|
|
1356
|
+
<!-- Slash Command Popover Menu -->
|
|
1357
|
+
{#if showSlashMenu && slashFilteredCommands.length > 0}
|
|
1358
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
1359
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
1360
|
+
<div
|
|
1361
|
+
class="absolute z-50 w-64 max-h-72 overflow-y-auto rounded-xl border border-[var(--ui-border)] bg-[var(--ui-popover)] p-1.5 shadow-2xl text-[var(--ui-popover-foreground)] animate-in fade-in-50 zoom-in-95"
|
|
1362
|
+
style="top: {slashMenuPos.top}px; left: {slashMenuPos.left}px;"
|
|
1363
|
+
>
|
|
1364
|
+
<div
|
|
1365
|
+
class="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-[var(--ui-muted-foreground)]"
|
|
1366
|
+
>
|
|
1367
|
+
Basic blocks
|
|
1368
|
+
</div>
|
|
1369
|
+
{#each slashFilteredCommands as cmd, i (cmd.title)}
|
|
1370
|
+
{@const Icon = cmd.icon}
|
|
1371
|
+
<button
|
|
1372
|
+
type="button"
|
|
1373
|
+
class={cn(
|
|
1374
|
+
"flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left transition-colors cursor-pointer",
|
|
1375
|
+
i === slashIndex
|
|
1376
|
+
? "bg-[var(--ui-accent)] text-[var(--ui-accent-foreground)]"
|
|
1377
|
+
: "hover:bg-[var(--ui-secondary)]",
|
|
1378
|
+
)}
|
|
1379
|
+
onmouseenter={() => (slashIndex = i)}
|
|
1380
|
+
onclick={() => executeSlashCommand(cmd)}
|
|
1381
|
+
>
|
|
1382
|
+
<div
|
|
1383
|
+
class="flex size-7 items-center justify-center rounded-md border border-[var(--ui-border)] bg-[var(--ui-card)] shrink-0"
|
|
1384
|
+
>
|
|
1385
|
+
<Icon class="size-4 text-[var(--ui-foreground)]" />
|
|
1386
|
+
</div>
|
|
1387
|
+
<div class="flex flex-col min-w-0">
|
|
1388
|
+
<span class="text-xs font-semibold text-[var(--ui-foreground)]"
|
|
1389
|
+
>{cmd.title}</span
|
|
1390
|
+
>
|
|
1391
|
+
<span class="text-[10px] text-[var(--ui-muted-foreground)] truncate"
|
|
1392
|
+
>{cmd.desc}</span
|
|
1393
|
+
>
|
|
1394
|
+
</div>
|
|
1395
|
+
</button>
|
|
1396
|
+
{/each}
|
|
1397
|
+
</div>
|
|
1398
|
+
{/if}
|
|
1399
|
+
|
|
605
1400
|
<!-- BubbleMenu: floating toolbar on text selection (element bound for TipTap) -->
|
|
606
1401
|
<div
|
|
607
1402
|
bind:this={bubbleMenuEl}
|
|
608
|
-
class="flex items-center gap-
|
|
1403
|
+
class="flex items-center gap-1 rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)]/90 px-1.5 py-1 shadow-xl backdrop-blur-md animate-in fade-in-50 zoom-in-95"
|
|
609
1404
|
>
|
|
610
1405
|
{#if editor}
|
|
611
1406
|
<Button
|
|
612
1407
|
variant="ghost"
|
|
613
1408
|
size="sm"
|
|
614
|
-
class={cn(
|
|
1409
|
+
class={cn(
|
|
1410
|
+
"size-7 p-0 rounded-md transition-colors",
|
|
1411
|
+
editor.isActive("bold")
|
|
1412
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1413
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1414
|
+
)}
|
|
615
1415
|
onclick={toggleBold}
|
|
616
1416
|
>
|
|
617
1417
|
<Bold class="size-3.5" />
|
|
@@ -619,7 +1419,12 @@
|
|
|
619
1419
|
<Button
|
|
620
1420
|
variant="ghost"
|
|
621
1421
|
size="sm"
|
|
622
|
-
class={cn(
|
|
1422
|
+
class={cn(
|
|
1423
|
+
"size-7 p-0 rounded-md transition-colors",
|
|
1424
|
+
editor.isActive("italic")
|
|
1425
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1426
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1427
|
+
)}
|
|
623
1428
|
onclick={toggleItalic}
|
|
624
1429
|
>
|
|
625
1430
|
<Italic class="size-3.5" />
|
|
@@ -627,7 +1432,12 @@
|
|
|
627
1432
|
<Button
|
|
628
1433
|
variant="ghost"
|
|
629
1434
|
size="sm"
|
|
630
|
-
class={cn(
|
|
1435
|
+
class={cn(
|
|
1436
|
+
"size-7 p-0 rounded-md transition-colors",
|
|
1437
|
+
editor.isActive("underline")
|
|
1438
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1439
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1440
|
+
)}
|
|
631
1441
|
onclick={toggleUnderline}
|
|
632
1442
|
>
|
|
633
1443
|
<UnderlineIcon class="size-3.5" />
|
|
@@ -635,7 +1445,12 @@
|
|
|
635
1445
|
<Button
|
|
636
1446
|
variant="ghost"
|
|
637
1447
|
size="sm"
|
|
638
|
-
class={cn(
|
|
1448
|
+
class={cn(
|
|
1449
|
+
"size-7 p-0 rounded-md transition-colors",
|
|
1450
|
+
editor.isActive("strike")
|
|
1451
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1452
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1453
|
+
)}
|
|
639
1454
|
onclick={toggleStrike}
|
|
640
1455
|
>
|
|
641
1456
|
<Strikethrough class="size-3.5" />
|
|
@@ -644,17 +1459,24 @@
|
|
|
644
1459
|
variant="ghost"
|
|
645
1460
|
size="sm"
|
|
646
1461
|
class={cn(
|
|
647
|
-
|
|
648
|
-
editor.isActive(
|
|
1462
|
+
"size-7 p-0 rounded-md transition-colors",
|
|
1463
|
+
editor.isActive("highlight")
|
|
1464
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1465
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
649
1466
|
)}
|
|
650
|
-
onclick={() => toggleHighlight(
|
|
1467
|
+
onclick={() => toggleHighlight("#fef08a")}
|
|
651
1468
|
>
|
|
652
1469
|
<Highlighter class="size-3.5" />
|
|
653
1470
|
</Button>
|
|
654
1471
|
<Button
|
|
655
1472
|
variant="ghost"
|
|
656
1473
|
size="sm"
|
|
657
|
-
class={cn(
|
|
1474
|
+
class={cn(
|
|
1475
|
+
"size-7 p-0 rounded-md transition-colors",
|
|
1476
|
+
editor.isActive("link")
|
|
1477
|
+
? "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs"
|
|
1478
|
+
: "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]",
|
|
1479
|
+
)}
|
|
658
1480
|
onclick={setLink}
|
|
659
1481
|
>
|
|
660
1482
|
<LinkIcon class="size-3.5" />
|
|
@@ -703,6 +1525,55 @@
|
|
|
703
1525
|
margin-left: 0;
|
|
704
1526
|
color: var(--ui-muted-foreground);
|
|
705
1527
|
}
|
|
1528
|
+
/* Task List / Checklists */
|
|
1529
|
+
:global(.tiptap ul[data-type="taskList"]) {
|
|
1530
|
+
list-style: none;
|
|
1531
|
+
padding: 0;
|
|
1532
|
+
}
|
|
1533
|
+
:global(.tiptap ul[data-type="taskList"] li) {
|
|
1534
|
+
display: flex;
|
|
1535
|
+
align-items: flex-start;
|
|
1536
|
+
gap: 0.5rem;
|
|
1537
|
+
margin: 0.25rem 0;
|
|
1538
|
+
}
|
|
1539
|
+
:global(.tiptap ul[data-type="taskList"] li > label) {
|
|
1540
|
+
user-select: none;
|
|
1541
|
+
margin-top: 0.2rem;
|
|
1542
|
+
}
|
|
1543
|
+
:global(.tiptap ul[data-type="taskList"] li > label input[type="checkbox"]) {
|
|
1544
|
+
cursor: pointer;
|
|
1545
|
+
accent-color: var(--ui-primary);
|
|
1546
|
+
width: 1rem;
|
|
1547
|
+
height: 1rem;
|
|
1548
|
+
border-radius: 0.25rem;
|
|
1549
|
+
}
|
|
1550
|
+
:global(.tiptap ul[data-type="taskList"] li[data-checked="true"] > div) {
|
|
1551
|
+
text-decoration: line-through;
|
|
1552
|
+
color: var(--ui-muted-foreground);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/* Callout Boxes */
|
|
1556
|
+
:global(.tiptap .callout-box) {
|
|
1557
|
+
background: var(--ui-secondary);
|
|
1558
|
+
border-radius: 0.75rem;
|
|
1559
|
+
padding: 0.875rem 1rem;
|
|
1560
|
+
border: 1px solid var(--ui-border);
|
|
1561
|
+
margin: 0.75rem 0;
|
|
1562
|
+
display: flex;
|
|
1563
|
+
gap: 0.75rem;
|
|
1564
|
+
}
|
|
1565
|
+
:global(.tiptap .callout-info) {
|
|
1566
|
+
background: color-mix(in oklch, var(--ui-info) 8%, transparent);
|
|
1567
|
+
border-color: color-mix(in oklch, var(--ui-info) 30%, transparent);
|
|
1568
|
+
}
|
|
1569
|
+
:global(.tiptap .callout-warning) {
|
|
1570
|
+
background: color-mix(in oklch, var(--ui-warning) 8%, transparent);
|
|
1571
|
+
border-color: color-mix(in oklch, var(--ui-warning) 30%, transparent);
|
|
1572
|
+
}
|
|
1573
|
+
:global(.tiptap .callout-tip) {
|
|
1574
|
+
background: color-mix(in oklch, var(--ui-success) 8%, transparent);
|
|
1575
|
+
border-color: color-mix(in oklch, var(--ui-success) 30%, transparent);
|
|
1576
|
+
}
|
|
706
1577
|
:global(.tiptap pre) {
|
|
707
1578
|
background: var(--ui-secondary);
|
|
708
1579
|
border-radius: 0.5rem;
|
|
@@ -759,7 +1630,7 @@
|
|
|
759
1630
|
background: color-mix(in srgb, var(--ui-primary) 10%, transparent);
|
|
760
1631
|
}
|
|
761
1632
|
:global(.tiptap .selectedCell::after) {
|
|
762
|
-
content:
|
|
1633
|
+
content: "";
|
|
763
1634
|
position: absolute;
|
|
764
1635
|
inset: 0;
|
|
765
1636
|
background: rgba(0, 0, 0, 0.05);
|
|
@@ -800,4 +1671,180 @@
|
|
|
800
1671
|
padding: 0.1em 0.2em;
|
|
801
1672
|
border-radius: 2px;
|
|
802
1673
|
}
|
|
1674
|
+
|
|
1675
|
+
/* Rich-text surface contract: survives host Tailwind scanning boundaries. */
|
|
1676
|
+
:global(.rich-text-editor) {
|
|
1677
|
+
--rich-text-content-width: 72ch;
|
|
1678
|
+
color: var(--ui-foreground);
|
|
1679
|
+
}
|
|
1680
|
+
:global(.rich-text-toolbar) {
|
|
1681
|
+
scrollbar-width: thin;
|
|
1682
|
+
scrollbar-color: var(--ui-border) transparent;
|
|
1683
|
+
}
|
|
1684
|
+
:global(.rich-text-toolbar > div) {
|
|
1685
|
+
flex-shrink: 0;
|
|
1686
|
+
}
|
|
1687
|
+
:global(.rich-text-content) {
|
|
1688
|
+
overflow-y: auto;
|
|
1689
|
+
overscroll-behavior: contain;
|
|
1690
|
+
background: var(--ui-card);
|
|
1691
|
+
}
|
|
1692
|
+
:global(.rich-text-content .tiptap) {
|
|
1693
|
+
max-width: var(--rich-text-content-width);
|
|
1694
|
+
margin: 0 auto;
|
|
1695
|
+
}
|
|
1696
|
+
:global(.rich-text-content .tiptap ::selection) {
|
|
1697
|
+
background: color-mix(in oklch, var(--ui-primary) 24%, transparent);
|
|
1698
|
+
}
|
|
1699
|
+
:global(.rich-text-content .tiptap p.is-editor-empty:first-child::before) {
|
|
1700
|
+
color: color-mix(in oklch, var(--ui-muted-foreground) 80%, transparent);
|
|
1701
|
+
}
|
|
1702
|
+
:global(.rich-text-content .tiptap:focus) {
|
|
1703
|
+
outline: none;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/* Polished document surface */
|
|
1707
|
+
:global(.rich-text-content .tiptap) {
|
|
1708
|
+
color: var(--ui-foreground);
|
|
1709
|
+
caret-color: var(--ui-primary);
|
|
1710
|
+
line-height: 1.75;
|
|
1711
|
+
font-size: 0.9375rem;
|
|
1712
|
+
}
|
|
1713
|
+
:global(.rich-text-content .tiptap > :first-child) {
|
|
1714
|
+
margin-top: 0;
|
|
1715
|
+
}
|
|
1716
|
+
:global(.rich-text-content .tiptap > :last-child) {
|
|
1717
|
+
margin-bottom: 0;
|
|
1718
|
+
}
|
|
1719
|
+
:global(.tiptap p) {
|
|
1720
|
+
margin: 0.65rem 0;
|
|
1721
|
+
}
|
|
1722
|
+
:global(.tiptap h1),
|
|
1723
|
+
:global(.tiptap h2),
|
|
1724
|
+
:global(.tiptap h3) {
|
|
1725
|
+
color: var(--ui-foreground);
|
|
1726
|
+
letter-spacing: -0.025em;
|
|
1727
|
+
line-height: 1.25;
|
|
1728
|
+
}
|
|
1729
|
+
:global(.tiptap h1) {
|
|
1730
|
+
font-size: clamp(1.5rem, 3vw, 2rem);
|
|
1731
|
+
margin: 1.75rem 0 0.75rem;
|
|
1732
|
+
}
|
|
1733
|
+
:global(.tiptap h2) {
|
|
1734
|
+
font-size: clamp(1.25rem, 2.5vw, 1.5rem);
|
|
1735
|
+
margin: 1.5rem 0 0.65rem;
|
|
1736
|
+
}
|
|
1737
|
+
:global(.tiptap h3) {
|
|
1738
|
+
font-size: 1.125rem;
|
|
1739
|
+
margin: 1.25rem 0 0.5rem;
|
|
1740
|
+
}
|
|
1741
|
+
:global(.tiptap strong) {
|
|
1742
|
+
font-weight: 700;
|
|
1743
|
+
color: var(--ui-foreground);
|
|
1744
|
+
}
|
|
1745
|
+
:global(.tiptap a:not(.image-link)) {
|
|
1746
|
+
color: var(--ui-primary);
|
|
1747
|
+
text-decoration: underline;
|
|
1748
|
+
text-decoration-color: color-mix(
|
|
1749
|
+
in oklch,
|
|
1750
|
+
var(--ui-primary) 35%,
|
|
1751
|
+
transparent
|
|
1752
|
+
);
|
|
1753
|
+
text-underline-offset: 3px;
|
|
1754
|
+
}
|
|
1755
|
+
:global(.tiptap a:not(.image-link):hover) {
|
|
1756
|
+
text-decoration-color: var(--ui-primary);
|
|
1757
|
+
}
|
|
1758
|
+
:global(.tiptap ul),
|
|
1759
|
+
:global(.tiptap ol) {
|
|
1760
|
+
margin: 0.75rem 0;
|
|
1761
|
+
}
|
|
1762
|
+
:global(.tiptap li + li) {
|
|
1763
|
+
margin-top: 0.25rem;
|
|
1764
|
+
}
|
|
1765
|
+
:global(.tiptap blockquote) {
|
|
1766
|
+
background: color-mix(in oklch, var(--ui-primary) 5%, transparent);
|
|
1767
|
+
border-left-width: 3px;
|
|
1768
|
+
border-radius: 0 0.5rem 0.5rem 0;
|
|
1769
|
+
padding: 0.75rem 1rem;
|
|
1770
|
+
margin: 1rem 0;
|
|
1771
|
+
}
|
|
1772
|
+
:global(.tiptap pre) {
|
|
1773
|
+
border: 1px solid var(--ui-border);
|
|
1774
|
+
background: var(--ui-secondary);
|
|
1775
|
+
color: var(--ui-foreground);
|
|
1776
|
+
box-shadow: inset 0 1px 0 color-mix(in oklch, white 5%, transparent);
|
|
1777
|
+
overflow-x: auto;
|
|
1778
|
+
}
|
|
1779
|
+
:global(.tiptap code:not(pre code)) {
|
|
1780
|
+
color: var(--ui-primary);
|
|
1781
|
+
border: 1px solid color-mix(in oklch, var(--ui-primary) 15%, transparent);
|
|
1782
|
+
}
|
|
1783
|
+
:global(.tiptap table) {
|
|
1784
|
+
display: block;
|
|
1785
|
+
overflow-x: auto;
|
|
1786
|
+
border: 1px solid var(--ui-border);
|
|
1787
|
+
border-radius: 0.625rem;
|
|
1788
|
+
background: var(--ui-card);
|
|
1789
|
+
}
|
|
1790
|
+
:global(.tiptap th),
|
|
1791
|
+
:global(.tiptap td) {
|
|
1792
|
+
min-width: 7rem;
|
|
1793
|
+
border-color: var(--ui-border);
|
|
1794
|
+
padding: 0.625rem 0.75rem;
|
|
1795
|
+
}
|
|
1796
|
+
:global(.tiptap th) {
|
|
1797
|
+
color: var(--ui-foreground);
|
|
1798
|
+
background: var(--ui-secondary);
|
|
1799
|
+
font-size: 0.8125rem;
|
|
1800
|
+
}
|
|
1801
|
+
:global(.tiptap td) {
|
|
1802
|
+
color: var(--ui-foreground);
|
|
1803
|
+
}
|
|
1804
|
+
:global(.tiptap .selectedCell) {
|
|
1805
|
+
background: color-mix(in oklch, var(--ui-primary) 12%, transparent);
|
|
1806
|
+
}
|
|
1807
|
+
:global(.tiptap .selectedCell::after) {
|
|
1808
|
+
background: color-mix(in oklch, var(--ui-primary) 10%, transparent);
|
|
1809
|
+
}
|
|
1810
|
+
:global(.tiptap img) {
|
|
1811
|
+
border: 1px solid var(--ui-border);
|
|
1812
|
+
box-shadow: 0 8px 24px
|
|
1813
|
+
color-mix(in oklch, var(--ui-foreground) 10%, transparent);
|
|
1814
|
+
transition: box-shadow 150ms ease-out;
|
|
1815
|
+
}
|
|
1816
|
+
:global(.tiptap img:hover) {
|
|
1817
|
+
box-shadow: 0 12px 30px
|
|
1818
|
+
color-mix(in oklch, var(--ui-foreground) 16%, transparent);
|
|
1819
|
+
}
|
|
1820
|
+
:global(.tiptap .callout-box) {
|
|
1821
|
+
box-shadow: 0 1px 2px
|
|
1822
|
+
color-mix(in oklch, var(--ui-foreground) 6%, transparent);
|
|
1823
|
+
}
|
|
1824
|
+
:global(.tiptap hr) {
|
|
1825
|
+
border-top-color: var(--ui-border);
|
|
1826
|
+
margin: 1.5rem 0;
|
|
1827
|
+
}
|
|
1828
|
+
:global(.tiptap:focus-visible) {
|
|
1829
|
+
outline: none;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
@media (max-width: 640px) {
|
|
1833
|
+
:global(.rich-text-toolbar) {
|
|
1834
|
+
scrollbar-width: none;
|
|
1835
|
+
}
|
|
1836
|
+
:global(.rich-text-toolbar::-webkit-scrollbar) {
|
|
1837
|
+
display: none;
|
|
1838
|
+
}
|
|
1839
|
+
:global(.rich-text-content .tiptap) {
|
|
1840
|
+
font-size: 0.875rem;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1845
|
+
:global(.rich-text-editor),
|
|
1846
|
+
:global(.tiptap img) {
|
|
1847
|
+
transition: none;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
803
1850
|
</style>
|