@teriusu/rich-editor 0.7.0 → 0.7.2

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.
Files changed (33) hide show
  1. package/dist/components/BubbleToolbar.svelte +169 -118
  2. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  3. package/dist/components/FixedToolbar.svelte +410 -814
  4. package/dist/components/FixedToolbar.svelte.d.ts +0 -1
  5. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  6. package/dist/components/SlashCommandMenu.svelte +28 -25
  7. package/dist/components/SlashCommandMenu.svelte.d.ts +1 -1
  8. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  9. package/dist/components/TipTapEditor.svelte +2 -64
  10. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  11. package/dist/extensions/Columns.d.ts.map +1 -1
  12. package/dist/extensions/Columns.js +35 -0
  13. package/dist/extensions/FixedDetails.d.ts.map +1 -1
  14. package/dist/extensions/FixedDetails.js +11 -0
  15. package/dist/extensions/MbusVideo.d.ts.map +1 -1
  16. package/dist/extensions/MbusVideo.js +15 -41
  17. package/dist/index.d.ts +0 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +0 -1
  20. package/dist/styles/editor.css +1 -1
  21. package/dist/types.d.ts +1 -1
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/types.js +8 -7
  24. package/dist/utils/sanitize.d.ts.map +1 -1
  25. package/dist/utils/sanitize.js +0 -3
  26. package/dist/utils/sanitize.test.js +0 -7
  27. package/package.json +1 -2
  28. package/dist/extensions/VideoBlock.d.ts +0 -16
  29. package/dist/extensions/VideoBlock.d.ts.map +0 -1
  30. package/dist/extensions/VideoBlock.js +0 -87
  31. package/dist/extensions/VideoBlock.test.d.ts +0 -2
  32. package/dist/extensions/VideoBlock.test.d.ts.map +0 -1
  33. 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,24 +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,
44
- Palette,
32
+ Plus,
33
+ Pilcrow,
45
34
  } from "lucide-svelte";
46
35
  import { cn } from "../utils/cn";
47
36
  import InputModal from "./InputModal.svelte";
@@ -52,7 +41,6 @@
52
41
  features,
53
42
  onPdfClick,
54
43
  onFileClick,
55
- onVideoClick,
56
44
  onPromptLink,
57
45
  onPromptImage,
58
46
  onPromptMbus,
@@ -61,7 +49,6 @@
61
49
  features: Set<ToolbarFeature>;
62
50
  onPdfClick: () => void;
63
51
  onFileClick?: () => void;
64
- onVideoClick?: () => void;
65
52
  onPromptLink?: PromptHandler;
66
53
  onPromptImage?: PromptHandler;
67
54
  onPromptMbus?: PromptHandler;
@@ -71,46 +58,24 @@
71
58
 
72
59
  const iconSize = 16;
73
60
 
74
- let codeMenuOpen = $state(false);
75
- let tableMenuOpen = $state(false);
76
- let colorMenuOpen = $state(false);
77
- let colorPickerActive = $state(false);
78
- let modalState: { type: "link" | "image" | "cellBg" | "mbus" } | null = $state(null);
79
- let codeMenuEl: HTMLDivElement | undefined = $state();
80
- let tableMenuEl: HTMLDivElement | undefined = $state();
81
- let colorMenuEl: HTMLDivElement | undefined = $state();
82
-
83
- const TEXT_COLORS = [
84
- { label: "기본", value: "" },
85
- { label: "검정", value: "#000000" },
86
- { label: "회색", value: "#6b7280" },
87
- { label: "빨강", value: "#dc2626" },
88
- { label: "주황", value: "#ea580c" },
89
- { label: "노랑", value: "#ca8a04" },
90
- { label: "초록", value: "#16a34a" },
91
- { label: "파랑", value: "#2563eb" },
92
- { label: "보라", value: "#7c3aed" }
93
- ];
61
+ let blockMenuOpen = $state(false);
62
+ let insertMenuOpen = $state(false);
63
+ let modalState: { type: "link" | "image" | "mbus" } | null = $state(null);
64
+ let blockMenuEl: HTMLDivElement | undefined = $state();
65
+ let insertMenuEl: HTMLDivElement | undefined = $state();
94
66
 
95
67
  $effect(() => {
96
- if (!codeMenuOpen && !tableMenuOpen && !colorMenuOpen) return;
68
+ if (!blockMenuOpen && !insertMenuOpen) return;
97
69
  function handleClick(e: MouseEvent) {
98
- if (codeMenuEl && !codeMenuEl.contains(e.target as Node))
99
- codeMenuOpen = false;
100
- if (tableMenuEl && !tableMenuEl.contains(e.target as Node))
101
- tableMenuOpen = false;
102
- if (
103
- colorMenuEl &&
104
- !colorMenuEl.contains(e.target as Node) &&
105
- !colorPickerActive
106
- )
107
- colorMenuOpen = false;
70
+ if (blockMenuEl && !blockMenuEl.contains(e.target as Node))
71
+ blockMenuOpen = false;
72
+ if (insertMenuEl && !insertMenuEl.contains(e.target as Node))
73
+ insertMenuOpen = false;
108
74
  }
109
75
  function handleKeyDown(e: KeyboardEvent) {
110
76
  if (e.key === "Escape") {
111
- codeMenuOpen = false;
112
- tableMenuOpen = false;
113
- colorMenuOpen = false;
77
+ blockMenuOpen = false;
78
+ insertMenuOpen = false;
114
79
  }
115
80
  }
116
81
  document.addEventListener("mousedown", handleClick);
@@ -122,6 +87,7 @@
122
87
  });
123
88
 
124
89
  async function addLink() {
90
+ insertMenuOpen = false;
125
91
  if (onPromptLink) {
126
92
  const previous = editor.isActive("link")
127
93
  ? (editor.getAttributes("link").href as string) || ""
@@ -144,6 +110,7 @@
144
110
  }
145
111
 
146
112
  async function addImage() {
113
+ insertMenuOpen = false;
147
114
  if (onPromptImage) {
148
115
  const url = await onPromptImage("");
149
116
  if (!url) return;
@@ -153,851 +120,521 @@
153
120
  modalState = { type: "image" };
154
121
  }
155
122
 
156
- function runTableCommand(command: () => boolean) {
157
- command();
158
- tableMenuOpen = false;
123
+ async function addMbus() {
124
+ insertMenuOpen = false;
125
+ if (onPromptMbus) {
126
+ const url = await onPromptMbus("");
127
+ if (!url) return;
128
+ editor.chain().focus().setMbusVideo({ src: url }).run();
129
+ return;
130
+ }
131
+ modalState = { type: "mbus" };
132
+ }
133
+
134
+ const currentBlockLabel = $derived.by(() => {
135
+ if (editor.isActive("heading", { level: 1 })) return "제목 1";
136
+ if (editor.isActive("heading", { level: 2 })) return "제목 2";
137
+ if (editor.isActive("heading", { level: 3 })) return "제목 3";
138
+ if (editor.isActive("bulletList")) return "글머리 목록";
139
+ if (editor.isActive("orderedList")) return "번호 목록";
140
+ if (editor.isActive("taskList")) return "체크리스트";
141
+ if (editor.isActive("blockquote")) return "인용문";
142
+ if (editor.isActive("details")) return "토글";
143
+ return "본문";
144
+ });
145
+
146
+ function runBlock(fn: () => void) {
147
+ fn();
148
+ blockMenuOpen = false;
149
+ }
150
+
151
+ function runInsert(fn: () => void) {
152
+ fn();
153
+ insertMenuOpen = false;
159
154
  }
160
155
 
161
- const isInTable = $derived(editor.isActive("table"));
156
+ const hasInsertItems = $derived(
157
+ has("image") ||
158
+ has("link") ||
159
+ has("pdf") ||
160
+ has("file") ||
161
+ has("mbus") ||
162
+ has("table") ||
163
+ has("columns-2") ||
164
+ has("columns-3") ||
165
+ has("horizontal-rule") ||
166
+ has("code-block"),
167
+ );
162
168
 
163
- const CELL_COLORS = [
164
- { label: "없음", value: "" },
165
- { label: "밝은 회색", value: "#f1f5f9" },
166
- { label: "밝은 파랑", value: "#dbeafe" },
167
- { label: "밝은 초록", value: "#dcfce7" },
168
- { label: "밝은 노랑", value: "#fef9c3" },
169
- { label: "밝은 주황", value: "#ffedd5" },
170
- { label: "밝은 빨강", value: "#fee2e2" },
171
- { label: "밝은 보라", value: "#ede9fe" },
172
- { label: "밝은 분홍", value: "#fce7f3" },
173
- ];
169
+ const hasBlockItems = $derived(
170
+ has("h1") ||
171
+ has("h2") ||
172
+ has("h3") ||
173
+ has("bullet-list") ||
174
+ has("ordered-list") ||
175
+ has("checklist") ||
176
+ has("blockquote") ||
177
+ has("toggle"),
178
+ );
174
179
  </script>
175
180
 
176
181
  <div
177
182
  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
183
  style="top: var(--header-height, 74px)"
179
184
  >
180
- <!-- Format -->
185
+ {#if has('undo') || has('redo')}
186
+ <!-- Undo / Redo -->
181
187
  <div class="hce-toolbar-group">
182
- {#if has('bold')}
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">
188
+ {#if has('undo')}
296
189
  <button
297
190
  type="button"
298
- onclick={() => (colorMenuOpen = !colorMenuOpen)}
299
- data-tooltip="글자색"
300
- aria-label="글자색"
191
+ onclick={() => editor.chain().focus().undo().run()}
192
+ disabled={!editor.can().undo()}
193
+ data-tooltip="실행 취소"
194
+ aria-label="실행 취소"
301
195
  class={cn(
302
- "flex items-center gap-0.5 p-1.5 rounded-md transition-colors",
303
- editor.getAttributes("textStyle").color
304
- ? "bg-primary/10 text-primary"
305
- : "text-muted-foreground hover:bg-muted hover:text-foreground",
196
+ "p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
197
+ !editor.can().undo() && "opacity-30 pointer-events-none",
306
198
  )}
307
199
  >
308
- <Palette size={iconSize} />
309
- <ChevronDown size={12} />
200
+ <Undo size={iconSize} />
310
201
  </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
202
  {/if}
360
- </div>
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')}
629
- <button
630
- type="button"
631
- onclick={async () => {
632
- if (onPromptMbus) {
633
- const url = await onPromptMbus("");
634
- if (!url) return;
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">
203
+ {#if has('redo')}
702
204
  <button
703
205
  type="button"
704
- onclick={() => (tableMenuOpen = !tableMenuOpen)}
705
- data-tooltip="표"
706
- aria-label="표"
206
+ onclick={() => editor.chain().focus().redo().run()}
207
+ disabled={!editor.can().redo()}
208
+ data-tooltip="다시 실행"
209
+ aria-label="다시 실행"
707
210
  class={cn(
708
- "flex items-center gap-0.5 p-1.5 rounded-md transition-colors",
709
- isInTable
710
- ? "bg-primary/10 text-primary"
711
- : "text-muted-foreground hover:bg-muted hover:text-foreground",
211
+ "p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
212
+ !editor.can().redo() && "opacity-30 pointer-events-none",
712
213
  )}
713
214
  >
714
- <TableIcon size={iconSize} />
715
- <ChevronDown size={12} />
215
+ <Redo size={iconSize} />
716
216
  </button>
717
- {#if tableMenuOpen}
718
- <div
719
- class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
720
- style="min-width: 220px"
217
+ {/if}
218
+ </div>
219
+ {/if}
220
+
221
+ {#if hasBlockItems}
222
+ <!-- Block type selector -->
223
+ <div class="hce-toolbar-group">
224
+ <div bind:this={blockMenuEl} class="relative">
225
+ <button
226
+ type="button"
227
+ onclick={() => (blockMenuOpen = !blockMenuOpen)}
228
+ data-tooltip="블록 타입"
229
+ aria-label="블록 타입"
230
+ 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
231
  >
722
- {#if !isInTable}
232
+ <span class="text-sm">{currentBlockLabel}</span>
233
+ <ChevronDown size={12} />
234
+ </button>
235
+ {#if blockMenuOpen}
236
+ <div
237
+ class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
238
+ style="min-width: 200px"
239
+ >
723
240
  <button
724
241
  type="button"
725
- class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
242
+ class={cn(
243
+ "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
244
+ !editor.isActive("heading") &&
245
+ !editor.isActive("bulletList") &&
246
+ !editor.isActive("orderedList") &&
247
+ !editor.isActive("taskList") &&
248
+ !editor.isActive("blockquote") &&
249
+ !editor.isActive("codeBlock") &&
250
+ !editor.isActive("details") &&
251
+ "bg-primary/10 text-primary",
252
+ )}
726
253
  onclick={() =>
727
- runTableCommand(() =>
728
- editor
729
- .chain()
730
- .focus()
731
- .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
732
- .run(),
733
- )}
254
+ runBlock(() => editor.chain().focus().setParagraph().run())}
734
255
  >
735
- <TableIcon size={14} />
736
- 표 삽입 (3x3)
256
+ <Pilcrow size={14} /> 본문
737
257
  </button>
258
+ {#if has('h1')}
738
259
  <button
739
260
  type="button"
740
- class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
261
+ class={cn(
262
+ "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
263
+ editor.isActive("heading", { level: 1 }) && "bg-primary/10 text-primary",
264
+ )}
741
265
  onclick={() =>
742
- runTableCommand(() =>
743
- editor
744
- .chain()
745
- .focus()
746
- .insertTable({ rows: 4, cols: 4, withHeaderRow: true })
747
- .run(),
266
+ runBlock(() =>
267
+ editor.chain().focus().toggleHeading({ level: 1 }).run(),
748
268
  )}
749
269
  >
750
- <PanelTop size={14} />
751
- 헤더 포함 4x4
270
+ <Heading1 size={14} /> 제목 1
752
271
  </button>
753
- {:else}
754
- <p
755
- class="px-3 py-1 text-xs font-semibold text-muted-foreground"
756
- >
757
- 구조
758
- </p>
272
+ {/if}
273
+ {#if has('h2')}
759
274
  <button
760
275
  type="button"
761
- disabled={!editor.can().addRowBefore()}
762
276
  class={cn(
763
277
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
764
- !editor.can().addRowBefore() && "opacity-30 pointer-events-none",
278
+ editor.isActive("heading", { level: 2 }) && "bg-primary/10 text-primary",
765
279
  )}
766
280
  onclick={() =>
767
- runTableCommand(() =>
768
- editor.chain().focus().addRowBefore().run(),
281
+ runBlock(() =>
282
+ editor.chain().focus().toggleHeading({ level: 2 }).run(),
769
283
  )}
770
284
  >
771
- <Rows3 size={14} /> 행 위에 추가
285
+ <Heading2 size={14} /> 제목 2
772
286
  </button>
287
+ {/if}
288
+ {#if has('h3')}
773
289
  <button
774
290
  type="button"
775
- disabled={!editor.can().addRowAfter()}
776
291
  class={cn(
777
292
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
778
- !editor.can().addRowAfter() && "opacity-30 pointer-events-none",
293
+ editor.isActive("heading", { level: 3 }) && "bg-primary/10 text-primary",
779
294
  )}
780
295
  onclick={() =>
781
- runTableCommand(() =>
782
- editor.chain().focus().addRowAfter().run(),
296
+ runBlock(() =>
297
+ editor.chain().focus().toggleHeading({ level: 3 }).run(),
783
298
  )}
784
299
  >
785
- <Rows3 size={14} /> 행 아래에 추가
300
+ <Heading3 size={14} /> 제목 3
786
301
  </button>
302
+ {/if}
303
+ {#if has('bullet-list') || has('ordered-list') || has('checklist')}
304
+ <div class="h-px bg-border my-1"></div>
305
+ {/if}
306
+ {#if has('bullet-list')}
787
307
  <button
788
308
  type="button"
789
- disabled={!editor.can().addColumnBefore()}
790
309
  class={cn(
791
310
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
792
- !editor.can().addColumnBefore() &&
793
- "opacity-30 pointer-events-none",
311
+ editor.isActive("bulletList") && "bg-primary/10 text-primary",
794
312
  )}
795
313
  onclick={() =>
796
- runTableCommand(() =>
797
- editor.chain().focus().addColumnBefore().run(),
798
- )}
314
+ runBlock(() => editor.chain().focus().toggleBulletList().run())}
799
315
  >
800
- <Columns3 size={14} /> 열 왼쪽에 추가
316
+ <List size={14} /> 글머리 목록
801
317
  </button>
318
+ {/if}
319
+ {#if has('ordered-list')}
802
320
  <button
803
321
  type="button"
804
- disabled={!editor.can().addColumnAfter()}
805
322
  class={cn(
806
323
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
807
- !editor.can().addColumnAfter() &&
808
- "opacity-30 pointer-events-none",
324
+ editor.isActive("orderedList") && "bg-primary/10 text-primary",
809
325
  )}
810
326
  onclick={() =>
811
- runTableCommand(() =>
812
- editor.chain().focus().addColumnAfter().run(),
813
- )}
327
+ runBlock(() => editor.chain().focus().toggleOrderedList().run())}
814
328
  >
815
- <Columns3 size={14} /> 열 오른쪽에 추가
329
+ <ListOrdered size={14} /> 번호 목록
816
330
  </button>
817
- <div class="h-px bg-border my-1"></div>
818
- <p
819
- class="px-3 py-1 text-xs font-semibold text-muted-foreground"
820
- >
821
- 헤더 / 셀
822
- </p>
331
+ {/if}
332
+ {#if has('checklist')}
823
333
  <button
824
334
  type="button"
825
- disabled={!editor.can().toggleHeaderRow()}
826
335
  class={cn(
827
336
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
828
- !editor.can().toggleHeaderRow() &&
829
- "opacity-30 pointer-events-none",
337
+ editor.isActive("taskList") && "bg-primary/10 text-primary",
830
338
  )}
831
339
  onclick={() =>
832
- runTableCommand(() =>
833
- editor.chain().focus().toggleHeaderRow().run(),
834
- )}
340
+ runBlock(() => editor.chain().focus().toggleTaskList().run())}
835
341
  >
836
- <PanelTop size={14} /> 헤더 행 토글
342
+ <CheckSquare size={14} /> 체크리스트
837
343
  </button>
344
+ {/if}
345
+ {#if has('blockquote') || has('toggle')}
346
+ <div class="h-px bg-border my-1"></div>
347
+ {/if}
348
+ {#if has('blockquote')}
838
349
  <button
839
350
  type="button"
840
- disabled={!editor.can().toggleHeaderColumn()}
841
351
  class={cn(
842
352
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
843
- !editor.can().toggleHeaderColumn() &&
844
- "opacity-30 pointer-events-none",
353
+ editor.isActive("blockquote") && "bg-primary/10 text-primary",
845
354
  )}
846
355
  onclick={() =>
847
- runTableCommand(() =>
848
- editor.chain().focus().toggleHeaderColumn().run(),
849
- )}
356
+ runBlock(() => editor.chain().focus().toggleBlockquote().run())}
850
357
  >
851
- <PanelLeft size={14} /> 헤더 열 토글
358
+ <Quote size={14} /> 인용문
852
359
  </button>
360
+ {/if}
361
+ {#if has('toggle')}
853
362
  <button
854
363
  type="button"
855
- disabled={!editor.can().mergeCells()}
856
364
  class={cn(
857
365
  "w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted",
858
- !editor.can().mergeCells() && "opacity-30 pointer-events-none",
366
+ editor.isActive("details") && "bg-primary/10 text-primary",
859
367
  )}
860
368
  onclick={() =>
861
- runTableCommand(() =>
862
- editor.chain().focus().mergeCells().run(),
863
- )}
369
+ runBlock(() => editor.chain().focus().setDetails().run())}
864
370
  >
865
- <Combine size={14} /> 셀 병합
371
+ <ChevronRight size={14} /> 토글
866
372
  </button>
373
+ {/if}
374
+ </div>
375
+ {/if}
376
+ </div>
377
+ </div>
378
+ {/if}
379
+
380
+ {#if has('bold') || has('italic') || has('underline') || has('strike')}
381
+ <!-- Inline marks (core 4) -->
382
+ <div class="hce-toolbar-group">
383
+ {#if has('bold')}
384
+ <button
385
+ type="button"
386
+ onclick={() => editor.chain().focus().toggleBold().run()}
387
+ data-tooltip="굵게"
388
+ aria-label="굵게"
389
+ class={cn(
390
+ "p-1.5 rounded-md transition-colors",
391
+ editor.isActive("bold")
392
+ ? "bg-primary/10 text-primary"
393
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
394
+ )}
395
+ >
396
+ <Bold size={iconSize} />
397
+ </button>
398
+ {/if}
399
+ {#if has('italic')}
400
+ <button
401
+ type="button"
402
+ onclick={() => editor.chain().focus().toggleItalic().run()}
403
+ data-tooltip="기울임"
404
+ aria-label="기울임"
405
+ class={cn(
406
+ "p-1.5 rounded-md transition-colors",
407
+ editor.isActive("italic")
408
+ ? "bg-primary/10 text-primary"
409
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
410
+ )}
411
+ >
412
+ <Italic size={iconSize} />
413
+ </button>
414
+ {/if}
415
+ {#if has('underline')}
416
+ <button
417
+ type="button"
418
+ onclick={() => editor.chain().focus().toggleUnderline().run()}
419
+ data-tooltip="밑줄"
420
+ aria-label="밑줄"
421
+ class={cn(
422
+ "p-1.5 rounded-md transition-colors",
423
+ editor.isActive("underline")
424
+ ? "bg-primary/10 text-primary"
425
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
426
+ )}
427
+ >
428
+ <UnderlineIcon size={iconSize} />
429
+ </button>
430
+ {/if}
431
+ {#if has('strike')}
432
+ <button
433
+ type="button"
434
+ onclick={() => editor.chain().focus().toggleStrike().run()}
435
+ data-tooltip="취소선"
436
+ aria-label="취소선"
437
+ class={cn(
438
+ "p-1.5 rounded-md transition-colors",
439
+ editor.isActive("strike")
440
+ ? "bg-primary/10 text-primary"
441
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
442
+ )}
443
+ >
444
+ <Strikethrough size={iconSize} />
445
+ </button>
446
+ {/if}
447
+ </div>
448
+ {/if}
449
+
450
+ {#if has('align-left') || has('align-center') || has('align-right')}
451
+ <!-- Alignment -->
452
+ <div class="hce-toolbar-group">
453
+ {#if has('align-left')}
454
+ <button
455
+ type="button"
456
+ onclick={() => editor.chain().focus().setTextAlign('left').run()}
457
+ data-tooltip="왼쪽 정렬"
458
+ aria-label="왼쪽 정렬"
459
+ class={cn(
460
+ "p-1.5 rounded-md transition-colors",
461
+ editor.isActive({ textAlign: 'left' })
462
+ ? "bg-primary/10 text-primary"
463
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
464
+ )}
465
+ >
466
+ <AlignLeft size={iconSize} />
467
+ </button>
468
+ {/if}
469
+ {#if has('align-center')}
470
+ <button
471
+ type="button"
472
+ onclick={() => editor.chain().focus().setTextAlign('center').run()}
473
+ data-tooltip="가운데 정렬"
474
+ aria-label="가운데 정렬"
475
+ class={cn(
476
+ "p-1.5 rounded-md transition-colors",
477
+ editor.isActive({ textAlign: 'center' })
478
+ ? "bg-primary/10 text-primary"
479
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
480
+ )}
481
+ >
482
+ <AlignCenter size={iconSize} />
483
+ </button>
484
+ {/if}
485
+ {#if has('align-right')}
486
+ <button
487
+ type="button"
488
+ onclick={() => editor.chain().focus().setTextAlign('right').run()}
489
+ data-tooltip="오른쪽 정렬"
490
+ aria-label="오른쪽 정렬"
491
+ class={cn(
492
+ "p-1.5 rounded-md transition-colors",
493
+ editor.isActive({ textAlign: 'right' })
494
+ ? "bg-primary/10 text-primary"
495
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
496
+ )}
497
+ >
498
+ <AlignRight size={iconSize} />
499
+ </button>
500
+ {/if}
501
+ </div>
502
+ {/if}
503
+
504
+ {#if hasInsertItems}
505
+ <!-- Insert + dropdown -->
506
+ <div class="hce-toolbar-group">
507
+ <div bind:this={insertMenuEl} class="relative">
508
+ <button
509
+ type="button"
510
+ onclick={() => (insertMenuOpen = !insertMenuOpen)}
511
+ data-tooltip="삽입"
512
+ aria-label="삽입"
513
+ class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
514
+ >
515
+ <Plus size={iconSize} />
516
+ <span class="text-sm">삽입</span>
517
+ <ChevronDown size={12} />
518
+ </button>
519
+ {#if insertMenuOpen}
520
+ <div
521
+ class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
522
+ style="min-width: 220px"
523
+ >
524
+ {#if has('code-block')}
867
525
  <button
868
526
  type="button"
869
- disabled={!editor.can().splitCell()}
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
- )}
527
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
874
528
  onclick={() =>
875
- runTableCommand(() =>
876
- editor.chain().focus().splitCell().run(),
877
- )}
529
+ runInsert(() => editor.chain().focus().setCodeBlock().run())}
878
530
  >
879
- <SplitSquareHorizontal size={14} /> 셀 분할
531
+ <Code2 size={14} /> 코드 블록
880
532
  </button>
533
+ {/if}
534
+ {#if has('pdf')}
881
535
  <button
882
536
  type="button"
883
537
  class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
884
- onclick={() => {
885
- tableMenuOpen = false;
886
- modalState = { type: "cellBg" };
887
- }}
538
+ onclick={() => runInsert(onPdfClick)}
888
539
  >
889
- <Paintbrush size={14} /> 셀 배경색
540
+ <FileText size={14} /> PDF
890
541
  </button>
891
- <div class="h-px bg-border my-1"></div>
892
- <p
893
- class="px-3 py-1 text-xs font-semibold text-muted-foreground"
542
+ {/if}
543
+ {#if has('file') && onFileClick}
544
+ <button
545
+ type="button"
546
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
547
+ onclick={() => runInsert(onFileClick!)}
894
548
  >
895
- 삭제
896
- </p>
549
+ <Paperclip size={14} /> 파일 첨부
550
+ </button>
551
+ {/if}
552
+ {#if has('table')}
897
553
  <button
898
554
  type="button"
899
- disabled={!editor.can().deleteRow()}
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
- )}
555
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
904
556
  onclick={() =>
905
- runTableCommand(() =>
906
- editor.chain().focus().deleteRow().run(),
557
+ runInsert(() =>
558
+ editor
559
+ .chain()
560
+ .focus()
561
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
562
+ .run(),
907
563
  )}
908
564
  >
909
- <Rows3 size={14} /> 행 삭제
565
+ <TableIcon size={14} /> 표 (3x3)
910
566
  </button>
567
+ {/if}
568
+
569
+ {#if has('horizontal-rule') || has('columns-2') || has('columns-3')}
570
+ <div class="h-px bg-border my-1"></div>
571
+ {/if}
572
+ {#if has('horizontal-rule')}
911
573
  <button
912
574
  type="button"
913
- disabled={!editor.can().deleteColumn()}
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
- )}
575
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
918
576
  onclick={() =>
919
- runTableCommand(() =>
920
- editor.chain().focus().deleteColumn().run(),
577
+ runInsert(() =>
578
+ editor.chain().focus().setHorizontalRule().run(),
921
579
  )}
922
580
  >
923
- <Columns3 size={14} /> 열 삭제
581
+ <Minus size={14} /> 구분선
924
582
  </button>
583
+ {/if}
584
+ {#if has('columns-2')}
925
585
  <button
926
586
  type="button"
927
- disabled={!editor.can().deleteTable()}
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
- )}
587
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
932
588
  onclick={() =>
933
- runTableCommand(() =>
934
- editor.chain().focus().deleteTable().run(),
935
- )}
589
+ runInsert(() => editor.chain().focus().setColumns(2).run())}
936
590
  >
937
- <Trash2 size={14} /> 표 삭제
591
+ <Columns2 size={14} /> 2단 컬럼
938
592
  </button>
939
- {/if}
940
- </div>
941
- {/if}
942
- </div>
943
- {/if}
944
-
945
- <!-- Code block -->
946
- {#if has('code-block')}
947
- <button
948
- type="button"
949
- onclick={() => editor.chain().focus().setCodeBlock().run()}
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}
593
+ {/if}
594
+ {#if has('columns-3')}
595
+ <button
596
+ type="button"
597
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
598
+ onclick={() =>
599
+ runInsert(() => editor.chain().focus().setColumns(3).run())}
600
+ >
601
+ <Columns3 size={14} /> 3단 컬럼
602
+ </button>
603
+ {/if}
962
604
 
605
+ {#if has('image') || has('link') || has('mbus')}
606
+ <div class="h-px bg-border my-1"></div>
607
+ {/if}
608
+ {#if has('image')}
609
+ <button
610
+ type="button"
611
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
612
+ onclick={addImage}
613
+ >
614
+ <ImageIcon size={14} /> 이미지
615
+ </button>
616
+ {/if}
617
+ {#if has('link')}
618
+ <button
619
+ type="button"
620
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
621
+ onclick={addLink}
622
+ >
623
+ <LinkIcon size={14} /> 링크
624
+ </button>
625
+ {/if}
626
+ {#if has('mbus')}
627
+ <button
628
+ type="button"
629
+ class="w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 hover:bg-muted"
630
+ onclick={addMbus}
631
+ >
632
+ <Tv size={14} /> 미디버스 영상
633
+ </button>
634
+ {/if}
635
+ </div>
636
+ {/if}
963
637
  </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
638
  </div>
1002
639
  {/if}
1003
640
 
@@ -1041,47 +678,6 @@
1041
678
  onCancel={() => (modalState = null)}
1042
679
  />
1043
680
  {/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
681
  </div>
1086
682
 
1087
683
  <style>