@teriusu/rich-editor 0.4.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.
- package/README.md +178 -0
- package/dist/components/BubbleToolbar.svelte +299 -0
- package/dist/components/BubbleToolbar.svelte.d.ts +9 -0
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -0
- package/dist/components/FixedToolbar.svelte +892 -0
- package/dist/components/FixedToolbar.svelte.d.ts +11 -0
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -0
- package/dist/components/InputModal.svelte +102 -0
- package/dist/components/InputModal.svelte.d.ts +11 -0
- package/dist/components/InputModal.svelte.d.ts.map +1 -0
- package/dist/components/PdfViewer.svelte +152 -0
- package/dist/components/PdfViewer.svelte.d.ts +8 -0
- package/dist/components/PdfViewer.svelte.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.svelte +315 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts +14 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableBubbleMenu.svelte +472 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts +8 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -0
- package/dist/components/TipTapEditor.svelte +649 -0
- package/dist/components/TipTapEditor.svelte.d.ts +17 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -0
- package/dist/extensions/Column.d.ts +3 -0
- package/dist/extensions/Column.d.ts.map +1 -0
- package/dist/extensions/Column.js +20 -0
- package/dist/extensions/Columns.d.ts +11 -0
- package/dist/extensions/Columns.d.ts.map +1 -0
- package/dist/extensions/Columns.js +103 -0
- package/dist/extensions/Columns.test.d.ts +2 -0
- package/dist/extensions/Columns.test.d.ts.map +1 -0
- package/dist/extensions/Columns.test.js +62 -0
- package/dist/extensions/FileAttachment.d.ts +24 -0
- package/dist/extensions/FileAttachment.d.ts.map +1 -0
- package/dist/extensions/FileAttachment.js +190 -0
- package/dist/extensions/FileAttachment.test.d.ts +2 -0
- package/dist/extensions/FileAttachment.test.d.ts.map +1 -0
- package/dist/extensions/FileAttachment.test.js +63 -0
- package/dist/extensions/FixedDetails.d.ts +2 -0
- package/dist/extensions/FixedDetails.d.ts.map +1 -0
- package/dist/extensions/FixedDetails.js +100 -0
- package/dist/extensions/FixedDetails.test.d.ts +2 -0
- package/dist/extensions/FixedDetails.test.d.ts.map +1 -0
- package/dist/extensions/FixedDetails.test.js +40 -0
- package/dist/extensions/Indent.d.ts +11 -0
- package/dist/extensions/Indent.d.ts.map +1 -0
- package/dist/extensions/Indent.js +87 -0
- package/dist/extensions/Indent.test.d.ts +2 -0
- package/dist/extensions/Indent.test.d.ts.map +1 -0
- package/dist/extensions/Indent.test.js +78 -0
- package/dist/extensions/PdfBlock.d.ts +3 -0
- package/dist/extensions/PdfBlock.d.ts.map +1 -0
- package/dist/extensions/PdfBlock.js +301 -0
- package/dist/extensions/PdfBlock.test.d.ts +2 -0
- package/dist/extensions/PdfBlock.test.d.ts.map +1 -0
- package/dist/extensions/PdfBlock.test.js +51 -0
- package/dist/extensions/VideoBlock.d.ts +16 -0
- package/dist/extensions/VideoBlock.d.ts.map +1 -0
- package/dist/extensions/VideoBlock.js +87 -0
- package/dist/extensions/VideoBlock.test.d.ts +2 -0
- package/dist/extensions/VideoBlock.test.d.ts.map +1 -0
- package/dist/extensions/VideoBlock.test.js +54 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/styles/editor.css +698 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/cn.d.ts +3 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/cn.test.d.ts +2 -0
- package/dist/utils/cn.test.d.ts.map +1 -0
- package/dist/utils/cn.test.js +28 -0
- package/dist/utils/escape-html.d.ts +2 -0
- package/dist/utils/escape-html.d.ts.map +1 -0
- package/dist/utils/escape-html.js +7 -0
- package/dist/utils/escape-html.test.d.ts +2 -0
- package/dist/utils/escape-html.test.d.ts.map +1 -0
- package/dist/utils/escape-html.test.js +25 -0
- package/dist/utils/pdf.d.ts +9 -0
- package/dist/utils/pdf.d.ts.map +1 -0
- package/dist/utils/pdf.js +33 -0
- package/dist/utils/sanitize.d.ts +9 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +115 -0
- package/dist/utils/sanitize.test.d.ts +2 -0
- package/dist/utils/sanitize.test.d.ts.map +1 -0
- package/dist/utils/sanitize.test.js +206 -0
- package/package.json +76 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import {
|
|
4
|
+
Bold,
|
|
5
|
+
Italic,
|
|
6
|
+
Underline as UnderlineIcon,
|
|
7
|
+
Strikethrough,
|
|
8
|
+
Highlighter,
|
|
9
|
+
LinkIcon,
|
|
10
|
+
AlignLeft,
|
|
11
|
+
AlignCenter,
|
|
12
|
+
AlignRight,
|
|
13
|
+
Heading1,
|
|
14
|
+
Heading2,
|
|
15
|
+
Heading3,
|
|
16
|
+
List,
|
|
17
|
+
ListOrdered,
|
|
18
|
+
Quote,
|
|
19
|
+
Minus,
|
|
20
|
+
ImageIcon,
|
|
21
|
+
FileText,
|
|
22
|
+
Code2,
|
|
23
|
+
ChevronDown,
|
|
24
|
+
Undo,
|
|
25
|
+
Redo,
|
|
26
|
+
Table as TableIcon,
|
|
27
|
+
Rows3,
|
|
28
|
+
Columns3,
|
|
29
|
+
Trash2,
|
|
30
|
+
SplitSquareHorizontal,
|
|
31
|
+
Combine,
|
|
32
|
+
PanelTop,
|
|
33
|
+
PanelLeft,
|
|
34
|
+
Paintbrush,
|
|
35
|
+
CheckSquare,
|
|
36
|
+
Superscript,
|
|
37
|
+
Subscript,
|
|
38
|
+
Video,
|
|
39
|
+
ChevronRight,
|
|
40
|
+
Paperclip,
|
|
41
|
+
Film,
|
|
42
|
+
Columns2,
|
|
43
|
+
} from "lucide-svelte";
|
|
44
|
+
import { cn } from "../utils/cn";
|
|
45
|
+
import InputModal from "./InputModal.svelte";
|
|
46
|
+
|
|
47
|
+
let {
|
|
48
|
+
editor,
|
|
49
|
+
onPdfClick,
|
|
50
|
+
onFileClick,
|
|
51
|
+
onVideoClick,
|
|
52
|
+
}: {
|
|
53
|
+
editor: Editor;
|
|
54
|
+
onPdfClick: () => void;
|
|
55
|
+
onFileClick?: () => void;
|
|
56
|
+
onVideoClick?: () => void;
|
|
57
|
+
} = $props();
|
|
58
|
+
|
|
59
|
+
const iconSize = 16;
|
|
60
|
+
|
|
61
|
+
let codeMenuOpen = $state(false);
|
|
62
|
+
let tableMenuOpen = $state(false);
|
|
63
|
+
let modalState: { type: "link" | "image" | "cellBg" } | null = $state(null);
|
|
64
|
+
let codeMenuEl: HTMLDivElement | undefined = $state();
|
|
65
|
+
let tableMenuEl: HTMLDivElement | undefined = $state();
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (!codeMenuOpen && !tableMenuOpen) return;
|
|
69
|
+
function handleClick(e: MouseEvent) {
|
|
70
|
+
if (codeMenuEl && !codeMenuEl.contains(e.target as Node))
|
|
71
|
+
codeMenuOpen = false;
|
|
72
|
+
if (tableMenuEl && !tableMenuEl.contains(e.target as Node))
|
|
73
|
+
tableMenuOpen = false;
|
|
74
|
+
}
|
|
75
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
76
|
+
if (e.key === "Escape") {
|
|
77
|
+
codeMenuOpen = false;
|
|
78
|
+
tableMenuOpen = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
document.addEventListener("mousedown", handleClick);
|
|
82
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
83
|
+
return () => {
|
|
84
|
+
document.removeEventListener("mousedown", handleClick);
|
|
85
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function addLink() {
|
|
90
|
+
modalState = { type: "link" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function addImage() {
|
|
94
|
+
modalState = { type: "image" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runTableCommand(command: () => boolean) {
|
|
98
|
+
command();
|
|
99
|
+
tableMenuOpen = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isInTable = $derived(editor.isActive("table"));
|
|
103
|
+
|
|
104
|
+
const CELL_COLORS = [
|
|
105
|
+
{ label: "없음", value: "" },
|
|
106
|
+
{ label: "밝은 회색", value: "#f1f5f9" },
|
|
107
|
+
{ label: "밝은 파랑", value: "#dbeafe" },
|
|
108
|
+
{ label: "밝은 초록", value: "#dcfce7" },
|
|
109
|
+
{ label: "밝은 노랑", value: "#fef9c3" },
|
|
110
|
+
{ label: "밝은 주황", value: "#ffedd5" },
|
|
111
|
+
{ label: "밝은 빨강", value: "#fee2e2" },
|
|
112
|
+
{ label: "밝은 보라", value: "#ede9fe" },
|
|
113
|
+
{ label: "밝은 분홍", value: "#fce7f3" },
|
|
114
|
+
];
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<div
|
|
118
|
+
class="sticky z-30 flex flex-wrap items-center gap-0.5 px-3 py-2 border-b border-border bg-background rounded-t-xl"
|
|
119
|
+
style="top: var(--header-height, 74px)"
|
|
120
|
+
>
|
|
121
|
+
<!-- Format -->
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
125
|
+
data-tooltip="굵게"
|
|
126
|
+
aria-label="굵게"
|
|
127
|
+
class={cn(
|
|
128
|
+
"p-1.5 rounded-md transition-colors",
|
|
129
|
+
editor.isActive("bold")
|
|
130
|
+
? "bg-primary/10 text-primary"
|
|
131
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<Bold size={iconSize} />
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
139
|
+
data-tooltip="기울임"
|
|
140
|
+
aria-label="기울임"
|
|
141
|
+
class={cn(
|
|
142
|
+
"p-1.5 rounded-md transition-colors",
|
|
143
|
+
editor.isActive("italic")
|
|
144
|
+
? "bg-primary/10 text-primary"
|
|
145
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<Italic size={iconSize} />
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
153
|
+
data-tooltip="밑줄"
|
|
154
|
+
aria-label="밑줄"
|
|
155
|
+
class={cn(
|
|
156
|
+
"p-1.5 rounded-md transition-colors",
|
|
157
|
+
editor.isActive("underline")
|
|
158
|
+
? "bg-primary/10 text-primary"
|
|
159
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<UnderlineIcon size={iconSize} />
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
167
|
+
data-tooltip="취소선"
|
|
168
|
+
aria-label="취소선"
|
|
169
|
+
class={cn(
|
|
170
|
+
"p-1.5 rounded-md transition-colors",
|
|
171
|
+
editor.isActive("strike")
|
|
172
|
+
? "bg-primary/10 text-primary"
|
|
173
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<Strikethrough size={iconSize} />
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
181
|
+
data-tooltip="하이라이트"
|
|
182
|
+
aria-label="하이라이트"
|
|
183
|
+
class={cn(
|
|
184
|
+
"p-1.5 rounded-md transition-colors",
|
|
185
|
+
editor.isActive("highlight")
|
|
186
|
+
? "bg-primary/10 text-primary"
|
|
187
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
<Highlighter size={iconSize} />
|
|
191
|
+
</button>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onclick={() => editor.chain().focus().toggleSuperscript().run()}
|
|
195
|
+
data-tooltip="위첨자"
|
|
196
|
+
aria-label="위첨자"
|
|
197
|
+
class={cn(
|
|
198
|
+
"p-1.5 rounded-md transition-colors",
|
|
199
|
+
editor.isActive("superscript")
|
|
200
|
+
? "bg-primary/10 text-primary"
|
|
201
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
<Superscript size={iconSize} />
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onclick={() => editor.chain().focus().toggleSubscript().run()}
|
|
209
|
+
data-tooltip="아래첨자"
|
|
210
|
+
aria-label="아래첨자"
|
|
211
|
+
class={cn(
|
|
212
|
+
"p-1.5 rounded-md transition-colors",
|
|
213
|
+
editor.isActive("subscript")
|
|
214
|
+
? "bg-primary/10 text-primary"
|
|
215
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<Subscript size={iconSize} />
|
|
219
|
+
</button>
|
|
220
|
+
|
|
221
|
+
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
222
|
+
|
|
223
|
+
<!-- Alignment -->
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onclick={() => editor.chain().focus().setTextAlign("left").run()}
|
|
227
|
+
data-tooltip="왼쪽 정렬"
|
|
228
|
+
aria-label="왼쪽 정렬"
|
|
229
|
+
class={cn(
|
|
230
|
+
"p-1.5 rounded-md transition-colors",
|
|
231
|
+
editor.isActive({ textAlign: "left" })
|
|
232
|
+
? "bg-primary/10 text-primary"
|
|
233
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
<AlignLeft size={iconSize} />
|
|
237
|
+
</button>
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onclick={() => editor.chain().focus().setTextAlign("center").run()}
|
|
241
|
+
data-tooltip="가운데 정렬"
|
|
242
|
+
aria-label="가운데 정렬"
|
|
243
|
+
class={cn(
|
|
244
|
+
"p-1.5 rounded-md transition-colors",
|
|
245
|
+
editor.isActive({ textAlign: "center" })
|
|
246
|
+
? "bg-primary/10 text-primary"
|
|
247
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
<AlignCenter size={iconSize} />
|
|
251
|
+
</button>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onclick={() => editor.chain().focus().setTextAlign("right").run()}
|
|
255
|
+
data-tooltip="오른쪽 정렬"
|
|
256
|
+
aria-label="오른쪽 정렬"
|
|
257
|
+
class={cn(
|
|
258
|
+
"p-1.5 rounded-md transition-colors",
|
|
259
|
+
editor.isActive({ textAlign: "right" })
|
|
260
|
+
? "bg-primary/10 text-primary"
|
|
261
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
264
|
+
<AlignRight size={iconSize} />
|
|
265
|
+
</button>
|
|
266
|
+
|
|
267
|
+
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
268
|
+
|
|
269
|
+
<!-- Headings -->
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
onclick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
273
|
+
data-tooltip="제목 1"
|
|
274
|
+
aria-label="제목 1"
|
|
275
|
+
class={cn(
|
|
276
|
+
"p-1.5 rounded-md transition-colors",
|
|
277
|
+
editor.isActive("heading", { level: 1 })
|
|
278
|
+
? "bg-primary/10 text-primary"
|
|
279
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
280
|
+
)}
|
|
281
|
+
>
|
|
282
|
+
<Heading1 size={iconSize} />
|
|
283
|
+
</button>
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
onclick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
287
|
+
data-tooltip="제목 2"
|
|
288
|
+
aria-label="제목 2"
|
|
289
|
+
class={cn(
|
|
290
|
+
"p-1.5 rounded-md transition-colors",
|
|
291
|
+
editor.isActive("heading", { level: 2 })
|
|
292
|
+
? "bg-primary/10 text-primary"
|
|
293
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
294
|
+
)}
|
|
295
|
+
>
|
|
296
|
+
<Heading2 size={iconSize} />
|
|
297
|
+
</button>
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onclick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
301
|
+
data-tooltip="제목 3"
|
|
302
|
+
aria-label="제목 3"
|
|
303
|
+
class={cn(
|
|
304
|
+
"p-1.5 rounded-md transition-colors",
|
|
305
|
+
editor.isActive("heading", { level: 3 })
|
|
306
|
+
? "bg-primary/10 text-primary"
|
|
307
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
<Heading3 size={iconSize} />
|
|
311
|
+
</button>
|
|
312
|
+
|
|
313
|
+
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
314
|
+
|
|
315
|
+
<!-- Lists -->
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
onclick={() => editor.chain().focus().toggleBulletList().run()}
|
|
319
|
+
data-tooltip="글머리 목록"
|
|
320
|
+
aria-label="글머리 목록"
|
|
321
|
+
class={cn(
|
|
322
|
+
"p-1.5 rounded-md transition-colors",
|
|
323
|
+
editor.isActive("bulletList")
|
|
324
|
+
? "bg-primary/10 text-primary"
|
|
325
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
326
|
+
)}
|
|
327
|
+
>
|
|
328
|
+
<List size={iconSize} />
|
|
329
|
+
</button>
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
onclick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
333
|
+
data-tooltip="번호 목록"
|
|
334
|
+
aria-label="번호 목록"
|
|
335
|
+
class={cn(
|
|
336
|
+
"p-1.5 rounded-md transition-colors",
|
|
337
|
+
editor.isActive("orderedList")
|
|
338
|
+
? "bg-primary/10 text-primary"
|
|
339
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
340
|
+
)}
|
|
341
|
+
>
|
|
342
|
+
<ListOrdered size={iconSize} />
|
|
343
|
+
</button>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onclick={() => editor.chain().focus().toggleTaskList().run()}
|
|
347
|
+
data-tooltip="체크리스트"
|
|
348
|
+
aria-label="체크리스트"
|
|
349
|
+
class={cn(
|
|
350
|
+
"p-1.5 rounded-md transition-colors",
|
|
351
|
+
editor.isActive("taskList")
|
|
352
|
+
? "bg-primary/10 text-primary"
|
|
353
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
<CheckSquare size={iconSize} />
|
|
357
|
+
</button>
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
onclick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
361
|
+
data-tooltip="인용문"
|
|
362
|
+
aria-label="인용문"
|
|
363
|
+
class={cn(
|
|
364
|
+
"p-1.5 rounded-md transition-colors",
|
|
365
|
+
editor.isActive("blockquote")
|
|
366
|
+
? "bg-primary/10 text-primary"
|
|
367
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
<Quote size={iconSize} />
|
|
371
|
+
</button>
|
|
372
|
+
<button
|
|
373
|
+
type="button"
|
|
374
|
+
onclick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
375
|
+
data-tooltip="구분선"
|
|
376
|
+
aria-label="구분선"
|
|
377
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
378
|
+
>
|
|
379
|
+
<Minus size={iconSize} />
|
|
380
|
+
</button>
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
onclick={() => editor.chain().focus().setDetails().run()}
|
|
384
|
+
data-tooltip="토글 (접기/펼치기)"
|
|
385
|
+
aria-label="토글 (접기/펼치기)"
|
|
386
|
+
class={cn(
|
|
387
|
+
"p-1.5 rounded-md transition-colors",
|
|
388
|
+
editor.isActive("details")
|
|
389
|
+
? "bg-primary/10 text-primary"
|
|
390
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
391
|
+
)}
|
|
392
|
+
>
|
|
393
|
+
<ChevronRight size={iconSize} />
|
|
394
|
+
</button>
|
|
395
|
+
|
|
396
|
+
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
397
|
+
|
|
398
|
+
<!-- Media -->
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
onclick={addLink}
|
|
402
|
+
data-tooltip="링크"
|
|
403
|
+
aria-label="링크"
|
|
404
|
+
class={cn(
|
|
405
|
+
"p-1.5 rounded-md transition-colors",
|
|
406
|
+
editor.isActive("link")
|
|
407
|
+
? "bg-primary/10 text-primary"
|
|
408
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
409
|
+
)}
|
|
410
|
+
>
|
|
411
|
+
<LinkIcon size={iconSize} />
|
|
412
|
+
</button>
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
onclick={addImage}
|
|
416
|
+
data-tooltip="이미지"
|
|
417
|
+
aria-label="이미지"
|
|
418
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
419
|
+
>
|
|
420
|
+
<ImageIcon size={iconSize} />
|
|
421
|
+
</button>
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onclick={onPdfClick}
|
|
425
|
+
data-tooltip="PDF 삽입"
|
|
426
|
+
aria-label="PDF 삽입"
|
|
427
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
428
|
+
>
|
|
429
|
+
<FileText size={iconSize} />
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
onclick={() => {
|
|
434
|
+
const url = window.prompt("YouTube URL을 입력하세요");
|
|
435
|
+
if (url) editor.chain().focus().setYoutubeVideo({ src: url }).run();
|
|
436
|
+
}}
|
|
437
|
+
data-tooltip="YouTube 영상"
|
|
438
|
+
aria-label="YouTube 영상"
|
|
439
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
440
|
+
>
|
|
441
|
+
<Video size={iconSize} />
|
|
442
|
+
</button>
|
|
443
|
+
{#if onVideoClick}
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
onclick={onVideoClick}
|
|
447
|
+
data-tooltip="영상 파일"
|
|
448
|
+
aria-label="영상 파일"
|
|
449
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
450
|
+
>
|
|
451
|
+
<Film size={iconSize} />
|
|
452
|
+
</button>
|
|
453
|
+
{/if}
|
|
454
|
+
{#if onFileClick}
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
onclick={onFileClick}
|
|
458
|
+
data-tooltip="파일 첨부"
|
|
459
|
+
aria-label="파일 첨부"
|
|
460
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
461
|
+
>
|
|
462
|
+
<Paperclip size={iconSize} />
|
|
463
|
+
</button>
|
|
464
|
+
{/if}
|
|
465
|
+
|
|
466
|
+
<!-- Columns -->
|
|
467
|
+
<button
|
|
468
|
+
type="button"
|
|
469
|
+
onclick={() => editor.chain().focus().setColumns(2).run()}
|
|
470
|
+
data-tooltip="2단 컬럼"
|
|
471
|
+
aria-label="2단 컬럼"
|
|
472
|
+
class={cn(
|
|
473
|
+
"p-1.5 rounded-md transition-colors",
|
|
474
|
+
editor.isActive("columns")
|
|
475
|
+
? "bg-primary/10 text-primary"
|
|
476
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
477
|
+
)}
|
|
478
|
+
>
|
|
479
|
+
<Columns2 size={iconSize} />
|
|
480
|
+
</button>
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
onclick={() => editor.chain().focus().setColumns(3).run()}
|
|
484
|
+
data-tooltip="3단 컬럼"
|
|
485
|
+
aria-label="3단 컬럼"
|
|
486
|
+
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
487
|
+
>
|
|
488
|
+
<Columns3 size={iconSize} />
|
|
489
|
+
</button>
|
|
490
|
+
|
|
491
|
+
<!-- Table menu -->
|
|
492
|
+
<div bind:this={tableMenuEl} class="relative">
|
|
493
|
+
<button
|
|
494
|
+
type="button"
|
|
495
|
+
onclick={() => (tableMenuOpen = !tableMenuOpen)}
|
|
496
|
+
data-tooltip="표"
|
|
497
|
+
aria-label="표"
|
|
498
|
+
class={cn(
|
|
499
|
+
"flex items-center gap-0.5 p-1.5 rounded-md transition-colors",
|
|
500
|
+
isInTable
|
|
501
|
+
? "bg-primary/10 text-primary"
|
|
502
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
503
|
+
)}
|
|
504
|
+
>
|
|
505
|
+
<TableIcon size={iconSize} />
|
|
506
|
+
<ChevronDown size={12} />
|
|
507
|
+
</button>
|
|
508
|
+
{#if tableMenuOpen}
|
|
509
|
+
<div
|
|
510
|
+
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
511
|
+
style="min-width: 220px"
|
|
512
|
+
>
|
|
513
|
+
{#if !isInTable}
|
|
514
|
+
<button
|
|
515
|
+
type="button"
|
|
516
|
+
class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
|
|
517
|
+
onclick={() =>
|
|
518
|
+
runTableCommand(() =>
|
|
519
|
+
editor
|
|
520
|
+
.chain()
|
|
521
|
+
.focus()
|
|
522
|
+
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
523
|
+
.run(),
|
|
524
|
+
)}
|
|
525
|
+
>
|
|
526
|
+
<TableIcon size={14} />
|
|
527
|
+
표 삽입 (3x3)
|
|
528
|
+
</button>
|
|
529
|
+
<button
|
|
530
|
+
type="button"
|
|
531
|
+
class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
|
|
532
|
+
onclick={() =>
|
|
533
|
+
runTableCommand(() =>
|
|
534
|
+
editor
|
|
535
|
+
.chain()
|
|
536
|
+
.focus()
|
|
537
|
+
.insertTable({ rows: 4, cols: 4, withHeaderRow: true })
|
|
538
|
+
.run(),
|
|
539
|
+
)}
|
|
540
|
+
>
|
|
541
|
+
<PanelTop size={14} />
|
|
542
|
+
헤더 포함 4x4
|
|
543
|
+
</button>
|
|
544
|
+
{:else}
|
|
545
|
+
<p
|
|
546
|
+
class="px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
547
|
+
>
|
|
548
|
+
구조
|
|
549
|
+
</p>
|
|
550
|
+
<button
|
|
551
|
+
type="button"
|
|
552
|
+
disabled={!editor.can().addRowBefore()}
|
|
553
|
+
class={cn(
|
|
554
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
555
|
+
!editor.can().addRowBefore() && "opacity-30 pointer-events-none",
|
|
556
|
+
)}
|
|
557
|
+
onclick={() =>
|
|
558
|
+
runTableCommand(() =>
|
|
559
|
+
editor.chain().focus().addRowBefore().run(),
|
|
560
|
+
)}
|
|
561
|
+
>
|
|
562
|
+
<Rows3 size={14} /> 행 위에 추가
|
|
563
|
+
</button>
|
|
564
|
+
<button
|
|
565
|
+
type="button"
|
|
566
|
+
disabled={!editor.can().addRowAfter()}
|
|
567
|
+
class={cn(
|
|
568
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
569
|
+
!editor.can().addRowAfter() && "opacity-30 pointer-events-none",
|
|
570
|
+
)}
|
|
571
|
+
onclick={() =>
|
|
572
|
+
runTableCommand(() =>
|
|
573
|
+
editor.chain().focus().addRowAfter().run(),
|
|
574
|
+
)}
|
|
575
|
+
>
|
|
576
|
+
<Rows3 size={14} /> 행 아래에 추가
|
|
577
|
+
</button>
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
disabled={!editor.can().addColumnBefore()}
|
|
581
|
+
class={cn(
|
|
582
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
583
|
+
!editor.can().addColumnBefore() &&
|
|
584
|
+
"opacity-30 pointer-events-none",
|
|
585
|
+
)}
|
|
586
|
+
onclick={() =>
|
|
587
|
+
runTableCommand(() =>
|
|
588
|
+
editor.chain().focus().addColumnBefore().run(),
|
|
589
|
+
)}
|
|
590
|
+
>
|
|
591
|
+
<Columns3 size={14} /> 열 왼쪽에 추가
|
|
592
|
+
</button>
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
disabled={!editor.can().addColumnAfter()}
|
|
596
|
+
class={cn(
|
|
597
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
598
|
+
!editor.can().addColumnAfter() &&
|
|
599
|
+
"opacity-30 pointer-events-none",
|
|
600
|
+
)}
|
|
601
|
+
onclick={() =>
|
|
602
|
+
runTableCommand(() =>
|
|
603
|
+
editor.chain().focus().addColumnAfter().run(),
|
|
604
|
+
)}
|
|
605
|
+
>
|
|
606
|
+
<Columns3 size={14} /> 열 오른쪽에 추가
|
|
607
|
+
</button>
|
|
608
|
+
<div class="h-px bg-border my-1"></div>
|
|
609
|
+
<p
|
|
610
|
+
class="px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
611
|
+
>
|
|
612
|
+
헤더 / 셀
|
|
613
|
+
</p>
|
|
614
|
+
<button
|
|
615
|
+
type="button"
|
|
616
|
+
disabled={!editor.can().toggleHeaderRow()}
|
|
617
|
+
class={cn(
|
|
618
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
619
|
+
!editor.can().toggleHeaderRow() &&
|
|
620
|
+
"opacity-30 pointer-events-none",
|
|
621
|
+
)}
|
|
622
|
+
onclick={() =>
|
|
623
|
+
runTableCommand(() =>
|
|
624
|
+
editor.chain().focus().toggleHeaderRow().run(),
|
|
625
|
+
)}
|
|
626
|
+
>
|
|
627
|
+
<PanelTop size={14} /> 헤더 행 토글
|
|
628
|
+
</button>
|
|
629
|
+
<button
|
|
630
|
+
type="button"
|
|
631
|
+
disabled={!editor.can().toggleHeaderColumn()}
|
|
632
|
+
class={cn(
|
|
633
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
634
|
+
!editor.can().toggleHeaderColumn() &&
|
|
635
|
+
"opacity-30 pointer-events-none",
|
|
636
|
+
)}
|
|
637
|
+
onclick={() =>
|
|
638
|
+
runTableCommand(() =>
|
|
639
|
+
editor.chain().focus().toggleHeaderColumn().run(),
|
|
640
|
+
)}
|
|
641
|
+
>
|
|
642
|
+
<PanelLeft size={14} /> 헤더 열 토글
|
|
643
|
+
</button>
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
disabled={!editor.can().mergeCells()}
|
|
647
|
+
class={cn(
|
|
648
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
649
|
+
!editor.can().mergeCells() && "opacity-30 pointer-events-none",
|
|
650
|
+
)}
|
|
651
|
+
onclick={() =>
|
|
652
|
+
runTableCommand(() =>
|
|
653
|
+
editor.chain().focus().mergeCells().run(),
|
|
654
|
+
)}
|
|
655
|
+
>
|
|
656
|
+
<Combine size={14} /> 셀 병합
|
|
657
|
+
</button>
|
|
658
|
+
<button
|
|
659
|
+
type="button"
|
|
660
|
+
disabled={!editor.can().splitCell()}
|
|
661
|
+
class={cn(
|
|
662
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
663
|
+
!editor.can().splitCell() && "opacity-30 pointer-events-none",
|
|
664
|
+
)}
|
|
665
|
+
onclick={() =>
|
|
666
|
+
runTableCommand(() =>
|
|
667
|
+
editor.chain().focus().splitCell().run(),
|
|
668
|
+
)}
|
|
669
|
+
>
|
|
670
|
+
<SplitSquareHorizontal size={14} /> 셀 분할
|
|
671
|
+
</button>
|
|
672
|
+
<button
|
|
673
|
+
type="button"
|
|
674
|
+
class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
|
|
675
|
+
onclick={() => {
|
|
676
|
+
tableMenuOpen = false;
|
|
677
|
+
modalState = { type: "cellBg" };
|
|
678
|
+
}}
|
|
679
|
+
>
|
|
680
|
+
<Paintbrush size={14} /> 셀 배경색
|
|
681
|
+
</button>
|
|
682
|
+
<div class="h-px bg-border my-1"></div>
|
|
683
|
+
<p
|
|
684
|
+
class="px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
685
|
+
>
|
|
686
|
+
삭제
|
|
687
|
+
</p>
|
|
688
|
+
<button
|
|
689
|
+
type="button"
|
|
690
|
+
disabled={!editor.can().deleteRow()}
|
|
691
|
+
class={cn(
|
|
692
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 text-destructive hover:bg-destructive/10",
|
|
693
|
+
!editor.can().deleteRow() && "opacity-30 pointer-events-none",
|
|
694
|
+
)}
|
|
695
|
+
onclick={() =>
|
|
696
|
+
runTableCommand(() =>
|
|
697
|
+
editor.chain().focus().deleteRow().run(),
|
|
698
|
+
)}
|
|
699
|
+
>
|
|
700
|
+
<Rows3 size={14} /> 행 삭제
|
|
701
|
+
</button>
|
|
702
|
+
<button
|
|
703
|
+
type="button"
|
|
704
|
+
disabled={!editor.can().deleteColumn()}
|
|
705
|
+
class={cn(
|
|
706
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 text-destructive hover:bg-destructive/10",
|
|
707
|
+
!editor.can().deleteColumn() && "opacity-30 pointer-events-none",
|
|
708
|
+
)}
|
|
709
|
+
onclick={() =>
|
|
710
|
+
runTableCommand(() =>
|
|
711
|
+
editor.chain().focus().deleteColumn().run(),
|
|
712
|
+
)}
|
|
713
|
+
>
|
|
714
|
+
<Columns3 size={14} /> 열 삭제
|
|
715
|
+
</button>
|
|
716
|
+
<button
|
|
717
|
+
type="button"
|
|
718
|
+
disabled={!editor.can().deleteTable()}
|
|
719
|
+
class={cn(
|
|
720
|
+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 text-destructive hover:bg-destructive/10",
|
|
721
|
+
!editor.can().deleteTable() && "opacity-30 pointer-events-none",
|
|
722
|
+
)}
|
|
723
|
+
onclick={() =>
|
|
724
|
+
runTableCommand(() =>
|
|
725
|
+
editor.chain().focus().deleteTable().run(),
|
|
726
|
+
)}
|
|
727
|
+
>
|
|
728
|
+
<Trash2 size={14} /> 표 삭제
|
|
729
|
+
</button>
|
|
730
|
+
{/if}
|
|
731
|
+
</div>
|
|
732
|
+
{/if}
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
<!-- Code block menu -->
|
|
736
|
+
<div bind:this={codeMenuEl} class="relative">
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
onclick={() => (codeMenuOpen = !codeMenuOpen)}
|
|
740
|
+
data-tooltip="코드 블록"
|
|
741
|
+
aria-label="코드 블록"
|
|
742
|
+
class={cn(
|
|
743
|
+
"flex items-center gap-0.5 p-1.5 rounded-md transition-colors",
|
|
744
|
+
editor.isActive("codeBlock")
|
|
745
|
+
? "bg-primary/10 text-primary"
|
|
746
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
747
|
+
)}
|
|
748
|
+
>
|
|
749
|
+
<Code2 size={iconSize} />
|
|
750
|
+
<ChevronDown size={12} />
|
|
751
|
+
</button>
|
|
752
|
+
{#if codeMenuOpen}
|
|
753
|
+
<div
|
|
754
|
+
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
755
|
+
style="min-width: 130px"
|
|
756
|
+
>
|
|
757
|
+
<button
|
|
758
|
+
type="button"
|
|
759
|
+
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
760
|
+
onclick={() => {
|
|
761
|
+
editor.chain().focus().setCodeBlock({ language: "cpp" }).run();
|
|
762
|
+
codeMenuOpen = false;
|
|
763
|
+
}}
|
|
764
|
+
>
|
|
765
|
+
C++
|
|
766
|
+
</button>
|
|
767
|
+
<button
|
|
768
|
+
type="button"
|
|
769
|
+
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
770
|
+
onclick={() => {
|
|
771
|
+
editor.chain().focus().setCodeBlock({ language: "python" }).run();
|
|
772
|
+
codeMenuOpen = false;
|
|
773
|
+
}}
|
|
774
|
+
>
|
|
775
|
+
Python
|
|
776
|
+
</button>
|
|
777
|
+
<div class="h-px bg-border my-1"></div>
|
|
778
|
+
<button
|
|
779
|
+
type="button"
|
|
780
|
+
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
781
|
+
onclick={() => {
|
|
782
|
+
editor.chain().focus().setCodeBlock({ language: "" }).run();
|
|
783
|
+
codeMenuOpen = false;
|
|
784
|
+
}}
|
|
785
|
+
>
|
|
786
|
+
일반 코드
|
|
787
|
+
</button>
|
|
788
|
+
</div>
|
|
789
|
+
{/if}
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
793
|
+
|
|
794
|
+
<!-- Undo / Redo -->
|
|
795
|
+
<button
|
|
796
|
+
type="button"
|
|
797
|
+
onclick={() => editor.chain().focus().undo().run()}
|
|
798
|
+
disabled={!editor.can().undo()}
|
|
799
|
+
data-tooltip="실행 취소"
|
|
800
|
+
aria-label="실행 취소"
|
|
801
|
+
class={cn(
|
|
802
|
+
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
803
|
+
!editor.can().undo() && "opacity-30 pointer-events-none",
|
|
804
|
+
)}
|
|
805
|
+
>
|
|
806
|
+
<Undo size={iconSize} />
|
|
807
|
+
</button>
|
|
808
|
+
<button
|
|
809
|
+
type="button"
|
|
810
|
+
onclick={() => editor.chain().focus().redo().run()}
|
|
811
|
+
disabled={!editor.can().redo()}
|
|
812
|
+
data-tooltip="다시 실행"
|
|
813
|
+
aria-label="다시 실행"
|
|
814
|
+
class={cn(
|
|
815
|
+
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
816
|
+
!editor.can().redo() && "opacity-30 pointer-events-none",
|
|
817
|
+
)}
|
|
818
|
+
>
|
|
819
|
+
<Redo size={iconSize} />
|
|
820
|
+
</button>
|
|
821
|
+
|
|
822
|
+
<!-- Modals -->
|
|
823
|
+
{#if modalState?.type === "link"}
|
|
824
|
+
<InputModal
|
|
825
|
+
data-tooltip="링크 URL 입력"
|
|
826
|
+
placeholder="https://example.com"
|
|
827
|
+
defaultValue={editor.isActive("link") ? editor.getAttributes("link").href || "" : ""}
|
|
828
|
+
onConfirm={(url) => {
|
|
829
|
+
editor
|
|
830
|
+
.chain()
|
|
831
|
+
.focus()
|
|
832
|
+
.extendMarkRange("link")
|
|
833
|
+
.setLink({ href: url })
|
|
834
|
+
.run();
|
|
835
|
+
modalState = null;
|
|
836
|
+
}}
|
|
837
|
+
onCancel={() => (modalState = null)}
|
|
838
|
+
/>
|
|
839
|
+
{/if}
|
|
840
|
+
{#if modalState?.type === "image"}
|
|
841
|
+
<InputModal
|
|
842
|
+
data-tooltip="이미지 URL 입력"
|
|
843
|
+
placeholder="https://example.com/image.png"
|
|
844
|
+
onConfirm={(url) => {
|
|
845
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
846
|
+
modalState = null;
|
|
847
|
+
}}
|
|
848
|
+
onCancel={() => (modalState = null)}
|
|
849
|
+
/>
|
|
850
|
+
{/if}
|
|
851
|
+
{#if modalState?.type === "cellBg"}
|
|
852
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
853
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
854
|
+
<div
|
|
855
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
|
856
|
+
onclick={() => (modalState = null)}
|
|
857
|
+
>
|
|
858
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
859
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
860
|
+
<div
|
|
861
|
+
class="bg-popover border border-border rounded-xl shadow-lg p-4"
|
|
862
|
+
style="min-width: 240px"
|
|
863
|
+
onclick={(e) => e.stopPropagation()}
|
|
864
|
+
>
|
|
865
|
+
<p class="text-sm font-semibold mb-3">셀 배경색</p>
|
|
866
|
+
<div class="grid grid-cols-3 gap-2">
|
|
867
|
+
{#each CELL_COLORS as c}
|
|
868
|
+
<button
|
|
869
|
+
type="button"
|
|
870
|
+
title={c.label}
|
|
871
|
+
class="h-9 rounded-lg border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs"
|
|
872
|
+
style="background: {c.value ||
|
|
873
|
+
'#fff'}; {!c.value
|
|
874
|
+
? 'background-image: linear-gradient(135deg, transparent 45%, #ef4444 45%, #ef4444 55%, transparent 55%)'
|
|
875
|
+
: ''}"
|
|
876
|
+
onclick={() => {
|
|
877
|
+
editor
|
|
878
|
+
.chain()
|
|
879
|
+
.focus()
|
|
880
|
+
.setCellAttribute("backgroundColor", c.value || null)
|
|
881
|
+
.run();
|
|
882
|
+
modalState = null;
|
|
883
|
+
}}
|
|
884
|
+
>
|
|
885
|
+
{c.value ? "" : ""}
|
|
886
|
+
</button>
|
|
887
|
+
{/each}
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
{/if}
|
|
892
|
+
</div>
|