@teriusu/rich-editor 0.5.3 → 0.6.1

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.
@@ -21,11 +21,35 @@
21
21
  Columns2,
22
22
  Columns3,
23
23
  } from "lucide-svelte";
24
- import type { SlashMenuItem, ToolbarFeature } from "../types";
24
+ import type { SlashMenuItem, ToolbarFeature, PromptHandler } from "../types";
25
25
  import type { Component } from "svelte";
26
26
 
27
27
  const SI = 14;
28
28
 
29
+ /** 각 feature가 속하는 섹션 라벨 */
30
+ const SECTION_MAP: Record<string, string> = {
31
+ "code-block": "자주 쓰는",
32
+ file: "자주 쓰는",
33
+ pdf: "자주 쓰는",
34
+ h1: "기본",
35
+ h2: "기본",
36
+ h3: "기본",
37
+ "bullet-list": "리스트",
38
+ "ordered-list": "리스트",
39
+ checklist: "리스트",
40
+ toggle: "리스트",
41
+ blockquote: "블록",
42
+ "horizontal-rule": "블록",
43
+ table: "레이아웃",
44
+ "columns-2": "레이아웃",
45
+ "columns-3": "레이아웃",
46
+ link: "미디어",
47
+ image: "미디어",
48
+ youtube: "미디어",
49
+ video: "미디어",
50
+ };
51
+ const SECTION_ORDER = ["자주 쓰는", "기본", "리스트", "블록", "레이아웃", "미디어"];
52
+
29
53
  const SLASH_MENU_ITEMS_DATA: {
30
54
  feature: ToolbarFeature;
31
55
  label: string;
@@ -94,20 +118,12 @@
94
118
  },
95
119
  {
96
120
  feature: "code-block",
97
- label: "C++ 코드",
98
- keywords: "code cpp c++ 코드 블록",
121
+ label: "코드",
122
+ keywords: "code 코드 블록 cpp c++ python 파이썬",
99
123
  icon: Code2,
100
124
  command: (editor) =>
101
125
  editor.chain().focus().setCodeBlock({ language: "cpp" }).run(),
102
126
  },
103
- {
104
- feature: "code-block",
105
- label: "Python 코드",
106
- keywords: "code python 파이썬 코드 블록",
107
- icon: Code2,
108
- command: (editor) =>
109
- editor.chain().focus().setCodeBlock({ language: "python" }).run(),
110
- },
111
127
  {
112
128
  feature: "toggle",
113
129
  label: "토글",
@@ -187,6 +203,8 @@
187
203
  onPdfUpload,
188
204
  onFileUpload,
189
205
  onVideoUpload,
206
+ onPromptLink,
207
+ onPromptImage,
190
208
  }: {
191
209
  editor: Editor;
192
210
  features: Set<ToolbarFeature>;
@@ -195,14 +213,45 @@
195
213
  onPdfUpload?: () => void;
196
214
  onFileUpload?: () => void;
197
215
  onVideoUpload?: () => void;
216
+ onPromptLink?: PromptHandler;
217
+ onPromptImage?: PromptHandler;
198
218
  } = $props();
199
219
 
220
+ async function runItem(item: (typeof SLASH_MENU_ITEMS_DATA)[number]) {
221
+ if (item.feature === "link" && onPromptLink) {
222
+ const url = await onPromptLink("");
223
+ if (url)
224
+ editor
225
+ .chain()
226
+ .focus()
227
+ .extendMarkRange("link")
228
+ .setLink({ href: url })
229
+ .run();
230
+ return;
231
+ }
232
+ if (item.feature === "image" && onPromptImage) {
233
+ const url = await onPromptImage("");
234
+ if (url) editor.chain().focus().setImage({ src: url }).run();
235
+ return;
236
+ }
237
+ item.command(editor);
238
+ }
239
+
200
240
  let selectedIndex = $state(0);
201
241
  let menuEl: HTMLDivElement | undefined = $state();
202
242
  let mouseMovedSinceKeyboard = $state(true);
203
243
 
204
244
  const allItems = $derived.by(() => {
205
245
  const items = SLASH_MENU_ITEMS_DATA.filter((item) => features.has(item.feature));
246
+ if (onFileUpload) {
247
+ items.push({
248
+ feature: "file" as ToolbarFeature,
249
+ label: "파일 첨부",
250
+ keywords: "file attach 파일 첨부",
251
+ icon: Paperclip,
252
+ command: () => onFileUpload!(),
253
+ });
254
+ }
206
255
  if (onPdfUpload) {
207
256
  items.push({
208
257
  feature: "pdf" as ToolbarFeature,
@@ -221,15 +270,6 @@
221
270
  command: () => onVideoUpload!(),
222
271
  });
223
272
  }
224
- if (onFileUpload) {
225
- items.push({
226
- feature: "file" as ToolbarFeature,
227
- label: "파일 첨부",
228
- keywords: "file attach 파일 첨부",
229
- icon: Paperclip,
230
- command: () => onFileUpload!(),
231
- });
232
- }
233
273
  return items;
234
274
  });
235
275
 
@@ -258,6 +298,16 @@
258
298
  }
259
299
  });
260
300
 
301
+ $effect(() => {
302
+ function handleMouseDown(e: MouseEvent) {
303
+ if (menuEl && !menuEl.contains(e.target as Node)) {
304
+ onClose();
305
+ }
306
+ }
307
+ document.addEventListener("mousedown", handleMouseDown, true);
308
+ return () => document.removeEventListener("mousedown", handleMouseDown, true);
309
+ });
310
+
261
311
  $effect(() => {
262
312
  function handleKeyDown(e: KeyboardEvent) {
263
313
  if (e.key === "ArrowDown") {
@@ -274,8 +324,9 @@
274
324
  } else if (e.key === "Enter") {
275
325
  e.preventDefault();
276
326
  if (filtered[selectedIndex]) {
327
+ const item = filtered[selectedIndex];
277
328
  onClose();
278
- filtered[selectedIndex].command(editor);
329
+ runItem(item);
279
330
  }
280
331
  } else if (e.key === "Escape") {
281
332
  e.preventDefault();
@@ -290,49 +341,127 @@
290
341
  {#if filtered.length === 0}
291
342
  <div
292
343
  bind:this={menuEl}
293
- class="z-50 bg-popover border border-border rounded-lg shadow-lg p-2"
294
- style="width: 180px"
344
+ class="slash-menu z-50 bg-popover border border-border rounded-xl shadow-xl p-2"
295
345
  >
296
346
  <p class="text-xs text-muted-foreground px-2 py-1">결과 없음</p>
297
347
  </div>
298
348
  {:else}
299
349
  <div
300
350
  bind:this={menuEl}
301
- class="z-50 bg-popover border border-border rounded-lg shadow-lg overflow-y-auto py-1"
302
- style="width: 180px; max-height: 200px"
351
+ class="slash-menu z-50 bg-popover border border-border rounded-xl shadow-xl overflow-y-auto py-1.5"
303
352
  >
304
- <p
305
- class="text-[10px] font-medium text-muted-foreground uppercase tracking-wider px-3 py-1.5"
306
- >
307
- 블록
308
- </p>
309
- {#each filtered as item, i}
310
- <button
311
- type="button"
312
- data-index={i}
313
- class="w-full flex items-center gap-2 px-2 py-1.5 text-left transition-colors rounded-md mx-0 {i ===
314
- selectedIndex
315
- ? 'bg-accent text-accent-foreground'
316
- : 'hover:bg-muted text-foreground'}"
317
- onmousemove={() => {
318
- if (!mouseMovedSinceKeyboard) {
319
- mouseMovedSinceKeyboard = true;
320
- return;
321
- }
322
- selectedIndex = i;
323
- }}
324
- onclick={() => {
325
- onClose();
326
- item.command(editor);
327
- }}
328
- >
329
- <span
330
- class="flex items-center justify-center w-6 h-6 rounded border border-border bg-background text-muted-foreground shrink-0"
331
- >
332
- <item.icon size={SI} />
333
- </span>
334
- <span class="text-sm font-medium">{item.label}</span>
335
- </button>
353
+ {#each SECTION_ORDER as section}
354
+ {@const sectionItems = filtered.filter(
355
+ (it) => (SECTION_MAP[it.feature] ?? '기본') === section,
356
+ )}
357
+ {#if sectionItems.length > 0}
358
+ <p class="slash-section">{section}</p>
359
+ {#each sectionItems as item}
360
+ {@const i = filtered.indexOf(item)}
361
+ <button
362
+ type="button"
363
+ data-index={i}
364
+ class="slash-item {i === selectedIndex ? 'is-selected' : ''}"
365
+ onmousemove={() => {
366
+ if (!mouseMovedSinceKeyboard) {
367
+ mouseMovedSinceKeyboard = true;
368
+ return;
369
+ }
370
+ selectedIndex = i;
371
+ }}
372
+ onclick={() => {
373
+ onClose();
374
+ runItem(item);
375
+ }}
376
+ >
377
+ <span class="slash-icon">
378
+ <item.icon size={SI} />
379
+ </span>
380
+ <span class="slash-label">{item.label}</span>
381
+ </button>
382
+ {/each}
383
+ {/if}
336
384
  {/each}
337
385
  </div>
338
386
  {/if}
387
+
388
+ <style>
389
+ .slash-menu {
390
+ width: 220px;
391
+ max-height: 320px;
392
+ }
393
+
394
+ .slash-section {
395
+ margin: 0;
396
+ padding: 6px 12px 4px;
397
+ font-size: var(--text-xs, 11px);
398
+ font-weight: 600;
399
+ color: var(--muted-foreground);
400
+ letter-spacing: 0.02em;
401
+ }
402
+ .slash-section:not(:first-of-type) {
403
+ margin-top: 4px;
404
+ padding-top: 8px;
405
+ border-top: 1px solid var(--border);
406
+ }
407
+
408
+ .slash-item {
409
+ position: relative;
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 10px;
413
+ width: 100%;
414
+ padding: 6px 12px;
415
+ text-align: left;
416
+ font-size: var(--text-sm, 13px);
417
+ font-weight: 500;
418
+ color: var(--foreground);
419
+ background: transparent;
420
+ border: 0;
421
+ cursor: pointer;
422
+ transition: background-color 0.12s;
423
+ }
424
+
425
+ .slash-item:hover,
426
+ .slash-item.is-selected {
427
+ background: color-mix(in srgb, var(--primary) 8%, transparent);
428
+ color: var(--primary);
429
+ }
430
+
431
+ .slash-item.is-selected::before {
432
+ content: '';
433
+ position: absolute;
434
+ left: 0;
435
+ top: 4px;
436
+ bottom: 4px;
437
+ width: 3px;
438
+ border-radius: 0 2px 2px 0;
439
+ background: var(--primary);
440
+ }
441
+
442
+ .slash-icon {
443
+ display: inline-flex;
444
+ align-items: center;
445
+ justify-content: center;
446
+ flex-shrink: 0;
447
+ width: 24px;
448
+ height: 24px;
449
+ border-radius: var(--radius-md, 6px);
450
+ background: var(--muted);
451
+ color: var(--muted-foreground);
452
+ }
453
+
454
+ .slash-item.is-selected .slash-icon,
455
+ .slash-item:hover .slash-icon {
456
+ background: color-mix(in srgb, var(--primary) 14%, transparent);
457
+ color: var(--primary);
458
+ }
459
+
460
+ .slash-label {
461
+ flex: 1;
462
+ min-width: 0;
463
+ overflow: hidden;
464
+ text-overflow: ellipsis;
465
+ white-space: nowrap;
466
+ }
467
+ </style>
@@ -1,5 +1,5 @@
1
1
  import type { Editor } from "@tiptap/core";
2
- import type { ToolbarFeature } from "../types";
2
+ import type { ToolbarFeature, PromptHandler } from "../types";
3
3
  import type { Component } from "svelte";
4
4
  type $$ComponentProps = {
5
5
  editor: Editor;
@@ -9,6 +9,8 @@ type $$ComponentProps = {
9
9
  onPdfUpload?: () => void;
10
10
  onFileUpload?: () => void;
11
11
  onVideoUpload?: () => void;
12
+ onPromptLink?: PromptHandler;
13
+ onPromptImage?: PromptHandler;
12
14
  };
13
15
  declare const SlashCommandMenu: Component<$$ComponentProps, {}, "">;
14
16
  type SlashCommandMenu = ReturnType<typeof SlashCommandMenu>;
@@ -1 +1 @@
1
- {"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAwSJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AA8VJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -124,7 +124,7 @@
124
124
  style="top: {pos.top}px; left: {pos.left}px; width: {pos.width}px"
125
125
  >
126
126
  <div
127
- class="flex items-center justify-center gap-0.5 px-1.5 py-1 bg-foreground rounded-lg shadow-xl w-fit mx-auto"
127
+ class="flex items-center justify-center gap-0.5 px-1.5 py-1 bg-foreground rounded-full shadow-xl w-fit mx-auto"
128
128
  >
129
129
  <!-- Row/Col buttons -->
130
130
  <div class="hce-table-btn-wrap">
@@ -133,7 +133,7 @@
133
133
  onclick={() => editor.chain().focus().addRowBefore().run()}
134
134
  disabled={!editor.can().addRowBefore()}
135
135
  class={cn(
136
- "p-1.5 rounded-md transition-colors",
136
+ "p-1.5 rounded-full transition-colors",
137
137
  "text-white/70 hover:text-white hover:bg-white/10",
138
138
  !editor.can().addRowBefore() && "opacity-30 pointer-events-none",
139
139
  )}
@@ -149,7 +149,7 @@
149
149
  onclick={() => editor.chain().focus().addRowAfter().run()}
150
150
  disabled={!editor.can().addRowAfter()}
151
151
  class={cn(
152
- "p-1.5 rounded-md transition-colors",
152
+ "p-1.5 rounded-full transition-colors",
153
153
  "text-white/70 hover:text-white hover:bg-white/10",
154
154
  !editor.can().addRowAfter() && "opacity-30 pointer-events-none",
155
155
  )}
@@ -165,7 +165,7 @@
165
165
  onclick={() => editor.chain().focus().addColumnBefore().run()}
166
166
  disabled={!editor.can().addColumnBefore()}
167
167
  class={cn(
168
- "p-1.5 rounded-md transition-colors",
168
+ "p-1.5 rounded-full transition-colors",
169
169
  "text-white/70 hover:text-white hover:bg-white/10",
170
170
  !editor.can().addColumnBefore() &&
171
171
  "opacity-30 pointer-events-none",
@@ -182,7 +182,7 @@
182
182
  onclick={() => editor.chain().focus().addColumnAfter().run()}
183
183
  disabled={!editor.can().addColumnAfter()}
184
184
  class={cn(
185
- "p-1.5 rounded-md transition-colors",
185
+ "p-1.5 rounded-full transition-colors",
186
186
  "text-white/70 hover:text-white hover:bg-white/10",
187
187
  !editor.can().addColumnAfter() &&
188
188
  "opacity-30 pointer-events-none",
@@ -201,7 +201,7 @@
201
201
  onclick={() => editor.chain().focus().toggleHeaderRow().run()}
202
202
  disabled={!editor.can().toggleHeaderRow()}
203
203
  class={cn(
204
- "p-1.5 rounded-md transition-colors",
204
+ "p-1.5 rounded-full transition-colors",
205
205
  "text-white/70 hover:text-white hover:bg-white/10",
206
206
  !editor.can().toggleHeaderRow() &&
207
207
  "opacity-30 pointer-events-none",
@@ -218,7 +218,7 @@
218
218
  onclick={() => editor.chain().focus().toggleHeaderColumn().run()}
219
219
  disabled={!editor.can().toggleHeaderColumn()}
220
220
  class={cn(
221
- "p-1.5 rounded-md transition-colors",
221
+ "p-1.5 rounded-full transition-colors",
222
222
  "text-white/70 hover:text-white hover:bg-white/10",
223
223
  !editor.can().toggleHeaderColumn() &&
224
224
  "opacity-30 pointer-events-none",
@@ -235,7 +235,7 @@
235
235
  onclick={() => editor.chain().focus().mergeCells().run()}
236
236
  disabled={!editor.can().mergeCells()}
237
237
  class={cn(
238
- "p-1.5 rounded-md transition-colors",
238
+ "p-1.5 rounded-full transition-colors",
239
239
  "text-white/70 hover:text-white hover:bg-white/10",
240
240
  !editor.can().mergeCells() && "opacity-30 pointer-events-none",
241
241
  )}
@@ -251,7 +251,7 @@
251
251
  onclick={() => editor.chain().focus().splitCell().run()}
252
252
  disabled={!editor.can().splitCell()}
253
253
  class={cn(
254
- "p-1.5 rounded-md transition-colors",
254
+ "p-1.5 rounded-full transition-colors",
255
255
  "text-white/70 hover:text-white hover:bg-white/10",
256
256
  !editor.can().splitCell() && "opacity-30 pointer-events-none",
257
257
  )}
@@ -272,7 +272,7 @@
272
272
  showLineHeight = !showLineHeight;
273
273
  showColors = false;
274
274
  }}
275
- class="p-1.5 rounded-md transition-colors text-white/70 hover:text-white hover:bg-white/10"
275
+ class="p-1.5 rounded-full transition-colors text-white/70 hover:text-white hover:bg-white/10"
276
276
  >
277
277
  <UnfoldVertical size={iconSize} />
278
278
  </button>
@@ -325,6 +325,7 @@
325
325
  <div class="hce-table-btn-wrap">
326
326
  <button
327
327
  type="button"
328
+ class="p-1.5 rounded-full transition-colors text-white/70 hover:text-white hover:bg-white/10"
328
329
  onclick={() => {
329
330
  const { tableNode, tablePos } = findTableNode();
330
331
  if (!tableNode || tablePos < 0) return;
@@ -360,7 +361,6 @@
360
361
  );
361
362
  editor.view.dispatch(tr);
362
363
  }}
363
- class="p-1.5 rounded-md transition-colors text-white/70 hover:text-white hover:bg-white/10"
364
364
  >
365
365
  <EqualApproximately size={iconSize} />
366
366
  </button>
@@ -376,7 +376,7 @@
376
376
  showColors = !showColors;
377
377
  showLineHeight = false;
378
378
  }}
379
- class="p-1.5 rounded-md transition-colors text-white/70 hover:text-white hover:bg-white/10"
379
+ class="p-1.5 rounded-full transition-colors text-white/70 hover:text-white hover:bg-white/10"
380
380
  >
381
381
  <Paintbrush size={iconSize} />
382
382
  </button>
@@ -388,7 +388,7 @@
388
388
  style="min-width: 160px"
389
389
  >
390
390
  <p
391
- class="text-[10px] font-medium text-white/50 uppercase tracking-wider mb-1.5 px-1"
391
+ class="text-xs font-medium text-white/60 mb-1.5 px-1"
392
392
  >
393
393
  배경색
394
394
  </p>
@@ -426,7 +426,7 @@
426
426
  onclick={() => editor.chain().focus().deleteRow().run()}
427
427
  disabled={!editor.can().deleteRow()}
428
428
  class={cn(
429
- "p-1.5 rounded-md transition-colors",
429
+ "p-1.5 rounded-full transition-colors",
430
430
  "text-white/70 hover:text-red-300 hover:bg-white/10",
431
431
  !editor.can().deleteRow() && "opacity-30 pointer-events-none",
432
432
  )}
@@ -442,7 +442,7 @@
442
442
  onclick={() => editor.chain().focus().deleteColumn().run()}
443
443
  disabled={!editor.can().deleteColumn()}
444
444
  class={cn(
445
- "p-1.5 rounded-md transition-colors",
445
+ "p-1.5 rounded-full transition-colors",
446
446
  "text-white/70 hover:text-red-300 hover:bg-white/10",
447
447
  !editor.can().deleteColumn() && "opacity-30 pointer-events-none",
448
448
  )}
@@ -458,7 +458,7 @@
458
458
  onclick={() => editor.chain().focus().deleteTable().run()}
459
459
  disabled={!editor.can().deleteTable()}
460
460
  class={cn(
461
- "p-1.5 rounded-md transition-colors",
461
+ "p-1.5 rounded-full transition-colors",
462
462
  "text-white/70 hover:text-red-300 hover:bg-white/10",
463
463
  !editor.can().deleteTable() && "opacity-30 pointer-events-none",
464
464
  )}
@@ -41,7 +41,7 @@
41
41
  import BubbleToolbar from "./BubbleToolbar.svelte";
42
42
  import SlashCommandMenu from "./SlashCommandMenu.svelte";
43
43
  import TableBubbleMenu from "./TableBubbleMenu.svelte";
44
- import type { UploadHandler, ToolbarMode, ToolbarFeature } from "../types";
44
+ import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
45
45
  import { resolveFeatures } from "../types";
46
46
  import type { FileResolver } from "../extensions/FileAttachment";
47
47
 
@@ -113,6 +113,8 @@
113
113
  placeholder = "'/'를 눌러 명령어를 입력하세요...",
114
114
  onUploadFile,
115
115
  onResolveFile,
116
+ onPromptLink,
117
+ onPromptImage,
116
118
  extensions: extraExtensions = [],
117
119
  editable = true,
118
120
  toolbar = 'full',
@@ -123,6 +125,8 @@
123
125
  placeholder?: string;
124
126
  onUploadFile?: UploadHandler;
125
127
  onResolveFile?: FileResolver;
128
+ onPromptLink?: PromptHandler;
129
+ onPromptImage?: PromptHandler;
126
130
  extensions?: AnyExtension[];
127
131
  editable?: boolean;
128
132
  toolbar?: ToolbarMode;
@@ -145,7 +149,7 @@
145
149
  let slashMenuPos = $state({ top: 0, left: 0 });
146
150
  let slashQuery = $state("");
147
151
  let slashStartPos: number | null = null;
148
- const MENU_HEIGHT = 200;
152
+ const MENU_HEIGHT = 320;
149
153
 
150
154
  function updateSlashMenuPosition() {
151
155
  if (!editor || slashStartPos === null) return;
@@ -332,6 +336,8 @@
332
336
  })]),
333
337
  Placeholder.configure({
334
338
  placeholder: ({ node }) => {
339
+ // 코드블록·인용·목록 등 컨테이너/의미 있는 노드에는 placeholder 안 띄움
340
+ if (node.type.name === "codeBlock") return "";
335
341
  if (node.type.name === "heading") {
336
342
  const level = node.attrs.level;
337
343
  if (level === 1) return "제목 1";
@@ -567,6 +573,8 @@
567
573
  <FixedToolbar
568
574
  {editor}
569
575
  {features}
576
+ {onPromptLink}
577
+ {onPromptImage}
570
578
  onPdfClick={() => pdfInputEl?.click()}
571
579
  onFileClick={onUploadFile ? () => fileInputEl?.click() : undefined}
572
580
  onVideoClick={onUploadFile ? () => videoInputEl?.click() : undefined}
@@ -577,7 +585,7 @@
577
585
 
578
586
  {#if editor && editable}
579
587
  {#if features.has('bubble-toolbar')}
580
- <BubbleToolbar {editor} {features} />
588
+ <BubbleToolbar {editor} {features} {onPromptLink} />
581
589
  {/if}
582
590
 
583
591
  {#if features.has('table-menu')}
@@ -602,6 +610,8 @@
602
610
  <SlashCommandMenu
603
611
  {editor}
604
612
  {features}
613
+ {onPromptLink}
614
+ {onPromptImage}
605
615
  query={slashQuery}
606
616
  onClose={closeSlashMenu}
607
617
  onPdfUpload={onUploadFile && features.has('pdf')
@@ -1,5 +1,5 @@
1
1
  import { type AnyExtension } from "@tiptap/core";
2
- import type { UploadHandler, ToolbarMode, ToolbarFeature } from "../types";
2
+ import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
3
3
  import type { FileResolver } from "../extensions/FileAttachment";
4
4
  type $$ComponentProps = {
5
5
  content: string;
@@ -7,6 +7,8 @@ type $$ComponentProps = {
7
7
  placeholder?: string;
8
8
  onUploadFile?: UploadHandler;
9
9
  onResolveFile?: FileResolver;
10
+ onPromptLink?: PromptHandler;
11
+ onPromptImage?: PromptHandler;
10
12
  extensions?: AnyExtension[];
11
13
  editable?: boolean;
12
14
  toolbar?: ToolbarMode;
@@ -1 +1 @@
1
- {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AA2mBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AA+mBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AAkCD,eAAO,MAAM,cAAc,kCAmNzB,CAAC"}
1
+ {"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AAkCD,eAAO,MAAM,cAAc,kCAqNzB,CAAC"}