@teriusu/rich-editor 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/BubbleToolbar.svelte +175 -118
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +486 -805
- package/dist/components/FixedToolbar.svelte.d.ts +0 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/SlashCommandMenu.svelte +43 -40
- package/dist/components/SlashCommandMenu.svelte.d.ts +1 -1
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +2 -64
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/Columns.d.ts.map +1 -1
- package/dist/extensions/Columns.js +35 -0
- package/dist/extensions/FixedDetails.d.ts.map +1 -1
- package/dist/extensions/FixedDetails.js +11 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -7
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +0 -3
- package/dist/utils/sanitize.test.js +0 -7
- package/package.json +1 -2
- package/dist/extensions/VideoBlock.d.ts +0 -16
- package/dist/extensions/VideoBlock.d.ts.map +0 -1
- package/dist/extensions/VideoBlock.js +0 -87
- package/dist/extensions/VideoBlock.test.d.ts +0 -2
- package/dist/extensions/VideoBlock.test.d.ts.map +0 -1
- package/dist/extensions/VideoBlock.test.js +0 -54
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
Italic,
|
|
6
6
|
Underline as UnderlineIcon,
|
|
7
7
|
Strikethrough,
|
|
8
|
-
Highlighter,
|
|
9
|
-
LinkIcon,
|
|
10
8
|
AlignLeft,
|
|
11
9
|
AlignCenter,
|
|
12
10
|
AlignRight,
|
|
11
|
+
LinkIcon,
|
|
13
12
|
Heading1,
|
|
14
13
|
Heading2,
|
|
15
14
|
Heading3,
|
|
@@ -24,23 +23,14 @@
|
|
|
24
23
|
Undo,
|
|
25
24
|
Redo,
|
|
26
25
|
Table as TableIcon,
|
|
27
|
-
Rows3,
|
|
28
|
-
Columns3,
|
|
29
|
-
Trash2,
|
|
30
|
-
SplitSquareHorizontal,
|
|
31
|
-
Combine,
|
|
32
|
-
PanelTop,
|
|
33
|
-
PanelLeft,
|
|
34
|
-
Paintbrush,
|
|
35
26
|
CheckSquare,
|
|
36
|
-
Superscript,
|
|
37
|
-
Subscript,
|
|
38
|
-
Video,
|
|
39
27
|
ChevronRight,
|
|
40
28
|
Paperclip,
|
|
41
|
-
Film,
|
|
42
29
|
Columns2,
|
|
30
|
+
Columns3,
|
|
43
31
|
Tv,
|
|
32
|
+
Plus,
|
|
33
|
+
Pilcrow,
|
|
44
34
|
Palette,
|
|
45
35
|
} from "lucide-svelte";
|
|
46
36
|
import { cn } from "../utils/cn";
|
|
@@ -52,7 +42,6 @@
|
|
|
52
42
|
features,
|
|
53
43
|
onPdfClick,
|
|
54
44
|
onFileClick,
|
|
55
|
-
onVideoClick,
|
|
56
45
|
onPromptLink,
|
|
57
46
|
onPromptImage,
|
|
58
47
|
onPromptMbus,
|
|
@@ -61,7 +50,6 @@
|
|
|
61
50
|
features: Set<ToolbarFeature>;
|
|
62
51
|
onPdfClick: () => void;
|
|
63
52
|
onFileClick?: () => void;
|
|
64
|
-
onVideoClick?: () => void;
|
|
65
53
|
onPromptLink?: PromptHandler;
|
|
66
54
|
onPromptImage?: PromptHandler;
|
|
67
55
|
onPromptMbus?: PromptHandler;
|
|
@@ -71,13 +59,12 @@
|
|
|
71
59
|
|
|
72
60
|
const iconSize = 16;
|
|
73
61
|
|
|
74
|
-
let
|
|
75
|
-
let
|
|
62
|
+
let blockMenuOpen = $state(false);
|
|
63
|
+
let insertMenuOpen = $state(false);
|
|
76
64
|
let colorMenuOpen = $state(false);
|
|
77
|
-
let
|
|
78
|
-
let
|
|
79
|
-
let
|
|
80
|
-
let tableMenuEl: HTMLDivElement | undefined = $state();
|
|
65
|
+
let modalState: { type: "link" | "image" | "mbus" } | null = $state(null);
|
|
66
|
+
let blockMenuEl: HTMLDivElement | undefined = $state();
|
|
67
|
+
let insertMenuEl: HTMLDivElement | undefined = $state();
|
|
81
68
|
let colorMenuEl: HTMLDivElement | undefined = $state();
|
|
82
69
|
|
|
83
70
|
const TEXT_COLORS = [
|
|
@@ -93,23 +80,19 @@
|
|
|
93
80
|
];
|
|
94
81
|
|
|
95
82
|
$effect(() => {
|
|
96
|
-
if (!
|
|
83
|
+
if (!blockMenuOpen && !insertMenuOpen && !colorMenuOpen) return;
|
|
97
84
|
function handleClick(e: MouseEvent) {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
colorMenuEl &&
|
|
104
|
-
!colorMenuEl.contains(e.target as Node) &&
|
|
105
|
-
!colorPickerActive
|
|
106
|
-
)
|
|
85
|
+
if (blockMenuEl && !blockMenuEl.contains(e.target as Node))
|
|
86
|
+
blockMenuOpen = false;
|
|
87
|
+
if (insertMenuEl && !insertMenuEl.contains(e.target as Node))
|
|
88
|
+
insertMenuOpen = false;
|
|
89
|
+
if (colorMenuEl && !colorMenuEl.contains(e.target as Node))
|
|
107
90
|
colorMenuOpen = false;
|
|
108
91
|
}
|
|
109
92
|
function handleKeyDown(e: KeyboardEvent) {
|
|
110
93
|
if (e.key === "Escape") {
|
|
111
|
-
|
|
112
|
-
|
|
94
|
+
blockMenuOpen = false;
|
|
95
|
+
insertMenuOpen = false;
|
|
113
96
|
colorMenuOpen = false;
|
|
114
97
|
}
|
|
115
98
|
}
|
|
@@ -122,6 +105,7 @@
|
|
|
122
105
|
});
|
|
123
106
|
|
|
124
107
|
async function addLink() {
|
|
108
|
+
insertMenuOpen = false;
|
|
125
109
|
if (onPromptLink) {
|
|
126
110
|
const previous = editor.isActive("link")
|
|
127
111
|
? (editor.getAttributes("link").href as string) || ""
|
|
@@ -144,6 +128,7 @@
|
|
|
144
128
|
}
|
|
145
129
|
|
|
146
130
|
async function addImage() {
|
|
131
|
+
insertMenuOpen = false;
|
|
147
132
|
if (onPromptImage) {
|
|
148
133
|
const url = await onPromptImage("");
|
|
149
134
|
if (!url) return;
|
|
@@ -153,851 +138,588 @@
|
|
|
153
138
|
modalState = { type: "image" };
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
async function addMbus() {
|
|
142
|
+
insertMenuOpen = false;
|
|
143
|
+
if (onPromptMbus) {
|
|
144
|
+
const url = await onPromptMbus("");
|
|
145
|
+
if (!url) return;
|
|
146
|
+
editor.chain().focus().setMbusVideo({ src: url }).run();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
modalState = { type: "mbus" };
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
const
|
|
152
|
+
const currentBlockLabel = $derived.by(() => {
|
|
153
|
+
if (editor.isActive("heading", { level: 1 })) return "제목 1";
|
|
154
|
+
if (editor.isActive("heading", { level: 2 })) return "제목 2";
|
|
155
|
+
if (editor.isActive("heading", { level: 3 })) return "제목 3";
|
|
156
|
+
if (editor.isActive("bulletList")) return "글머리 목록";
|
|
157
|
+
if (editor.isActive("orderedList")) return "번호 목록";
|
|
158
|
+
if (editor.isActive("taskList")) return "체크리스트";
|
|
159
|
+
if (editor.isActive("blockquote")) return "인용문";
|
|
160
|
+
if (editor.isActive("details")) return "토글";
|
|
161
|
+
return "본문";
|
|
162
|
+
});
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
function runBlock(fn: () => void) {
|
|
165
|
+
fn();
|
|
166
|
+
blockMenuOpen = false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function runInsert(fn: () => void) {
|
|
170
|
+
fn();
|
|
171
|
+
insertMenuOpen = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const hasInsertItems = $derived(
|
|
175
|
+
has("image") ||
|
|
176
|
+
has("link") ||
|
|
177
|
+
has("pdf") ||
|
|
178
|
+
has("file") ||
|
|
179
|
+
has("mbus") ||
|
|
180
|
+
has("table") ||
|
|
181
|
+
has("columns-2") ||
|
|
182
|
+
has("columns-3") ||
|
|
183
|
+
has("horizontal-rule") ||
|
|
184
|
+
has("code-block"),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const hasBlockItems = $derived(
|
|
188
|
+
has("h1") ||
|
|
189
|
+
has("h2") ||
|
|
190
|
+
has("h3") ||
|
|
191
|
+
has("bullet-list") ||
|
|
192
|
+
has("ordered-list") ||
|
|
193
|
+
has("checklist") ||
|
|
194
|
+
has("blockquote") ||
|
|
195
|
+
has("toggle"),
|
|
196
|
+
);
|
|
174
197
|
</script>
|
|
175
198
|
|
|
176
199
|
<div
|
|
177
200
|
class="hce-toolbar sticky z-30 flex flex-wrap items-center gap-1.5 px-3 py-2 border-b border-border bg-background rounded-t-xl"
|
|
178
201
|
style="top: var(--header-height, 74px)"
|
|
179
202
|
>
|
|
180
|
-
|
|
203
|
+
{#if has('undo') || has('redo')}
|
|
204
|
+
<!-- Undo / Redo -->
|
|
181
205
|
<div class="hce-toolbar-group">
|
|
182
|
-
|
|
183
|
-
<button
|
|
184
|
-
type="button"
|
|
185
|
-
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
186
|
-
data-tooltip="굵게"
|
|
187
|
-
aria-label="굵게"
|
|
188
|
-
class={cn(
|
|
189
|
-
"p-1.5 rounded-md transition-colors",
|
|
190
|
-
editor.isActive("bold")
|
|
191
|
-
? "bg-primary/10 text-primary"
|
|
192
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
193
|
-
)}
|
|
194
|
-
>
|
|
195
|
-
<Bold size={iconSize} />
|
|
196
|
-
</button>
|
|
197
|
-
{/if}
|
|
198
|
-
{#if has('italic')}
|
|
199
|
-
<button
|
|
200
|
-
type="button"
|
|
201
|
-
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
202
|
-
data-tooltip="기울임"
|
|
203
|
-
aria-label="기울임"
|
|
204
|
-
class={cn(
|
|
205
|
-
"p-1.5 rounded-md transition-colors",
|
|
206
|
-
editor.isActive("italic")
|
|
207
|
-
? "bg-primary/10 text-primary"
|
|
208
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
209
|
-
)}
|
|
210
|
-
>
|
|
211
|
-
<Italic size={iconSize} />
|
|
212
|
-
</button>
|
|
213
|
-
{/if}
|
|
214
|
-
{#if has('underline')}
|
|
215
|
-
<button
|
|
216
|
-
type="button"
|
|
217
|
-
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
218
|
-
data-tooltip="밑줄"
|
|
219
|
-
aria-label="밑줄"
|
|
220
|
-
class={cn(
|
|
221
|
-
"p-1.5 rounded-md transition-colors",
|
|
222
|
-
editor.isActive("underline")
|
|
223
|
-
? "bg-primary/10 text-primary"
|
|
224
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
225
|
-
)}
|
|
226
|
-
>
|
|
227
|
-
<UnderlineIcon size={iconSize} />
|
|
228
|
-
</button>
|
|
229
|
-
{/if}
|
|
230
|
-
{#if has('strike')}
|
|
231
|
-
<button
|
|
232
|
-
type="button"
|
|
233
|
-
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
234
|
-
data-tooltip="취소선"
|
|
235
|
-
aria-label="취소선"
|
|
236
|
-
class={cn(
|
|
237
|
-
"p-1.5 rounded-md transition-colors",
|
|
238
|
-
editor.isActive("strike")
|
|
239
|
-
? "bg-primary/10 text-primary"
|
|
240
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
241
|
-
)}
|
|
242
|
-
>
|
|
243
|
-
<Strikethrough size={iconSize} />
|
|
244
|
-
</button>
|
|
245
|
-
{/if}
|
|
246
|
-
{#if has('highlight')}
|
|
247
|
-
<button
|
|
248
|
-
type="button"
|
|
249
|
-
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
250
|
-
data-tooltip="하이라이트"
|
|
251
|
-
aria-label="하이라이트"
|
|
252
|
-
class={cn(
|
|
253
|
-
"p-1.5 rounded-md transition-colors",
|
|
254
|
-
editor.isActive("highlight")
|
|
255
|
-
? "bg-primary/10 text-primary"
|
|
256
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
257
|
-
)}
|
|
258
|
-
>
|
|
259
|
-
<Highlighter size={iconSize} />
|
|
260
|
-
</button>
|
|
261
|
-
{/if}
|
|
262
|
-
{#if has('superscript')}
|
|
263
|
-
<button
|
|
264
|
-
type="button"
|
|
265
|
-
onclick={() => editor.chain().focus().toggleSuperscript().run()}
|
|
266
|
-
data-tooltip="위첨자"
|
|
267
|
-
aria-label="위첨자"
|
|
268
|
-
class={cn(
|
|
269
|
-
"p-1.5 rounded-md transition-colors",
|
|
270
|
-
editor.isActive("superscript")
|
|
271
|
-
? "bg-primary/10 text-primary"
|
|
272
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
273
|
-
)}
|
|
274
|
-
>
|
|
275
|
-
<Superscript size={iconSize} />
|
|
276
|
-
</button>
|
|
277
|
-
{/if}
|
|
278
|
-
{#if has('subscript')}
|
|
279
|
-
<button
|
|
280
|
-
type="button"
|
|
281
|
-
onclick={() => editor.chain().focus().toggleSubscript().run()}
|
|
282
|
-
data-tooltip="아래첨자"
|
|
283
|
-
aria-label="아래첨자"
|
|
284
|
-
class={cn(
|
|
285
|
-
"p-1.5 rounded-md transition-colors",
|
|
286
|
-
editor.isActive("subscript")
|
|
287
|
-
? "bg-primary/10 text-primary"
|
|
288
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
289
|
-
)}
|
|
290
|
-
>
|
|
291
|
-
<Subscript size={iconSize} />
|
|
292
|
-
</button>
|
|
293
|
-
{/if}
|
|
294
|
-
{#if has('text-color')}
|
|
295
|
-
<div bind:this={colorMenuEl} class="relative">
|
|
206
|
+
{#if has('undo')}
|
|
296
207
|
<button
|
|
297
208
|
type="button"
|
|
298
|
-
onclick={() => (
|
|
299
|
-
|
|
300
|
-
|
|
209
|
+
onclick={() => editor.chain().focus().undo().run()}
|
|
210
|
+
disabled={!editor.can().undo()}
|
|
211
|
+
data-tooltip="실행 취소"
|
|
212
|
+
aria-label="실행 취소"
|
|
301
213
|
class={cn(
|
|
302
|
-
"
|
|
303
|
-
editor.
|
|
304
|
-
? "bg-primary/10 text-primary"
|
|
305
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
214
|
+
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
215
|
+
!editor.can().undo() && "opacity-30 pointer-events-none",
|
|
306
216
|
)}
|
|
307
217
|
>
|
|
308
|
-
<
|
|
309
|
-
<ChevronDown size={12} />
|
|
218
|
+
<Undo size={iconSize} />
|
|
310
219
|
</button>
|
|
311
|
-
{#if colorMenuOpen}
|
|
312
|
-
<div
|
|
313
|
-
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 p-2"
|
|
314
|
-
style="min-width: 180px"
|
|
315
|
-
>
|
|
316
|
-
<div class="grid grid-cols-3 gap-1.5">
|
|
317
|
-
{#each TEXT_COLORS as c}
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
title={c.label}
|
|
321
|
-
class="h-8 rounded-md border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold"
|
|
322
|
-
style="color: {c.value || '#000'}"
|
|
323
|
-
onclick={() => {
|
|
324
|
-
if (c.value) {
|
|
325
|
-
editor.chain().focus().setColor(c.value).run();
|
|
326
|
-
} else {
|
|
327
|
-
editor.chain().focus().unsetColor().run();
|
|
328
|
-
}
|
|
329
|
-
colorMenuOpen = false;
|
|
330
|
-
}}
|
|
331
|
-
>
|
|
332
|
-
{c.value ? "A" : "×"}
|
|
333
|
-
</button>
|
|
334
|
-
{/each}
|
|
335
|
-
</div>
|
|
336
|
-
<label
|
|
337
|
-
class="mt-2 flex items-center justify-between gap-2 px-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground"
|
|
338
|
-
>
|
|
339
|
-
<span>직접 선택</span>
|
|
340
|
-
<input
|
|
341
|
-
type="color"
|
|
342
|
-
class="h-6 w-10 cursor-pointer rounded border border-border bg-transparent p-0"
|
|
343
|
-
value={(editor.getAttributes("textStyle").color as string) || "#000000"}
|
|
344
|
-
onpointerdown={() => (colorPickerActive = true)}
|
|
345
|
-
onclick={(e) => e.stopPropagation()}
|
|
346
|
-
oninput={(e) => {
|
|
347
|
-
const v = (e.target as HTMLInputElement).value;
|
|
348
|
-
editor.chain().focus().setColor(v).run();
|
|
349
|
-
}}
|
|
350
|
-
onchange={() => {
|
|
351
|
-
setTimeout(() => (colorPickerActive = false), 200);
|
|
352
|
-
}}
|
|
353
|
-
onblur={() => {
|
|
354
|
-
setTimeout(() => (colorPickerActive = false), 200);
|
|
355
|
-
}}
|
|
356
|
-
/>
|
|
357
|
-
</label>
|
|
358
|
-
</div>
|
|
359
220
|
{/if}
|
|
360
|
-
|
|
361
|
-
{/if}
|
|
362
|
-
|
|
363
|
-
</div>
|
|
364
|
-
|
|
365
|
-
{#if has('align-left') || has('align-center') || has('align-right')}
|
|
366
|
-
<!-- Alignment -->
|
|
367
|
-
<div class="hce-toolbar-group">
|
|
368
|
-
{#if has('align-left')}
|
|
369
|
-
<button
|
|
370
|
-
type="button"
|
|
371
|
-
onclick={() => editor.chain().focus().setTextAlign("left").run()}
|
|
372
|
-
data-tooltip="왼쪽 정렬"
|
|
373
|
-
aria-label="왼쪽 정렬"
|
|
374
|
-
class={cn(
|
|
375
|
-
"p-1.5 rounded-md transition-colors",
|
|
376
|
-
editor.isActive({ textAlign: "left" })
|
|
377
|
-
? "bg-primary/10 text-primary"
|
|
378
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
379
|
-
)}
|
|
380
|
-
>
|
|
381
|
-
<AlignLeft size={iconSize} />
|
|
382
|
-
</button>
|
|
383
|
-
{/if}
|
|
384
|
-
{#if has('align-center')}
|
|
385
|
-
<button
|
|
386
|
-
type="button"
|
|
387
|
-
onclick={() => editor.chain().focus().setTextAlign("center").run()}
|
|
388
|
-
data-tooltip="가운데 정렬"
|
|
389
|
-
aria-label="가운데 정렬"
|
|
390
|
-
class={cn(
|
|
391
|
-
"p-1.5 rounded-md transition-colors",
|
|
392
|
-
editor.isActive({ textAlign: "center" })
|
|
393
|
-
? "bg-primary/10 text-primary"
|
|
394
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
395
|
-
)}
|
|
396
|
-
>
|
|
397
|
-
<AlignCenter size={iconSize} />
|
|
398
|
-
</button>
|
|
399
|
-
{/if}
|
|
400
|
-
{#if has('align-right')}
|
|
401
|
-
<button
|
|
402
|
-
type="button"
|
|
403
|
-
onclick={() => editor.chain().focus().setTextAlign("right").run()}
|
|
404
|
-
data-tooltip="오른쪽 정렬"
|
|
405
|
-
aria-label="오른쪽 정렬"
|
|
406
|
-
class={cn(
|
|
407
|
-
"p-1.5 rounded-md transition-colors",
|
|
408
|
-
editor.isActive({ textAlign: "right" })
|
|
409
|
-
? "bg-primary/10 text-primary"
|
|
410
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
411
|
-
)}
|
|
412
|
-
>
|
|
413
|
-
<AlignRight size={iconSize} />
|
|
414
|
-
</button>
|
|
415
|
-
{/if}
|
|
416
|
-
|
|
417
|
-
</div>
|
|
418
|
-
{/if}
|
|
419
|
-
|
|
420
|
-
{#if has('h1') || has('h2') || has('h3')}
|
|
421
|
-
<!-- Headings -->
|
|
422
|
-
<div class="hce-toolbar-group">
|
|
423
|
-
{#if has('h1')}
|
|
424
|
-
<button
|
|
425
|
-
type="button"
|
|
426
|
-
onclick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
427
|
-
data-tooltip="제목 1"
|
|
428
|
-
aria-label="제목 1"
|
|
429
|
-
class={cn(
|
|
430
|
-
"p-1.5 rounded-md transition-colors",
|
|
431
|
-
editor.isActive("heading", { level: 1 })
|
|
432
|
-
? "bg-primary/10 text-primary"
|
|
433
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
434
|
-
)}
|
|
435
|
-
>
|
|
436
|
-
<Heading1 size={iconSize} />
|
|
437
|
-
</button>
|
|
438
|
-
{/if}
|
|
439
|
-
{#if has('h2')}
|
|
440
|
-
<button
|
|
441
|
-
type="button"
|
|
442
|
-
onclick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
443
|
-
data-tooltip="제목 2"
|
|
444
|
-
aria-label="제목 2"
|
|
445
|
-
class={cn(
|
|
446
|
-
"p-1.5 rounded-md transition-colors",
|
|
447
|
-
editor.isActive("heading", { level: 2 })
|
|
448
|
-
? "bg-primary/10 text-primary"
|
|
449
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
450
|
-
)}
|
|
451
|
-
>
|
|
452
|
-
<Heading2 size={iconSize} />
|
|
453
|
-
</button>
|
|
454
|
-
{/if}
|
|
455
|
-
{#if has('h3')}
|
|
456
|
-
<button
|
|
457
|
-
type="button"
|
|
458
|
-
onclick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
459
|
-
data-tooltip="제목 3"
|
|
460
|
-
aria-label="제목 3"
|
|
461
|
-
class={cn(
|
|
462
|
-
"p-1.5 rounded-md transition-colors",
|
|
463
|
-
editor.isActive("heading", { level: 3 })
|
|
464
|
-
? "bg-primary/10 text-primary"
|
|
465
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
466
|
-
)}
|
|
467
|
-
>
|
|
468
|
-
<Heading3 size={iconSize} />
|
|
469
|
-
</button>
|
|
470
|
-
{/if}
|
|
471
|
-
|
|
472
|
-
</div>
|
|
473
|
-
{/if}
|
|
474
|
-
|
|
475
|
-
{#if has('bullet-list') || has('ordered-list') || has('checklist') || has('blockquote') || has('horizontal-rule') || has('toggle')}
|
|
476
|
-
<!-- Lists & Blocks -->
|
|
477
|
-
<div class="hce-toolbar-group">
|
|
478
|
-
{#if has('bullet-list')}
|
|
479
|
-
<button
|
|
480
|
-
type="button"
|
|
481
|
-
onclick={() => editor.chain().focus().toggleBulletList().run()}
|
|
482
|
-
data-tooltip="글머리 목록"
|
|
483
|
-
aria-label="글머리 목록"
|
|
484
|
-
class={cn(
|
|
485
|
-
"p-1.5 rounded-md transition-colors",
|
|
486
|
-
editor.isActive("bulletList")
|
|
487
|
-
? "bg-primary/10 text-primary"
|
|
488
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
489
|
-
)}
|
|
490
|
-
>
|
|
491
|
-
<List size={iconSize} />
|
|
492
|
-
</button>
|
|
493
|
-
{/if}
|
|
494
|
-
{#if has('ordered-list')}
|
|
495
|
-
<button
|
|
496
|
-
type="button"
|
|
497
|
-
onclick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
498
|
-
data-tooltip="번호 목록"
|
|
499
|
-
aria-label="번호 목록"
|
|
500
|
-
class={cn(
|
|
501
|
-
"p-1.5 rounded-md transition-colors",
|
|
502
|
-
editor.isActive("orderedList")
|
|
503
|
-
? "bg-primary/10 text-primary"
|
|
504
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
505
|
-
)}
|
|
506
|
-
>
|
|
507
|
-
<ListOrdered size={iconSize} />
|
|
508
|
-
</button>
|
|
509
|
-
{/if}
|
|
510
|
-
{#if has('checklist')}
|
|
511
|
-
<button
|
|
512
|
-
type="button"
|
|
513
|
-
onclick={() => editor.chain().focus().toggleTaskList().run()}
|
|
514
|
-
data-tooltip="체크리스트"
|
|
515
|
-
aria-label="체크리스트"
|
|
516
|
-
class={cn(
|
|
517
|
-
"p-1.5 rounded-md transition-colors",
|
|
518
|
-
editor.isActive("taskList")
|
|
519
|
-
? "bg-primary/10 text-primary"
|
|
520
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
521
|
-
)}
|
|
522
|
-
>
|
|
523
|
-
<CheckSquare size={iconSize} />
|
|
524
|
-
</button>
|
|
525
|
-
{/if}
|
|
526
|
-
{#if has('blockquote')}
|
|
527
|
-
<button
|
|
528
|
-
type="button"
|
|
529
|
-
onclick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
530
|
-
data-tooltip="인용문"
|
|
531
|
-
aria-label="인용문"
|
|
532
|
-
class={cn(
|
|
533
|
-
"p-1.5 rounded-md transition-colors",
|
|
534
|
-
editor.isActive("blockquote")
|
|
535
|
-
? "bg-primary/10 text-primary"
|
|
536
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
537
|
-
)}
|
|
538
|
-
>
|
|
539
|
-
<Quote size={iconSize} />
|
|
540
|
-
</button>
|
|
541
|
-
{/if}
|
|
542
|
-
{#if has('horizontal-rule')}
|
|
543
|
-
<button
|
|
544
|
-
type="button"
|
|
545
|
-
onclick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
546
|
-
data-tooltip="구분선"
|
|
547
|
-
aria-label="구분선"
|
|
548
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
549
|
-
>
|
|
550
|
-
<Minus size={iconSize} />
|
|
551
|
-
</button>
|
|
552
|
-
{/if}
|
|
553
|
-
{#if has('toggle')}
|
|
554
|
-
<button
|
|
555
|
-
type="button"
|
|
556
|
-
onclick={() => editor.chain().focus().setDetails().run()}
|
|
557
|
-
data-tooltip="토글 (접기/펼치기)"
|
|
558
|
-
aria-label="토글 (접기/펼치기)"
|
|
559
|
-
class={cn(
|
|
560
|
-
"p-1.5 rounded-md transition-colors",
|
|
561
|
-
editor.isActive("details")
|
|
562
|
-
? "bg-primary/10 text-primary"
|
|
563
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
564
|
-
)}
|
|
565
|
-
>
|
|
566
|
-
<ChevronRight size={iconSize} />
|
|
567
|
-
</button>
|
|
568
|
-
{/if}
|
|
569
|
-
|
|
570
|
-
</div>
|
|
571
|
-
{/if}
|
|
572
|
-
|
|
573
|
-
{#if has('link') || has('image') || has('pdf') || has('youtube') || has('video') || has('file') || has('mbus') || has('columns-2') || has('columns-3') || has('table') || has('code-block')}
|
|
574
|
-
<!-- Media & Layout -->
|
|
575
|
-
<div class="hce-toolbar-group">
|
|
576
|
-
{#if has('link')}
|
|
577
|
-
<button
|
|
578
|
-
type="button"
|
|
579
|
-
onclick={addLink}
|
|
580
|
-
data-tooltip="링크"
|
|
581
|
-
aria-label="링크"
|
|
582
|
-
class={cn(
|
|
583
|
-
"p-1.5 rounded-md transition-colors",
|
|
584
|
-
editor.isActive("link")
|
|
585
|
-
? "bg-primary/10 text-primary"
|
|
586
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
587
|
-
)}
|
|
588
|
-
>
|
|
589
|
-
<LinkIcon size={iconSize} />
|
|
590
|
-
</button>
|
|
591
|
-
{/if}
|
|
592
|
-
{#if has('image')}
|
|
593
|
-
<button
|
|
594
|
-
type="button"
|
|
595
|
-
onclick={addImage}
|
|
596
|
-
data-tooltip="이미지"
|
|
597
|
-
aria-label="이미지"
|
|
598
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
599
|
-
>
|
|
600
|
-
<ImageIcon size={iconSize} />
|
|
601
|
-
</button>
|
|
602
|
-
{/if}
|
|
603
|
-
{#if has('pdf')}
|
|
604
|
-
<button
|
|
605
|
-
type="button"
|
|
606
|
-
onclick={onPdfClick}
|
|
607
|
-
data-tooltip="PDF 삽입"
|
|
608
|
-
aria-label="PDF 삽입"
|
|
609
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
610
|
-
>
|
|
611
|
-
<FileText size={iconSize} />
|
|
612
|
-
</button>
|
|
613
|
-
{/if}
|
|
614
|
-
{#if has('youtube')}
|
|
615
|
-
<button
|
|
616
|
-
type="button"
|
|
617
|
-
onclick={() => {
|
|
618
|
-
const url = window.prompt("YouTube URL을 입력하세요");
|
|
619
|
-
if (url) editor.chain().focus().setYoutubeVideo({ src: url }).run();
|
|
620
|
-
}}
|
|
621
|
-
data-tooltip="YouTube 영상"
|
|
622
|
-
aria-label="YouTube 영상"
|
|
623
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
624
|
-
>
|
|
625
|
-
<Video size={iconSize} />
|
|
626
|
-
</button>
|
|
627
|
-
{/if}
|
|
628
|
-
{#if has('mbus')}
|
|
221
|
+
{#if has('redo')}
|
|
629
222
|
<button
|
|
630
223
|
type="button"
|
|
631
|
-
onclick={
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
editor.chain().focus().setMbusVideo({ src: url }).run();
|
|
636
|
-
} else {
|
|
637
|
-
modalState = { type: "mbus" };
|
|
638
|
-
}
|
|
639
|
-
}}
|
|
640
|
-
data-tooltip="미디버스 영상"
|
|
641
|
-
aria-label="미디버스 영상"
|
|
642
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
643
|
-
>
|
|
644
|
-
<Tv size={iconSize} />
|
|
645
|
-
</button>
|
|
646
|
-
{/if}
|
|
647
|
-
{#if has('video') && onVideoClick}
|
|
648
|
-
<button
|
|
649
|
-
type="button"
|
|
650
|
-
onclick={onVideoClick}
|
|
651
|
-
data-tooltip="영상 파일"
|
|
652
|
-
aria-label="영상 파일"
|
|
653
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
654
|
-
>
|
|
655
|
-
<Film size={iconSize} />
|
|
656
|
-
</button>
|
|
657
|
-
{/if}
|
|
658
|
-
{#if has('file') && onFileClick}
|
|
659
|
-
<button
|
|
660
|
-
type="button"
|
|
661
|
-
onclick={onFileClick}
|
|
662
|
-
data-tooltip="파일 첨부"
|
|
663
|
-
aria-label="파일 첨부"
|
|
664
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
665
|
-
>
|
|
666
|
-
<Paperclip size={iconSize} />
|
|
667
|
-
</button>
|
|
668
|
-
{/if}
|
|
669
|
-
|
|
670
|
-
<!-- Columns -->
|
|
671
|
-
{#if has('columns-2')}
|
|
672
|
-
<button
|
|
673
|
-
type="button"
|
|
674
|
-
onclick={() => editor.chain().focus().setColumns(2).run()}
|
|
675
|
-
data-tooltip="2단 컬럼"
|
|
676
|
-
aria-label="2단 컬럼"
|
|
677
|
-
class={cn(
|
|
678
|
-
"p-1.5 rounded-md transition-colors",
|
|
679
|
-
editor.isActive("columns")
|
|
680
|
-
? "bg-primary/10 text-primary"
|
|
681
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
682
|
-
)}
|
|
683
|
-
>
|
|
684
|
-
<Columns2 size={iconSize} />
|
|
685
|
-
</button>
|
|
686
|
-
{/if}
|
|
687
|
-
{#if has('columns-3')}
|
|
688
|
-
<button
|
|
689
|
-
type="button"
|
|
690
|
-
onclick={() => editor.chain().focus().setColumns(3).run()}
|
|
691
|
-
data-tooltip="3단 컬럼"
|
|
692
|
-
aria-label="3단 컬럼"
|
|
693
|
-
class="p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
694
|
-
>
|
|
695
|
-
<Columns3 size={iconSize} />
|
|
696
|
-
</button>
|
|
697
|
-
{/if}
|
|
698
|
-
|
|
699
|
-
<!-- Table menu -->
|
|
700
|
-
{#if has('table')}
|
|
701
|
-
<div bind:this={tableMenuEl} class="relative">
|
|
702
|
-
<button
|
|
703
|
-
type="button"
|
|
704
|
-
onclick={() => (tableMenuOpen = !tableMenuOpen)}
|
|
705
|
-
data-tooltip="표"
|
|
706
|
-
aria-label="표"
|
|
224
|
+
onclick={() => editor.chain().focus().redo().run()}
|
|
225
|
+
disabled={!editor.can().redo()}
|
|
226
|
+
data-tooltip="다시 실행"
|
|
227
|
+
aria-label="다시 실행"
|
|
707
228
|
class={cn(
|
|
708
|
-
"
|
|
709
|
-
|
|
710
|
-
? "bg-primary/10 text-primary"
|
|
711
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
229
|
+
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
230
|
+
!editor.can().redo() && "opacity-30 pointer-events-none",
|
|
712
231
|
)}
|
|
713
232
|
>
|
|
714
|
-
<
|
|
715
|
-
<ChevronDown size={12} />
|
|
233
|
+
<Redo size={iconSize} />
|
|
716
234
|
</button>
|
|
717
|
-
{
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
{/if}
|
|
238
|
+
|
|
239
|
+
{#if hasBlockItems}
|
|
240
|
+
<!-- Block type selector -->
|
|
241
|
+
<div class="hce-toolbar-group">
|
|
242
|
+
<div bind:this={blockMenuEl} class="relative">
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onclick={() => (blockMenuOpen = !blockMenuOpen)}
|
|
246
|
+
data-tooltip="블록 타입"
|
|
247
|
+
aria-label="블록 타입"
|
|
248
|
+
class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground min-w-[96px]"
|
|
721
249
|
>
|
|
722
|
-
{
|
|
250
|
+
<span class="text-sm">{currentBlockLabel}</span>
|
|
251
|
+
<ChevronDown size={12} />
|
|
252
|
+
</button>
|
|
253
|
+
{#if blockMenuOpen}
|
|
254
|
+
<div
|
|
255
|
+
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
256
|
+
style="min-width: 200px"
|
|
257
|
+
onmousedown={(e) => e.preventDefault()}
|
|
258
|
+
role="menu"
|
|
259
|
+
tabindex="-1"
|
|
260
|
+
>
|
|
723
261
|
<button
|
|
724
262
|
type="button"
|
|
725
|
-
class=
|
|
263
|
+
class={cn(
|
|
264
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
265
|
+
!editor.isActive("heading") &&
|
|
266
|
+
!editor.isActive("bulletList") &&
|
|
267
|
+
!editor.isActive("orderedList") &&
|
|
268
|
+
!editor.isActive("taskList") &&
|
|
269
|
+
!editor.isActive("blockquote") &&
|
|
270
|
+
!editor.isActive("codeBlock") &&
|
|
271
|
+
!editor.isActive("details") &&
|
|
272
|
+
"bg-primary/10 text-primary",
|
|
273
|
+
)}
|
|
726
274
|
onclick={() =>
|
|
727
|
-
|
|
728
|
-
editor
|
|
729
|
-
.chain()
|
|
730
|
-
.focus()
|
|
731
|
-
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
732
|
-
.run(),
|
|
733
|
-
)}
|
|
275
|
+
runBlock(() => editor.chain().focus().setParagraph().run())}
|
|
734
276
|
>
|
|
735
|
-
<
|
|
736
|
-
표 삽입 (3x3)
|
|
277
|
+
<Pilcrow size={14} /> 본문
|
|
737
278
|
</button>
|
|
279
|
+
{#if has('h1')}
|
|
738
280
|
<button
|
|
739
281
|
type="button"
|
|
740
|
-
class=
|
|
282
|
+
class={cn(
|
|
283
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
284
|
+
editor.isActive("heading", { level: 1 }) && "bg-primary/10 text-primary",
|
|
285
|
+
)}
|
|
741
286
|
onclick={() =>
|
|
742
|
-
|
|
743
|
-
editor
|
|
744
|
-
.chain()
|
|
745
|
-
.focus()
|
|
746
|
-
.insertTable({ rows: 4, cols: 4, withHeaderRow: true })
|
|
747
|
-
.run(),
|
|
287
|
+
runBlock(() =>
|
|
288
|
+
editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
748
289
|
)}
|
|
749
290
|
>
|
|
750
|
-
<
|
|
751
|
-
헤더 포함 4x4
|
|
291
|
+
<Heading1 size={14} /> 제목 1
|
|
752
292
|
</button>
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
class="px-3 py-1 text-xs font-semibold text-muted-foreground"
|
|
756
|
-
>
|
|
757
|
-
구조
|
|
758
|
-
</p>
|
|
293
|
+
{/if}
|
|
294
|
+
{#if has('h2')}
|
|
759
295
|
<button
|
|
760
296
|
type="button"
|
|
761
|
-
disabled={!editor.can().addRowBefore()}
|
|
762
297
|
class={cn(
|
|
763
|
-
"w-full text-left px-
|
|
764
|
-
|
|
298
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
299
|
+
editor.isActive("heading", { level: 2 }) && "bg-primary/10 text-primary",
|
|
765
300
|
)}
|
|
766
301
|
onclick={() =>
|
|
767
|
-
|
|
768
|
-
editor.chain().focus().
|
|
302
|
+
runBlock(() =>
|
|
303
|
+
editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
769
304
|
)}
|
|
770
305
|
>
|
|
771
|
-
<
|
|
306
|
+
<Heading2 size={14} /> 제목 2
|
|
772
307
|
</button>
|
|
308
|
+
{/if}
|
|
309
|
+
{#if has('h3')}
|
|
773
310
|
<button
|
|
774
311
|
type="button"
|
|
775
|
-
disabled={!editor.can().addRowAfter()}
|
|
776
312
|
class={cn(
|
|
777
|
-
"w-full text-left px-
|
|
778
|
-
|
|
313
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
314
|
+
editor.isActive("heading", { level: 3 }) && "bg-primary/10 text-primary",
|
|
779
315
|
)}
|
|
780
316
|
onclick={() =>
|
|
781
|
-
|
|
782
|
-
editor.chain().focus().
|
|
317
|
+
runBlock(() =>
|
|
318
|
+
editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
783
319
|
)}
|
|
784
320
|
>
|
|
785
|
-
<
|
|
321
|
+
<Heading3 size={14} /> 제목 3
|
|
786
322
|
</button>
|
|
323
|
+
{/if}
|
|
324
|
+
{#if has('bullet-list') || has('ordered-list') || has('checklist')}
|
|
325
|
+
<div class="h-px bg-border my-1"></div>
|
|
326
|
+
{/if}
|
|
327
|
+
{#if has('bullet-list')}
|
|
787
328
|
<button
|
|
788
329
|
type="button"
|
|
789
|
-
disabled={!editor.can().addColumnBefore()}
|
|
790
330
|
class={cn(
|
|
791
|
-
"w-full text-left px-
|
|
792
|
-
|
|
793
|
-
"opacity-30 pointer-events-none",
|
|
331
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
332
|
+
editor.isActive("bulletList") && "bg-primary/10 text-primary",
|
|
794
333
|
)}
|
|
795
334
|
onclick={() =>
|
|
796
|
-
|
|
797
|
-
editor.chain().focus().addColumnBefore().run(),
|
|
798
|
-
)}
|
|
335
|
+
runBlock(() => editor.chain().focus().toggleBulletList().run())}
|
|
799
336
|
>
|
|
800
|
-
<
|
|
337
|
+
<List size={14} /> 글머리 목록
|
|
801
338
|
</button>
|
|
339
|
+
{/if}
|
|
340
|
+
{#if has('ordered-list')}
|
|
802
341
|
<button
|
|
803
342
|
type="button"
|
|
804
|
-
disabled={!editor.can().addColumnAfter()}
|
|
805
343
|
class={cn(
|
|
806
|
-
"w-full text-left px-
|
|
807
|
-
|
|
808
|
-
"opacity-30 pointer-events-none",
|
|
344
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
345
|
+
editor.isActive("orderedList") && "bg-primary/10 text-primary",
|
|
809
346
|
)}
|
|
810
347
|
onclick={() =>
|
|
811
|
-
|
|
812
|
-
editor.chain().focus().addColumnAfter().run(),
|
|
813
|
-
)}
|
|
348
|
+
runBlock(() => editor.chain().focus().toggleOrderedList().run())}
|
|
814
349
|
>
|
|
815
|
-
<
|
|
350
|
+
<ListOrdered size={14} /> 번호 목록
|
|
816
351
|
</button>
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
class="px-3 py-1 text-xs font-semibold text-muted-foreground"
|
|
820
|
-
>
|
|
821
|
-
헤더 / 셀
|
|
822
|
-
</p>
|
|
352
|
+
{/if}
|
|
353
|
+
{#if has('checklist')}
|
|
823
354
|
<button
|
|
824
355
|
type="button"
|
|
825
|
-
disabled={!editor.can().toggleHeaderRow()}
|
|
826
356
|
class={cn(
|
|
827
|
-
"w-full text-left px-
|
|
828
|
-
|
|
829
|
-
"opacity-30 pointer-events-none",
|
|
357
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
358
|
+
editor.isActive("taskList") && "bg-primary/10 text-primary",
|
|
830
359
|
)}
|
|
831
360
|
onclick={() =>
|
|
832
|
-
|
|
833
|
-
editor.chain().focus().toggleHeaderRow().run(),
|
|
834
|
-
)}
|
|
361
|
+
runBlock(() => editor.chain().focus().toggleTaskList().run())}
|
|
835
362
|
>
|
|
836
|
-
<
|
|
363
|
+
<CheckSquare size={14} /> 체크리스트
|
|
837
364
|
</button>
|
|
365
|
+
{/if}
|
|
366
|
+
{#if has('blockquote') || has('toggle')}
|
|
367
|
+
<div class="h-px bg-border my-1"></div>
|
|
368
|
+
{/if}
|
|
369
|
+
{#if has('blockquote')}
|
|
838
370
|
<button
|
|
839
371
|
type="button"
|
|
840
|
-
disabled={!editor.can().toggleHeaderColumn()}
|
|
841
372
|
class={cn(
|
|
842
|
-
"w-full text-left px-
|
|
843
|
-
|
|
844
|
-
"opacity-30 pointer-events-none",
|
|
373
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
374
|
+
editor.isActive("blockquote") && "bg-primary/10 text-primary",
|
|
845
375
|
)}
|
|
846
376
|
onclick={() =>
|
|
847
|
-
|
|
848
|
-
editor.chain().focus().toggleHeaderColumn().run(),
|
|
849
|
-
)}
|
|
377
|
+
runBlock(() => editor.chain().focus().toggleBlockquote().run())}
|
|
850
378
|
>
|
|
851
|
-
<
|
|
379
|
+
<Quote size={14} /> 인용문
|
|
852
380
|
</button>
|
|
381
|
+
{/if}
|
|
382
|
+
{#if has('toggle')}
|
|
853
383
|
<button
|
|
854
384
|
type="button"
|
|
855
|
-
disabled={!editor.can().mergeCells()}
|
|
856
385
|
class={cn(
|
|
857
|
-
"w-full text-left px-
|
|
858
|
-
|
|
386
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
387
|
+
editor.isActive("details") && "bg-primary/10 text-primary",
|
|
859
388
|
)}
|
|
860
389
|
onclick={() =>
|
|
861
|
-
|
|
862
|
-
editor.chain().focus().mergeCells().run(),
|
|
863
|
-
)}
|
|
390
|
+
runBlock(() => editor.chain().focus().setDetails().run())}
|
|
864
391
|
>
|
|
865
|
-
<
|
|
392
|
+
<ChevronRight size={14} /> 토글
|
|
866
393
|
</button>
|
|
394
|
+
{/if}
|
|
395
|
+
</div>
|
|
396
|
+
{/if}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
{/if}
|
|
400
|
+
|
|
401
|
+
{#if has('bold') || has('italic') || has('underline') || has('strike')}
|
|
402
|
+
<!-- Inline marks (core 4) -->
|
|
403
|
+
<div class="hce-toolbar-group">
|
|
404
|
+
{#if has('bold')}
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
408
|
+
data-tooltip="굵게"
|
|
409
|
+
aria-label="굵게"
|
|
410
|
+
class={cn(
|
|
411
|
+
"p-1.5 rounded-md transition-colors",
|
|
412
|
+
editor.isActive("bold")
|
|
413
|
+
? "bg-primary/10 text-primary"
|
|
414
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
415
|
+
)}
|
|
416
|
+
>
|
|
417
|
+
<Bold size={iconSize} />
|
|
418
|
+
</button>
|
|
419
|
+
{/if}
|
|
420
|
+
{#if has('italic')}
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
424
|
+
data-tooltip="기울임"
|
|
425
|
+
aria-label="기울임"
|
|
426
|
+
class={cn(
|
|
427
|
+
"p-1.5 rounded-md transition-colors",
|
|
428
|
+
editor.isActive("italic")
|
|
429
|
+
? "bg-primary/10 text-primary"
|
|
430
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
431
|
+
)}
|
|
432
|
+
>
|
|
433
|
+
<Italic size={iconSize} />
|
|
434
|
+
</button>
|
|
435
|
+
{/if}
|
|
436
|
+
{#if has('underline')}
|
|
437
|
+
<button
|
|
438
|
+
type="button"
|
|
439
|
+
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
440
|
+
data-tooltip="밑줄"
|
|
441
|
+
aria-label="밑줄"
|
|
442
|
+
class={cn(
|
|
443
|
+
"p-1.5 rounded-md transition-colors",
|
|
444
|
+
editor.isActive("underline")
|
|
445
|
+
? "bg-primary/10 text-primary"
|
|
446
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
447
|
+
)}
|
|
448
|
+
>
|
|
449
|
+
<UnderlineIcon size={iconSize} />
|
|
450
|
+
</button>
|
|
451
|
+
{/if}
|
|
452
|
+
{#if has('strike')}
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
456
|
+
data-tooltip="취소선"
|
|
457
|
+
aria-label="취소선"
|
|
458
|
+
class={cn(
|
|
459
|
+
"p-1.5 rounded-md transition-colors",
|
|
460
|
+
editor.isActive("strike")
|
|
461
|
+
? "bg-primary/10 text-primary"
|
|
462
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
463
|
+
)}
|
|
464
|
+
>
|
|
465
|
+
<Strikethrough size={iconSize} />
|
|
466
|
+
</button>
|
|
467
|
+
{/if}
|
|
468
|
+
{#if has('text-color')}
|
|
469
|
+
<div bind:this={colorMenuEl} class="relative">
|
|
470
|
+
<button
|
|
471
|
+
type="button"
|
|
472
|
+
onclick={() => (colorMenuOpen = !colorMenuOpen)}
|
|
473
|
+
data-tooltip="글자색"
|
|
474
|
+
aria-label="글자색"
|
|
475
|
+
class={cn(
|
|
476
|
+
"flex items-center gap-0.5 p-1.5 rounded-md transition-colors",
|
|
477
|
+
editor.getAttributes('textStyle').color
|
|
478
|
+
? "bg-primary/10 text-primary"
|
|
479
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
480
|
+
)}
|
|
481
|
+
>
|
|
482
|
+
<Palette size={iconSize} />
|
|
483
|
+
<ChevronDown size={12} />
|
|
484
|
+
</button>
|
|
485
|
+
{#if colorMenuOpen}
|
|
486
|
+
<div
|
|
487
|
+
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 p-2"
|
|
488
|
+
style="min-width: 180px"
|
|
489
|
+
onmousedown={(e) => e.preventDefault()}
|
|
490
|
+
role="menu"
|
|
491
|
+
tabindex="-1"
|
|
492
|
+
>
|
|
493
|
+
<div class="grid grid-cols-3 gap-1.5">
|
|
494
|
+
{#each TEXT_COLORS as c}
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
title={c.label}
|
|
498
|
+
class="h-8 rounded-md border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold bg-background"
|
|
499
|
+
style="color: {c.value || '#6b7280'}"
|
|
500
|
+
onclick={() => {
|
|
501
|
+
if (c.value) {
|
|
502
|
+
editor.chain().focus().setColor(c.value).run();
|
|
503
|
+
} else {
|
|
504
|
+
editor.chain().focus().unsetColor().run();
|
|
505
|
+
}
|
|
506
|
+
colorMenuOpen = false;
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
{c.value ? "A" : "×"}
|
|
510
|
+
</button>
|
|
511
|
+
{/each}
|
|
512
|
+
</div>
|
|
513
|
+
<label
|
|
514
|
+
class="mt-2 flex items-center justify-between gap-2 px-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground"
|
|
515
|
+
>
|
|
516
|
+
<span>직접 선택</span>
|
|
517
|
+
<input
|
|
518
|
+
type="color"
|
|
519
|
+
class="h-6 w-10 cursor-pointer rounded border border-border bg-transparent p-0"
|
|
520
|
+
value={(editor.getAttributes('textStyle').color as string) || '#000000'}
|
|
521
|
+
onclick={(e) => e.stopPropagation()}
|
|
522
|
+
oninput={(e) => {
|
|
523
|
+
const v = (e.target as HTMLInputElement).value;
|
|
524
|
+
editor.chain().focus().setColor(v).run();
|
|
525
|
+
}}
|
|
526
|
+
/>
|
|
527
|
+
</label>
|
|
528
|
+
</div>
|
|
529
|
+
{/if}
|
|
530
|
+
</div>
|
|
531
|
+
{/if}
|
|
532
|
+
</div>
|
|
533
|
+
{/if}
|
|
534
|
+
|
|
535
|
+
{#if has('align-left') || has('align-center') || has('align-right')}
|
|
536
|
+
<!-- Alignment -->
|
|
537
|
+
<div class="hce-toolbar-group">
|
|
538
|
+
{#if has('align-left')}
|
|
539
|
+
<button
|
|
540
|
+
type="button"
|
|
541
|
+
onclick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
542
|
+
data-tooltip="왼쪽 정렬"
|
|
543
|
+
aria-label="왼쪽 정렬"
|
|
544
|
+
class={cn(
|
|
545
|
+
"p-1.5 rounded-md transition-colors",
|
|
546
|
+
editor.isActive({ textAlign: 'left' })
|
|
547
|
+
? "bg-primary/10 text-primary"
|
|
548
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
549
|
+
)}
|
|
550
|
+
>
|
|
551
|
+
<AlignLeft size={iconSize} />
|
|
552
|
+
</button>
|
|
553
|
+
{/if}
|
|
554
|
+
{#if has('align-center')}
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
onclick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
558
|
+
data-tooltip="가운데 정렬"
|
|
559
|
+
aria-label="가운데 정렬"
|
|
560
|
+
class={cn(
|
|
561
|
+
"p-1.5 rounded-md transition-colors",
|
|
562
|
+
editor.isActive({ textAlign: 'center' })
|
|
563
|
+
? "bg-primary/10 text-primary"
|
|
564
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
565
|
+
)}
|
|
566
|
+
>
|
|
567
|
+
<AlignCenter size={iconSize} />
|
|
568
|
+
</button>
|
|
569
|
+
{/if}
|
|
570
|
+
{#if has('align-right')}
|
|
571
|
+
<button
|
|
572
|
+
type="button"
|
|
573
|
+
onclick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
574
|
+
data-tooltip="오른쪽 정렬"
|
|
575
|
+
aria-label="오른쪽 정렬"
|
|
576
|
+
class={cn(
|
|
577
|
+
"p-1.5 rounded-md transition-colors",
|
|
578
|
+
editor.isActive({ textAlign: 'right' })
|
|
579
|
+
? "bg-primary/10 text-primary"
|
|
580
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
581
|
+
)}
|
|
582
|
+
>
|
|
583
|
+
<AlignRight size={iconSize} />
|
|
584
|
+
</button>
|
|
585
|
+
{/if}
|
|
586
|
+
</div>
|
|
587
|
+
{/if}
|
|
588
|
+
|
|
589
|
+
{#if hasInsertItems}
|
|
590
|
+
<!-- Insert + dropdown -->
|
|
591
|
+
<div class="hce-toolbar-group">
|
|
592
|
+
<div bind:this={insertMenuEl} class="relative">
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
onclick={() => (insertMenuOpen = !insertMenuOpen)}
|
|
596
|
+
data-tooltip="삽입"
|
|
597
|
+
aria-label="삽입"
|
|
598
|
+
class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
599
|
+
>
|
|
600
|
+
<Plus size={iconSize} />
|
|
601
|
+
<span class="text-sm">삽입</span>
|
|
602
|
+
<ChevronDown size={12} />
|
|
603
|
+
</button>
|
|
604
|
+
{#if insertMenuOpen}
|
|
605
|
+
<div
|
|
606
|
+
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
607
|
+
style="min-width: 220px"
|
|
608
|
+
>
|
|
609
|
+
{#if has('code-block')}
|
|
867
610
|
<button
|
|
868
611
|
type="button"
|
|
869
|
-
|
|
870
|
-
class={cn(
|
|
871
|
-
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
|
|
872
|
-
!editor.can().splitCell() && "opacity-30 pointer-events-none",
|
|
873
|
-
)}
|
|
612
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
874
613
|
onclick={() =>
|
|
875
|
-
|
|
876
|
-
editor.chain().focus().splitCell().run(),
|
|
877
|
-
)}
|
|
614
|
+
runInsert(() => editor.chain().focus().setCodeBlock().run())}
|
|
878
615
|
>
|
|
879
|
-
<
|
|
616
|
+
<Code2 size={14} /> 코드 블록
|
|
880
617
|
</button>
|
|
618
|
+
{/if}
|
|
619
|
+
{#if has('pdf')}
|
|
881
620
|
<button
|
|
882
621
|
type="button"
|
|
883
|
-
class="w-full text-left px-
|
|
884
|
-
onclick={() =>
|
|
885
|
-
tableMenuOpen = false;
|
|
886
|
-
modalState = { type: "cellBg" };
|
|
887
|
-
}}
|
|
622
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
623
|
+
onclick={() => runInsert(onPdfClick)}
|
|
888
624
|
>
|
|
889
|
-
<
|
|
625
|
+
<FileText size={14} /> PDF
|
|
890
626
|
</button>
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
627
|
+
{/if}
|
|
628
|
+
{#if has('file') && onFileClick}
|
|
629
|
+
<button
|
|
630
|
+
type="button"
|
|
631
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
632
|
+
onclick={() => runInsert(onFileClick!)}
|
|
894
633
|
>
|
|
895
|
-
|
|
896
|
-
</
|
|
634
|
+
<Paperclip size={14} /> 파일 첨부
|
|
635
|
+
</button>
|
|
636
|
+
{/if}
|
|
637
|
+
{#if has('table')}
|
|
897
638
|
<button
|
|
898
639
|
type="button"
|
|
899
|
-
|
|
900
|
-
class={cn(
|
|
901
|
-
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 text-destructive hover:bg-destructive/10",
|
|
902
|
-
!editor.can().deleteRow() && "opacity-30 pointer-events-none",
|
|
903
|
-
)}
|
|
640
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
904
641
|
onclick={() =>
|
|
905
|
-
|
|
906
|
-
editor
|
|
642
|
+
runInsert(() =>
|
|
643
|
+
editor
|
|
644
|
+
.chain()
|
|
645
|
+
.focus()
|
|
646
|
+
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
647
|
+
.run(),
|
|
907
648
|
)}
|
|
908
649
|
>
|
|
909
|
-
<
|
|
650
|
+
<TableIcon size={14} /> 표 (3x3)
|
|
910
651
|
</button>
|
|
652
|
+
{/if}
|
|
653
|
+
|
|
654
|
+
{#if has('horizontal-rule') || has('columns-2') || has('columns-3')}
|
|
655
|
+
<div class="h-px bg-border my-1"></div>
|
|
656
|
+
{/if}
|
|
657
|
+
{#if has('horizontal-rule')}
|
|
911
658
|
<button
|
|
912
659
|
type="button"
|
|
913
|
-
|
|
914
|
-
class={cn(
|
|
915
|
-
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 text-destructive hover:bg-destructive/10",
|
|
916
|
-
!editor.can().deleteColumn() && "opacity-30 pointer-events-none",
|
|
917
|
-
)}
|
|
660
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
918
661
|
onclick={() =>
|
|
919
|
-
|
|
920
|
-
editor.chain().focus().
|
|
662
|
+
runInsert(() =>
|
|
663
|
+
editor.chain().focus().setHorizontalRule().run(),
|
|
921
664
|
)}
|
|
922
665
|
>
|
|
923
|
-
<
|
|
666
|
+
<Minus size={14} /> 구분선
|
|
924
667
|
</button>
|
|
668
|
+
{/if}
|
|
669
|
+
{#if has('columns-2')}
|
|
925
670
|
<button
|
|
926
671
|
type="button"
|
|
927
|
-
|
|
928
|
-
class={cn(
|
|
929
|
-
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 text-destructive hover:bg-destructive/10",
|
|
930
|
-
!editor.can().deleteTable() && "opacity-30 pointer-events-none",
|
|
931
|
-
)}
|
|
672
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
932
673
|
onclick={() =>
|
|
933
|
-
|
|
934
|
-
editor.chain().focus().deleteTable().run(),
|
|
935
|
-
)}
|
|
674
|
+
runInsert(() => editor.chain().focus().setColumns(2).run())}
|
|
936
675
|
>
|
|
937
|
-
<
|
|
676
|
+
<Columns2 size={14} /> 2단 컬럼
|
|
938
677
|
</button>
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
data-tooltip="코드 블록"
|
|
951
|
-
aria-label="코드 블록"
|
|
952
|
-
class={cn(
|
|
953
|
-
"p-1.5 rounded-md transition-colors",
|
|
954
|
-
editor.isActive("codeBlock")
|
|
955
|
-
? "bg-primary/10 text-primary"
|
|
956
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
957
|
-
)}
|
|
958
|
-
>
|
|
959
|
-
<Code2 size={iconSize} />
|
|
960
|
-
</button>
|
|
961
|
-
{/if}
|
|
678
|
+
{/if}
|
|
679
|
+
{#if has('columns-3')}
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
683
|
+
onclick={() =>
|
|
684
|
+
runInsert(() => editor.chain().focus().setColumns(3).run())}
|
|
685
|
+
>
|
|
686
|
+
<Columns3 size={14} /> 3단 컬럼
|
|
687
|
+
</button>
|
|
688
|
+
{/if}
|
|
962
689
|
|
|
690
|
+
{#if has('image') || has('link') || has('mbus')}
|
|
691
|
+
<div class="h-px bg-border my-1"></div>
|
|
692
|
+
{/if}
|
|
693
|
+
{#if has('image')}
|
|
694
|
+
<button
|
|
695
|
+
type="button"
|
|
696
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
697
|
+
onclick={addImage}
|
|
698
|
+
>
|
|
699
|
+
<ImageIcon size={14} /> 이미지
|
|
700
|
+
</button>
|
|
701
|
+
{/if}
|
|
702
|
+
{#if has('link')}
|
|
703
|
+
<button
|
|
704
|
+
type="button"
|
|
705
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
706
|
+
onclick={addLink}
|
|
707
|
+
>
|
|
708
|
+
<LinkIcon size={14} /> 링크
|
|
709
|
+
</button>
|
|
710
|
+
{/if}
|
|
711
|
+
{#if has('mbus')}
|
|
712
|
+
<button
|
|
713
|
+
type="button"
|
|
714
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
715
|
+
onclick={addMbus}
|
|
716
|
+
>
|
|
717
|
+
<Tv size={14} /> 미디버스 영상
|
|
718
|
+
</button>
|
|
719
|
+
{/if}
|
|
720
|
+
</div>
|
|
721
|
+
{/if}
|
|
963
722
|
</div>
|
|
964
|
-
{/if}
|
|
965
|
-
|
|
966
|
-
{#if has('undo') || has('redo')}
|
|
967
|
-
<!-- Undo / Redo -->
|
|
968
|
-
<div class="hce-toolbar-group">
|
|
969
|
-
|
|
970
|
-
{#if has('undo')}
|
|
971
|
-
<button
|
|
972
|
-
type="button"
|
|
973
|
-
onclick={() => editor.chain().focus().undo().run()}
|
|
974
|
-
disabled={!editor.can().undo()}
|
|
975
|
-
data-tooltip="실행 취소"
|
|
976
|
-
aria-label="실행 취소"
|
|
977
|
-
class={cn(
|
|
978
|
-
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
979
|
-
!editor.can().undo() && "opacity-30 pointer-events-none",
|
|
980
|
-
)}
|
|
981
|
-
>
|
|
982
|
-
<Undo size={iconSize} />
|
|
983
|
-
</button>
|
|
984
|
-
{/if}
|
|
985
|
-
{#if has('redo')}
|
|
986
|
-
<button
|
|
987
|
-
type="button"
|
|
988
|
-
onclick={() => editor.chain().focus().redo().run()}
|
|
989
|
-
disabled={!editor.can().redo()}
|
|
990
|
-
data-tooltip="다시 실행"
|
|
991
|
-
aria-label="다시 실행"
|
|
992
|
-
class={cn(
|
|
993
|
-
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
994
|
-
!editor.can().redo() && "opacity-30 pointer-events-none",
|
|
995
|
-
)}
|
|
996
|
-
>
|
|
997
|
-
<Redo size={iconSize} />
|
|
998
|
-
</button>
|
|
999
|
-
{/if}
|
|
1000
|
-
|
|
1001
723
|
</div>
|
|
1002
724
|
{/if}
|
|
1003
725
|
|
|
@@ -1041,47 +763,6 @@
|
|
|
1041
763
|
onCancel={() => (modalState = null)}
|
|
1042
764
|
/>
|
|
1043
765
|
{/if}
|
|
1044
|
-
{#if modalState?.type === "cellBg"}
|
|
1045
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
1046
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
1047
|
-
<div
|
|
1048
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
|
1049
|
-
onclick={() => (modalState = null)}
|
|
1050
|
-
>
|
|
1051
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
1052
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
1053
|
-
<div
|
|
1054
|
-
class="bg-popover border border-border rounded-xl shadow-lg p-4"
|
|
1055
|
-
style="min-width: 240px"
|
|
1056
|
-
onclick={(e) => e.stopPropagation()}
|
|
1057
|
-
>
|
|
1058
|
-
<p class="text-sm font-semibold mb-3">셀 배경색</p>
|
|
1059
|
-
<div class="grid grid-cols-3 gap-2">
|
|
1060
|
-
{#each CELL_COLORS as c}
|
|
1061
|
-
<button
|
|
1062
|
-
type="button"
|
|
1063
|
-
title={c.label}
|
|
1064
|
-
class="h-9 rounded-lg border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs"
|
|
1065
|
-
style="background: {c.value ||
|
|
1066
|
-
'#fff'}; {!c.value
|
|
1067
|
-
? 'background-image: linear-gradient(135deg, transparent 45%, #ef4444 45%, #ef4444 55%, transparent 55%)'
|
|
1068
|
-
: ''}"
|
|
1069
|
-
onclick={() => {
|
|
1070
|
-
editor
|
|
1071
|
-
.chain()
|
|
1072
|
-
.focus()
|
|
1073
|
-
.setCellAttribute("backgroundColor", c.value || null)
|
|
1074
|
-
.run();
|
|
1075
|
-
modalState = null;
|
|
1076
|
-
}}
|
|
1077
|
-
>
|
|
1078
|
-
{c.value ? "" : ""}
|
|
1079
|
-
</button>
|
|
1080
|
-
{/each}
|
|
1081
|
-
</div>
|
|
1082
|
-
</div>
|
|
1083
|
-
</div>
|
|
1084
|
-
{/if}
|
|
1085
766
|
</div>
|
|
1086
767
|
|
|
1087
768
|
<style>
|