@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
@@ -12,15 +12,16 @@
12
12
  Strikethrough,
13
13
  Highlighter,
14
14
  LinkIcon,
15
- AlignLeft,
16
- AlignCenter,
17
- AlignRight,
18
15
  Heading1,
19
16
  Heading2,
20
17
  Heading3,
21
18
  Type,
22
- Code,
23
19
  Palette,
20
+ List,
21
+ ListOrdered,
22
+ ListChecks,
23
+ Quote,
24
+ ChevronDown,
24
25
  } from "lucide-svelte";
25
26
  import { cn } from "../utils/cn";
26
27
  import type { ToolbarFeature, PromptHandler } from "../types";
@@ -37,9 +38,11 @@
37
38
 
38
39
  const has = (f: ToolbarFeature) => features.has(f);
39
40
 
40
- let showHeadings = $state(false);
41
+ let showBlockMenu = $state(false);
41
42
  let showColors = $state(false);
42
43
  let menuEl: HTMLDivElement | undefined = $state();
44
+ let blockMenuEl: HTMLDivElement | undefined = $state();
45
+ let colorMenuEl: HTMLDivElement | undefined = $state();
43
46
  const iconSize = 14;
44
47
 
45
48
  const TEXT_COLORS = [
@@ -58,9 +61,23 @@
58
61
  if (editor.isActive("heading", { level: 1 })) return "제목 1";
59
62
  if (editor.isActive("heading", { level: 2 })) return "제목 2";
60
63
  if (editor.isActive("heading", { level: 3 })) return "제목 3";
64
+ if (editor.isActive("bulletList")) return "글머리 목록";
65
+ if (editor.isActive("orderedList")) return "번호 목록";
66
+ if (editor.isActive("taskList")) return "체크리스트";
67
+ if (editor.isActive("blockquote")) return "인용문";
61
68
  return "본문";
62
69
  }
63
70
 
71
+ function isParagraphActive(): boolean {
72
+ return (
73
+ !editor.isActive("heading") &&
74
+ !editor.isActive("bulletList") &&
75
+ !editor.isActive("orderedList") &&
76
+ !editor.isActive("taskList") &&
77
+ !editor.isActive("blockquote")
78
+ );
79
+ }
80
+
64
81
  async function addLink() {
65
82
  const previousUrl = editor.getAttributes("link").href || "";
66
83
  const url = onPromptLink
@@ -79,6 +96,23 @@
79
96
  }
80
97
  }
81
98
 
99
+ function handleDocClick(e: MouseEvent) {
100
+ const target = e.target as Node;
101
+ if (showBlockMenu && blockMenuEl && !blockMenuEl.contains(target)) {
102
+ showBlockMenu = false;
103
+ }
104
+ if (showColors && colorMenuEl && !colorMenuEl.contains(target)) {
105
+ showColors = false;
106
+ }
107
+ }
108
+
109
+ function handleKeydown(e: KeyboardEvent) {
110
+ if (e.key === "Escape") {
111
+ showBlockMenu = false;
112
+ showColors = false;
113
+ }
114
+ }
115
+
82
116
  onMount(() => {
83
117
  if (!menuEl) return;
84
118
 
@@ -99,70 +133,153 @@
99
133
  });
100
134
 
101
135
  editor.registerPlugin(plugin);
136
+ document.addEventListener("mousedown", handleDocClick);
137
+ document.addEventListener("keydown", handleKeydown);
102
138
 
103
139
  return () => {
104
140
  editor.unregisterPlugin(bubbleToolbarKey);
141
+ document.removeEventListener("mousedown", handleDocClick);
142
+ document.removeEventListener("keydown", handleKeydown);
105
143
  };
106
144
  });
145
+
146
+ const hasBlockMenu = $derived(
147
+ has('h1') ||
148
+ has('h2') ||
149
+ has('h3') ||
150
+ has('bullet-list') ||
151
+ has('ordered-list') ||
152
+ has('checklist') ||
153
+ has('blockquote'),
154
+ );
107
155
  </script>
108
156
 
109
157
  <div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
110
158
  <div class="flex items-center gap-0.5 px-1.5 py-1 bg-foreground rounded-full shadow-xl">
111
- {#if has('h1') || has('h2') || has('h3')}
159
+ {#if hasBlockMenu}
112
160
  <!-- Block type selector -->
113
- <div class="relative">
161
+ <div class="relative" bind:this={blockMenuEl}>
114
162
  <button
115
163
  type="button"
116
- onclick={() => (showHeadings = !showHeadings)}
164
+ onclick={() => (showBlockMenu = !showBlockMenu)}
117
165
  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"
118
166
  >
119
167
  <Type size={12} />
120
168
  {getCurrentBlockLabel()}
169
+ <ChevronDown size={12} />
121
170
  </button>
122
- {#if showHeadings}
171
+ {#if showBlockMenu}
123
172
  <div
124
173
  class="absolute bottom-full left-0 mb-1 bg-foreground rounded-lg shadow-xl border border-white/10 py-1"
125
- style="min-width: 120px"
174
+ style="min-width: 140px"
126
175
  >
127
176
  <button
128
177
  type="button"
129
178
  class={cn(
130
179
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
131
- !editor.isActive("heading")
132
- ? "text-white bg-white/10"
180
+ isParagraphActive()
181
+ ? "bg-primary/10 text-primary"
133
182
  : "text-white/70 hover:text-white hover:bg-white/10",
134
183
  )}
135
184
  onclick={() => {
136
185
  editor.chain().focus().setParagraph().run();
137
- showHeadings = false;
186
+ showBlockMenu = false;
138
187
  }}
139
188
  >
140
189
  <Type size={12} /> 본문
141
190
  </button>
142
191
  {#each [1, 2, 3] as level}
143
192
  {#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
144
- {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
193
+ {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
194
+ <button
195
+ type="button"
196
+ class={cn(
197
+ "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
198
+ editor.isActive("heading", { level })
199
+ ? "bg-primary/10 text-primary"
200
+ : "text-white/70 hover:text-white hover:bg-white/10",
201
+ )}
202
+ onclick={() => {
203
+ editor
204
+ .chain()
205
+ .focus()
206
+ .toggleHeading({ level: level as 1 | 2 | 3 })
207
+ .run();
208
+ showBlockMenu = false;
209
+ }}
210
+ >
211
+ <Icon size={12} /> 제목 {level}
212
+ </button>
213
+ {/if}
214
+ {/each}
215
+ {#if has('bullet-list')}
145
216
  <button
146
217
  type="button"
147
218
  class={cn(
148
219
  "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
149
- editor.isActive("heading", { level })
150
- ? "text-white bg-white/10"
220
+ editor.isActive("bulletList")
221
+ ? "bg-primary/10 text-primary"
151
222
  : "text-white/70 hover:text-white hover:bg-white/10",
152
223
  )}
153
224
  onclick={() => {
154
- editor
155
- .chain()
156
- .focus()
157
- .toggleHeading({ level: level as 1 | 2 | 3 })
158
- .run();
159
- showHeadings = false;
225
+ editor.chain().focus().toggleBulletList().run();
226
+ showBlockMenu = false;
160
227
  }}
161
228
  >
162
- <Icon size={12} /> 제목 {level}
229
+ <List size={12} /> 글머리 목록
163
230
  </button>
164
- {/if}
165
- {/each}
231
+ {/if}
232
+ {#if has('ordered-list')}
233
+ <button
234
+ type="button"
235
+ class={cn(
236
+ "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
237
+ editor.isActive("orderedList")
238
+ ? "bg-primary/10 text-primary"
239
+ : "text-white/70 hover:text-white hover:bg-white/10",
240
+ )}
241
+ onclick={() => {
242
+ editor.chain().focus().toggleOrderedList().run();
243
+ showBlockMenu = false;
244
+ }}
245
+ >
246
+ <ListOrdered size={12} /> 번호 목록
247
+ </button>
248
+ {/if}
249
+ {#if has('checklist')}
250
+ <button
251
+ type="button"
252
+ class={cn(
253
+ "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
254
+ editor.isActive("taskList")
255
+ ? "bg-primary/10 text-primary"
256
+ : "text-white/70 hover:text-white hover:bg-white/10",
257
+ )}
258
+ onclick={() => {
259
+ editor.chain().focus().toggleTaskList().run();
260
+ showBlockMenu = false;
261
+ }}
262
+ >
263
+ <ListChecks size={12} /> 체크리스트
264
+ </button>
265
+ {/if}
266
+ {#if has('blockquote')}
267
+ <button
268
+ type="button"
269
+ class={cn(
270
+ "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
271
+ editor.isActive("blockquote")
272
+ ? "bg-primary/10 text-primary"
273
+ : "text-white/70 hover:text-white hover:bg-white/10",
274
+ )}
275
+ onclick={() => {
276
+ editor.chain().focus().toggleBlockquote().run();
277
+ showBlockMenu = false;
278
+ }}
279
+ >
280
+ <Quote size={12} /> 인용문
281
+ </button>
282
+ {/if}
166
283
  </div>
167
284
  {/if}
168
285
  </div>
@@ -235,8 +352,29 @@
235
352
  <Strikethrough size={iconSize} />
236
353
  </button>
237
354
  {/if}
355
+
356
+ {#if has('highlight') || has('text-color')}
357
+ <div class="w-px h-5 bg-white/20 mx-0.5"></div>
358
+ {/if}
359
+
360
+ {#if has('highlight')}
361
+ <button
362
+ type="button"
363
+ onclick={() => editor.chain().focus().toggleHighlight().run()}
364
+ title="하이라이트"
365
+ aria-label="하이라이트"
366
+ class={cn(
367
+ "p-1.5 rounded-full transition-colors",
368
+ editor.isActive("highlight")
369
+ ? "bg-white/20 text-white"
370
+ : "text-white/70 hover:text-white hover:bg-white/10",
371
+ )}
372
+ >
373
+ <Highlighter size={iconSize} />
374
+ </button>
375
+ {/if}
238
376
  {#if has('text-color')}
239
- <div class="relative">
377
+ <div class="relative" bind:this={colorMenuEl}>
240
378
  <button
241
379
  type="button"
242
380
  onclick={() => (showColors = !showColors)}
@@ -295,110 +433,23 @@
295
433
  {/if}
296
434
  </div>
297
435
  {/if}
298
- {#if has('highlight')}
299
- <button
300
- type="button"
301
- onclick={() => editor.chain().focus().toggleHighlight().run()}
302
- title="하이라이트"
303
- aria-label="하이라이트"
304
- class={cn(
305
- "p-1.5 rounded-full transition-colors",
306
- editor.isActive("highlight")
307
- ? "bg-white/20 text-white"
308
- : "text-white/70 hover:text-white hover:bg-white/10",
309
- )}
310
- >
311
- <Highlighter size={iconSize} />
312
- </button>
313
- {/if}
314
- {#if has('code')}
315
- <button
316
- type="button"
317
- onclick={() => editor.chain().focus().toggleCode().run()}
318
- title="코드"
319
- aria-label="코드"
320
- class={cn(
321
- "p-1.5 rounded-full transition-colors",
322
- editor.isActive("code")
323
- ? "bg-white/20 text-white"
324
- : "text-white/70 hover:text-white hover:bg-white/10",
325
- )}
326
- >
327
- <Code size={iconSize} />
328
- </button>
329
- {/if}
330
436
 
331
437
  {#if has('link')}
332
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
333
-
334
- <button
335
- type="button"
336
- onclick={addLink}
337
- title="링크"
338
- aria-label="링크"
339
- class={cn(
340
- "p-1.5 rounded-full transition-colors",
341
- editor.isActive("link")
342
- ? "bg-white/20 text-white"
343
- : "text-white/70 hover:text-white hover:bg-white/10",
344
- )}
345
- >
346
- <LinkIcon size={iconSize} />
347
- </button>
348
- {/if}
349
-
350
- {#if has('align-left') || has('align-center') || has('align-right')}
351
438
  <div class="w-px h-5 bg-white/20 mx-0.5"></div>
352
-
353
- <!-- Alignment -->
354
- {#if has('align-left')}
355
- <button
356
- type="button"
357
- onclick={() => editor.chain().focus().setTextAlign("left").run()}
358
- title="왼쪽 정렬"
359
- aria-label="왼쪽 정렬"
360
- class={cn(
361
- "p-1.5 rounded-full transition-colors",
362
- editor.isActive({ textAlign: "left" })
363
- ? "bg-white/20 text-white"
364
- : "text-white/70 hover:text-white hover:bg-white/10",
365
- )}
366
- >
367
- <AlignLeft size={iconSize} />
368
- </button>
369
- {/if}
370
- {#if has('align-center')}
371
- <button
372
- type="button"
373
- onclick={() => editor.chain().focus().setTextAlign("center").run()}
374
- title="가운데 정렬"
375
- aria-label="가운데 정렬"
376
- class={cn(
377
- "p-1.5 rounded-full transition-colors",
378
- editor.isActive({ textAlign: "center" })
379
- ? "bg-white/20 text-white"
380
- : "text-white/70 hover:text-white hover:bg-white/10",
381
- )}
382
- >
383
- <AlignCenter size={iconSize} />
384
- </button>
385
- {/if}
386
- {#if has('align-right')}
387
439
  <button
388
440
  type="button"
389
- onclick={() => editor.chain().focus().setTextAlign("right").run()}
390
- title="오른쪽 정렬"
391
- aria-label="오른쪽 정렬"
441
+ onclick={addLink}
442
+ title="링크"
443
+ aria-label="링크"
392
444
  class={cn(
393
445
  "p-1.5 rounded-full transition-colors",
394
- editor.isActive({ textAlign: "right" })
446
+ editor.isActive("link")
395
447
  ? "bg-white/20 text-white"
396
448
  : "text-white/70 hover:text-white hover:bg-white/10",
397
449
  )}
398
450
  >
399
- <AlignRight size={iconSize} />
451
+ <LinkIcon size={iconSize} />
400
452
  </button>
401
- {/if}
402
453
  {/if}
403
454
  </div>
404
455
  </div>
@@ -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;AAsB3C,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;AAkSJ,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;AAuB3C,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;AA4VJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}