@teriusu/rich-editor 0.10.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/components/BubbleToolbar.svelte +40 -36
  2. package/dist/components/BubbleToolbar.svelte.d.ts +3 -0
  3. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  4. package/dist/components/EmojiSuggestionMenu.svelte +291 -0
  5. package/dist/components/EmojiSuggestionMenu.svelte.d.ts +9 -0
  6. package/dist/components/EmojiSuggestionMenu.svelte.d.ts.map +1 -0
  7. package/dist/components/FixedToolbar.svelte +53 -47
  8. package/dist/components/FixedToolbar.svelte.d.ts +3 -0
  9. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  10. package/dist/components/FloatingHelper.svelte +219 -0
  11. package/dist/components/FloatingHelper.svelte.d.ts +14 -0
  12. package/dist/components/FloatingHelper.svelte.d.ts.map +1 -0
  13. package/dist/components/InputModal.svelte +7 -3
  14. package/dist/components/InputModal.svelte.d.ts +3 -0
  15. package/dist/components/InputModal.svelte.d.ts.map +1 -1
  16. package/dist/components/MathModal.svelte +33 -29
  17. package/dist/components/MathModal.svelte.d.ts +3 -0
  18. package/dist/components/MathModal.svelte.d.ts.map +1 -1
  19. package/dist/components/MediaPickerModal.svelte +7 -3
  20. package/dist/components/MediaPickerModal.svelte.d.ts +3 -0
  21. package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -1
  22. package/dist/components/SlashCommandMenu.svelte +88 -72
  23. package/dist/components/SlashCommandMenu.svelte.d.ts +3 -0
  24. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  25. package/dist/components/TableBubbleMenu.svelte +41 -33
  26. package/dist/components/TableBubbleMenu.svelte.d.ts +3 -0
  27. package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
  28. package/dist/components/TipTapEditor.svelte +239 -127
  29. package/dist/components/TipTapEditor.svelte.d.ts +7 -0
  30. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  31. package/dist/extensions/CardBlock.d.ts.map +1 -1
  32. package/dist/extensions/CardBlock.js +6 -4
  33. package/dist/extensions/FileAttachment.d.ts.map +1 -1
  34. package/dist/extensions/FileAttachment.js +2 -1
  35. package/dist/extensions/FixedDetails.d.ts.map +1 -1
  36. package/dist/extensions/FixedDetails.js +5 -3
  37. package/dist/extensions/LegacyBlock.d.ts.map +1 -1
  38. package/dist/extensions/LegacyBlock.js +8 -6
  39. package/dist/extensions/Math.d.ts.map +1 -1
  40. package/dist/extensions/Math.js +5 -2
  41. package/dist/extensions/MbusVideo.d.ts.map +1 -1
  42. package/dist/extensions/MbusVideo.js +73 -9
  43. package/dist/extensions/MediaToolbar.d.ts +41 -0
  44. package/dist/extensions/MediaToolbar.d.ts.map +1 -0
  45. package/dist/extensions/MediaToolbar.js +196 -0
  46. package/dist/extensions/MediaToolbar.test.d.ts +2 -0
  47. package/dist/extensions/MediaToolbar.test.d.ts.map +1 -0
  48. package/dist/extensions/MediaToolbar.test.js +231 -0
  49. package/dist/extensions/OrderedListMarker.d.ts +2 -0
  50. package/dist/extensions/OrderedListMarker.d.ts.map +1 -0
  51. package/dist/extensions/OrderedListMarker.js +114 -0
  52. package/dist/extensions/OrderedListMarker.test.d.ts +2 -0
  53. package/dist/extensions/OrderedListMarker.test.d.ts.map +1 -0
  54. package/dist/extensions/OrderedListMarker.test.js +176 -0
  55. package/dist/extensions/PdfBlock.d.ts.map +1 -1
  56. package/dist/extensions/PdfBlock.js +25 -23
  57. package/dist/extensions/ResizableImage.d.ts.map +1 -1
  58. package/dist/extensions/ResizableImage.js +20 -1
  59. package/dist/extensions/TableKeymap.d.ts +16 -0
  60. package/dist/extensions/TableKeymap.d.ts.map +1 -0
  61. package/dist/extensions/TableKeymap.js +97 -0
  62. package/dist/extensions/TableKeymap.test.d.ts +2 -0
  63. package/dist/extensions/TableKeymap.test.d.ts.map +1 -0
  64. package/dist/extensions/TableKeymap.test.js +147 -0
  65. package/dist/extensions/TabsBlock.d.ts.map +1 -1
  66. package/dist/extensions/TabsBlock.js +12 -9
  67. package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
  68. package/dist/extensions/TiptapMidibus.js +60 -6
  69. package/dist/extensions/UploadSkeleton.d.ts +61 -0
  70. package/dist/extensions/UploadSkeleton.d.ts.map +1 -0
  71. package/dist/extensions/UploadSkeleton.js +233 -0
  72. package/dist/extensions/UploadSkeleton.test.d.ts +2 -0
  73. package/dist/extensions/UploadSkeleton.test.d.ts.map +1 -0
  74. package/dist/extensions/UploadSkeleton.test.js +128 -0
  75. package/dist/extensions/VideoEmbed.d.ts +8 -0
  76. package/dist/extensions/VideoEmbed.d.ts.map +1 -1
  77. package/dist/extensions/VideoEmbed.js +226 -45
  78. package/dist/extensions/VideoEmbed.test.js +131 -2
  79. package/dist/i18n/en.d.ts +3 -0
  80. package/dist/i18n/en.d.ts.map +1 -0
  81. package/dist/i18n/en.js +191 -0
  82. package/dist/i18n/es.d.ts +3 -0
  83. package/dist/i18n/es.d.ts.map +1 -0
  84. package/dist/i18n/es.js +191 -0
  85. package/dist/i18n/index.d.ts +43 -0
  86. package/dist/i18n/index.d.ts.map +1 -0
  87. package/dist/i18n/index.js +109 -0
  88. package/dist/i18n/index.test.d.ts +2 -0
  89. package/dist/i18n/index.test.d.ts.map +1 -0
  90. package/dist/i18n/index.test.js +173 -0
  91. package/dist/i18n/ja.d.ts +3 -0
  92. package/dist/i18n/ja.d.ts.map +1 -0
  93. package/dist/i18n/ja.js +191 -0
  94. package/dist/i18n/ko.d.ts +198 -0
  95. package/dist/i18n/ko.d.ts.map +1 -0
  96. package/dist/i18n/ko.js +215 -0
  97. package/dist/i18n/zh-hans.d.ts +3 -0
  98. package/dist/i18n/zh-hans.d.ts.map +1 -0
  99. package/dist/i18n/zh-hans.js +191 -0
  100. package/dist/i18n/zh-hant.d.ts +3 -0
  101. package/dist/i18n/zh-hant.d.ts.map +1 -0
  102. package/dist/i18n/zh-hant.js +192 -0
  103. package/dist/index.d.ts +7 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +13 -1
  106. package/dist/static/highlight.d.ts +13 -0
  107. package/dist/static/highlight.d.ts.map +1 -0
  108. package/dist/static/highlight.js +99 -0
  109. package/dist/static/index.d.ts +33 -0
  110. package/dist/static/index.d.ts.map +1 -0
  111. package/dist/static/index.js +367 -0
  112. package/dist/static/index.test.d.ts +2 -0
  113. package/dist/static/index.test.d.ts.map +1 -0
  114. package/dist/static/index.test.js +194 -0
  115. package/dist/static/tokenizer.d.ts +52 -0
  116. package/dist/static/tokenizer.d.ts.map +1 -0
  117. package/dist/static/tokenizer.js +289 -0
  118. package/dist/styles/editor.css +1 -1
  119. package/dist/utils/emoji-data.d.ts +15 -0
  120. package/dist/utils/emoji-suggestion.d.ts +48 -0
  121. package/dist/utils/emoji-suggestion.d.ts.map +1 -0
  122. package/dist/utils/emoji-suggestion.js +51 -0
  123. package/dist/utils/emoji-suggestion.test.d.ts +2 -0
  124. package/dist/utils/emoji-suggestion.test.d.ts.map +1 -0
  125. package/dist/utils/emoji-suggestion.test.js +117 -0
  126. package/dist/utils/floating-helper.d.ts +60 -0
  127. package/dist/utils/floating-helper.d.ts.map +1 -0
  128. package/dist/utils/floating-helper.js +63 -0
  129. package/dist/utils/floating-helper.test.d.ts +2 -0
  130. package/dist/utils/floating-helper.test.d.ts.map +1 -0
  131. package/dist/utils/floating-helper.test.js +141 -0
  132. package/dist/utils/guard-vscode-paste.d.ts +19 -0
  133. package/dist/utils/guard-vscode-paste.d.ts.map +1 -0
  134. package/dist/utils/guard-vscode-paste.js +46 -0
  135. package/dist/utils/guard-vscode-paste.test.d.ts +2 -0
  136. package/dist/utils/guard-vscode-paste.test.d.ts.map +1 -0
  137. package/dist/utils/guard-vscode-paste.test.js +86 -0
  138. package/dist/utils/media-size.d.ts +29 -0
  139. package/dist/utils/media-size.d.ts.map +1 -0
  140. package/dist/utils/media-size.js +90 -0
  141. package/dist/utils/resize.d.ts +12 -0
  142. package/dist/utils/resize.d.ts.map +1 -1
  143. package/dist/utils/resize.js +5 -5
  144. package/dist/utils/sanitize.d.ts +2 -0
  145. package/dist/utils/sanitize.d.ts.map +1 -1
  146. package/dist/utils/sanitize.js +28 -3
  147. package/package.json +8 -1
@@ -41,6 +41,7 @@
41
41
  import ToggleHeadingIcon from "./icons/ToggleHeadingIcon.svelte";
42
42
  import InputModal from "./InputModal.svelte";
43
43
  import type { ToolbarFeature, PromptHandler } from "../types";
44
+ import { defaultTranslator, type EditorTranslator } from "../i18n";
44
45
 
45
46
  let {
46
47
  editor,
@@ -52,6 +53,7 @@
52
53
  onPromptMbus,
53
54
  onPromptVideo,
54
55
  toolbarEnd,
56
+ t = defaultTranslator,
55
57
  }: {
56
58
  editor: Editor;
57
59
  features: Set<ToolbarFeature>;
@@ -76,6 +78,8 @@
76
78
  * 여기에 넣으면 툴바의 flex 행에 그대로 얹혀 정렬이 저절로 맞는다.
77
79
  */
78
80
  toolbarEnd?: Snippet;
81
+ /** 에디터 UI 번역 함수. 미주입 시 ko. */
82
+ t?: EditorTranslator;
79
83
  } = $props();
80
84
 
81
85
  /*
@@ -171,21 +175,21 @@
171
175
  }
172
176
 
173
177
  const currentBlockLabel = $derived.by(() => {
174
- if (isActive("heading", { level: 1 })) return "제목 1";
175
- if (isActive("heading", { level: 2 })) return "제목 2";
176
- if (isActive("heading", { level: 3 })) return "제목 3";
177
- if (isActive("bulletList")) return "글머리 목록";
178
- if (isActive("orderedList")) return "번호 목록";
179
- if (isActive("taskList")) return "체크리스트";
180
- if (isActive("blockquote")) return "인용문";
178
+ if (isActive("heading", { level: 1 })) return t("heading1");
179
+ if (isActive("heading", { level: 2 })) return t("heading2");
180
+ if (isActive("heading", { level: 3 })) return t("heading3");
181
+ if (isActive("bulletList")) return t("bulletList");
182
+ if (isActive("orderedList")) return t("orderedList");
183
+ if (isActive("taskList")) return t("taskList");
184
+ if (isActive("blockquote")) return t("blockquote");
181
185
  if (isActive("details")) {
182
186
  // 토글 제목이면 단계까지 보여 준다 — 그냥 "토글" 이면 무엇을 고른 건지 안 보인다.
183
187
  for (const lv of [1, 2, 3]) {
184
- if (isActive("detailsSummary", { level: lv })) return `토글 제목 ${lv}`;
188
+ if (isActive("detailsSummary", { level: lv })) return t("toggleHeading", { level: lv });
185
189
  }
186
- return "토글";
190
+ return t("toggle");
187
191
  }
188
- return "본문";
192
+ return t("paragraph");
189
193
  });
190
194
 
191
195
  function runBlock(fn: () => void) {
@@ -238,7 +242,7 @@
238
242
  type="button"
239
243
  onclick={() => editor.chain().focus().undo().run()}
240
244
  disabled={!canDo((c) => c.undo())}
241
- aria-label="실행 취소"
245
+ aria-label={t('undo')}
242
246
  class={cn(
243
247
  "p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
244
248
  !canDo((c) => c.undo()) && "opacity-30 pointer-events-none",
@@ -252,7 +256,7 @@
252
256
  type="button"
253
257
  onclick={() => editor.chain().focus().redo().run()}
254
258
  disabled={!canDo((c) => c.redo())}
255
- aria-label="다시 실행"
259
+ aria-label={t('redo')}
256
260
  class={cn(
257
261
  "p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
258
262
  !canDo((c) => c.redo()) && "opacity-30 pointer-events-none",
@@ -271,7 +275,7 @@
271
275
  <button
272
276
  type="button"
273
277
  onclick={() => (blockMenuOpen = !blockMenuOpen)}
274
- aria-label="블록 타입"
278
+ aria-label={t('blockType')}
275
279
  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]"
276
280
  >
277
281
  <span class="text-sm">{currentBlockLabel}</span>
@@ -301,7 +305,7 @@
301
305
  onclick={() =>
302
306
  runBlock(() => editor.chain().focus().setParagraph().run())}
303
307
  >
304
- <Pilcrow size={14} /> 본문
308
+ <Pilcrow size={14} /> {t('paragraph')}
305
309
  </button>
306
310
  {#if has('h1')}
307
311
  <button
@@ -315,7 +319,7 @@
315
319
  editor.chain().focus().toggleHeading({ level: 1 }).run(),
316
320
  )}
317
321
  >
318
- <Heading1 size={14} /> 제목 1
322
+ <Heading1 size={14} /> {t('heading1')}
319
323
  <span class="hce-menu-shortcut"># </span>
320
324
  </button>
321
325
  {/if}
@@ -331,7 +335,7 @@
331
335
  editor.chain().focus().toggleHeading({ level: 2 }).run(),
332
336
  )}
333
337
  >
334
- <Heading2 size={14} /> 제목 2
338
+ <Heading2 size={14} /> {t('heading2')}
335
339
  <span class="hce-menu-shortcut">## </span>
336
340
  </button>
337
341
  {/if}
@@ -347,7 +351,7 @@
347
351
  editor.chain().focus().toggleHeading({ level: 3 }).run(),
348
352
  )}
349
353
  >
350
- <Heading3 size={14} /> 제목 3
354
+ <Heading3 size={14} /> {t('heading3')}
351
355
  <span class="hce-menu-shortcut">### </span>
352
356
  </button>
353
357
  {/if}
@@ -364,7 +368,7 @@
364
368
  onclick={() =>
365
369
  runBlock(() => editor.chain().focus().toggleBulletList().run())}
366
370
  >
367
- <List size={14} /> 글머리 목록
371
+ <List size={14} /> {t('bulletList')}
368
372
  <span class="hce-menu-shortcut">- </span>
369
373
  </button>
370
374
  {/if}
@@ -378,7 +382,7 @@
378
382
  onclick={() =>
379
383
  runBlock(() => editor.chain().focus().toggleOrderedList().run())}
380
384
  >
381
- <ListOrdered size={14} /> 번호 목록
385
+ <ListOrdered size={14} /> {t('orderedList')}
382
386
  <span class="hce-menu-shortcut">1. </span>
383
387
  </button>
384
388
  {/if}
@@ -392,7 +396,7 @@
392
396
  onclick={() =>
393
397
  runBlock(() => editor.chain().focus().toggleTaskList().run())}
394
398
  >
395
- <CheckSquare size={14} /> 체크리스트
399
+ <CheckSquare size={14} /> {t('taskList')}
396
400
  <span class="hce-menu-shortcut">[] </span>
397
401
  </button>
398
402
  {/if}
@@ -409,7 +413,7 @@
409
413
  onclick={() =>
410
414
  runBlock(() => editor.chain().focus().toggleBlockquote().run())}
411
415
  >
412
- <Quote size={14} /> 인용문
416
+ <Quote size={14} /> {t('blockquote')}
413
417
  <span class="hce-menu-shortcut">" </span>
414
418
  </button>
415
419
  {/if}
@@ -423,7 +427,7 @@
423
427
  onclick={() =>
424
428
  runBlock(() => editor.chain().focus().setDetails().run())}
425
429
  >
426
- <ChevronRight size={14} /> 토글
430
+ <ChevronRight size={14} /> {t('toggle')}
427
431
  <span class="hce-menu-shortcut">> </span>
428
432
  </button>
429
433
  <!--
@@ -450,7 +454,7 @@
450
454
  .run(),
451
455
  )}
452
456
  >
453
- <ToggleHeadingIcon size={14} level={level as 1 | 2 | 3} /> 토글 제목 {level}
457
+ <ToggleHeadingIcon size={14} level={level as 1 | 2 | 3} /> {t('toggleHeading', { level })}
454
458
  <span class="hce-menu-shortcut">{'#'.repeat(level)} &gt; </span>
455
459
  </button>
456
460
  {/each}
@@ -475,7 +479,7 @@
475
479
  <button
476
480
  type="button"
477
481
  onclick={() => editor.chain().focus().toggleBold().run()}
478
- aria-label="굵게"
482
+ aria-label={t('bold')}
479
483
  class={cn(
480
484
  "p-1.5 rounded-md transition-colors",
481
485
  isActive("bold")
@@ -490,7 +494,7 @@
490
494
  <button
491
495
  type="button"
492
496
  onclick={() => editor.chain().focus().toggleItalic().run()}
493
- aria-label="기울임"
497
+ aria-label={t('italic')}
494
498
  class={cn(
495
499
  "p-1.5 rounded-md transition-colors",
496
500
  isActive("italic")
@@ -505,7 +509,7 @@
505
509
  <button
506
510
  type="button"
507
511
  onclick={() => editor.chain().focus().toggleUnderline().run()}
508
- aria-label="밑줄"
512
+ aria-label={t('underline')}
509
513
  class={cn(
510
514
  "p-1.5 rounded-md transition-colors",
511
515
  isActive("underline")
@@ -520,7 +524,7 @@
520
524
  <button
521
525
  type="button"
522
526
  onclick={() => editor.chain().focus().toggleStrike().run()}
523
- aria-label="취소선"
527
+ aria-label={t('strike')}
524
528
  class={cn(
525
529
  "p-1.5 rounded-md transition-colors",
526
530
  isActive("strike")
@@ -552,7 +556,7 @@
552
556
  <button
553
557
  type="button"
554
558
  onclick={() => editor.chain().focus().setTextAlign('left').run()}
555
- aria-label="왼쪽 정렬"
559
+ aria-label={t('alignLeft')}
556
560
  class={cn(
557
561
  "p-1.5 rounded-md transition-colors",
558
562
  isActive({ textAlign: 'left' })
@@ -567,7 +571,7 @@
567
571
  <button
568
572
  type="button"
569
573
  onclick={() => editor.chain().focus().setTextAlign('center').run()}
570
- aria-label="가운데 정렬"
574
+ aria-label={t('alignCenter')}
571
575
  class={cn(
572
576
  "p-1.5 rounded-md transition-colors",
573
577
  isActive({ textAlign: 'center' })
@@ -582,7 +586,7 @@
582
586
  <button
583
587
  type="button"
584
588
  onclick={() => editor.chain().focus().setTextAlign('right').run()}
585
- aria-label="오른쪽 정렬"
589
+ aria-label={t('alignRight')}
586
590
  class={cn(
587
591
  "p-1.5 rounded-md transition-colors",
588
592
  isActive({ textAlign: 'right' })
@@ -603,11 +607,11 @@
603
607
  <button
604
608
  type="button"
605
609
  onclick={() => (insertMenuOpen = !insertMenuOpen)}
606
- aria-label="삽입"
610
+ aria-label={t('insert')}
607
611
  class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
608
612
  >
609
613
  <Plus size={iconSize} />
610
- <span class="text-sm">삽입</span>
614
+ <span class="text-sm">{t('insert')}</span>
611
615
  <ChevronDown size={12} />
612
616
  </button>
613
617
  {#if insertMenuOpen}
@@ -622,7 +626,7 @@
622
626
  onclick={() =>
623
627
  runInsert(() => editor.chain().focus().setCodeBlock().run())}
624
628
  >
625
- <Code2 size={14} /> 코드 블록
629
+ <Code2 size={14} /> {t('codeBlock')}
626
630
  <span class="hce-menu-shortcut">```</span>
627
631
  </button>
628
632
  {/if}
@@ -633,7 +637,7 @@
633
637
  onclick={() =>
634
638
  runInsert(() => editor.chain().focus().promptMathDisplay().run())}
635
639
  >
636
- <Sigma size={14} /> 수식 블록
640
+ <Sigma size={14} /> {t('mathBlock')}
637
641
  <span class="hce-menu-shortcut">$$</span>
638
642
  </button>
639
643
  <!--
@@ -648,7 +652,7 @@
648
652
  onclick={() =>
649
653
  runInsert(() => editor.chain().focus().promptMathInline().run())}
650
654
  >
651
- <Sigma size={14} /> 인라인 수식
655
+ <Sigma size={14} /> {t('mathInline')}
652
656
  <span class="hce-menu-shortcut">$ $</span>
653
657
  </button>
654
658
  {/if}
@@ -667,7 +671,7 @@
667
671
  class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
668
672
  onclick={() => runInsert(onFileClick!)}
669
673
  >
670
- <Paperclip size={14} /> 파일
674
+ <Paperclip size={14} /> {t('file')}
671
675
  </button>
672
676
  {/if}
673
677
  {#if has('table')}
@@ -679,7 +683,7 @@
679
683
  insertTableSized(editor, { rows: 3, cols: 3, withHeaderRow: true }),
680
684
  )}
681
685
  >
682
- <TableIcon size={14} /> 표 (3x3)
686
+ <TableIcon size={14} /> {t('table3x3')}
683
687
  </button>
684
688
  {/if}
685
689
 
@@ -695,7 +699,7 @@
695
699
  editor.chain().focus().setHorizontalRule().run(),
696
700
  )}
697
701
  >
698
- <Minus size={14} /> 구분선
702
+ <Minus size={14} /> {t('divider')}
699
703
  <span class="hce-menu-shortcut">---</span>
700
704
  </button>
701
705
  {/if}
@@ -706,7 +710,7 @@
706
710
  onclick={() =>
707
711
  runInsert(() => editor.chain().focus().setColumns(2).run())}
708
712
  >
709
- <Columns2 size={14} /> 2단 컬럼
713
+ <Columns2 size={14} /> {t('columns2')}
710
714
  </button>
711
715
  {/if}
712
716
  {#if has('columns-3')}
@@ -716,7 +720,7 @@
716
720
  onclick={() =>
717
721
  runInsert(() => editor.chain().focus().setColumns(3).run())}
718
722
  >
719
- <Columns3 size={14} /> 3단 컬럼
723
+ <Columns3 size={14} /> {t('columns3')}
720
724
  </button>
721
725
  {/if}
722
726
  {#if has('tabs')}
@@ -726,7 +730,7 @@
726
730
  onclick={() =>
727
731
  runInsert(() => editor.chain().focus().setTabs(3).run())}
728
732
  >
729
- <PanelTop size={14} /> 탭
733
+ <PanelTop size={14} /> {t('tabs')}
730
734
  </button>
731
735
  {/if}
732
736
  {#if has('card')}
@@ -735,7 +739,7 @@
735
739
  class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
736
740
  onclick={() => runInsert(() => editor.chain().focus().setCard().run())}
737
741
  >
738
- <SquareDashed size={14} /> 카드
742
+ <SquareDashed size={14} /> {t('card')}
739
743
  </button>
740
744
  {/if}
741
745
 
@@ -748,7 +752,7 @@
748
752
  class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
749
753
  onclick={addImage}
750
754
  >
751
- <ImageIcon size={14} /> 이미지
755
+ <ImageIcon size={14} /> {t('image')}
752
756
  </button>
753
757
  {/if}
754
758
  {#if has('video')}
@@ -761,7 +765,7 @@
761
765
  class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
762
766
  onclick={addVideo}
763
767
  >
764
- <Youtube size={14} /> 영상
768
+ <Youtube size={14} /> {t('video')}
765
769
  </button>
766
770
  {/if}
767
771
  {#if has('mbus')}
@@ -770,7 +774,7 @@
770
774
  class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
771
775
  onclick={addMbus}
772
776
  >
773
- <Tv size={14} /> 미디버스 영상
777
+ <Tv size={14} /> {t('mbusVideo')}
774
778
  </button>
775
779
  {/if}
776
780
  </div>
@@ -783,24 +787,26 @@
783
787
  <!-- (이미지 모달은 없다 — 에디터가 업로드/링크 탭 모달을 띄운다. `onImageClick` 참고.) -->
784
788
  {#if modalState?.type === "video"}
785
789
  <InputModal
786
- title="영상 URL"
790
+ title={t('videoUrl')}
787
791
  placeholder="https://www.youtube.com/watch?v=..."
788
792
  onConfirm={(url) => {
789
793
  editor.chain().focus().setVideoEmbed({ src: url }).run();
790
794
  modalState = null;
791
795
  }}
792
796
  onCancel={() => (modalState = null)}
797
+ {t}
793
798
  />
794
799
  {/if}
795
800
  {#if modalState?.type === "mbus"}
796
801
  <InputModal
797
- title="미디버스 영상 URL"
802
+ title={t('mbusVideoUrl')}
798
803
  placeholder="https://play.mbus.tv/v1/hls/..."
799
804
  onConfirm={(url) => {
800
805
  editor.chain().focus().setMbusVideo({ src: url }).run();
801
806
  modalState = null;
802
807
  }}
803
808
  onCancel={() => (modalState = null)}
809
+ {t}
804
810
  />
805
811
  {/if}
806
812
 
@@ -1,6 +1,7 @@
1
1
  import type { Editor } from "@tiptap/core";
2
2
  import type { Snippet } from "svelte";
3
3
  import type { ToolbarFeature, PromptHandler } from "../types";
4
+ import { type EditorTranslator } from "../i18n";
4
5
  type $$ComponentProps = {
5
6
  editor: Editor;
6
7
  features: Set<ToolbarFeature>;
@@ -25,6 +26,8 @@ type $$ComponentProps = {
25
26
  * 여기에 넣으면 툴바의 flex 행에 그대로 얹혀 정렬이 저절로 맞는다.
26
27
  */
27
28
  toolbarEnd?: Snippet;
29
+ /** 에디터 UI 번역 함수. 미주입 시 ko. */
30
+ t?: EditorTranslator;
28
31
  };
29
32
  declare const FixedToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
30
33
  type FixedToolbar = ReturnType<typeof FixedToolbar>;
@@ -1 +1 @@
1
- {"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAwCtC,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,0CAA0C;IAC1C,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAuiBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAwCtC,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAElE,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,0CAA0C;IAC1C,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AAyiBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,219 @@
1
+ <script lang="ts">
2
+ import type { Editor } from "@tiptap/core";
3
+ import { Sigma, Code2 } from "lucide-svelte";
4
+ import type { ToolbarFeature } from "../types";
5
+ import {
6
+ floatingHelperVisible,
7
+ helperInsertMath,
8
+ helperInsertCodeBlock,
9
+ FLOATING_HELPER_ON_CLASS,
10
+ } from "../utils/floating-helper";
11
+ import { defaultTranslator, type EditorTranslator } from "../i18n";
12
+
13
+ /*
14
+ * 빈 줄 플로팅 헬퍼의 **화면 절반** — 판정·커맨드는 `utils/floating-helper.ts`.
15
+ *
16
+ * 포커스된 최상위 빈 문단 옆에 "/로 명령어 입력" 안내와 수식·코드 즉시 삽입 버튼을
17
+ * 띄운다(main 정올 `Floating.svelte` 의 직접 구현 이식). 에디터 컨테이너
18
+ * (`.hce-editor-wrapper`, `position:relative`) 안에 `position:absolute` 로 그린다 —
19
+ * 포털이 없고 문서 흐름의 자리도 차지하지 않는다.
20
+ *
21
+ * ⚠️ 위치 계산 셋이 main 사고 이력의 재발 방지다:
22
+ * - `.hce-floating-helper` 는 CSS 에서 **처음부터** `position:absolute; width:max-content`
23
+ * 다. 블록 폭으로 측정된 뒤에 좁히면 넘침 보정이 화면 밖(left:-437px)으로 민 전력이
24
+ * 있다(main floating-ui 시절).
25
+ * - 등장 애니메이션은 **안쪽 요소에만** 건다. 바깥(위치 담당)에 transform 을 걸면
26
+ * 위치가 애니메이션 프레임에 덮인다(main `79f64c18`→`c9840fdb`).
27
+ * - 슬래시·이모지 메뉴가 열리면 `suppressed` 로 끈다 — 그 둘은 `position:fixed`
28
+ * 뷰포트 좌표라 좌표계가 달라 서로 피해 갈 수 없다.
29
+ */
30
+ let {
31
+ editor,
32
+ features,
33
+ suppressed = false,
34
+ t = defaultTranslator,
35
+ }: {
36
+ editor: Editor;
37
+ features: Set<ToolbarFeature>;
38
+ suppressed?: boolean;
39
+ /** 에디터 UI 번역 함수. 미주입 시 ko. */
40
+ t?: EditorTranslator;
41
+ } = $props();
42
+
43
+ let visible = $state(false);
44
+ let pos = $state({ top: 0, left: 0 });
45
+ let helperEl = $state<HTMLDivElement>();
46
+ let focused = $state(false);
47
+
48
+ const showMath = $derived(features.has("math"));
49
+ const showCode = $derived(features.has("code-block"));
50
+ const hasButtons = $derived(showMath || showCode);
51
+
52
+ function updatePosition() {
53
+ const wrapper = editor.view.dom.closest(".hce-editor-wrapper") as HTMLElement | null;
54
+ if (!wrapper) return;
55
+ const { from } = editor.state.selection;
56
+ const caret = editor.view.coordsAtPos(from);
57
+ const box = wrapper.getBoundingClientRect();
58
+ let left = caret.right - box.left + 8;
59
+ let top = caret.top - box.top;
60
+ /*
61
+ * 요소가 이미 서 있으면 실측으로 보정한다 — 세로는 커서 줄에 가운데 맞춤,
62
+ * 가로는 래퍼 오른쪽을 넘으면 안으로 죈다(첫 프레임은 아래 `$effect` 가 재계산).
63
+ */
64
+ if (helperEl) {
65
+ const h = helperEl.offsetHeight;
66
+ if (h) top = caret.top - box.top + (caret.bottom - caret.top - h) / 2;
67
+ const w = helperEl.offsetWidth;
68
+ const maxLeft = wrapper.clientWidth - w - 4;
69
+ if (w && left > maxLeft) left = Math.max(4, maxLeft);
70
+ }
71
+ pos = { top, left };
72
+ }
73
+
74
+ function evaluate() {
75
+ const show = floatingHelperVisible(editor, { focused, suppressed });
76
+ if (show) updatePosition();
77
+ visible = show;
78
+ /* placeholder 겹침 방지 — 근거는 `FLOATING_HELPER_ON_CLASS` 주석. */
79
+ editor.view.dom.classList.toggle(FLOATING_HELPER_ON_CLASS, show);
80
+ }
81
+
82
+ $effect(() => {
83
+ focused = editor.view.hasFocus();
84
+ const onFocus = () => {
85
+ focused = true;
86
+ };
87
+ const onBlur = () => {
88
+ focused = false;
89
+ };
90
+ editor.on("transaction", evaluate);
91
+ editor.on("focus", onFocus);
92
+ editor.on("blur", onBlur);
93
+ return () => {
94
+ editor.off("transaction", evaluate);
95
+ editor.off("focus", onFocus);
96
+ editor.off("blur", onBlur);
97
+ editor.view.dom.classList.remove(FLOATING_HELPER_ON_CLASS);
98
+ };
99
+ });
100
+
101
+ /* 포커스·suppressed 가 바뀌면 재판정 (트랜잭션 없이 바뀌는 두 축). */
102
+ $effect(() => {
103
+ focused;
104
+ suppressed;
105
+ evaluate();
106
+ });
107
+
108
+ /* 마운트 직후 실측(폭·높이)으로 한 번 더 자리를 잡는다. */
109
+ $effect(() => {
110
+ if (visible && helperEl) updatePosition();
111
+ });
112
+
113
+ /* 래퍼 안쪽이 스크롤되는 호스트 대비 — 슬래시·이모지 메뉴와 같은 캡처 구독. */
114
+ $effect(() => {
115
+ if (!visible) return;
116
+ const onMove = () => updatePosition();
117
+ window.addEventListener("scroll", onMove, true);
118
+ window.addEventListener("resize", onMove);
119
+ return () => {
120
+ window.removeEventListener("scroll", onMove, true);
121
+ window.removeEventListener("resize", onMove);
122
+ };
123
+ });
124
+ </script>
125
+
126
+ {#if visible}
127
+ <!--
128
+ mousedown preventDefault: 버튼을 누르는 순간 에디터가 blur 되면 클릭이 닿기 전에
129
+ 헬퍼가 사라진다(버튼과 함께). 포커스를 아예 뺏기지 않는 쪽이 main 의
130
+ `setTimeout(focus)` 복원보다 깔끔하다 — 툴바들이 쓰는 표준 수법.
131
+ -->
132
+ <div
133
+ bind:this={helperEl}
134
+ class="hce-floating-helper"
135
+ style="top: {pos.top}px; left: {pos.left}px"
136
+ role="toolbar"
137
+ tabindex="-1"
138
+ aria-label={t('emptyLineTools')}
139
+ onmousedown={(e) => e.preventDefault()}
140
+ >
141
+ <div class="hce-floating-helper-inner">
142
+ <span class="hce-floating-helper-hint">{t('floatingHint')}{hasButtons ? t('floatingHintOr') : ""}</span>
143
+ {#if showMath}
144
+ <button
145
+ type="button"
146
+ class="hce-floating-plus"
147
+ title={t('math')}
148
+ aria-label={t('insertMath')}
149
+ onclick={() => helperInsertMath(editor)}
150
+ >
151
+ <Sigma size={14} />
152
+ </button>
153
+ {/if}
154
+ {#if showCode}
155
+ <button
156
+ type="button"
157
+ class="hce-floating-plus"
158
+ title={t('codeBlock')}
159
+ aria-label={t('insertCodeBlock')}
160
+ onclick={() => helperInsertCodeBlock(editor)}
161
+ >
162
+ <Code2 size={14} />
163
+ </button>
164
+ {/if}
165
+ </div>
166
+ </div>
167
+ {/if}
168
+
169
+ <style>
170
+ /*
171
+ * ⚠️ `position:absolute` + `width:max-content` 는 **여기 고정**이다 — 위치·크기가
172
+ * 런타임 계산보다 먼저 서 있어야 실측 보정(`updatePosition`)이 옳은 폭을 잰다.
173
+ * z-index 100: 정올 sticky 브레드크럼(z:93)보다 위, 모달(z-50=50 계열 Tailwind 층과는
174
+ * 별개 스택) 아래를 노린 값.
175
+ */
176
+ .hce-floating-helper {
177
+ position: absolute;
178
+ width: -moz-max-content;
179
+ width: max-content;
180
+ z-index: 100;
181
+ pointer-events: auto;
182
+ }
183
+
184
+ /* 등장 애니메이션은 안쪽에만 — 바깥 transform 은 위치 계산과 충돌한다. */
185
+ .hce-floating-helper-inner {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 6px;
189
+ animation: hce-floating-helper-in 0.18s ease-out;
190
+ }
191
+
192
+ @keyframes hce-floating-helper-in {
193
+ from {
194
+ opacity: 0;
195
+ transform: translateX(-4px);
196
+ }
197
+ to {
198
+ opacity: 1;
199
+ transform: none;
200
+ }
201
+ }
202
+
203
+ @media (prefers-reduced-motion: reduce) {
204
+ .hce-floating-helper-inner {
205
+ animation: none;
206
+ }
207
+ }
208
+
209
+ .hce-floating-helper-hint {
210
+ font-size: 0.875rem;
211
+ color: var(--muted-foreground);
212
+ opacity: 0.6;
213
+ pointer-events: none;
214
+ -webkit-user-select: none;
215
+ -moz-user-select: none;
216
+ user-select: none;
217
+ white-space: nowrap;
218
+ }
219
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ import type { ToolbarFeature } from "../types";
3
+ import { type EditorTranslator } from "../i18n";
4
+ type $$ComponentProps = {
5
+ editor: Editor;
6
+ features: Set<ToolbarFeature>;
7
+ suppressed?: boolean;
8
+ /** 에디터 UI 번역 함수. 미주입 시 ko. */
9
+ t?: EditorTranslator;
10
+ };
11
+ declare const FloatingHelper: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type FloatingHelper = ReturnType<typeof FloatingHelper>;
13
+ export default FloatingHelper;
14
+ //# sourceMappingURL=FloatingHelper.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FloatingHelper.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FloatingHelper.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAO/C,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAElE,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AA0IJ,QAAA,MAAM,cAAc,sDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}