@teriusu/rich-editor 0.9.6 → 0.9.7
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.
- package/dist/components/BubbleToolbar.svelte +102 -49
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +136 -214
- package/dist/components/FixedToolbar.svelte.d.ts +9 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/InputModal.svelte +1 -1
- package/dist/components/MathModal.svelte +111 -1
- package/dist/components/MathModal.svelte.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.svelte +206 -0
- package/dist/components/MediaPickerModal.svelte.d.ts +18 -0
- package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.svelte +114 -13
- package/dist/components/SlashCommandMenu.svelte.d.ts +10 -1
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TableBubbleMenu.svelte +71 -48
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +163 -24
- package/dist/components/TipTapEditor.svelte.d.ts +2 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/NotionInputRules.d.ts +59 -0
- package/dist/extensions/NotionInputRules.d.ts.map +1 -0
- package/dist/extensions/NotionInputRules.js +141 -0
- package/dist/extensions/NotionInputRules.test.d.ts +2 -0
- package/dist/extensions/NotionInputRules.test.d.ts.map +1 -0
- package/dist/extensions/NotionInputRules.test.js +117 -0
- package/dist/extensions/VideoEmbed.d.ts +36 -0
- package/dist/extensions/VideoEmbed.d.ts.map +1 -0
- package/dist/extensions/VideoEmbed.js +246 -0
- package/dist/extensions/VideoEmbed.test.d.ts +2 -0
- package/dist/extensions/VideoEmbed.test.d.ts.map +1 -0
- package/dist/extensions/VideoEmbed.test.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +20 -6
- package/dist/utils/lowlight.d.ts +17 -0
- package/dist/utils/lowlight.d.ts.map +1 -0
- package/dist/utils/lowlight.js +106 -0
- package/dist/utils/lowlight.test.d.ts +2 -0
- package/dist/utils/lowlight.test.d.ts.map +1 -0
- package/dist/utils/lowlight.test.js +56 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +14 -0
- package/dist/utils/splitCell.d.ts +20 -0
- package/dist/utils/splitCell.d.ts.map +1 -0
- package/dist/utils/splitCell.js +74 -0
- package/dist/utils/table.d.ts +22 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/dist/utils/table.js +68 -0
- package/package.json +6 -1
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
AlignLeft,
|
|
10
10
|
AlignCenter,
|
|
11
11
|
AlignRight,
|
|
12
|
-
LinkIcon,
|
|
13
12
|
Heading1,
|
|
14
13
|
Heading2,
|
|
15
14
|
Heading3,
|
|
@@ -31,12 +30,13 @@
|
|
|
31
30
|
Columns3,
|
|
32
31
|
SquareDashed,
|
|
33
32
|
Tv,
|
|
33
|
+
Youtube,
|
|
34
34
|
Plus,
|
|
35
35
|
Pilcrow,
|
|
36
|
-
Palette,
|
|
37
36
|
Sigma,
|
|
38
37
|
} from "lucide-svelte";
|
|
39
38
|
import { cn } from "../utils/cn";
|
|
39
|
+
import { insertTableSized } from "../utils/table";
|
|
40
40
|
import InputModal from "./InputModal.svelte";
|
|
41
41
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
42
42
|
|
|
@@ -44,19 +44,28 @@
|
|
|
44
44
|
editor,
|
|
45
45
|
features,
|
|
46
46
|
onPdfClick,
|
|
47
|
+
onImageClick,
|
|
47
48
|
onFileClick,
|
|
48
49
|
onPromptLink,
|
|
49
|
-
onPromptImage,
|
|
50
50
|
onPromptMbus,
|
|
51
|
+
onPromptVideo,
|
|
51
52
|
toolbarEnd,
|
|
52
53
|
}: {
|
|
53
54
|
editor: Editor;
|
|
54
55
|
features: Set<ToolbarFeature>;
|
|
55
56
|
onPdfClick: () => void;
|
|
57
|
+
/**
|
|
58
|
+
* 이미지 넣기. **툴바는 무엇을 넣을지 정하지 않는다** — 업로드/링크 탭 모달을 띄울지
|
|
59
|
+
* URL 만 물을지는 에디터가 정한다(`TipTapEditor` 의 `pickImage`).
|
|
60
|
+
* 예전엔 여기서 `onPromptImage` 를 직접 불러 URL 만 받았고, 그래서 툴바로는
|
|
61
|
+
* 내 컴퓨터의 그림을 넣을 방법이 없었다.
|
|
62
|
+
*/
|
|
63
|
+
onImageClick: () => void;
|
|
56
64
|
onFileClick?: () => void;
|
|
57
65
|
onPromptLink?: PromptHandler;
|
|
58
|
-
onPromptImage?: PromptHandler;
|
|
59
66
|
onPromptMbus?: PromptHandler;
|
|
67
|
+
/** 영상 URL 프롬프트. 미제공 시 내장 InputModal 폴백 */
|
|
68
|
+
onPromptVideo?: PromptHandler;
|
|
60
69
|
/**
|
|
61
70
|
* 툴바 **오른쪽 끝**에 호스트가 끼워 넣는 조각(예: HTML ↔ 에디터 토글).
|
|
62
71
|
*
|
|
@@ -99,8 +108,6 @@
|
|
|
99
108
|
*/
|
|
100
109
|
const canDo = (fn: (c: ReturnType<Editor["can"]>) => boolean) =>
|
|
101
110
|
(tick, fn(editor.can()));
|
|
102
|
-
const attrOf = (name: string, key: string) =>
|
|
103
|
-
(tick, editor.getAttributes(name)[key] as string | undefined);
|
|
104
111
|
|
|
105
112
|
const has = (f: ToolbarFeature) => features.has(f);
|
|
106
113
|
|
|
@@ -108,61 +115,22 @@
|
|
|
108
115
|
|
|
109
116
|
let blockMenuOpen = $state(false);
|
|
110
117
|
let insertMenuOpen = $state(false);
|
|
111
|
-
let
|
|
112
|
-
let modalState: { type: "link" | "image" | "mbus" } | null = $state(null);
|
|
118
|
+
let modalState: { type: "mbus" | "video" } | null = $state(null);
|
|
113
119
|
let blockMenuEl: HTMLDivElement | undefined = $state();
|
|
114
120
|
let insertMenuEl: HTMLDivElement | undefined = $state();
|
|
115
|
-
let colorMenuEl: HTMLDivElement | undefined = $state();
|
|
116
|
-
|
|
117
|
-
/*
|
|
118
|
-
* ⚠️ **`검정(#000000)` 은 뺐다.** `기본`(×, 색 지정 해제)과 눈으로 구별이 안 되는데
|
|
119
|
-
* (본문 기본색이 거의 검정이다) 두 가지가 다 있으면 헷갈린다 — 사용자가 짚은 부분이다.
|
|
120
|
-
*
|
|
121
|
-
* 남기는 쪽은 **`기본`** 이다. `검정` 만 남기면 본문 기본색으로 **되돌릴 방법이 없어지고**,
|
|
122
|
-
* 저장된 HTML 에 `#000000` 이 눌러앉는다. 그 글은 다크 모드에서 검정 글자가 되어
|
|
123
|
-
* 안 보인다(코드패스가 `sanitizeBodyColors` 로 이 자국을 걷어내고 있는 이유다).
|
|
124
|
-
*
|
|
125
|
-
* `브랜드` 는 앱마다 다르므로 값을 고정하지 않고 `--primary` 를 읽는다 —
|
|
126
|
-
* 정올과 코드패스가 각자 자기 색을 얻는다. 저장은 계산된 실제 색으로 한다
|
|
127
|
-
* (`var(--primary)` 를 그대로 저장하면 앱 밖에서 렌더할 때 색이 사라진다).
|
|
128
|
-
*
|
|
129
|
-
* ⚠️ **아홉 개로 맞춰 둔다.** 팔레트가 `grid-cols-3` 라 여덟 개면 마지막 줄에 빈 칸이
|
|
130
|
-
* 남아 덜 만든 것처럼 보인다(사용자 지적). `기본` + 색 여덟이 3×3 을 정확히 채운다.
|
|
131
|
-
* 색은 색상환을 고르게 훑도록 골랐다 — 빨강·주황·노랑·초록·파랑에 브랜드(남보라)와
|
|
132
|
-
* 그 사이를 메우는 분홍, 그리고 무채색 회색. 여기서 더 늘리려면 12개(3×4)로 가야 한다.
|
|
133
|
-
*/
|
|
134
|
-
const TEXT_COLORS: { label: string; value: string; cssVar?: string }[] = [
|
|
135
|
-
{ label: "기본", value: "" },
|
|
136
|
-
{ label: "브랜드", value: "", cssVar: "--primary" },
|
|
137
|
-
{ label: "회색", value: "#6b7280" },
|
|
138
|
-
{ label: "빨강", value: "#dc2626" },
|
|
139
|
-
{ label: "주황", value: "#ea580c" },
|
|
140
|
-
{ label: "노랑", value: "#ca8a04" },
|
|
141
|
-
{ label: "초록", value: "#16a34a" },
|
|
142
|
-
{ label: "파랑", value: "#2563eb" },
|
|
143
|
-
{ label: "분홍", value: "#db2777" }
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
/** `--primary` 같은 CSS 변수를 그 자리에서 실제 색 문자열로 바꾼다. */
|
|
147
|
-
function resolveCssVar(el: HTMLElement, name: string): string {
|
|
148
|
-
return getComputedStyle(el).getPropertyValue(name).trim();
|
|
149
|
-
}
|
|
150
121
|
|
|
151
122
|
$effect(() => {
|
|
152
|
-
if (!blockMenuOpen && !insertMenuOpen
|
|
123
|
+
if (!blockMenuOpen && !insertMenuOpen) return;
|
|
153
124
|
function handleClick(e: MouseEvent) {
|
|
154
125
|
if (blockMenuEl && !blockMenuEl.contains(e.target as Node))
|
|
155
126
|
blockMenuOpen = false;
|
|
156
127
|
if (insertMenuEl && !insertMenuEl.contains(e.target as Node))
|
|
157
128
|
insertMenuOpen = false;
|
|
158
|
-
if (colorMenuEl && !colorMenuEl.contains(e.target as Node))
|
|
159
|
-
colorMenuOpen = false;
|
|
160
129
|
}
|
|
161
130
|
function handleKeyDown(e: KeyboardEvent) {
|
|
162
131
|
if (e.key === "Escape") {
|
|
163
132
|
blockMenuOpen = false;
|
|
164
133
|
insertMenuOpen = false;
|
|
165
|
-
colorMenuOpen = false;
|
|
166
134
|
}
|
|
167
135
|
}
|
|
168
136
|
document.addEventListener("mousedown", handleClick);
|
|
@@ -173,38 +141,20 @@
|
|
|
173
141
|
};
|
|
174
142
|
});
|
|
175
143
|
|
|
176
|
-
|
|
144
|
+
function addImage() {
|
|
177
145
|
insertMenuOpen = false;
|
|
178
|
-
|
|
179
|
-
const previous = isActive("link")
|
|
180
|
-
? (editor.getAttributes("link").href as string) || ""
|
|
181
|
-
: "";
|
|
182
|
-
const url = await onPromptLink(previous);
|
|
183
|
-
if (url === null) return;
|
|
184
|
-
if (url === "") {
|
|
185
|
-
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
186
|
-
} else {
|
|
187
|
-
editor
|
|
188
|
-
.chain()
|
|
189
|
-
.focus()
|
|
190
|
-
.extendMarkRange("link")
|
|
191
|
-
.setLink({ href: url })
|
|
192
|
-
.run();
|
|
193
|
-
}
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
modalState = { type: "link" };
|
|
146
|
+
onImageClick();
|
|
197
147
|
}
|
|
198
148
|
|
|
199
|
-
async function
|
|
149
|
+
async function addVideo() {
|
|
200
150
|
insertMenuOpen = false;
|
|
201
|
-
if (
|
|
202
|
-
const url = await
|
|
151
|
+
if (onPromptVideo) {
|
|
152
|
+
const url = await onPromptVideo("");
|
|
203
153
|
if (!url) return;
|
|
204
|
-
editor.chain().focus().
|
|
154
|
+
editor.chain().focus().setVideoEmbed({ src: url }).run();
|
|
205
155
|
return;
|
|
206
156
|
}
|
|
207
|
-
modalState = { type: "
|
|
157
|
+
modalState = { type: "video" };
|
|
208
158
|
}
|
|
209
159
|
|
|
210
160
|
async function addMbus() {
|
|
@@ -226,7 +176,13 @@
|
|
|
226
176
|
if (isActive("orderedList")) return "번호 목록";
|
|
227
177
|
if (isActive("taskList")) return "체크리스트";
|
|
228
178
|
if (isActive("blockquote")) return "인용문";
|
|
229
|
-
if (isActive("details"))
|
|
179
|
+
if (isActive("details")) {
|
|
180
|
+
// 토글 제목이면 단계까지 보여 준다 — 그냥 "토글" 이면 무엇을 고른 건지 안 보인다.
|
|
181
|
+
for (const lv of [1, 2, 3]) {
|
|
182
|
+
if (isActive("detailsSummary", { level: lv })) return `토글 제목 ${lv}`;
|
|
183
|
+
}
|
|
184
|
+
return "토글";
|
|
185
|
+
}
|
|
230
186
|
return "본문";
|
|
231
187
|
});
|
|
232
188
|
|
|
@@ -240,12 +196,13 @@
|
|
|
240
196
|
insertMenuOpen = false;
|
|
241
197
|
}
|
|
242
198
|
|
|
199
|
+
/* ⚠️ `link` 는 여기 없다 — 인라인 서식 그룹으로 옮겼다(위 주석 참고). */
|
|
243
200
|
const hasInsertItems = $derived(
|
|
244
201
|
has("image") ||
|
|
245
|
-
has("link") ||
|
|
246
202
|
has("pdf") ||
|
|
247
203
|
has("file") ||
|
|
248
204
|
has("mbus") ||
|
|
205
|
+
has("video") ||
|
|
249
206
|
has("table") ||
|
|
250
207
|
has("columns-2") ||
|
|
251
208
|
has("columns-3") ||
|
|
@@ -278,7 +235,6 @@
|
|
|
278
235
|
type="button"
|
|
279
236
|
onclick={() => editor.chain().focus().undo().run()}
|
|
280
237
|
disabled={!canDo((c) => c.undo())}
|
|
281
|
-
data-tooltip="실행 취소"
|
|
282
238
|
aria-label="실행 취소"
|
|
283
239
|
class={cn(
|
|
284
240
|
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
@@ -293,7 +249,6 @@
|
|
|
293
249
|
type="button"
|
|
294
250
|
onclick={() => editor.chain().focus().redo().run()}
|
|
295
251
|
disabled={!canDo((c) => c.redo())}
|
|
296
|
-
data-tooltip="다시 실행"
|
|
297
252
|
aria-label="다시 실행"
|
|
298
253
|
class={cn(
|
|
299
254
|
"p-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
@@ -313,7 +268,6 @@
|
|
|
313
268
|
<button
|
|
314
269
|
type="button"
|
|
315
270
|
onclick={() => (blockMenuOpen = !blockMenuOpen)}
|
|
316
|
-
data-tooltip="블록 타입"
|
|
317
271
|
aria-label="블록 타입"
|
|
318
272
|
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]"
|
|
319
273
|
>
|
|
@@ -323,7 +277,7 @@
|
|
|
323
277
|
{#if blockMenuOpen}
|
|
324
278
|
<div
|
|
325
279
|
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
326
|
-
style="min-width:
|
|
280
|
+
style="min-width: 236px"
|
|
327
281
|
onmousedown={(e) => e.preventDefault()}
|
|
328
282
|
role="menu"
|
|
329
283
|
tabindex="-1"
|
|
@@ -359,6 +313,7 @@
|
|
|
359
313
|
)}
|
|
360
314
|
>
|
|
361
315
|
<Heading1 size={14} /> 제목 1
|
|
316
|
+
<span class="hce-menu-shortcut"># </span>
|
|
362
317
|
</button>
|
|
363
318
|
{/if}
|
|
364
319
|
{#if has('h2')}
|
|
@@ -374,6 +329,7 @@
|
|
|
374
329
|
)}
|
|
375
330
|
>
|
|
376
331
|
<Heading2 size={14} /> 제목 2
|
|
332
|
+
<span class="hce-menu-shortcut">## </span>
|
|
377
333
|
</button>
|
|
378
334
|
{/if}
|
|
379
335
|
{#if has('h3')}
|
|
@@ -389,6 +345,7 @@
|
|
|
389
345
|
)}
|
|
390
346
|
>
|
|
391
347
|
<Heading3 size={14} /> 제목 3
|
|
348
|
+
<span class="hce-menu-shortcut">### </span>
|
|
392
349
|
</button>
|
|
393
350
|
{/if}
|
|
394
351
|
{#if has('bullet-list') || has('ordered-list') || has('checklist')}
|
|
@@ -405,6 +362,7 @@
|
|
|
405
362
|
runBlock(() => editor.chain().focus().toggleBulletList().run())}
|
|
406
363
|
>
|
|
407
364
|
<List size={14} /> 글머리 목록
|
|
365
|
+
<span class="hce-menu-shortcut">- </span>
|
|
408
366
|
</button>
|
|
409
367
|
{/if}
|
|
410
368
|
{#if has('ordered-list')}
|
|
@@ -418,6 +376,7 @@
|
|
|
418
376
|
runBlock(() => editor.chain().focus().toggleOrderedList().run())}
|
|
419
377
|
>
|
|
420
378
|
<ListOrdered size={14} /> 번호 목록
|
|
379
|
+
<span class="hce-menu-shortcut">1. </span>
|
|
421
380
|
</button>
|
|
422
381
|
{/if}
|
|
423
382
|
{#if has('checklist')}
|
|
@@ -431,6 +390,7 @@
|
|
|
431
390
|
runBlock(() => editor.chain().focus().toggleTaskList().run())}
|
|
432
391
|
>
|
|
433
392
|
<CheckSquare size={14} /> 체크리스트
|
|
393
|
+
<span class="hce-menu-shortcut">[] </span>
|
|
434
394
|
</button>
|
|
435
395
|
{/if}
|
|
436
396
|
{#if has('blockquote') || has('toggle')}
|
|
@@ -447,6 +407,7 @@
|
|
|
447
407
|
runBlock(() => editor.chain().focus().toggleBlockquote().run())}
|
|
448
408
|
>
|
|
449
409
|
<Quote size={14} /> 인용문
|
|
410
|
+
<span class="hce-menu-shortcut">" </span>
|
|
450
411
|
</button>
|
|
451
412
|
{/if}
|
|
452
413
|
{#if has('toggle')}
|
|
@@ -460,7 +421,34 @@
|
|
|
460
421
|
runBlock(() => editor.chain().focus().setDetails().run())}
|
|
461
422
|
>
|
|
462
423
|
<ChevronRight size={14} /> 토글
|
|
424
|
+
<span class="hce-menu-shortcut">> </span>
|
|
463
425
|
</button>
|
|
426
|
+
<!--
|
|
427
|
+
토글 제목 — 접히는 제목. 만드는 일은 입력 규칙(`# > `)과 **같은 커맨드**가 한다.
|
|
428
|
+
제목 바로 아래에 두는 게 맞지만 이 메뉴는 `본문 → 제목 → 목록 → 블록` 순이라,
|
|
429
|
+
토글 옆에 붙여 **접히는 것끼리** 모은다.
|
|
430
|
+
-->
|
|
431
|
+
{#each [1, 2, 3] as level}
|
|
432
|
+
{@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
class={cn(
|
|
436
|
+
"w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted",
|
|
437
|
+
isActive("detailsSummary", { level }) && "hce-active",
|
|
438
|
+
)}
|
|
439
|
+
onclick={() =>
|
|
440
|
+
runBlock(() =>
|
|
441
|
+
editor
|
|
442
|
+
.chain()
|
|
443
|
+
.focus()
|
|
444
|
+
.setToggleHeading(level as 1 | 2 | 3)
|
|
445
|
+
.run(),
|
|
446
|
+
)}
|
|
447
|
+
>
|
|
448
|
+
<Icon size={14} /> 토글 제목 {level}
|
|
449
|
+
<span class="hce-menu-shortcut">{'#'.repeat(level)} > </span>
|
|
450
|
+
</button>
|
|
451
|
+
{/each}
|
|
464
452
|
{/if}
|
|
465
453
|
</div>
|
|
466
454
|
{/if}
|
|
@@ -469,13 +457,19 @@
|
|
|
469
457
|
{/if}
|
|
470
458
|
|
|
471
459
|
{#if has('bold') || has('italic') || has('underline') || has('strike')}
|
|
472
|
-
<!--
|
|
460
|
+
<!--
|
|
461
|
+
인라인 서식. **글자에 붙는 것은 전부 여기 모은다** — 굵게·기울임·밑줄·취소선에
|
|
462
|
+
`코드`(`<code>`)와 `링크`가 더해진다.
|
|
463
|
+
|
|
464
|
+
⚠️ `링크` 는 원래 `삽입` 드롭다운에 있었다. 하지만 링크는 표·PDF 처럼 **새 덩어리를
|
|
465
|
+
끼워 넣는 것**이 아니라 **선택한 글자에 씌우는 서식**이라, 쓰려면 글자를 골라 둔 상태에서
|
|
466
|
+
메뉴를 두 번 여는 꼴이었다(사용자 요청으로 이동). 삽입 쪽에는 이제 없다.
|
|
467
|
+
-->
|
|
473
468
|
<div class="hce-toolbar-group">
|
|
474
469
|
{#if has('bold')}
|
|
475
470
|
<button
|
|
476
471
|
type="button"
|
|
477
472
|
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
478
|
-
data-tooltip="굵게"
|
|
479
473
|
aria-label="굵게"
|
|
480
474
|
class={cn(
|
|
481
475
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -491,7 +485,6 @@
|
|
|
491
485
|
<button
|
|
492
486
|
type="button"
|
|
493
487
|
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
494
|
-
data-tooltip="기울임"
|
|
495
488
|
aria-label="기울임"
|
|
496
489
|
class={cn(
|
|
497
490
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -507,7 +500,6 @@
|
|
|
507
500
|
<button
|
|
508
501
|
type="button"
|
|
509
502
|
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
510
|
-
data-tooltip="밑줄"
|
|
511
503
|
aria-label="밑줄"
|
|
512
504
|
class={cn(
|
|
513
505
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -523,7 +515,6 @@
|
|
|
523
515
|
<button
|
|
524
516
|
type="button"
|
|
525
517
|
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
526
|
-
data-tooltip="취소선"
|
|
527
518
|
aria-label="취소선"
|
|
528
519
|
class={cn(
|
|
529
520
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -535,104 +526,17 @@
|
|
|
535
526
|
<Strikethrough size={iconSize} />
|
|
536
527
|
</button>
|
|
537
528
|
{/if}
|
|
538
|
-
{#if has('math')}
|
|
539
529
|
<!--
|
|
540
|
-
|
|
541
|
-
|
|
530
|
+
⚠️ **`코드`·`링크`·`수식`·`글자색` 은 여기 없다 — 버블 툴바에 있다**(사용자 결정).
|
|
531
|
+
|
|
532
|
+
이 넷은 **글자를 골라 놓고** 쓰는 것이라, 고르는 순간 뜨는 버블이 손에 더 가깝다.
|
|
533
|
+
고정 툴바에도 두면 같은 버튼이 두 벌이 되고 인라인 그룹만 여덟 칸을 먹는다.
|
|
534
|
+
남긴 `굵게·기울임·밑줄·취소선` 은 **선택 없이 켜 두고 이어 치는** 쓰임이 있어서
|
|
535
|
+
늘 보이는 자리가 필요하다.
|
|
542
536
|
|
|
543
|
-
⚠️
|
|
544
|
-
|
|
545
|
-
섞는다), 감싸 놓고 프롬프트까지 같이 여는 걸 브라우저에서 확인했다.
|
|
546
|
-
**무엇을 할지는 실행 전에 상태로 정한다.**
|
|
537
|
+
⚠️ 빈 선택에서 **인라인 수식을 새로 넣던 길**은 이 버튼이 유일했으므로,
|
|
538
|
+
`삽입` 드롭다운으로 옮겨 두었다(아래 `인라인 수식`).
|
|
547
539
|
-->
|
|
548
|
-
<button
|
|
549
|
-
type="button"
|
|
550
|
-
onclick={() => {
|
|
551
|
-
if (editor.state.selection.empty && !editor.isActive('math_inline')) {
|
|
552
|
-
editor.chain().focus().promptMathInline().run();
|
|
553
|
-
} else {
|
|
554
|
-
editor.chain().focus().toggleMathInline().run();
|
|
555
|
-
}
|
|
556
|
-
}}
|
|
557
|
-
data-tooltip="인라인 수식"
|
|
558
|
-
aria-label="인라인 수식"
|
|
559
|
-
class={cn(
|
|
560
|
-
"p-1.5 rounded-md transition-colors",
|
|
561
|
-
isActive("math_inline")
|
|
562
|
-
? "hce-active"
|
|
563
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
564
|
-
)}
|
|
565
|
-
>
|
|
566
|
-
<Sigma size={iconSize} />
|
|
567
|
-
</button>
|
|
568
|
-
{/if}
|
|
569
|
-
{#if has('text-color')}
|
|
570
|
-
<div bind:this={colorMenuEl} class="relative">
|
|
571
|
-
<button
|
|
572
|
-
type="button"
|
|
573
|
-
onclick={() => (colorMenuOpen = !colorMenuOpen)}
|
|
574
|
-
data-tooltip="글자색"
|
|
575
|
-
aria-label="글자색"
|
|
576
|
-
class={cn(
|
|
577
|
-
"flex items-center gap-0.5 p-1.5 rounded-md transition-colors",
|
|
578
|
-
attrOf('textStyle', 'color')
|
|
579
|
-
? "hce-active"
|
|
580
|
-
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
581
|
-
)}
|
|
582
|
-
>
|
|
583
|
-
<Palette size={iconSize} />
|
|
584
|
-
<ChevronDown size={12} />
|
|
585
|
-
</button>
|
|
586
|
-
{#if colorMenuOpen}
|
|
587
|
-
<div
|
|
588
|
-
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 p-2"
|
|
589
|
-
style="min-width: 180px"
|
|
590
|
-
onmousedown={(e) => e.preventDefault()}
|
|
591
|
-
role="menu"
|
|
592
|
-
tabindex="-1"
|
|
593
|
-
>
|
|
594
|
-
<div class="grid grid-cols-3 gap-1.5">
|
|
595
|
-
{#each TEXT_COLORS as c}
|
|
596
|
-
<button
|
|
597
|
-
type="button"
|
|
598
|
-
title={c.label}
|
|
599
|
-
class="h-8 rounded-md border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold bg-background"
|
|
600
|
-
style="color: {c.cssVar ? `var(${c.cssVar})` : c.value || '#6b7280'}"
|
|
601
|
-
onclick={(e) => {
|
|
602
|
-
const color = c.cssVar
|
|
603
|
-
? resolveCssVar(e.currentTarget as HTMLElement, c.cssVar)
|
|
604
|
-
: c.value;
|
|
605
|
-
if (color) {
|
|
606
|
-
editor.chain().focus().setColor(color).run();
|
|
607
|
-
} else {
|
|
608
|
-
editor.chain().focus().unsetColor().run();
|
|
609
|
-
}
|
|
610
|
-
colorMenuOpen = false;
|
|
611
|
-
}}
|
|
612
|
-
>
|
|
613
|
-
{c.cssVar || c.value ? "A" : "×"}
|
|
614
|
-
</button>
|
|
615
|
-
{/each}
|
|
616
|
-
</div>
|
|
617
|
-
<label
|
|
618
|
-
class="mt-2 flex items-center justify-between gap-2 px-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground"
|
|
619
|
-
>
|
|
620
|
-
<span>직접 선택</span>
|
|
621
|
-
<input
|
|
622
|
-
type="color"
|
|
623
|
-
class="h-6 w-10 cursor-pointer rounded border border-border bg-transparent p-0"
|
|
624
|
-
value={attrOf('textStyle', 'color') || '#000000'}
|
|
625
|
-
onclick={(e) => e.stopPropagation()}
|
|
626
|
-
oninput={(e) => {
|
|
627
|
-
const v = (e.target as HTMLInputElement).value;
|
|
628
|
-
editor.chain().focus().setColor(v).run();
|
|
629
|
-
}}
|
|
630
|
-
/>
|
|
631
|
-
</label>
|
|
632
|
-
</div>
|
|
633
|
-
{/if}
|
|
634
|
-
</div>
|
|
635
|
-
{/if}
|
|
636
540
|
</div>
|
|
637
541
|
{/if}
|
|
638
542
|
|
|
@@ -643,7 +547,6 @@
|
|
|
643
547
|
<button
|
|
644
548
|
type="button"
|
|
645
549
|
onclick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
646
|
-
data-tooltip="왼쪽 정렬"
|
|
647
550
|
aria-label="왼쪽 정렬"
|
|
648
551
|
class={cn(
|
|
649
552
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -659,7 +562,6 @@
|
|
|
659
562
|
<button
|
|
660
563
|
type="button"
|
|
661
564
|
onclick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
662
|
-
data-tooltip="가운데 정렬"
|
|
663
565
|
aria-label="가운데 정렬"
|
|
664
566
|
class={cn(
|
|
665
567
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -675,7 +577,6 @@
|
|
|
675
577
|
<button
|
|
676
578
|
type="button"
|
|
677
579
|
onclick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
678
|
-
data-tooltip="오른쪽 정렬"
|
|
679
580
|
aria-label="오른쪽 정렬"
|
|
680
581
|
class={cn(
|
|
681
582
|
"p-1.5 rounded-md transition-colors",
|
|
@@ -697,7 +598,6 @@
|
|
|
697
598
|
<button
|
|
698
599
|
type="button"
|
|
699
600
|
onclick={() => (insertMenuOpen = !insertMenuOpen)}
|
|
700
|
-
data-tooltip="삽입"
|
|
701
601
|
aria-label="삽입"
|
|
702
602
|
class="flex items-center gap-1 px-2 py-1.5 rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
703
603
|
>
|
|
@@ -708,7 +608,7 @@
|
|
|
708
608
|
{#if insertMenuOpen}
|
|
709
609
|
<div
|
|
710
610
|
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
711
|
-
style="min-width:
|
|
611
|
+
style="min-width: 236px"
|
|
712
612
|
>
|
|
713
613
|
{#if has('code-block')}
|
|
714
614
|
<button
|
|
@@ -718,6 +618,7 @@
|
|
|
718
618
|
runInsert(() => editor.chain().focus().setCodeBlock().run())}
|
|
719
619
|
>
|
|
720
620
|
<Code2 size={14} /> 코드 블록
|
|
621
|
+
<span class="hce-menu-shortcut">```</span>
|
|
721
622
|
</button>
|
|
722
623
|
{/if}
|
|
723
624
|
{#if has('math')}
|
|
@@ -728,6 +629,22 @@
|
|
|
728
629
|
runInsert(() => editor.chain().focus().promptMathDisplay().run())}
|
|
729
630
|
>
|
|
730
631
|
<Sigma size={14} /> 수식 블록
|
|
632
|
+
<span class="hce-menu-shortcut">$$</span>
|
|
633
|
+
</button>
|
|
634
|
+
<!--
|
|
635
|
+
⚠️ **빈 선택에서 인라인 수식을 새로 넣는 유일한 길**이다.
|
|
636
|
+
예전엔 인라인 그룹의 `Σ` 버튼이 그 일을 했는데(선택이 없으면 프롬프트를 열었다)
|
|
637
|
+
그 버튼을 버블로 옮기면서 길이 끊겼다 — 버블은 뭔가 골라야만 뜨기 때문이다.
|
|
638
|
+
(`$…$` 를 쳐서 만드는 입력 규칙은 그대로 살아 있다.)
|
|
639
|
+
-->
|
|
640
|
+
<button
|
|
641
|
+
type="button"
|
|
642
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
643
|
+
onclick={() =>
|
|
644
|
+
runInsert(() => editor.chain().focus().promptMathInline().run())}
|
|
645
|
+
>
|
|
646
|
+
<Sigma size={14} /> 인라인 수식
|
|
647
|
+
<span class="hce-menu-shortcut">$ $</span>
|
|
731
648
|
</button>
|
|
732
649
|
{/if}
|
|
733
650
|
{#if has('pdf')}
|
|
@@ -745,7 +662,7 @@
|
|
|
745
662
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
746
663
|
onclick={() => runInsert(onFileClick!)}
|
|
747
664
|
>
|
|
748
|
-
<Paperclip size={14} /> 파일
|
|
665
|
+
<Paperclip size={14} /> 파일
|
|
749
666
|
</button>
|
|
750
667
|
{/if}
|
|
751
668
|
{#if has('table')}
|
|
@@ -754,11 +671,7 @@
|
|
|
754
671
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
755
672
|
onclick={() =>
|
|
756
673
|
runInsert(() =>
|
|
757
|
-
editor
|
|
758
|
-
.chain()
|
|
759
|
-
.focus()
|
|
760
|
-
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
761
|
-
.run(),
|
|
674
|
+
insertTableSized(editor, { rows: 3, cols: 3, withHeaderRow: true }),
|
|
762
675
|
)}
|
|
763
676
|
>
|
|
764
677
|
<TableIcon size={14} /> 표 (3x3)
|
|
@@ -778,6 +691,7 @@
|
|
|
778
691
|
)}
|
|
779
692
|
>
|
|
780
693
|
<Minus size={14} /> 구분선
|
|
694
|
+
<span class="hce-menu-shortcut">---</span>
|
|
781
695
|
</button>
|
|
782
696
|
{/if}
|
|
783
697
|
{#if has('columns-2')}
|
|
@@ -810,7 +724,7 @@
|
|
|
810
724
|
</button>
|
|
811
725
|
{/if}
|
|
812
726
|
|
|
813
|
-
{#if has('image') || has('
|
|
727
|
+
{#if has('image') || has('mbus') || has('video')}
|
|
814
728
|
<div class="h-px bg-border my-1"></div>
|
|
815
729
|
{/if}
|
|
816
730
|
{#if has('image')}
|
|
@@ -822,13 +736,17 @@
|
|
|
822
736
|
<ImageIcon size={14} /> 이미지
|
|
823
737
|
</button>
|
|
824
738
|
{/if}
|
|
825
|
-
{#if has('
|
|
739
|
+
{#if has('video')}
|
|
740
|
+
<!--
|
|
741
|
+
유튜브·Vimeo 등 바깥 영상. 붙여넣은 주소를 임베드용으로 바꿔 주므로
|
|
742
|
+
`watch?v=…` 를 그대로 넣어도 된다(`extensions/VideoEmbed.ts`).
|
|
743
|
+
-->
|
|
826
744
|
<button
|
|
827
745
|
type="button"
|
|
828
746
|
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
829
|
-
onclick={
|
|
747
|
+
onclick={addVideo}
|
|
830
748
|
>
|
|
831
|
-
<
|
|
749
|
+
<Youtube size={14} /> 영상
|
|
832
750
|
</button>
|
|
833
751
|
{/if}
|
|
834
752
|
{#if has('mbus')}
|
|
@@ -847,29 +765,13 @@
|
|
|
847
765
|
{/if}
|
|
848
766
|
|
|
849
767
|
<!-- Modals -->
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
title="링크 URL 입력"
|
|
853
|
-
placeholder="https://example.com"
|
|
854
|
-
defaultValue={isActive("link") ? editor.getAttributes("link").href || "" : ""}
|
|
855
|
-
onConfirm={(url) => {
|
|
856
|
-
editor
|
|
857
|
-
.chain()
|
|
858
|
-
.focus()
|
|
859
|
-
.extendMarkRange("link")
|
|
860
|
-
.setLink({ href: url })
|
|
861
|
-
.run();
|
|
862
|
-
modalState = null;
|
|
863
|
-
}}
|
|
864
|
-
onCancel={() => (modalState = null)}
|
|
865
|
-
/>
|
|
866
|
-
{/if}
|
|
867
|
-
{#if modalState?.type === "image"}
|
|
768
|
+
<!-- (이미지 모달은 없다 — 에디터가 업로드/링크 탭 모달을 띄운다. `onImageClick` 참고.) -->
|
|
769
|
+
{#if modalState?.type === "video"}
|
|
868
770
|
<InputModal
|
|
869
|
-
title="
|
|
870
|
-
placeholder="https://
|
|
771
|
+
title="영상 URL"
|
|
772
|
+
placeholder="https://www.youtube.com/watch?v=..."
|
|
871
773
|
onConfirm={(url) => {
|
|
872
|
-
editor.chain().focus().
|
|
774
|
+
editor.chain().focus().setVideoEmbed({ src: url }).run();
|
|
873
775
|
modalState = null;
|
|
874
776
|
}}
|
|
875
777
|
onCancel={() => (modalState = null)}
|
|
@@ -943,4 +845,24 @@
|
|
|
943
845
|
.hce-toolbar-group :global(button) {
|
|
944
846
|
flex-shrink: 0;
|
|
945
847
|
}
|
|
848
|
+
|
|
849
|
+
/*
|
|
850
|
+
* 메뉴 항목 오른쪽의 입력 규칙 표시(사용자 요청). 메뉴를 한 번 쓰고 나면 다음부터는
|
|
851
|
+
* 쳐서 만들게 되는 게 목적이라, 눈에 띄되 이름을 가리지 않을 만큼만 흐리게 둔다.
|
|
852
|
+
*
|
|
853
|
+
* ⚠️ **드롭다운은 툴바 밖(`.bg-popover`)에 그려지므로 `:global` 이 필요하다.**
|
|
854
|
+
* ⚠️ `margin-left: auto` 로 오른쪽 끝에 붙이고 `flex-shrink: 0` 으로 지킨다 —
|
|
855
|
+
* 안 그러면 이름이 길 때 `# >` 쪽이 먼저 찌그러진다.
|
|
856
|
+
*/
|
|
857
|
+
:global(.hce-menu-shortcut) {
|
|
858
|
+
margin-left: auto;
|
|
859
|
+
flex-shrink: 0;
|
|
860
|
+
padding-left: 10px;
|
|
861
|
+
font-size: 11px;
|
|
862
|
+
font-weight: 400;
|
|
863
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
864
|
+
color: var(--muted-foreground);
|
|
865
|
+
opacity: 0.75;
|
|
866
|
+
white-space: pre;
|
|
867
|
+
}
|
|
946
868
|
</style>
|
|
@@ -5,10 +5,18 @@ type $$ComponentProps = {
|
|
|
5
5
|
editor: Editor;
|
|
6
6
|
features: Set<ToolbarFeature>;
|
|
7
7
|
onPdfClick: () => void;
|
|
8
|
+
/**
|
|
9
|
+
* 이미지 넣기. **툴바는 무엇을 넣을지 정하지 않는다** — 업로드/링크 탭 모달을 띄울지
|
|
10
|
+
* URL 만 물을지는 에디터가 정한다(`TipTapEditor` 의 `pickImage`).
|
|
11
|
+
* 예전엔 여기서 `onPromptImage` 를 직접 불러 URL 만 받았고, 그래서 툴바로는
|
|
12
|
+
* 내 컴퓨터의 그림을 넣을 방법이 없었다.
|
|
13
|
+
*/
|
|
14
|
+
onImageClick: () => void;
|
|
8
15
|
onFileClick?: () => void;
|
|
9
16
|
onPromptLink?: PromptHandler;
|
|
10
|
-
onPromptImage?: PromptHandler;
|
|
11
17
|
onPromptMbus?: PromptHandler;
|
|
18
|
+
/** 영상 URL 프롬프트. 미제공 시 내장 InputModal 폴백 */
|
|
19
|
+
onPromptVideo?: PromptHandler;
|
|
12
20
|
/**
|
|
13
21
|
* 툴바 **오른쪽 끝**에 호스트가 끼워 넣는 조각(예: HTML ↔ 에디터 토글).
|
|
14
22
|
*
|
|
@@ -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;AAsCtC,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,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,
|
|
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;AAsCtC,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;AAgiBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|