@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.
- package/README.md +43 -10
- package/dist/components/BubbleToolbar.svelte +26 -16
- package/dist/components/BubbleToolbar.svelte.d.ts +2 -1
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +100 -77
- package/dist/components/FixedToolbar.svelte.d.ts +3 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/InputModal.svelte +19 -16
- package/dist/components/InputModal.svelte.d.ts.map +1 -1
- package/dist/components/SlashCommandMenu.svelte +186 -57
- package/dist/components/SlashCommandMenu.svelte.d.ts +3 -1
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TableBubbleMenu.svelte +16 -16
- package/dist/components/TipTapEditor.svelte +13 -3
- package/dist/components/TipTapEditor.svelte.d.ts +3 -1
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/FileAttachment.d.ts.map +1 -1
- package/dist/extensions/FileAttachment.js +20 -16
- package/dist/extensions/PdfBlock.d.ts.map +1 -1
- package/dist/extensions/PdfBlock.js +84 -1
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @teriusu/rich-editor
|
|
2
2
|
|
|
3
3
|
TipTap v3 기반 리치 텍스트 에디터. Svelte 5 컴포넌트 라이브러리.
|
|
4
4
|
|
|
5
5
|
## 설치
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @
|
|
8
|
+
npm install @teriusu/rich-editor
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Tailwind CSS v4 사용 시 `@source` 추가:
|
|
12
12
|
```css
|
|
13
|
-
@import "@
|
|
14
|
-
@source "../node_modules/@
|
|
13
|
+
@import "@teriusu/rich-editor/styles";
|
|
14
|
+
@source "../node_modules/@teriusu/rich-editor/dist";
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## 기본 사용법
|
|
18
18
|
|
|
19
19
|
```svelte
|
|
20
20
|
<script>
|
|
21
|
-
import { TipTapEditor } from '@
|
|
22
|
-
import '@
|
|
21
|
+
import { TipTapEditor } from '@teriusu/rich-editor';
|
|
22
|
+
import '@teriusu/rich-editor/styles';
|
|
23
23
|
|
|
24
24
|
let html = $state('');
|
|
25
25
|
</script>
|
|
@@ -121,7 +121,7 @@ TipTap v2 커스텀 태그를 자동 변환합니다. 에디터 로드 시 `tran
|
|
|
121
121
|
게시물 조회 페이지(읽기 전용)에서도 적용:
|
|
122
122
|
|
|
123
123
|
```js
|
|
124
|
-
import { transformLegacyHtml } from '@
|
|
124
|
+
import { transformLegacyHtml } from '@teriusu/rich-editor';
|
|
125
125
|
|
|
126
126
|
const html = transformLegacyHtml(post.content);
|
|
127
127
|
```
|
|
@@ -132,7 +132,7 @@ const html = transformLegacyHtml(post.content);
|
|
|
132
132
|
|
|
133
133
|
```svelte
|
|
134
134
|
<script>
|
|
135
|
-
import { TipTapEditor } from '@
|
|
135
|
+
import { TipTapEditor } from '@teriusu/rich-editor';
|
|
136
136
|
import { Node } from '@tiptap/core';
|
|
137
137
|
|
|
138
138
|
const MyExtension = Node.create({ /* ... */ });
|
|
@@ -159,13 +159,46 @@ const html = transformLegacyHtml(post.content);
|
|
|
159
159
|
### 타입
|
|
160
160
|
`UploadHandler`, `FileResolver`, `FileResolveResult`, `TipTapEditorProps`, `SlashMenuItem`
|
|
161
161
|
|
|
162
|
-
## 빌드
|
|
162
|
+
## 빌드 / 배포
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
165
|
npm run build # svelte-package → dist/
|
|
166
|
-
npm
|
|
166
|
+
npm test # vitest
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
배포는 **main 브랜치 push 시 GitHub Actions가 자동 수행** (`.github/workflows/publish.yml`).
|
|
170
|
+
수동 `npm publish`는 사용하지 않음.
|
|
171
|
+
|
|
172
|
+
### 버전 정책 (SemVer)
|
|
173
|
+
|
|
174
|
+
| 단계 | 조건 | 커밋 타입 |
|
|
175
|
+
|------|------|-----------|
|
|
176
|
+
| **patch** (`0.0.x`) | 버그 수정, 내부 리팩토링, 스타일 조정, 의존성 patch | `fix:`, `chore:`, `refactor:`, `style:` |
|
|
177
|
+
| **minor** (`0.x.0`) | 하위 호환 기능 추가 (새 prop, 새 extension, 새 toolbar feature) | `feat:` |
|
|
178
|
+
| **major** (`x.0.0`) | 깨는 변경 (prop 제거·rename, peer dep major bump, export 경로 변경) | `feat!:` / `BREAKING CHANGE:` |
|
|
179
|
+
|
|
180
|
+
> `0.x`대에서는 관행상 **minor도 breaking일 수 있음**. 호스트 앱은 `^0.5.3` 대신 `~0.5.3` 사용을 권장.
|
|
181
|
+
|
|
182
|
+
### 릴리스 플로우
|
|
183
|
+
|
|
184
|
+
1. **patch만 필요한 변경**: 소스 수정 후 `main`에 push → CI가 `package.json`의 버전이 이미 publish된 최신과 같으면 **자동 patch bump** 후 publish
|
|
185
|
+
2. **minor / major 변경**: push 전에 수동 bump
|
|
186
|
+
```bash
|
|
187
|
+
npm version minor --no-git-tag-version # 또는 major
|
|
188
|
+
git add package.json package-lock.json
|
|
189
|
+
git commit -m "chore: v$(node -p 'require(\"./package.json\").version')"
|
|
190
|
+
git push
|
|
191
|
+
```
|
|
192
|
+
CI가 "이미 bump됨"을 감지하고 그 버전으로 publish.
|
|
193
|
+
|
|
194
|
+
### 자동 전파
|
|
195
|
+
|
|
196
|
+
publish 성공 시 `repository_dispatch` (`rich-editor-published`) 이벤트가 다음 repo로 전송되고, 각 repo의 `update-*` 워크플로우가 `package.json`을 새 버전으로 업데이트 + 커밋:
|
|
197
|
+
- `teriusu-ko/hancomac` → `@teriusu/rich-editor`
|
|
198
|
+
- `HancomAC/arcturus-next` → `apps/jungol`, `apps/codepass`의 `@teriusu/rich-editor`
|
|
199
|
+
|
|
200
|
+
`teriusu-ko/teriusu`는 현재 수동 업데이트 (hook 미설정 — `GH_PAT` secret 추가 시 연결 가능).
|
|
201
|
+
|
|
169
202
|
## 기술 스택
|
|
170
203
|
|
|
171
204
|
| 구분 | 기술 |
|
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
Code,
|
|
23
23
|
} from "lucide-svelte";
|
|
24
24
|
import { cn } from "../utils/cn";
|
|
25
|
-
import type { ToolbarFeature } from "../types";
|
|
25
|
+
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
26
26
|
|
|
27
|
-
let {
|
|
27
|
+
let {
|
|
28
|
+
editor,
|
|
29
|
+
features,
|
|
30
|
+
onPromptLink,
|
|
31
|
+
}: {
|
|
32
|
+
editor: Editor;
|
|
33
|
+
features: Set<ToolbarFeature>;
|
|
34
|
+
onPromptLink?: PromptHandler;
|
|
35
|
+
} = $props();
|
|
28
36
|
|
|
29
37
|
const has = (f: ToolbarFeature) => features.has(f);
|
|
30
38
|
|
|
@@ -39,9 +47,11 @@
|
|
|
39
47
|
return "본문";
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
function addLink() {
|
|
50
|
+
async function addLink() {
|
|
43
51
|
const previousUrl = editor.getAttributes("link").href || "";
|
|
44
|
-
const url =
|
|
52
|
+
const url = onPromptLink
|
|
53
|
+
? await onPromptLink(previousUrl)
|
|
54
|
+
: window.prompt("링크 URL을 입력하세요", previousUrl);
|
|
45
55
|
if (url === null) return;
|
|
46
56
|
if (url === "") {
|
|
47
57
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
@@ -83,14 +93,14 @@
|
|
|
83
93
|
</script>
|
|
84
94
|
|
|
85
95
|
<div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
|
|
86
|
-
<div class="flex items-center gap-0.5 px-1.5 py-1 bg-foreground rounded-
|
|
96
|
+
<div class="flex items-center gap-0.5 px-1.5 py-1 bg-foreground rounded-full shadow-xl">
|
|
87
97
|
{#if has('h1') || has('h2') || has('h3')}
|
|
88
98
|
<!-- Block type selector -->
|
|
89
99
|
<div class="relative">
|
|
90
100
|
<button
|
|
91
101
|
type="button"
|
|
92
102
|
onclick={() => (showHeadings = !showHeadings)}
|
|
93
|
-
class="flex items-center gap-1 px-2 py-1.5 rounded-
|
|
103
|
+
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"
|
|
94
104
|
>
|
|
95
105
|
<Type size={12} />
|
|
96
106
|
{getCurrentBlockLabel()}
|
|
@@ -154,7 +164,7 @@
|
|
|
154
164
|
title="굵게"
|
|
155
165
|
aria-label="굵게"
|
|
156
166
|
class={cn(
|
|
157
|
-
"p-1.5 rounded-
|
|
167
|
+
"p-1.5 rounded-full transition-colors",
|
|
158
168
|
editor.isActive("bold")
|
|
159
169
|
? "bg-white/20 text-white"
|
|
160
170
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -170,7 +180,7 @@
|
|
|
170
180
|
title="기울임"
|
|
171
181
|
aria-label="기울임"
|
|
172
182
|
class={cn(
|
|
173
|
-
"p-1.5 rounded-
|
|
183
|
+
"p-1.5 rounded-full transition-colors",
|
|
174
184
|
editor.isActive("italic")
|
|
175
185
|
? "bg-white/20 text-white"
|
|
176
186
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -186,7 +196,7 @@
|
|
|
186
196
|
title="밑줄"
|
|
187
197
|
aria-label="밑줄"
|
|
188
198
|
class={cn(
|
|
189
|
-
"p-1.5 rounded-
|
|
199
|
+
"p-1.5 rounded-full transition-colors",
|
|
190
200
|
editor.isActive("underline")
|
|
191
201
|
? "bg-white/20 text-white"
|
|
192
202
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -202,7 +212,7 @@
|
|
|
202
212
|
title="취소선"
|
|
203
213
|
aria-label="취소선"
|
|
204
214
|
class={cn(
|
|
205
|
-
"p-1.5 rounded-
|
|
215
|
+
"p-1.5 rounded-full transition-colors",
|
|
206
216
|
editor.isActive("strike")
|
|
207
217
|
? "bg-white/20 text-white"
|
|
208
218
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -218,7 +228,7 @@
|
|
|
218
228
|
title="하이라이트"
|
|
219
229
|
aria-label="하이라이트"
|
|
220
230
|
class={cn(
|
|
221
|
-
"p-1.5 rounded-
|
|
231
|
+
"p-1.5 rounded-full transition-colors",
|
|
222
232
|
editor.isActive("highlight")
|
|
223
233
|
? "bg-white/20 text-white"
|
|
224
234
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -234,7 +244,7 @@
|
|
|
234
244
|
title="코드"
|
|
235
245
|
aria-label="코드"
|
|
236
246
|
class={cn(
|
|
237
|
-
"p-1.5 rounded-
|
|
247
|
+
"p-1.5 rounded-full transition-colors",
|
|
238
248
|
editor.isActive("code")
|
|
239
249
|
? "bg-white/20 text-white"
|
|
240
250
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -253,7 +263,7 @@
|
|
|
253
263
|
title="링크"
|
|
254
264
|
aria-label="링크"
|
|
255
265
|
class={cn(
|
|
256
|
-
"p-1.5 rounded-
|
|
266
|
+
"p-1.5 rounded-full transition-colors",
|
|
257
267
|
editor.isActive("link")
|
|
258
268
|
? "bg-white/20 text-white"
|
|
259
269
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -274,7 +284,7 @@
|
|
|
274
284
|
title="왼쪽 정렬"
|
|
275
285
|
aria-label="왼쪽 정렬"
|
|
276
286
|
class={cn(
|
|
277
|
-
"p-1.5 rounded-
|
|
287
|
+
"p-1.5 rounded-full transition-colors",
|
|
278
288
|
editor.isActive({ textAlign: "left" })
|
|
279
289
|
? "bg-white/20 text-white"
|
|
280
290
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -290,7 +300,7 @@
|
|
|
290
300
|
title="가운데 정렬"
|
|
291
301
|
aria-label="가운데 정렬"
|
|
292
302
|
class={cn(
|
|
293
|
-
"p-1.5 rounded-
|
|
303
|
+
"p-1.5 rounded-full transition-colors",
|
|
294
304
|
editor.isActive({ textAlign: "center" })
|
|
295
305
|
? "bg-white/20 text-white"
|
|
296
306
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -306,7 +316,7 @@
|
|
|
306
316
|
title="오른쪽 정렬"
|
|
307
317
|
aria-label="오른쪽 정렬"
|
|
308
318
|
class={cn(
|
|
309
|
-
"p-1.5 rounded-
|
|
319
|
+
"p-1.5 rounded-full transition-colors",
|
|
310
320
|
editor.isActive({ textAlign: "right" })
|
|
311
321
|
? "bg-white/20 text-white"
|
|
312
322
|
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
|
-
import type { ToolbarFeature } from "../types";
|
|
2
|
+
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
editor: Editor;
|
|
5
5
|
features: Set<ToolbarFeature>;
|
|
6
|
+
onPromptLink?: PromptHandler;
|
|
6
7
|
};
|
|
7
8
|
declare const BubbleToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
9
|
type BubbleToolbar = ReturnType<typeof BubbleToolbar>;
|
|
@@ -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;AAqB3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,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;AAqB3C,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;AAgPJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
} from "lucide-svelte";
|
|
44
44
|
import { cn } from "../utils/cn";
|
|
45
45
|
import InputModal from "./InputModal.svelte";
|
|
46
|
-
import type { ToolbarFeature } from "../types";
|
|
46
|
+
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
47
47
|
|
|
48
48
|
let {
|
|
49
49
|
editor,
|
|
@@ -51,12 +51,16 @@
|
|
|
51
51
|
onPdfClick,
|
|
52
52
|
onFileClick,
|
|
53
53
|
onVideoClick,
|
|
54
|
+
onPromptLink,
|
|
55
|
+
onPromptImage,
|
|
54
56
|
}: {
|
|
55
57
|
editor: Editor;
|
|
56
58
|
features: Set<ToolbarFeature>;
|
|
57
59
|
onPdfClick: () => void;
|
|
58
60
|
onFileClick?: () => void;
|
|
59
61
|
onVideoClick?: () => void;
|
|
62
|
+
onPromptLink?: PromptHandler;
|
|
63
|
+
onPromptImage?: PromptHandler;
|
|
60
64
|
} = $props();
|
|
61
65
|
|
|
62
66
|
const has = (f: ToolbarFeature) => features.has(f);
|
|
@@ -91,11 +95,35 @@
|
|
|
91
95
|
};
|
|
92
96
|
});
|
|
93
97
|
|
|
94
|
-
function addLink() {
|
|
98
|
+
async function addLink() {
|
|
99
|
+
if (onPromptLink) {
|
|
100
|
+
const previous = editor.isActive("link")
|
|
101
|
+
? (editor.getAttributes("link").href as string) || ""
|
|
102
|
+
: "";
|
|
103
|
+
const url = await onPromptLink(previous);
|
|
104
|
+
if (url === null) return;
|
|
105
|
+
if (url === "") {
|
|
106
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
107
|
+
} else {
|
|
108
|
+
editor
|
|
109
|
+
.chain()
|
|
110
|
+
.focus()
|
|
111
|
+
.extendMarkRange("link")
|
|
112
|
+
.setLink({ href: url })
|
|
113
|
+
.run();
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
95
117
|
modalState = { type: "link" };
|
|
96
118
|
}
|
|
97
119
|
|
|
98
|
-
function addImage() {
|
|
120
|
+
async function addImage() {
|
|
121
|
+
if (onPromptImage) {
|
|
122
|
+
const url = await onPromptImage("");
|
|
123
|
+
if (!url) return;
|
|
124
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
99
127
|
modalState = { type: "image" };
|
|
100
128
|
}
|
|
101
129
|
|
|
@@ -120,10 +148,11 @@
|
|
|
120
148
|
</script>
|
|
121
149
|
|
|
122
150
|
<div
|
|
123
|
-
class="sticky z-30 flex flex-wrap items-center gap-
|
|
151
|
+
class="hce-toolbar sticky z-30 flex flex-wrap items-center gap-1.5 px-3 py-2 border-b border-border bg-background rounded-t-xl"
|
|
124
152
|
style="top: var(--header-height, 74px)"
|
|
125
153
|
>
|
|
126
154
|
<!-- Format -->
|
|
155
|
+
<div class="hce-toolbar-group">
|
|
127
156
|
{#if has('bold')}
|
|
128
157
|
<button
|
|
129
158
|
type="button"
|
|
@@ -237,11 +266,11 @@
|
|
|
237
266
|
</button>
|
|
238
267
|
{/if}
|
|
239
268
|
|
|
240
|
-
|
|
241
|
-
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
242
|
-
{/if}
|
|
269
|
+
</div>
|
|
243
270
|
|
|
271
|
+
{#if has('align-left') || has('align-center') || has('align-right')}
|
|
244
272
|
<!-- Alignment -->
|
|
273
|
+
<div class="hce-toolbar-group">
|
|
245
274
|
{#if has('align-left')}
|
|
246
275
|
<button
|
|
247
276
|
type="button"
|
|
@@ -291,11 +320,12 @@
|
|
|
291
320
|
</button>
|
|
292
321
|
{/if}
|
|
293
322
|
|
|
294
|
-
|
|
295
|
-
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
323
|
+
</div>
|
|
296
324
|
{/if}
|
|
297
325
|
|
|
326
|
+
{#if has('h1') || has('h2') || has('h3')}
|
|
298
327
|
<!-- Headings -->
|
|
328
|
+
<div class="hce-toolbar-group">
|
|
299
329
|
{#if has('h1')}
|
|
300
330
|
<button
|
|
301
331
|
type="button"
|
|
@@ -345,11 +375,12 @@
|
|
|
345
375
|
</button>
|
|
346
376
|
{/if}
|
|
347
377
|
|
|
348
|
-
|
|
349
|
-
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
378
|
+
</div>
|
|
350
379
|
{/if}
|
|
351
380
|
|
|
381
|
+
{#if has('bullet-list') || has('ordered-list') || has('checklist') || has('blockquote') || has('horizontal-rule') || has('toggle')}
|
|
352
382
|
<!-- Lists & Blocks -->
|
|
383
|
+
<div class="hce-toolbar-group">
|
|
353
384
|
{#if has('bullet-list')}
|
|
354
385
|
<button
|
|
355
386
|
type="button"
|
|
@@ -442,11 +473,12 @@
|
|
|
442
473
|
</button>
|
|
443
474
|
{/if}
|
|
444
475
|
|
|
445
|
-
|
|
446
|
-
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
476
|
+
</div>
|
|
447
477
|
{/if}
|
|
448
478
|
|
|
449
|
-
|
|
479
|
+
{#if has('link') || has('image') || has('pdf') || has('youtube') || has('video') || has('file') || has('columns-2') || has('columns-3') || has('table') || has('code-block')}
|
|
480
|
+
<!-- Media & Layout -->
|
|
481
|
+
<div class="hce-toolbar-group">
|
|
450
482
|
{#if has('link')}
|
|
451
483
|
<button
|
|
452
484
|
type="button"
|
|
@@ -607,7 +639,7 @@
|
|
|
607
639
|
</button>
|
|
608
640
|
{:else}
|
|
609
641
|
<p
|
|
610
|
-
class="px-3 py-1 text-
|
|
642
|
+
class="px-3 py-1 text-xs font-semibold text-muted-foreground"
|
|
611
643
|
>
|
|
612
644
|
구조
|
|
613
645
|
</p>
|
|
@@ -671,7 +703,7 @@
|
|
|
671
703
|
</button>
|
|
672
704
|
<div class="h-px bg-border my-1"></div>
|
|
673
705
|
<p
|
|
674
|
-
class="px-3 py-1 text-
|
|
706
|
+
class="px-3 py-1 text-xs font-semibold text-muted-foreground"
|
|
675
707
|
>
|
|
676
708
|
헤더 / 셀
|
|
677
709
|
</p>
|
|
@@ -745,7 +777,7 @@
|
|
|
745
777
|
</button>
|
|
746
778
|
<div class="h-px bg-border my-1"></div>
|
|
747
779
|
<p
|
|
748
|
-
class="px-3 py-1 text-
|
|
780
|
+
class="px-3 py-1 text-xs font-semibold text-muted-foreground"
|
|
749
781
|
>
|
|
750
782
|
삭제
|
|
751
783
|
</p>
|
|
@@ -797,70 +829,31 @@
|
|
|
797
829
|
</div>
|
|
798
830
|
{/if}
|
|
799
831
|
|
|
800
|
-
<!-- Code block
|
|
832
|
+
<!-- Code block -->
|
|
801
833
|
{#if has('code-block')}
|
|
802
|
-
<
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
<ChevronDown size={12} />
|
|
817
|
-
</button>
|
|
818
|
-
{#if codeMenuOpen}
|
|
819
|
-
<div
|
|
820
|
-
class="absolute top-full left-0 mt-1 bg-popover border border-border rounded-lg shadow-lg z-50 py-1"
|
|
821
|
-
style="min-width: 130px"
|
|
822
|
-
>
|
|
823
|
-
<button
|
|
824
|
-
type="button"
|
|
825
|
-
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
826
|
-
onclick={() => {
|
|
827
|
-
editor.chain().focus().setCodeBlock({ language: "cpp" }).run();
|
|
828
|
-
codeMenuOpen = false;
|
|
829
|
-
}}
|
|
830
|
-
>
|
|
831
|
-
C++
|
|
832
|
-
</button>
|
|
833
|
-
<button
|
|
834
|
-
type="button"
|
|
835
|
-
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
836
|
-
onclick={() => {
|
|
837
|
-
editor.chain().focus().setCodeBlock({ language: "python" }).run();
|
|
838
|
-
codeMenuOpen = false;
|
|
839
|
-
}}
|
|
840
|
-
>
|
|
841
|
-
Python
|
|
842
|
-
</button>
|
|
843
|
-
<div class="h-px bg-border my-1"></div>
|
|
844
|
-
<button
|
|
845
|
-
type="button"
|
|
846
|
-
class="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
847
|
-
onclick={() => {
|
|
848
|
-
editor.chain().focus().setCodeBlock({ language: "" }).run();
|
|
849
|
-
codeMenuOpen = false;
|
|
850
|
-
}}
|
|
851
|
-
>
|
|
852
|
-
일반 코드
|
|
853
|
-
</button>
|
|
854
|
-
</div>
|
|
855
|
-
{/if}
|
|
856
|
-
</div>
|
|
834
|
+
<button
|
|
835
|
+
type="button"
|
|
836
|
+
onclick={() => editor.chain().focus().setCodeBlock({ language: 'cpp' }).run()}
|
|
837
|
+
data-tooltip="코드 블록"
|
|
838
|
+
aria-label="코드 블록"
|
|
839
|
+
class={cn(
|
|
840
|
+
"p-1.5 rounded-md transition-colors",
|
|
841
|
+
editor.isActive("codeBlock")
|
|
842
|
+
? "bg-primary/10 text-primary"
|
|
843
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
844
|
+
)}
|
|
845
|
+
>
|
|
846
|
+
<Code2 size={iconSize} />
|
|
847
|
+
</button>
|
|
857
848
|
{/if}
|
|
858
849
|
|
|
859
|
-
|
|
860
|
-
<div class="w-px h-6 bg-border mx-0.5"></div>
|
|
850
|
+
</div>
|
|
861
851
|
{/if}
|
|
862
852
|
|
|
853
|
+
{#if has('undo') || has('redo')}
|
|
863
854
|
<!-- Undo / Redo -->
|
|
855
|
+
<div class="hce-toolbar-group">
|
|
856
|
+
|
|
864
857
|
{#if has('undo')}
|
|
865
858
|
<button
|
|
866
859
|
type="button"
|
|
@@ -892,10 +885,13 @@
|
|
|
892
885
|
</button>
|
|
893
886
|
{/if}
|
|
894
887
|
|
|
888
|
+
</div>
|
|
889
|
+
{/if}
|
|
890
|
+
|
|
895
891
|
<!-- Modals -->
|
|
896
892
|
{#if modalState?.type === "link"}
|
|
897
893
|
<InputModal
|
|
898
|
-
|
|
894
|
+
title="링크 URL 입력"
|
|
899
895
|
placeholder="https://example.com"
|
|
900
896
|
defaultValue={editor.isActive("link") ? editor.getAttributes("link").href || "" : ""}
|
|
901
897
|
onConfirm={(url) => {
|
|
@@ -912,7 +908,7 @@
|
|
|
912
908
|
{/if}
|
|
913
909
|
{#if modalState?.type === "image"}
|
|
914
910
|
<InputModal
|
|
915
|
-
|
|
911
|
+
title="이미지 URL 입력"
|
|
916
912
|
placeholder="https://example.com/image.png"
|
|
917
913
|
onConfirm={(url) => {
|
|
918
914
|
editor.chain().focus().setImage({ src: url }).run();
|
|
@@ -963,3 +959,30 @@
|
|
|
963
959
|
</div>
|
|
964
960
|
{/if}
|
|
965
961
|
</div>
|
|
962
|
+
|
|
963
|
+
<style>
|
|
964
|
+
/* 툴바 버튼 기본 리셋 (Tailwind preflight 비활성화 상태라 UA 기본값 제거) */
|
|
965
|
+
.hce-toolbar :global(button) {
|
|
966
|
+
background: transparent;
|
|
967
|
+
border: 0;
|
|
968
|
+
color: inherit;
|
|
969
|
+
font: inherit;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
}
|
|
972
|
+
.hce-toolbar :global(button:disabled) {
|
|
973
|
+
cursor: default;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.hce-toolbar-group {
|
|
977
|
+
display: flex;
|
|
978
|
+
align-items: center;
|
|
979
|
+
gap: 2px;
|
|
980
|
+
padding: 3px;
|
|
981
|
+
border-radius: var(--radius-xl, 8px);
|
|
982
|
+
background: color-mix(in srgb, var(--muted, #f6f7f9) 55%, transparent);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.hce-toolbar-group :global(button) {
|
|
986
|
+
flex-shrink: 0;
|
|
987
|
+
}
|
|
988
|
+
</style>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
|
-
import type { ToolbarFeature } from "../types";
|
|
2
|
+
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
editor: Editor;
|
|
5
5
|
features: Set<ToolbarFeature>;
|
|
6
6
|
onPdfClick: () => void;
|
|
7
7
|
onFileClick?: () => void;
|
|
8
8
|
onVideoClick?: () => void;
|
|
9
|
+
onPromptLink?: PromptHandler;
|
|
10
|
+
onPromptImage?: PromptHandler;
|
|
9
11
|
};
|
|
10
12
|
declare const FixedToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
13
|
type FixedToolbar = ReturnType<typeof FixedToolbar>;
|
|
@@ -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;AA4C3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;
|
|
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;AA4C3C,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,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AAonBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -55,44 +55,47 @@
|
|
|
55
55
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
56
56
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
57
57
|
<div
|
|
58
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-black/
|
|
58
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
|
59
59
|
onclick={handleBackdropClick}
|
|
60
60
|
>
|
|
61
61
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
62
62
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
63
63
|
<div
|
|
64
|
-
class="bg-background border border-border rounded-
|
|
64
|
+
class="bg-background border border-border rounded-xl shadow-xl p-5 w-[360px] max-w-[90vw]"
|
|
65
65
|
onclick={(e) => e.stopPropagation()}
|
|
66
66
|
>
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
{#if title}
|
|
68
|
+
<div class="flex items-center justify-between mb-3">
|
|
69
|
+
<span class="text-sm font-semibold">{title}</span>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="p-1 rounded-md hover:bg-muted text-muted-foreground"
|
|
73
|
+
onclick={onCancel}
|
|
74
|
+
aria-label="닫기"
|
|
75
|
+
>
|
|
76
|
+
<X size={14} />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
77
80
|
<input
|
|
78
81
|
bind:this={inputEl}
|
|
79
82
|
type="text"
|
|
80
|
-
class="w-full border border-border rounded px-3 py-
|
|
83
|
+
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"
|
|
81
84
|
{placeholder}
|
|
82
85
|
bind:value
|
|
83
86
|
onkeydown={handleKeydown}
|
|
84
87
|
/>
|
|
85
|
-
<div class="flex justify-end gap-2 mt-
|
|
88
|
+
<div class="flex justify-end gap-2 mt-4">
|
|
86
89
|
<button
|
|
87
90
|
type="button"
|
|
88
|
-
class="px-3 py-1 text-sm rounded border border-border hover:bg-muted"
|
|
91
|
+
class="px-3.5 py-1.5 text-sm rounded-md border border-border hover:bg-muted transition-colors"
|
|
89
92
|
onclick={onCancel}
|
|
90
93
|
>
|
|
91
94
|
취소
|
|
92
95
|
</button>
|
|
93
96
|
<button
|
|
94
97
|
type="button"
|
|
95
|
-
class="px-3 py-1 text-sm rounded bg-primary text-primary-foreground hover:opacity-90"
|
|
98
|
+
class="px-3.5 py-1.5 text-sm rounded-md bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
|
|
96
99
|
onclick={handleSubmit}
|
|
97
100
|
>
|
|
98
101
|
확인
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/InputModal.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;
|
|
1
|
+
{"version":3,"file":"InputModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/InputModal.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AA6EJ,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|