@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.
- 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
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
</button>
|
|
96
96
|
<button
|
|
97
97
|
type="button"
|
|
98
|
-
class="px-3.5 py-1.5 text-sm rounded-md
|
|
98
|
+
class="px-3.5 py-1.5 text-sm rounded-md hce-btn-primary hover:opacity-90 transition-opacity"
|
|
99
99
|
onclick={handleSubmit}
|
|
100
100
|
>
|
|
101
101
|
확인
|
|
@@ -76,6 +76,65 @@
|
|
|
76
76
|
handleSubmit();
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* ── 자주 쓰는 기호 ─────────────────────────────────────────────────────────
|
|
82
|
+
* LaTeX 를 외우고 있는 사람만 수식을 쓸 수 있으면 기능이 반만 있는 것이다(사용자 제안).
|
|
83
|
+
*
|
|
84
|
+
* 고른 기준은 **온라인저지 본문에 실제로 나오는 것** — 복잡도 표기(빅오·log·합),
|
|
85
|
+
* 제약 조건(부등호·지수), 수열 첨자, 그리고 그리스 문자 몇이다. 미분·적분처럼
|
|
86
|
+
* 여기서 드문 것은 넣지 않았다. 늘리면 팔레트가 본문보다 커진다.
|
|
87
|
+
*
|
|
88
|
+
* `$` 는 커서를 놓을 자리다(아래 `insert` 가 그 자리에 커서를 둔다). 없으면 끝에 놓인다.
|
|
89
|
+
*/
|
|
90
|
+
const SYMBOLS: { label: string; snippet: string; title: string }[] = [
|
|
91
|
+
{ label: "x²", snippet: "^{$}", title: "위첨자" },
|
|
92
|
+
{ label: "xᵢ", snippet: "_{$}", title: "아래첨자" },
|
|
93
|
+
{ label: "a/b", snippet: "\\frac{$}{}", title: "분수" },
|
|
94
|
+
{ label: "√", snippet: "\\sqrt{$}", title: "제곱근" },
|
|
95
|
+
{ label: "∑", snippet: "\\sum_{i=1}^{n} $", title: "합" },
|
|
96
|
+
{ label: "∏", snippet: "\\prod_{i=1}^{n} $", title: "곱" },
|
|
97
|
+
{ label: "log", snippet: "\\log $", title: "로그" },
|
|
98
|
+
{ label: "O(n)", snippet: "O($)", title: "빅오 표기" },
|
|
99
|
+
{ label: "≤", snippet: "\\le $", title: "작거나 같다" },
|
|
100
|
+
{ label: "≥", snippet: "\\ge $", title: "크거나 같다" },
|
|
101
|
+
{ label: "≠", snippet: "\\ne $", title: "같지 않다" },
|
|
102
|
+
{ label: "×", snippet: "\\times $", title: "곱하기" },
|
|
103
|
+
{ label: "÷", snippet: "\\div $", title: "나누기" },
|
|
104
|
+
{ label: "∈", snippet: "\\in $", title: "원소" },
|
|
105
|
+
{ label: "∞", snippet: "\\infty $", title: "무한대" },
|
|
106
|
+
{ label: "α", snippet: "\\alpha $", title: "알파" },
|
|
107
|
+
{ label: "θ", snippet: "\\theta $", title: "세타" },
|
|
108
|
+
{ label: "π", snippet: "\\pi $", title: "파이" },
|
|
109
|
+
{ label: "→", snippet: "\\to $", title: "화살표" },
|
|
110
|
+
{ label: "⋯", snippet: "\\cdots $", title: "가운데 말줄임" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 커서 자리에 조각을 끼워 넣는다.
|
|
115
|
+
*
|
|
116
|
+
* ⚠️ `value` 를 그냥 이어 붙이면 **커서가 맨 뒤로 튄다** — 수식 가운데를 고치던 중에
|
|
117
|
+
* 기호를 누르면 엉뚱한 곳에 들어간다. 선택 구간을 직접 다루고, 넣은 뒤 커서를
|
|
118
|
+
* `$` 자리(없으면 조각 끝)에 되돌려 놓는다.
|
|
119
|
+
*/
|
|
120
|
+
function insert(snippet: string) {
|
|
121
|
+
const el = inputEl;
|
|
122
|
+
if (!el) return;
|
|
123
|
+
|
|
124
|
+
const caret = snippet.indexOf("$");
|
|
125
|
+
const text = snippet.replace("$", "");
|
|
126
|
+
const start = el.selectionStart ?? value.length;
|
|
127
|
+
const end = el.selectionEnd ?? start;
|
|
128
|
+
|
|
129
|
+
value = value.slice(0, start) + text + value.slice(end);
|
|
130
|
+
|
|
131
|
+
const next = start + (caret === -1 ? text.length : caret);
|
|
132
|
+
/* 값 반영이 DOM 에 적용된 뒤에 커서를 옮겨야 한다. */
|
|
133
|
+
requestAnimationFrame(() => {
|
|
134
|
+
el.focus();
|
|
135
|
+
el.setSelectionRange(next, next);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
79
138
|
</script>
|
|
80
139
|
|
|
81
140
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -121,6 +180,24 @@
|
|
|
121
180
|
onkeydown={handleKeydown}
|
|
122
181
|
></textarea>
|
|
123
182
|
|
|
183
|
+
<!--
|
|
184
|
+
기호 팔레트. `textarea` **바로 아래**에 둔다 — 누르면 글자가 들어가는 곳이라
|
|
185
|
+
입력창과 붙어 있어야 어디로 들어갈지 짐작이 간다.
|
|
186
|
+
-->
|
|
187
|
+
<div class="hce-math-symbols mt-2 flex flex-wrap gap-1">
|
|
188
|
+
{#each SYMBOLS as sym}
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
class="hce-math-sym"
|
|
192
|
+
title={sym.title}
|
|
193
|
+
aria-label={sym.title}
|
|
194
|
+
onclick={() => insert(sym.snippet)}
|
|
195
|
+
>
|
|
196
|
+
{sym.label}
|
|
197
|
+
</button>
|
|
198
|
+
{/each}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
124
201
|
<!--
|
|
125
202
|
⚠️ 바탕을 `bg-muted/40` 으로 주지 않는다. 호스트가 `--muted` 를 **hex** 로 주는데
|
|
126
203
|
Tailwind 3 의 투명도 변형은 채널 표기를 요구해 그 클래스를 **조용히 만들지 않는다**
|
|
@@ -154,7 +231,7 @@
|
|
|
154
231
|
</button>
|
|
155
232
|
<button
|
|
156
233
|
type="button"
|
|
157
|
-
class="px-3.5 py-1.5 text-sm rounded-md
|
|
234
|
+
class="px-3.5 py-1.5 text-sm rounded-md hce-btn-primary hover:opacity-90 transition-opacity"
|
|
158
235
|
onclick={handleSubmit}
|
|
159
236
|
>
|
|
160
237
|
확인
|
|
@@ -187,4 +264,37 @@
|
|
|
187
264
|
.hce-math-panel textarea {
|
|
188
265
|
max-width: 100%;
|
|
189
266
|
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* 기호 버튼. 스무 개가 두 줄로 앉으므로 **작고 조용해야** 한다 — 입력창보다 눈에 띄면
|
|
270
|
+
* 무엇이 본체인지 흐려진다.
|
|
271
|
+
*
|
|
272
|
+
* ⚠️ 색·면을 직접 준다. 이 모달은 `.hce-editor-wrapper` 안에 렌더되어 패키지 리셋
|
|
273
|
+
* (`background: transparent`, 0,1,1)을 맞는데, Tailwind 유틸리티(0,1,0)로는 못 이긴다
|
|
274
|
+
* — 확인 버튼이 통째로 안 보이던 것과 같은 뿌리다(`editor.css` 의 `.hce-btn-primary` 주석).
|
|
275
|
+
* 여기는 Svelte 스코프가 붙어 특이도가 더 높으므로 그대로 먹는다.
|
|
276
|
+
*/
|
|
277
|
+
.hce-math-sym {
|
|
278
|
+
box-sizing: border-box;
|
|
279
|
+
min-width: 30px;
|
|
280
|
+
padding: 3px 7px;
|
|
281
|
+
border: 1px solid var(--border);
|
|
282
|
+
border-radius: 6px;
|
|
283
|
+
background: var(--background);
|
|
284
|
+
color: var(--muted-foreground);
|
|
285
|
+
font-size: 12px;
|
|
286
|
+
line-height: 1.4;
|
|
287
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
transition:
|
|
290
|
+
background-color 0.12s,
|
|
291
|
+
color 0.12s,
|
|
292
|
+
border-color 0.12s;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.hce-math-sym:hover {
|
|
296
|
+
background: var(--muted);
|
|
297
|
+
color: var(--foreground);
|
|
298
|
+
border-color: var(--ring, var(--muted-foreground));
|
|
299
|
+
}
|
|
190
300
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MathModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/MathModal.svelte.ts"],"names":[],"mappings":"AAgBC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yCAAyC;IACzC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;
|
|
1
|
+
{"version":3,"file":"MathModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/MathModal.svelte.ts"],"names":[],"mappings":"AAgBC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yCAAyC;IACzC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAuLJ,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 이미지·파일을 넣는 **한 개의 모달**. 탭 두 개(업로드 / 링크)를 갖는다.
|
|
4
|
+
*
|
|
5
|
+
* 예전에는 두 길이 완전히 갈려 있었다 — 툴바의 `이미지` 는 URL 입력창만 띄웠고
|
|
6
|
+
* (그래서 내 컴퓨터의 그림을 넣을 수 없었다), `파일 첨부` 는 곧장 파일 선택창을 열었다
|
|
7
|
+
* (그래서 이미 올라가 있는 파일의 주소로는 넣을 수 없었다). 넣으려는 것이 같은데
|
|
8
|
+
* 어느 버튼을 눌렀느냐로 할 수 있는 일이 갈렸다(사용자 요청으로 통합).
|
|
9
|
+
*
|
|
10
|
+
* 이 컴포넌트는 **고르기만** 한다. 실제로 문서에 꽂는 일은 부모(`TipTapEditor`)가 한다 —
|
|
11
|
+
* 업로드 함수도, 노드를 만드는 방법도 거기 있기 때문이다.
|
|
12
|
+
*/
|
|
13
|
+
import { X } from "lucide-svelte";
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
title,
|
|
17
|
+
accept,
|
|
18
|
+
uploadLabel,
|
|
19
|
+
linkPlaceholder,
|
|
20
|
+
linkConfirmLabel,
|
|
21
|
+
linkHint,
|
|
22
|
+
onUpload,
|
|
23
|
+
onLink,
|
|
24
|
+
onCancel,
|
|
25
|
+
}: {
|
|
26
|
+
title: string;
|
|
27
|
+
/** 파일 선택창에서 거를 확장자. 파일 첨부처럼 아무거나 받는 곳은 비운다. */
|
|
28
|
+
accept?: string;
|
|
29
|
+
uploadLabel: string;
|
|
30
|
+
linkPlaceholder: string;
|
|
31
|
+
linkConfirmLabel: string;
|
|
32
|
+
/** 링크 탭 아래 작은 안내문. 없으면 안 그린다. */
|
|
33
|
+
linkHint?: string;
|
|
34
|
+
/** 고른 파일을 부모가 올리고 문서에 꽂는다. 끝나면 모달을 닫는 것도 부모 몫. */
|
|
35
|
+
onUpload: (file: File) => void;
|
|
36
|
+
onLink: (url: string) => void;
|
|
37
|
+
onCancel: () => void;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
let tab: "upload" | "link" = $state("upload");
|
|
41
|
+
let url = $state("");
|
|
42
|
+
let fileInputEl: HTMLInputElement | undefined = $state();
|
|
43
|
+
let urlInputEl: HTMLInputElement | undefined = $state();
|
|
44
|
+
|
|
45
|
+
/* 링크 탭으로 옮기면 바로 붙여넣을 수 있게 커서를 넣어 준다. */
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (tab === "link") urlInputEl?.focus();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
$effect(() => {
|
|
51
|
+
function onKeydown(e: KeyboardEvent) {
|
|
52
|
+
if (e.key === "Escape") {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
onCancel();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
window.addEventListener("keydown", onKeydown);
|
|
58
|
+
return () => window.removeEventListener("keydown", onKeydown);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function submitLink() {
|
|
62
|
+
const trimmed = url.trim();
|
|
63
|
+
if (trimmed) onLink(trimmed);
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
68
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
69
|
+
<div
|
|
70
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
|
71
|
+
onclick={onCancel}
|
|
72
|
+
>
|
|
73
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
74
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
75
|
+
<div
|
|
76
|
+
class="hce-picker-panel bg-background border border-border rounded-xl shadow-xl w-[420px] max-w-[90vw]"
|
|
77
|
+
onclick={(e) => e.stopPropagation()}
|
|
78
|
+
>
|
|
79
|
+
<div class="flex items-center justify-between px-4 pt-3">
|
|
80
|
+
<span class="text-sm font-semibold">{title}</span>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="p-1 rounded-md hover:bg-muted text-muted-foreground"
|
|
84
|
+
onclick={onCancel}
|
|
85
|
+
aria-label="닫기"
|
|
86
|
+
>
|
|
87
|
+
<X size={14} />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!--
|
|
92
|
+
탭 줄. 고른 탭에 **밑줄**을 그어 표시한다 — 알약 버튼으로 하면 그 아래 내용과
|
|
93
|
+
경계가 겹쳐 보여서, 눌린 것인지 내용의 일부인지 헷갈린다.
|
|
94
|
+
-->
|
|
95
|
+
<div class="hce-picker-tabs flex gap-1 px-3 pt-2 border-b border-border" role="tablist">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
role="tab"
|
|
99
|
+
aria-selected={tab === "upload"}
|
|
100
|
+
class="hce-picker-tab px-2.5 py-1.5 text-sm transition-colors"
|
|
101
|
+
class:hce-picker-tab-on={tab === "upload"}
|
|
102
|
+
onclick={() => (tab = "upload")}
|
|
103
|
+
>
|
|
104
|
+
업로드
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
role="tab"
|
|
109
|
+
aria-selected={tab === "link"}
|
|
110
|
+
class="hce-picker-tab px-2.5 py-1.5 text-sm transition-colors"
|
|
111
|
+
class:hce-picker-tab-on={tab === "link"}
|
|
112
|
+
onclick={() => (tab = "link")}
|
|
113
|
+
>
|
|
114
|
+
링크
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="p-4">
|
|
119
|
+
{#if tab === "upload"}
|
|
120
|
+
<!--
|
|
121
|
+
⚠️ **강조 버튼이다.** 테두리만 두른 판은 다크에서 배경과 거의 같아 보여
|
|
122
|
+
"누르는 것"으로 읽히지 않았다(사용자 지적: 버튼 같지가 않다).
|
|
123
|
+
링크 탭의 `임베드` 와 **같은 위계** — 둘 다 그 탭에서 할 일이 하나뿐이다.
|
|
124
|
+
-->
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
class="w-full rounded-md px-3 py-2.5 text-sm hce-btn-primary hover:opacity-90 transition-opacity"
|
|
128
|
+
onclick={() => fileInputEl?.click()}
|
|
129
|
+
>
|
|
130
|
+
{uploadLabel}
|
|
131
|
+
</button>
|
|
132
|
+
<input
|
|
133
|
+
bind:this={fileInputEl}
|
|
134
|
+
type="file"
|
|
135
|
+
{accept}
|
|
136
|
+
class="hidden"
|
|
137
|
+
onchange={(e) => {
|
|
138
|
+
const target = e.target as HTMLInputElement;
|
|
139
|
+
const file = target.files?.[0];
|
|
140
|
+
target.value = "";
|
|
141
|
+
if (file) onUpload(file);
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
{:else}
|
|
145
|
+
<input
|
|
146
|
+
bind:this={urlInputEl}
|
|
147
|
+
type="text"
|
|
148
|
+
class="w-full border border-border rounded-md px-3 py-2 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary"
|
|
149
|
+
placeholder={linkPlaceholder}
|
|
150
|
+
bind:value={url}
|
|
151
|
+
onkeydown={(e) => {
|
|
152
|
+
if (e.key === "Enter") submitLink();
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
class="w-full mt-3 px-3.5 py-2 text-sm rounded-md hce-btn-primary hover:opacity-90 transition-opacity"
|
|
158
|
+
onclick={submitLink}
|
|
159
|
+
>
|
|
160
|
+
{linkConfirmLabel}
|
|
161
|
+
</button>
|
|
162
|
+
{#if linkHint}
|
|
163
|
+
<p class="mt-2 text-center text-xs text-muted-foreground">{linkHint}</p>
|
|
164
|
+
{/if}
|
|
165
|
+
{/if}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<style>
|
|
171
|
+
/*
|
|
172
|
+
* `w-full` 은 `width: 100%` 일 뿐이라 `box-sizing` 이 `content-box` 면 좌우 패딩·테두리가
|
|
173
|
+
* 그대로 더해져 입력창이 판 밖으로 나간다. 이 패키지는 Tailwind preflight 를 끄고
|
|
174
|
+
* 배포하고 소비 앱에도 전역 리셋이 없다 — 수식 모달에서 실제로 터진 적이 있다.
|
|
175
|
+
*/
|
|
176
|
+
.hce-picker-panel,
|
|
177
|
+
.hce-picker-panel input,
|
|
178
|
+
.hce-picker-panel button {
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* 고르지 않은 탭은 흐리게, 고른 탭은 본문색 + 밑줄.
|
|
184
|
+
* ⚠️ 밑줄은 `border-bottom` 으로 그리고 **꺼진 탭에도 투명한 같은 두께**를 준다.
|
|
185
|
+
* 켜질 때만 테두리를 붙이면 그 2px 만큼 글자가 위로 밀려 탭이 덜컥거린다.
|
|
186
|
+
*/
|
|
187
|
+
.hce-picker-tab {
|
|
188
|
+
background: transparent;
|
|
189
|
+
border: 0;
|
|
190
|
+
border-bottom: 2px solid transparent;
|
|
191
|
+
margin-bottom: -1px;
|
|
192
|
+
color: var(--muted-foreground);
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
font: inherit;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.hce-picker-tab:hover {
|
|
198
|
+
color: var(--foreground);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.hce-picker-tab-on {
|
|
202
|
+
color: var(--foreground);
|
|
203
|
+
border-bottom-color: var(--foreground);
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
title: string;
|
|
3
|
+
/** 파일 선택창에서 거를 확장자. 파일 첨부처럼 아무거나 받는 곳은 비운다. */
|
|
4
|
+
accept?: string;
|
|
5
|
+
uploadLabel: string;
|
|
6
|
+
linkPlaceholder: string;
|
|
7
|
+
linkConfirmLabel: string;
|
|
8
|
+
/** 링크 탭 아래 작은 안내문. 없으면 안 그린다. */
|
|
9
|
+
linkHint?: string;
|
|
10
|
+
/** 고른 파일을 부모가 올리고 문서에 꽂는다. 끝나면 모달을 닫는 것도 부모 몫. */
|
|
11
|
+
onUpload: (file: File) => void;
|
|
12
|
+
onLink: (url: string) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
};
|
|
15
|
+
declare const MediaPickerModal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
type MediaPickerModal = ReturnType<typeof MediaPickerModal>;
|
|
17
|
+
export default MediaPickerModal;
|
|
18
|
+
//# sourceMappingURL=MediaPickerModal.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaPickerModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/MediaPickerModal.svelte.ts"],"names":[],"mappings":"AAgBC,KAAK,gBAAgB,GAAI;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAkGJ,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
Columns3,
|
|
21
21
|
Type,
|
|
22
22
|
Tv,
|
|
23
|
+
Youtube,
|
|
23
24
|
SquareDashed,
|
|
24
25
|
Sigma,
|
|
25
26
|
} from "lucide-svelte";
|
|
27
|
+
import { insertTableSized } from "../utils/table";
|
|
26
28
|
import type { SlashMenuItem, ToolbarFeature, PromptHandler } from "../types";
|
|
27
29
|
import type { Component } from "svelte";
|
|
28
30
|
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
h2: "기본",
|
|
40
42
|
h3: "기본",
|
|
41
43
|
mbus: "미디어",
|
|
44
|
+
video: "미디어",
|
|
42
45
|
card: "블록",
|
|
43
46
|
"bullet-list": "리스트",
|
|
44
47
|
"ordered-list": "리스트",
|
|
@@ -59,6 +62,14 @@
|
|
|
59
62
|
label: string;
|
|
60
63
|
keywords: string;
|
|
61
64
|
icon: Component<{ size?: number }>;
|
|
65
|
+
/** 항목 오른쪽에 작게 보여 줄 입력 규칙. 없으면 안 그린다. */
|
|
66
|
+
shortcut?: string;
|
|
67
|
+
/**
|
|
68
|
+
* 이 항목이 들어갈 섹션. 없으면 `SECTION_MAP` 이 feature 로 정한다.
|
|
69
|
+
* ⚠️ 토글 제목처럼 **같은 feature 인데 다른 자리**에 놓아야 하는 것 때문에 필요하다
|
|
70
|
+
* (셋 다 `toggle` 이지만 토글은 리스트, 토글 제목은 제목 옆이 맞다).
|
|
71
|
+
*/
|
|
72
|
+
section?: string;
|
|
62
73
|
command: (editor: Editor) => void;
|
|
63
74
|
}[] = [
|
|
64
75
|
{
|
|
@@ -73,6 +84,7 @@
|
|
|
73
84
|
label: "제목 1",
|
|
74
85
|
keywords: "heading h1 제목",
|
|
75
86
|
icon: Heading1,
|
|
87
|
+
shortcut: "# ",
|
|
76
88
|
command: (editor) =>
|
|
77
89
|
editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
78
90
|
},
|
|
@@ -81,6 +93,7 @@
|
|
|
81
93
|
label: "제목 2",
|
|
82
94
|
keywords: "heading h2 제목",
|
|
83
95
|
icon: Heading2,
|
|
96
|
+
shortcut: "## ",
|
|
84
97
|
command: (editor) =>
|
|
85
98
|
editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
86
99
|
},
|
|
@@ -89,6 +102,7 @@
|
|
|
89
102
|
label: "제목 3",
|
|
90
103
|
keywords: "heading h3 제목",
|
|
91
104
|
icon: Heading3,
|
|
105
|
+
shortcut: "### ",
|
|
92
106
|
command: (editor) =>
|
|
93
107
|
editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
94
108
|
},
|
|
@@ -97,6 +111,7 @@
|
|
|
97
111
|
label: "글머리 목록",
|
|
98
112
|
keywords: "bullet list 목록 리스트",
|
|
99
113
|
icon: List,
|
|
114
|
+
shortcut: "- ",
|
|
100
115
|
command: (editor) => editor.chain().focus().toggleBulletList().run(),
|
|
101
116
|
},
|
|
102
117
|
{
|
|
@@ -104,6 +119,7 @@
|
|
|
104
119
|
label: "번호 목록",
|
|
105
120
|
keywords: "ordered number list 번호 리스트",
|
|
106
121
|
icon: ListOrdered,
|
|
122
|
+
shortcut: "1. ",
|
|
107
123
|
command: (editor) => editor.chain().focus().toggleOrderedList().run(),
|
|
108
124
|
},
|
|
109
125
|
{
|
|
@@ -111,6 +127,7 @@
|
|
|
111
127
|
label: "체크리스트",
|
|
112
128
|
keywords: "checklist task todo 체크 할일",
|
|
113
129
|
icon: CheckSquare,
|
|
130
|
+
shortcut: "[] ",
|
|
114
131
|
command: (editor) => editor.chain().focus().toggleTaskList().run(),
|
|
115
132
|
},
|
|
116
133
|
{
|
|
@@ -118,6 +135,7 @@
|
|
|
118
135
|
label: "인용문",
|
|
119
136
|
keywords: "quote blockquote 인용",
|
|
120
137
|
icon: Quote,
|
|
138
|
+
shortcut: "\" ",
|
|
121
139
|
command: (editor) => editor.chain().focus().toggleBlockquote().run(),
|
|
122
140
|
},
|
|
123
141
|
{
|
|
@@ -125,6 +143,7 @@
|
|
|
125
143
|
label: "구분선",
|
|
126
144
|
keywords: "divider hr horizontal rule 구분",
|
|
127
145
|
icon: Minus,
|
|
146
|
+
shortcut: "---",
|
|
128
147
|
command: (editor) => editor.chain().focus().setHorizontalRule().run(),
|
|
129
148
|
},
|
|
130
149
|
{
|
|
@@ -132,6 +151,7 @@
|
|
|
132
151
|
label: "코드",
|
|
133
152
|
keywords: "code 코드 블록 cpp c++ python 파이썬",
|
|
134
153
|
icon: Code2,
|
|
154
|
+
shortcut: "```",
|
|
135
155
|
command: (editor) =>
|
|
136
156
|
editor.chain().focus().setCodeBlock().run(),
|
|
137
157
|
},
|
|
@@ -140,6 +160,7 @@
|
|
|
140
160
|
label: "수식",
|
|
141
161
|
keywords: "math latex tex 수식 공식 수학 katex 시그마 분수",
|
|
142
162
|
icon: Sigma,
|
|
163
|
+
shortcut: "$$",
|
|
143
164
|
// 슬래시 메뉴는 `/수식` 을 지우면서 커맨드를 부른다. 프롬프트는 그 뒤에 열려야
|
|
144
165
|
// 지운 자리에 결과가 들어간다 — `promptMathDisplay` 가 비동기라 순서가 맞는다.
|
|
145
166
|
command: (editor) => editor.chain().focus().promptMathDisplay().run(),
|
|
@@ -149,7 +170,40 @@
|
|
|
149
170
|
label: "토글",
|
|
150
171
|
keywords: "toggle details 접기 펼치기 토글",
|
|
151
172
|
icon: ChevronRight,
|
|
152
|
-
|
|
173
|
+
shortcut: "> ",
|
|
174
|
+
command: (editor) => editor.chain().focus().setToggleHeading(0).run(),
|
|
175
|
+
},
|
|
176
|
+
/*
|
|
177
|
+
* 토글 제목 — 접히는 제목. 입력 규칙(`# > `)으로도 만들 수 있지만 그것만 있으면
|
|
178
|
+
* **메뉴에는 없는 기능**이 된다(사용자 요청으로 추가).
|
|
179
|
+
* 만드는 일은 입력 규칙과 **같은 커맨드**가 한다(`setToggleHeading`).
|
|
180
|
+
*/
|
|
181
|
+
{
|
|
182
|
+
feature: "toggle",
|
|
183
|
+
label: "토글 제목 1",
|
|
184
|
+
section: "기본",
|
|
185
|
+
keywords: "toggle heading 접기 제목 토글제목 h1",
|
|
186
|
+
icon: Heading1,
|
|
187
|
+
shortcut: "# > ",
|
|
188
|
+
command: (editor) => editor.chain().focus().setToggleHeading(1).run(),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
feature: "toggle",
|
|
192
|
+
label: "토글 제목 2",
|
|
193
|
+
section: "기본",
|
|
194
|
+
keywords: "toggle heading 접기 제목 토글제목 h2",
|
|
195
|
+
icon: Heading2,
|
|
196
|
+
shortcut: "## > ",
|
|
197
|
+
command: (editor) => editor.chain().focus().setToggleHeading(2).run(),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
feature: "toggle",
|
|
201
|
+
label: "토글 제목 3",
|
|
202
|
+
section: "기본",
|
|
203
|
+
keywords: "toggle heading 접기 제목 토글제목 h3",
|
|
204
|
+
icon: Heading3,
|
|
205
|
+
shortcut: "### > ",
|
|
206
|
+
command: (editor) => editor.chain().focus().setToggleHeading(3).run(),
|
|
153
207
|
},
|
|
154
208
|
{
|
|
155
209
|
feature: "table",
|
|
@@ -157,11 +211,7 @@
|
|
|
157
211
|
keywords: "table 표 테이블",
|
|
158
212
|
icon: TableIcon,
|
|
159
213
|
command: (editor) =>
|
|
160
|
-
editor
|
|
161
|
-
.chain()
|
|
162
|
-
.focus()
|
|
163
|
-
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
164
|
-
.run(),
|
|
214
|
+
insertTableSized(editor, { rows: 3, cols: 3, withHeaderRow: true }),
|
|
165
215
|
},
|
|
166
216
|
{
|
|
167
217
|
feature: "columns-2",
|
|
@@ -187,6 +237,17 @@
|
|
|
187
237
|
if (url) editor.chain().focus().setImage({ src: url }).run();
|
|
188
238
|
},
|
|
189
239
|
},
|
|
240
|
+
{
|
|
241
|
+
feature: "video",
|
|
242
|
+
label: "영상",
|
|
243
|
+
keywords: "video 영상 유튜브 youtube vimeo 동영상",
|
|
244
|
+
icon: Youtube,
|
|
245
|
+
command: (editor) => {
|
|
246
|
+
// 붙여넣은 주소는 `setVideoEmbed` 가 임베드용으로 바꿔 준다.
|
|
247
|
+
const url = window.prompt("영상 URL을 입력하세요 (유튜브·Vimeo 등)");
|
|
248
|
+
if (url) editor.chain().focus().setVideoEmbed({ src: url }).run();
|
|
249
|
+
},
|
|
250
|
+
},
|
|
190
251
|
{
|
|
191
252
|
feature: "mbus",
|
|
192
253
|
label: "미디버스 영상",
|
|
@@ -231,9 +292,10 @@
|
|
|
231
292
|
onClose,
|
|
232
293
|
onPdfUpload,
|
|
233
294
|
onFileUpload,
|
|
295
|
+
onImagePick,
|
|
234
296
|
onPromptLink,
|
|
235
|
-
onPromptImage,
|
|
236
297
|
onPromptMbus,
|
|
298
|
+
onPromptVideo,
|
|
237
299
|
}: {
|
|
238
300
|
editor: Editor;
|
|
239
301
|
features: Set<ToolbarFeature>;
|
|
@@ -241,9 +303,18 @@
|
|
|
241
303
|
onClose: () => void;
|
|
242
304
|
onPdfUpload?: () => void;
|
|
243
305
|
onFileUpload?: () => void;
|
|
306
|
+
/**
|
|
307
|
+
* 이미지 넣기 — 툴바의 `이미지` 와 **같은 모달**을 연다(에디터가 띄운다).
|
|
308
|
+
* 없으면 아래 항목의 기본 동작(`window.prompt`)으로 떨어진다.
|
|
309
|
+
*/
|
|
310
|
+
onImagePick?: () => void;
|
|
244
311
|
onPromptLink?: PromptHandler;
|
|
245
|
-
onPromptImage?: PromptHandler;
|
|
246
312
|
onPromptMbus?: PromptHandler;
|
|
313
|
+
/**
|
|
314
|
+
* 영상 URL 프롬프트. 없으면 아래 항목의 `window.prompt` 로 떨어지는데, 그건 브라우저
|
|
315
|
+
* 기본 대화상자라 화면이 멈추고 앱과 모양이 따로 논다(사용자 지적).
|
|
316
|
+
*/
|
|
317
|
+
onPromptVideo?: PromptHandler;
|
|
247
318
|
} = $props();
|
|
248
319
|
|
|
249
320
|
async function runItem(item: (typeof SLASH_MENU_ITEMS_DATA)[number]) {
|
|
@@ -258,9 +329,13 @@
|
|
|
258
329
|
.run();
|
|
259
330
|
return;
|
|
260
331
|
}
|
|
261
|
-
if (item.feature === "image" &&
|
|
262
|
-
|
|
263
|
-
|
|
332
|
+
if (item.feature === "image" && onImagePick) {
|
|
333
|
+
onImagePick();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (item.feature === "video" && onPromptVideo) {
|
|
337
|
+
const url = await onPromptVideo("");
|
|
338
|
+
if (url) editor.chain().focus().setVideoEmbed({ src: url }).run();
|
|
264
339
|
return;
|
|
265
340
|
}
|
|
266
341
|
if (item.feature === "mbus" && onPromptMbus) {
|
|
@@ -377,7 +452,7 @@
|
|
|
377
452
|
>
|
|
378
453
|
{#each SECTION_ORDER as section}
|
|
379
454
|
{@const sectionItems = filtered.filter(
|
|
380
|
-
(it) => (SECTION_MAP[it.feature] ?? '기본') === section,
|
|
455
|
+
(it) => (it.section ?? SECTION_MAP[it.feature] ?? '기본') === section,
|
|
381
456
|
)}
|
|
382
457
|
{#if sectionItems.length > 0}
|
|
383
458
|
<p class="slash-section">{section}</p>
|
|
@@ -403,6 +478,14 @@
|
|
|
403
478
|
<item.icon size={SI} />
|
|
404
479
|
</span>
|
|
405
480
|
<span class="slash-label">{item.label}</span>
|
|
481
|
+
{#if item.shortcut}
|
|
482
|
+
<!--
|
|
483
|
+
쳐서 만드는 법을 항목 옆에 적어 둔다(사용자 요청). 메뉴를 한 번 쓰고 나면
|
|
484
|
+
다음부터는 메뉴를 안 열게 되는 게 목적이라, 눈에 띄되 이름을 가리지 않을
|
|
485
|
+
만큼만 흐리게 둔다.
|
|
486
|
+
-->
|
|
487
|
+
<span class="slash-shortcut">{item.shortcut}</span>
|
|
488
|
+
{/if}
|
|
406
489
|
</button>
|
|
407
490
|
{/each}
|
|
408
491
|
{/if}
|
|
@@ -412,7 +495,8 @@
|
|
|
412
495
|
|
|
413
496
|
<style>
|
|
414
497
|
.slash-menu {
|
|
415
|
-
|
|
498
|
+
/* 단축키 표시가 오른쪽에 붙으면서 200px 로는 이름이 잘렸다. */
|
|
499
|
+
width: 244px;
|
|
416
500
|
max-height: 280px;
|
|
417
501
|
}
|
|
418
502
|
|
|
@@ -489,4 +573,21 @@
|
|
|
489
573
|
text-overflow: ellipsis;
|
|
490
574
|
white-space: nowrap;
|
|
491
575
|
}
|
|
576
|
+
|
|
577
|
+
/*
|
|
578
|
+
* 오른쪽 끝에 붙는 입력 규칙 표시.
|
|
579
|
+
* ⚠️ `flex-shrink: 0` — 이름이 길어지면 이쪽이 먼저 찌그러져 `# >` 가 `#…` 이 된다.
|
|
580
|
+
* 줄이는 쪽은 위 `.slash-label`(말줄임 처리가 되어 있다)이어야 한다.
|
|
581
|
+
*/
|
|
582
|
+
.slash-shortcut {
|
|
583
|
+
flex-shrink: 0;
|
|
584
|
+
margin-left: auto;
|
|
585
|
+
padding-left: 8px;
|
|
586
|
+
font-size: 11px;
|
|
587
|
+
font-weight: 400;
|
|
588
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
589
|
+
color: var(--muted-foreground);
|
|
590
|
+
opacity: 0.75;
|
|
591
|
+
white-space: pre;
|
|
592
|
+
}
|
|
492
593
|
</style>
|
|
@@ -8,9 +8,18 @@ type $$ComponentProps = {
|
|
|
8
8
|
onClose: () => void;
|
|
9
9
|
onPdfUpload?: () => void;
|
|
10
10
|
onFileUpload?: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* 이미지 넣기 — 툴바의 `이미지` 와 **같은 모달**을 연다(에디터가 띄운다).
|
|
13
|
+
* 없으면 아래 항목의 기본 동작(`window.prompt`)으로 떨어진다.
|
|
14
|
+
*/
|
|
15
|
+
onImagePick?: () => void;
|
|
11
16
|
onPromptLink?: PromptHandler;
|
|
12
|
-
onPromptImage?: PromptHandler;
|
|
13
17
|
onPromptMbus?: PromptHandler;
|
|
18
|
+
/**
|
|
19
|
+
* 영상 URL 프롬프트. 없으면 아래 항목의 `window.prompt` 로 떨어지는데, 그건 브라우저
|
|
20
|
+
* 기본 대화상자라 화면이 멈추고 앱과 모양이 따로 논다(사용자 지적).
|
|
21
|
+
*/
|
|
22
|
+
onPromptVideo?: PromptHandler;
|
|
14
23
|
};
|
|
15
24
|
declare const SlashCommandMenu: Component<$$ComponentProps, {}, "">;
|
|
16
25
|
type SlashCommandMenu = ReturnType<typeof SlashCommandMenu>;
|