@teriusu/rich-editor 0.9.6 → 0.9.8

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 (53) hide show
  1. package/dist/components/BubbleToolbar.svelte +102 -49
  2. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  3. package/dist/components/FixedToolbar.svelte +136 -214
  4. package/dist/components/FixedToolbar.svelte.d.ts +9 -1
  5. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  6. package/dist/components/InputModal.svelte +1 -1
  7. package/dist/components/MathModal.svelte +111 -1
  8. package/dist/components/MathModal.svelte.d.ts.map +1 -1
  9. package/dist/components/MediaPickerModal.svelte +206 -0
  10. package/dist/components/MediaPickerModal.svelte.d.ts +18 -0
  11. package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -0
  12. package/dist/components/SlashCommandMenu.svelte +114 -13
  13. package/dist/components/SlashCommandMenu.svelte.d.ts +10 -1
  14. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  15. package/dist/components/TableBubbleMenu.svelte +71 -48
  16. package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
  17. package/dist/components/TipTapEditor.svelte +163 -24
  18. package/dist/components/TipTapEditor.svelte.d.ts +2 -0
  19. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  20. package/dist/extensions/NotionInputRules.d.ts +59 -0
  21. package/dist/extensions/NotionInputRules.d.ts.map +1 -0
  22. package/dist/extensions/NotionInputRules.js +141 -0
  23. package/dist/extensions/NotionInputRules.test.d.ts +2 -0
  24. package/dist/extensions/NotionInputRules.test.d.ts.map +1 -0
  25. package/dist/extensions/NotionInputRules.test.js +117 -0
  26. package/dist/extensions/VideoEmbed.d.ts +36 -0
  27. package/dist/extensions/VideoEmbed.d.ts.map +1 -0
  28. package/dist/extensions/VideoEmbed.js +246 -0
  29. package/dist/extensions/VideoEmbed.test.d.ts +2 -0
  30. package/dist/extensions/VideoEmbed.test.d.ts.map +1 -0
  31. package/dist/extensions/VideoEmbed.test.js +57 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +13 -0
  35. package/dist/styles/editor.css +1 -1
  36. package/dist/types.d.ts +14 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +20 -6
  39. package/dist/utils/lowlight.d.ts +17 -0
  40. package/dist/utils/lowlight.d.ts.map +1 -0
  41. package/dist/utils/lowlight.js +106 -0
  42. package/dist/utils/lowlight.test.d.ts +2 -0
  43. package/dist/utils/lowlight.test.d.ts.map +1 -0
  44. package/dist/utils/lowlight.test.js +56 -0
  45. package/dist/utils/sanitize.d.ts.map +1 -1
  46. package/dist/utils/sanitize.js +14 -0
  47. package/dist/utils/splitCell.d.ts +20 -0
  48. package/dist/utils/splitCell.d.ts.map +1 -0
  49. package/dist/utils/splitCell.js +74 -0
  50. package/dist/utils/table.d.ts +22 -0
  51. package/dist/utils/table.d.ts.map +1 -0
  52. package/dist/utils/table.js +68 -0
  53. package/package.json +6 -1
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Editor } from "@tiptap/core";
3
3
  import { BubbleMenuPlugin } from "@tiptap/extension-bubble-menu";
4
- import { PluginKey } from "@tiptap/pm/state";
4
+ import { PluginKey, NodeSelection } from "@tiptap/pm/state";
5
+ import { CellSelection } from "@tiptap/pm/tables";
5
6
  import { onMount } from "svelte";
6
7
 
7
8
  const bubbleToolbarKey = new PluginKey("bubbleToolbar");
@@ -11,6 +12,7 @@
11
12
  Underline as UnderlineIcon,
12
13
  Strikethrough,
13
14
  Highlighter,
15
+ Code,
14
16
  LinkIcon,
15
17
  Heading1,
16
18
  Heading2,
@@ -148,10 +150,42 @@
148
150
  shouldShow: ({ editor: e, state }) => {
149
151
  const { from, to } = state.selection;
150
152
  if (from === to) return false;
153
+ /*
154
+ * ⚠️ **덩어리를 고른 것(NodeSelection)에는 뜨지 않는다.**
155
+ *
156
+ * 이미지·파일 첨부·PDF·카드 같은 노드를 클릭하면 `from !== to` 가 되어 "글자를
157
+ * 골랐다"와 구별되지 않는다. 그러면 굵게·기울임처럼 **글자에만 걸리는 서식** 메뉴가
158
+ * 그림 위에 떠서, 눌러도 아무 일이 없는 버튼만 보여 준다.
159
+ *
160
+ * 예전에는 `isActive("image")` 로 이미지 하나만 막았는데, 그건 노드가 늘 때마다
161
+ * 빠뜨리게 되는 방식이다(실제로 파일 첨부에서 떴다). 종류를 세지 말고 **선택의
162
+ * 종류**로 가른다.
163
+ */
164
+ if (state.selection instanceof NodeSelection) return false;
165
+ /*
166
+ * ⚠️ **셀을 고른 것(CellSelection)에도 뜨지 않는다.**
167
+ *
168
+ * 표에서 셀을 끌어 고르면 "글자를 골랐다"와 구별이 안 돼 서식 버블이 떴는데,
169
+ * 그게 **표 메뉴의 병합·분할 버튼을 가렸다**(사용자 지적). 그 상태에서 손이 가는
170
+ * 건 서식이 아니라 표 도구다.
171
+ *
172
+ * ⚠️ z 순서로 풀지 않는다 — 버블을 아래로 내리면 이번엔 셀 안에서 글자를 골랐을 때
173
+ * 버블이 표 메뉴에 가린다(그게 원래 신고였다). **겹치는 상황 자체를 없애는** 쪽이 맞다.
174
+ * 셀 하나 안에서 글자를 고르는 건 여전히 `TextSelection` 이라 버블이 정상으로 뜬다.
175
+ */
176
+ if (state.selection instanceof CellSelection) return false;
151
177
  if (e.isActive("codeBlock")) return false;
152
- if (e.isActive("image")) return false;
153
178
  return true;
154
179
  },
180
+ /*
181
+ * ⚠️ **기본값 250ms 는 눈에 띄게 느리다**(사용자 지적). 플러그인은 선택이 비어 있지
182
+ * 않으면 **무조건** 이 디바운스를 태우므로(`update()` 의 `hasValidSelection` 분기),
183
+ * 글자를 다 골라 놓고도 4분의 1초를 기다리게 된다.
184
+ *
185
+ * 그렇다고 `0` 으로 두면 드래그하는 **내내** 위치를 다시 잡아 버블이 따라다닌다.
186
+ * 100ms 면 끌기가 끝난 직후로 느껴지면서 그 재계산은 여전히 묶인다.
187
+ */
188
+ updateDelay: 100,
155
189
  /*
156
190
  * ⚠️ `tippyOptions` 는 **TipTap 2 시절 이름이라 v3 에서는 통째로 무시된다.**
157
191
  * v3 의 BubbleMenu 는 tippy 가 아니라 floating-ui 기반이고, 받는 키가 `options`
@@ -175,26 +209,36 @@
175
209
  };
176
210
  });
177
211
 
212
+ /*
213
+ * 블록 타입 선택기는 **고정 툴바가 없을 때만** 버블에 넣는다.
214
+ *
215
+ * 고정 툴바(`full`·`standard`)에는 이미 같은 선택기가 늘 보이는 자리에 있어서, 버블에도
216
+ * 넣으면 같은 것이 두 벌 뜨고 버블이 그만큼 길어진다. 글자를 끌어 골랐을 때 손이 가는 건
217
+ * 대개 **서식**이지 블록 바꾸기가 아니다.
218
+ *
219
+ * `minimal`(댓글 등)에는 고정 툴바가 없어 버블이 유일한 메뉴이므로 그대로 남는다.
220
+ */
178
221
  const hasBlockMenu = $derived(
179
- has('h1') ||
180
- has('h2') ||
181
- has('h3') ||
182
- has('bullet-list') ||
183
- has('ordered-list') ||
184
- has('checklist') ||
185
- has('blockquote'),
222
+ !has('fixed-toolbar') &&
223
+ (has('h1') ||
224
+ has('h2') ||
225
+ has('h3') ||
226
+ has('bullet-list') ||
227
+ has('ordered-list') ||
228
+ has('checklist') ||
229
+ has('blockquote')),
186
230
  );
187
231
  </script>
188
232
 
189
233
  <div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
190
- <div class="flex items-center gap-0.5 px-1.5 py-1 hce-menu-surface rounded-full shadow-xl">
234
+ <div class="flex items-center gap-0.5 px-1.5 py-1 rounded-full hce-floating-panel">
191
235
  {#if hasBlockMenu}
192
236
  <!-- Block type selector -->
193
237
  <div class="relative" bind:this={blockMenuEl}>
194
238
  <button
195
239
  type="button"
196
240
  onclick={() => (showBlockMenu = !showBlockMenu)}
197
- class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-white/80 hover:text-white hover:bg-white/10 transition-colors"
241
+ class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
198
242
  >
199
243
  <Type size={12} />
200
244
  {getCurrentBlockLabel()}
@@ -202,7 +246,7 @@
202
246
  </button>
203
247
  {#if showBlockMenu}
204
248
  <div
205
- class="absolute bottom-full left-0 mb-1 hce-menu-surface rounded-lg shadow-xl border border-white/10 py-1"
249
+ class="absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
206
250
  style="min-width: 140px"
207
251
  onmousedown={(e) => e.preventDefault()}
208
252
  role="menu"
@@ -214,7 +258,7 @@
214
258
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
215
259
  isParagraphActive()
216
260
  ? "hce-active"
217
- : "text-white/70 hover:text-white hover:bg-white/10",
261
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
218
262
  )}
219
263
  onclick={() => {
220
264
  editor.chain().focus().setParagraph().run();
@@ -232,7 +276,7 @@
232
276
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
233
277
  isActive("heading", { level })
234
278
  ? "hce-active"
235
- : "text-white/70 hover:text-white hover:bg-white/10",
279
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
236
280
  )}
237
281
  onclick={() => {
238
282
  editor
@@ -254,7 +298,7 @@
254
298
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
255
299
  isActive("bulletList")
256
300
  ? "hce-active"
257
- : "text-white/70 hover:text-white hover:bg-white/10",
301
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
258
302
  )}
259
303
  onclick={() => {
260
304
  editor.chain().focus().toggleBulletList().run();
@@ -271,7 +315,7 @@
271
315
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
272
316
  isActive("orderedList")
273
317
  ? "hce-active"
274
- : "text-white/70 hover:text-white hover:bg-white/10",
318
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
275
319
  )}
276
320
  onclick={() => {
277
321
  editor.chain().focus().toggleOrderedList().run();
@@ -288,7 +332,7 @@
288
332
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
289
333
  isActive("taskList")
290
334
  ? "hce-active"
291
- : "text-white/70 hover:text-white hover:bg-white/10",
335
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
292
336
  )}
293
337
  onclick={() => {
294
338
  editor.chain().focus().toggleTaskList().run();
@@ -305,7 +349,7 @@
305
349
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
306
350
  isActive("blockquote")
307
351
  ? "hce-active"
308
- : "text-white/70 hover:text-white hover:bg-white/10",
352
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
309
353
  )}
310
354
  onclick={() => {
311
355
  editor.chain().focus().toggleBlockquote().run();
@@ -319,7 +363,7 @@
319
363
  {/if}
320
364
  </div>
321
365
 
322
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
366
+ <div class="w-px h-5 bg-border mx-0.5"></div>
323
367
  {/if}
324
368
 
325
369
  <!-- Format buttons -->
@@ -327,13 +371,12 @@
327
371
  <button
328
372
  type="button"
329
373
  onclick={() => editor.chain().focus().toggleBold().run()}
330
- title="굵게"
331
374
  aria-label="굵게"
332
375
  class={cn(
333
376
  "p-1.5 rounded-full transition-colors",
334
377
  isActive("bold")
335
- ? "bg-white/20 text-white"
336
- : "text-white/70 hover:text-white hover:bg-white/10",
378
+ ? "hce-active"
379
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
337
380
  )}
338
381
  >
339
382
  <Bold size={iconSize} />
@@ -343,13 +386,12 @@
343
386
  <button
344
387
  type="button"
345
388
  onclick={() => editor.chain().focus().toggleItalic().run()}
346
- title="기울임"
347
389
  aria-label="기울임"
348
390
  class={cn(
349
391
  "p-1.5 rounded-full transition-colors",
350
392
  isActive("italic")
351
- ? "bg-white/20 text-white"
352
- : "text-white/70 hover:text-white hover:bg-white/10",
393
+ ? "hce-active"
394
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
353
395
  )}
354
396
  >
355
397
  <Italic size={iconSize} />
@@ -359,13 +401,12 @@
359
401
  <button
360
402
  type="button"
361
403
  onclick={() => editor.chain().focus().toggleUnderline().run()}
362
- title="밑줄"
363
404
  aria-label="밑줄"
364
405
  class={cn(
365
406
  "p-1.5 rounded-full transition-colors",
366
407
  isActive("underline")
367
- ? "bg-white/20 text-white"
368
- : "text-white/70 hover:text-white hover:bg-white/10",
408
+ ? "hce-active"
409
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
369
410
  )}
370
411
  >
371
412
  <UnderlineIcon size={iconSize} />
@@ -375,19 +416,35 @@
375
416
  <button
376
417
  type="button"
377
418
  onclick={() => editor.chain().focus().toggleStrike().run()}
378
- title="취소선"
379
419
  aria-label="취소선"
380
420
  class={cn(
381
421
  "p-1.5 rounded-full transition-colors",
382
422
  isActive("strike")
383
- ? "bg-white/20 text-white"
384
- : "text-white/70 hover:text-white hover:bg-white/10",
423
+ ? "hce-active"
424
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
385
425
  )}
386
426
  >
387
427
  <Strikethrough size={iconSize} />
388
428
  </button>
389
429
  {/if}
390
430
 
431
+ {#if has('code')}
432
+ <!-- 인라인 코드(`<code>`). 고정 툴바와 같은 자리(취소선 다음)에 둔다. -->
433
+ <button
434
+ type="button"
435
+ onclick={() => editor.chain().focus().toggleCode().run()}
436
+ aria-label="코드"
437
+ class={cn(
438
+ "p-1.5 rounded-full transition-colors",
439
+ isActive("code")
440
+ ? "hce-active"
441
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
442
+ )}
443
+ >
444
+ <Code size={iconSize} />
445
+ </button>
446
+ {/if}
447
+
391
448
  {#if has('math')}
392
449
  <!--
393
450
  버블 툴바는 **뭔가 선택돼 있을 때만** 뜨므로 여기서는 감싸기/풀기만 하면 된다
@@ -397,13 +454,12 @@
397
454
  <button
398
455
  type="button"
399
456
  onclick={() => editor.chain().focus().toggleMathInline().run()}
400
- title="인라인 수식"
401
457
  aria-label="인라인 수식"
402
458
  class={cn(
403
459
  "p-1.5 rounded-full transition-colors",
404
460
  isActive("math_inline")
405
- ? "bg-white/20 text-white"
406
- : "text-white/70 hover:text-white hover:bg-white/10",
461
+ ? "hce-active"
462
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
407
463
  )}
408
464
  >
409
465
  <Sigma size={iconSize} />
@@ -411,20 +467,19 @@
411
467
  {/if}
412
468
 
413
469
  {#if has('highlight') || has('text-color')}
414
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
470
+ <div class="w-px h-5 bg-border mx-0.5"></div>
415
471
  {/if}
416
472
 
417
473
  {#if has('highlight')}
418
474
  <button
419
475
  type="button"
420
476
  onclick={() => editor.chain().focus().toggleHighlight().run()}
421
- title="하이라이트"
422
477
  aria-label="하이라이트"
423
478
  class={cn(
424
479
  "p-1.5 rounded-full transition-colors",
425
480
  isActive("highlight")
426
- ? "bg-white/20 text-white"
427
- : "text-white/70 hover:text-white hover:bg-white/10",
481
+ ? "hce-active"
482
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
428
483
  )}
429
484
  >
430
485
  <Highlighter size={iconSize} />
@@ -435,20 +490,19 @@
435
490
  <button
436
491
  type="button"
437
492
  onclick={() => (showColors = !showColors)}
438
- title="글자색"
439
493
  aria-label="글자색"
440
494
  class={cn(
441
495
  "p-1.5 rounded-full transition-colors",
442
496
  editor.getAttributes("textStyle").color
443
- ? "bg-white/20 text-white"
444
- : "text-white/70 hover:text-white hover:bg-white/10",
497
+ ? "hce-active"
498
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
445
499
  )}
446
500
  >
447
501
  <Palette size={iconSize} />
448
502
  </button>
449
503
  {#if showColors}
450
504
  <div
451
- class="absolute bottom-full left-0 mb-1 hce-menu-surface rounded-lg shadow-xl border border-white/10 p-2"
505
+ class="absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl p-2"
452
506
  style="min-width: 160px"
453
507
  onmousedown={(e) => e.preventDefault()}
454
508
  role="menu"
@@ -459,7 +513,7 @@
459
513
  <button
460
514
  type="button"
461
515
  title={c.label}
462
- class="h-7 rounded-md border border-white/20 transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold"
516
+ class="h-7 rounded-md border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold"
463
517
  style="color: {c.value || '#000'}; background: #fff"
464
518
  onclick={() => {
465
519
  if (c.value) {
@@ -475,12 +529,12 @@
475
529
  {/each}
476
530
  </div>
477
531
  <label
478
- class="hce-color-divider mt-2 pt-2 flex items-center justify-between gap-2 px-1 text-xs text-white/70 cursor-pointer hover:text-white"
532
+ class="hce-color-divider mt-2 pt-2 flex items-center justify-between gap-2 px-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground"
479
533
  >
480
534
  <span>직접 선택</span>
481
535
  <input
482
536
  type="color"
483
- class="h-6 w-10 cursor-pointer rounded border border-white/20 bg-transparent p-0"
537
+ class="h-6 w-10 cursor-pointer rounded border border-border bg-transparent p-0"
484
538
  value={(editor.getAttributes("textStyle").color as string) || "#000000"}
485
539
  onclick={(e) => e.stopPropagation()}
486
540
  oninput={(e) => {
@@ -495,17 +549,16 @@
495
549
  {/if}
496
550
 
497
551
  {#if has('link')}
498
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
552
+ <div class="w-px h-5 bg-border mx-0.5"></div>
499
553
  <button
500
554
  type="button"
501
555
  onclick={addLink}
502
- title="링크"
503
556
  aria-label="링크"
504
557
  class={cn(
505
558
  "p-1.5 rounded-full transition-colors",
506
559
  isActive("link")
507
- ? "bg-white/20 text-white"
508
- : "text-white/70 hover:text-white hover:bg-white/10",
560
+ ? "hce-active"
561
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
509
562
  )}
510
563
  >
511
564
  <LinkIcon size={iconSize} />
@@ -1 +1 @@
1
- {"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAwB3C,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,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AAuYJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,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,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AA8bJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}