@teriusu/rich-editor 0.4.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 +178 -0
- package/dist/components/BubbleToolbar.svelte +299 -0
- package/dist/components/BubbleToolbar.svelte.d.ts +9 -0
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -0
- package/dist/components/FixedToolbar.svelte +892 -0
- package/dist/components/FixedToolbar.svelte.d.ts +11 -0
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -0
- package/dist/components/InputModal.svelte +102 -0
- package/dist/components/InputModal.svelte.d.ts +11 -0
- package/dist/components/InputModal.svelte.d.ts.map +1 -0
- package/dist/components/PdfViewer.svelte +152 -0
- package/dist/components/PdfViewer.svelte.d.ts +8 -0
- package/dist/components/PdfViewer.svelte.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.svelte +315 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts +14 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableBubbleMenu.svelte +472 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts +8 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -0
- package/dist/components/TipTapEditor.svelte +649 -0
- package/dist/components/TipTapEditor.svelte.d.ts +17 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -0
- package/dist/extensions/Column.d.ts +3 -0
- package/dist/extensions/Column.d.ts.map +1 -0
- package/dist/extensions/Column.js +20 -0
- package/dist/extensions/Columns.d.ts +11 -0
- package/dist/extensions/Columns.d.ts.map +1 -0
- package/dist/extensions/Columns.js +103 -0
- package/dist/extensions/Columns.test.d.ts +2 -0
- package/dist/extensions/Columns.test.d.ts.map +1 -0
- package/dist/extensions/Columns.test.js +62 -0
- package/dist/extensions/FileAttachment.d.ts +24 -0
- package/dist/extensions/FileAttachment.d.ts.map +1 -0
- package/dist/extensions/FileAttachment.js +190 -0
- package/dist/extensions/FileAttachment.test.d.ts +2 -0
- package/dist/extensions/FileAttachment.test.d.ts.map +1 -0
- package/dist/extensions/FileAttachment.test.js +63 -0
- package/dist/extensions/FixedDetails.d.ts +2 -0
- package/dist/extensions/FixedDetails.d.ts.map +1 -0
- package/dist/extensions/FixedDetails.js +100 -0
- package/dist/extensions/FixedDetails.test.d.ts +2 -0
- package/dist/extensions/FixedDetails.test.d.ts.map +1 -0
- package/dist/extensions/FixedDetails.test.js +40 -0
- package/dist/extensions/Indent.d.ts +11 -0
- package/dist/extensions/Indent.d.ts.map +1 -0
- package/dist/extensions/Indent.js +87 -0
- package/dist/extensions/Indent.test.d.ts +2 -0
- package/dist/extensions/Indent.test.d.ts.map +1 -0
- package/dist/extensions/Indent.test.js +78 -0
- package/dist/extensions/PdfBlock.d.ts +3 -0
- package/dist/extensions/PdfBlock.d.ts.map +1 -0
- package/dist/extensions/PdfBlock.js +301 -0
- package/dist/extensions/PdfBlock.test.d.ts +2 -0
- package/dist/extensions/PdfBlock.test.d.ts.map +1 -0
- package/dist/extensions/PdfBlock.test.js +51 -0
- package/dist/extensions/VideoBlock.d.ts +16 -0
- package/dist/extensions/VideoBlock.d.ts.map +1 -0
- package/dist/extensions/VideoBlock.js +87 -0
- package/dist/extensions/VideoBlock.test.d.ts +2 -0
- package/dist/extensions/VideoBlock.test.d.ts.map +1 -0
- package/dist/extensions/VideoBlock.test.js +54 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/styles/editor.css +698 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/cn.d.ts +3 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/cn.test.d.ts +2 -0
- package/dist/utils/cn.test.d.ts.map +1 -0
- package/dist/utils/cn.test.js +28 -0
- package/dist/utils/escape-html.d.ts +2 -0
- package/dist/utils/escape-html.d.ts.map +1 -0
- package/dist/utils/escape-html.js +7 -0
- package/dist/utils/escape-html.test.d.ts +2 -0
- package/dist/utils/escape-html.test.d.ts.map +1 -0
- package/dist/utils/escape-html.test.js +25 -0
- package/dist/utils/pdf.d.ts +9 -0
- package/dist/utils/pdf.d.ts.map +1 -0
- package/dist/utils/pdf.js +33 -0
- package/dist/utils/sanitize.d.ts +9 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +115 -0
- package/dist/utils/sanitize.test.d.ts +2 -0
- package/dist/utils/sanitize.test.d.ts.map +1 -0
- package/dist/utils/sanitize.test.js +206 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @teriusu/rich-editor
|
|
2
|
+
|
|
3
|
+
TipTap v3 기반 리치 텍스트 에디터. Svelte 5 컴포넌트 라이브러리.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @teriusu/rich-editor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Tailwind CSS v4 사용 시 `@source` 추가:
|
|
12
|
+
```css
|
|
13
|
+
@import "@teriusu/rich-editor/styles";
|
|
14
|
+
@source "../node_modules/@teriusu/rich-editor/dist";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 기본 사용법
|
|
18
|
+
|
|
19
|
+
```svelte
|
|
20
|
+
<script>
|
|
21
|
+
import { TipTapEditor } from '@teriusu/rich-editor';
|
|
22
|
+
import '@teriusu/rich-editor/styles';
|
|
23
|
+
|
|
24
|
+
let html = $state('');
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<TipTapEditor
|
|
28
|
+
content={html}
|
|
29
|
+
onChange={(h) => html = h}
|
|
30
|
+
onUploadFile={async (file) => {
|
|
31
|
+
const form = new FormData();
|
|
32
|
+
form.append('file', file);
|
|
33
|
+
const res = await fetch('/api/upload', { method: 'POST', body: form });
|
|
34
|
+
const { url } = await res.json();
|
|
35
|
+
return url;
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Props
|
|
41
|
+
|
|
42
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
43
|
+
|------|------|--------|------|
|
|
44
|
+
| `content` | `string` | `""` | HTML 콘텐츠 |
|
|
45
|
+
| `onChange` | `(html: string) => void` | 필수 | 콘텐츠 변경 콜백 |
|
|
46
|
+
| `placeholder` | `string` | `"'/'를 눌러..."` | 플레이스홀더 |
|
|
47
|
+
| `onUploadFile` | `(file: File) => Promise<string>` | - | 파일 업로드 핸들러 (URL 반환) |
|
|
48
|
+
| `onResolveFile` | `(fileId: string) => Promise<{src, name?, size?}>` | - | 파일 ID → URL 변환 |
|
|
49
|
+
| `extensions` | `AnyExtension[]` | `[]` | 추가 TipTap 확장 주입 |
|
|
50
|
+
| `editable` | `boolean` | `true` | 읽기 전용 모드 |
|
|
51
|
+
|
|
52
|
+
## 파일 저장 방식
|
|
53
|
+
|
|
54
|
+
### URL 직접 (기본)
|
|
55
|
+
|
|
56
|
+
`onUploadFile`만 제공하면 파일 URL이 HTML에 직접 저장됩니다.
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<div data-file-src="/files/report.pdf" data-file-name="report.pdf" data-file-size="204800"></div>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### ID 기반 (하이브리드)
|
|
63
|
+
|
|
64
|
+
`onResolveFile`을 추가하면 파일 ID + 캐시 URL이 함께 저장됩니다.
|
|
65
|
+
|
|
66
|
+
```svelte
|
|
67
|
+
<TipTapEditor
|
|
68
|
+
content={html}
|
|
69
|
+
onChange={(h) => html = h}
|
|
70
|
+
onUploadFile={async (file) => {
|
|
71
|
+
const res = await fetch('/api/upload', { method: 'POST', body: file });
|
|
72
|
+
return await res.json(); // { id, url, name, size }
|
|
73
|
+
}}
|
|
74
|
+
onResolveFile={async (fileId) => {
|
|
75
|
+
const res = await fetch(`/api/files/${fileId}`);
|
|
76
|
+
return await res.json(); // { src, name, size }
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<div data-file-id="abc123" data-file-src="/files/report.pdf" data-file-name="report.pdf"></div>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
ID 기반은 URL이 변경되어도 대응 가능하고, 권한 제어(signed URL)에 활용할 수 있습니다.
|
|
86
|
+
|
|
87
|
+
## 기능
|
|
88
|
+
|
|
89
|
+
### 텍스트 서식
|
|
90
|
+
Bold, Italic, Underline, Strikethrough, Highlight, Superscript, Subscript, 텍스트 색상
|
|
91
|
+
|
|
92
|
+
### 구조
|
|
93
|
+
제목 (H1~H3), 글머리/번호 목록, 체크리스트, 인용문, 구분선, 접기/펼치기 (Details), 들여쓰기 (Tab/Shift-Tab)
|
|
94
|
+
|
|
95
|
+
### 미디어
|
|
96
|
+
이미지, PDF (PDF.js 캔버스 렌더링), 영상 파일 (mp4/webm), YouTube, 파일 첨부, 드래그 앤 드롭 업로드
|
|
97
|
+
|
|
98
|
+
### 레이아웃
|
|
99
|
+
2단/3단 컬럼, 텍스트 정렬 (좌/중/우)
|
|
100
|
+
|
|
101
|
+
### 테이블
|
|
102
|
+
삽입, 행/열 추가/삭제, 헤더 토글, 셀 병합/분할, 셀 배경색, 열 리사이즈
|
|
103
|
+
|
|
104
|
+
### 입력
|
|
105
|
+
`/` 슬래시 커맨드, 고정 툴바, 테이블 버블 메뉴, Undo/Redo
|
|
106
|
+
|
|
107
|
+
## 레거시 HTML 호환
|
|
108
|
+
|
|
109
|
+
TipTap v2 커스텀 태그를 자동 변환합니다. 에디터 로드 시 `transformLegacyHtml`이 자동 호출됩니다.
|
|
110
|
+
|
|
111
|
+
| 레거시 태그 | 변환 결과 |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `<tiptap-file id="X">` | `<div data-file-id="X">` |
|
|
114
|
+
| `<tiptap-midibus id="X">` | iframe (play.mbus.tv) |
|
|
115
|
+
| `<tiptap-collapsable title="X">` | `<details><summary>` |
|
|
116
|
+
| `<lite-youtube videoid="X">` | YouTube iframe |
|
|
117
|
+
| `<embed type="application/pdf">` | `<div data-pdf-src>` |
|
|
118
|
+
| `<div class="tiptap-columns">` | `<div data-type="columns">` |
|
|
119
|
+
| `margin-left: 40px` | indent level 1 (em 출력) |
|
|
120
|
+
|
|
121
|
+
게시물 조회 페이지(읽기 전용)에서도 적용:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { transformLegacyHtml } from '@teriusu/rich-editor';
|
|
125
|
+
|
|
126
|
+
const html = transformLegacyHtml(post.content);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 커스텀 확장 주입
|
|
130
|
+
|
|
131
|
+
`extensions` prop으로 호스트 앱 전용 확장을 주입할 수 있습니다.
|
|
132
|
+
|
|
133
|
+
```svelte
|
|
134
|
+
<script>
|
|
135
|
+
import { TipTapEditor } from '@teriusu/rich-editor';
|
|
136
|
+
import { Node } from '@tiptap/core';
|
|
137
|
+
|
|
138
|
+
const MyExtension = Node.create({ /* ... */ });
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<TipTapEditor
|
|
142
|
+
content={html}
|
|
143
|
+
onChange={handleChange}
|
|
144
|
+
extensions={[MyExtension]}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Exports
|
|
149
|
+
|
|
150
|
+
### 컴포넌트
|
|
151
|
+
`TipTapEditor`, `FixedToolbar`, `BubbleToolbar`, `SlashCommandMenu`, `TableBubbleMenu`, `InputModal`, `PdfViewer`
|
|
152
|
+
|
|
153
|
+
### 확장
|
|
154
|
+
`PdfBlock`, `FileAttachment`, `VideoBlock`, `Columns`, `Column`, `Indent`, `FixedDetails`
|
|
155
|
+
|
|
156
|
+
### 유틸리티
|
|
157
|
+
`transformLegacyHtml`, `sanitizeHtml`, `stripHtmlToExcerpt`, `configurePdfJs`, `getPdfJs`, `cn`
|
|
158
|
+
|
|
159
|
+
### 타입
|
|
160
|
+
`UploadHandler`, `FileResolver`, `FileResolveResult`, `TipTapEditorProps`, `SlashMenuItem`
|
|
161
|
+
|
|
162
|
+
## 빌드
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm run build # svelte-package → dist/
|
|
166
|
+
npm version patch # 버전 올리기
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## 기술 스택
|
|
170
|
+
|
|
171
|
+
| 구분 | 기술 |
|
|
172
|
+
|------|------|
|
|
173
|
+
| 에디터 엔진 | TipTap v3 |
|
|
174
|
+
| UI | Svelte 5 (runes) |
|
|
175
|
+
| 언어 | TypeScript |
|
|
176
|
+
| 빌드 | svelte-package |
|
|
177
|
+
| 아이콘 | lucide-svelte |
|
|
178
|
+
| 스타일 | Tailwind CSS + CSS 변수 |
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import { BubbleMenuPlugin } from "@tiptap/extension-bubble-menu";
|
|
4
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
5
|
+
import { onMount } from "svelte";
|
|
6
|
+
|
|
7
|
+
const bubbleToolbarKey = new PluginKey("bubbleToolbar");
|
|
8
|
+
import {
|
|
9
|
+
Bold,
|
|
10
|
+
Italic,
|
|
11
|
+
Underline as UnderlineIcon,
|
|
12
|
+
Strikethrough,
|
|
13
|
+
Highlighter,
|
|
14
|
+
LinkIcon,
|
|
15
|
+
AlignLeft,
|
|
16
|
+
AlignCenter,
|
|
17
|
+
AlignRight,
|
|
18
|
+
Heading1,
|
|
19
|
+
Heading2,
|
|
20
|
+
Heading3,
|
|
21
|
+
Type,
|
|
22
|
+
Code,
|
|
23
|
+
} from "lucide-svelte";
|
|
24
|
+
import { cn } from "../utils/cn";
|
|
25
|
+
|
|
26
|
+
let { editor, minimal = false }: { editor: Editor; minimal?: boolean } = $props();
|
|
27
|
+
|
|
28
|
+
let showHeadings = $state(false);
|
|
29
|
+
let menuEl: HTMLDivElement | undefined = $state();
|
|
30
|
+
const iconSize = 14;
|
|
31
|
+
|
|
32
|
+
function getCurrentBlockLabel(): string {
|
|
33
|
+
if (editor.isActive("heading", { level: 1 })) return "제목 1";
|
|
34
|
+
if (editor.isActive("heading", { level: 2 })) return "제목 2";
|
|
35
|
+
if (editor.isActive("heading", { level: 3 })) return "제목 3";
|
|
36
|
+
return "본문";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addLink() {
|
|
40
|
+
const previousUrl = editor.getAttributes("link").href || "";
|
|
41
|
+
const url = window.prompt("링크 URL을 입력하세요", previousUrl);
|
|
42
|
+
if (url === null) return;
|
|
43
|
+
if (url === "") {
|
|
44
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
45
|
+
} else {
|
|
46
|
+
editor
|
|
47
|
+
.chain()
|
|
48
|
+
.focus()
|
|
49
|
+
.extendMarkRange("link")
|
|
50
|
+
.setLink({ href: url })
|
|
51
|
+
.run();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMount(() => {
|
|
56
|
+
if (!menuEl) return;
|
|
57
|
+
|
|
58
|
+
const plugin = BubbleMenuPlugin({
|
|
59
|
+
pluginKey: bubbleToolbarKey,
|
|
60
|
+
editor,
|
|
61
|
+
element: menuEl,
|
|
62
|
+
shouldShow: ({ editor: e, state }) => {
|
|
63
|
+
const { from, to } = state.selection;
|
|
64
|
+
if (from === to) return false;
|
|
65
|
+
if (e.isActive("codeBlock")) return false;
|
|
66
|
+
if (e.isActive("image")) return false;
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
tippyOptions: {
|
|
70
|
+
placement: "top",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
editor.registerPlugin(plugin);
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
editor.unregisterPlugin(bubbleToolbarKey);
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
|
|
83
|
+
<div class="flex items-center gap-0.5 px-1.5 py-1 bg-foreground rounded-lg shadow-xl">
|
|
84
|
+
{#if !minimal}
|
|
85
|
+
<!-- Block type selector -->
|
|
86
|
+
<div class="relative">
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onclick={() => (showHeadings = !showHeadings)}
|
|
90
|
+
class="flex items-center gap-1 px-2 py-1.5 rounded-md text-xs font-medium text-white/80 hover:text-white hover:bg-white/10 transition-colors"
|
|
91
|
+
>
|
|
92
|
+
<Type size={12} />
|
|
93
|
+
{getCurrentBlockLabel()}
|
|
94
|
+
</button>
|
|
95
|
+
{#if showHeadings}
|
|
96
|
+
<div
|
|
97
|
+
class="absolute bottom-full left-0 mb-1 bg-foreground rounded-lg shadow-xl border border-white/10 py-1"
|
|
98
|
+
style="min-width: 120px"
|
|
99
|
+
>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
class={cn(
|
|
103
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
104
|
+
!editor.isActive("heading")
|
|
105
|
+
? "text-white bg-white/10"
|
|
106
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
107
|
+
)}
|
|
108
|
+
onclick={() => {
|
|
109
|
+
editor.chain().focus().setParagraph().run();
|
|
110
|
+
showHeadings = false;
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<Type size={12} /> 본문
|
|
114
|
+
</button>
|
|
115
|
+
{#each [1, 2, 3] as level}
|
|
116
|
+
{@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
class={cn(
|
|
120
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
121
|
+
editor.isActive("heading", { level })
|
|
122
|
+
? "text-white bg-white/10"
|
|
123
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
124
|
+
)}
|
|
125
|
+
onclick={() => {
|
|
126
|
+
editor
|
|
127
|
+
.chain()
|
|
128
|
+
.focus()
|
|
129
|
+
.toggleHeading({ level: level as 1 | 2 | 3 })
|
|
130
|
+
.run();
|
|
131
|
+
showHeadings = false;
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<Icon size={12} /> 제목 {level}
|
|
135
|
+
</button>
|
|
136
|
+
{/each}
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
142
|
+
{/if}
|
|
143
|
+
|
|
144
|
+
<!-- Format buttons -->
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
148
|
+
title="굵게"
|
|
149
|
+
aria-label="굵게"
|
|
150
|
+
class={cn(
|
|
151
|
+
"p-1.5 rounded-md transition-colors",
|
|
152
|
+
editor.isActive("bold")
|
|
153
|
+
? "bg-white/20 text-white"
|
|
154
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
<Bold size={iconSize} />
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
162
|
+
title="기울임"
|
|
163
|
+
aria-label="기울임"
|
|
164
|
+
class={cn(
|
|
165
|
+
"p-1.5 rounded-md transition-colors",
|
|
166
|
+
editor.isActive("italic")
|
|
167
|
+
? "bg-white/20 text-white"
|
|
168
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
<Italic size={iconSize} />
|
|
172
|
+
</button>
|
|
173
|
+
{#if !minimal}
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
177
|
+
title="밑줄"
|
|
178
|
+
aria-label="밑줄"
|
|
179
|
+
class={cn(
|
|
180
|
+
"p-1.5 rounded-md transition-colors",
|
|
181
|
+
editor.isActive("underline")
|
|
182
|
+
? "bg-white/20 text-white"
|
|
183
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<UnderlineIcon size={iconSize} />
|
|
187
|
+
</button>
|
|
188
|
+
{/if}
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
192
|
+
title="취소선"
|
|
193
|
+
aria-label="취소선"
|
|
194
|
+
class={cn(
|
|
195
|
+
"p-1.5 rounded-md transition-colors",
|
|
196
|
+
editor.isActive("strike")
|
|
197
|
+
? "bg-white/20 text-white"
|
|
198
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
<Strikethrough size={iconSize} />
|
|
202
|
+
</button>
|
|
203
|
+
{#if !minimal}
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
207
|
+
title="하이라이트"
|
|
208
|
+
aria-label="하이라이트"
|
|
209
|
+
class={cn(
|
|
210
|
+
"p-1.5 rounded-md transition-colors",
|
|
211
|
+
editor.isActive("highlight")
|
|
212
|
+
? "bg-white/20 text-white"
|
|
213
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
214
|
+
)}
|
|
215
|
+
>
|
|
216
|
+
<Highlighter size={iconSize} />
|
|
217
|
+
</button>
|
|
218
|
+
{/if}
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onclick={() => editor.chain().focus().toggleCode().run()}
|
|
222
|
+
title="코드"
|
|
223
|
+
aria-label="코드"
|
|
224
|
+
class={cn(
|
|
225
|
+
"p-1.5 rounded-md transition-colors",
|
|
226
|
+
editor.isActive("code")
|
|
227
|
+
? "bg-white/20 text-white"
|
|
228
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
229
|
+
)}
|
|
230
|
+
>
|
|
231
|
+
<Code size={iconSize} />
|
|
232
|
+
</button>
|
|
233
|
+
|
|
234
|
+
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
235
|
+
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
onclick={addLink}
|
|
239
|
+
title="링크"
|
|
240
|
+
aria-label="링크"
|
|
241
|
+
class={cn(
|
|
242
|
+
"p-1.5 rounded-md transition-colors",
|
|
243
|
+
editor.isActive("link")
|
|
244
|
+
? "bg-white/20 text-white"
|
|
245
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
246
|
+
)}
|
|
247
|
+
>
|
|
248
|
+
<LinkIcon size={iconSize} />
|
|
249
|
+
</button>
|
|
250
|
+
|
|
251
|
+
{#if !minimal}
|
|
252
|
+
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
253
|
+
|
|
254
|
+
<!-- Alignment -->
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
onclick={() => editor.chain().focus().setTextAlign("left").run()}
|
|
258
|
+
title="왼쪽 정렬"
|
|
259
|
+
aria-label="왼쪽 정렬"
|
|
260
|
+
class={cn(
|
|
261
|
+
"p-1.5 rounded-md transition-colors",
|
|
262
|
+
editor.isActive({ textAlign: "left" })
|
|
263
|
+
? "bg-white/20 text-white"
|
|
264
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
<AlignLeft size={iconSize} />
|
|
268
|
+
</button>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onclick={() => editor.chain().focus().setTextAlign("center").run()}
|
|
272
|
+
title="가운데 정렬"
|
|
273
|
+
aria-label="가운데 정렬"
|
|
274
|
+
class={cn(
|
|
275
|
+
"p-1.5 rounded-md transition-colors",
|
|
276
|
+
editor.isActive({ textAlign: "center" })
|
|
277
|
+
? "bg-white/20 text-white"
|
|
278
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
279
|
+
)}
|
|
280
|
+
>
|
|
281
|
+
<AlignCenter size={iconSize} />
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onclick={() => editor.chain().focus().setTextAlign("right").run()}
|
|
286
|
+
title="오른쪽 정렬"
|
|
287
|
+
aria-label="오른쪽 정렬"
|
|
288
|
+
class={cn(
|
|
289
|
+
"p-1.5 rounded-md transition-colors",
|
|
290
|
+
editor.isActive({ textAlign: "right" })
|
|
291
|
+
? "bg-white/20 text-white"
|
|
292
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
<AlignRight size={iconSize} />
|
|
296
|
+
</button>
|
|
297
|
+
{/if}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
minimal?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const BubbleToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type BubbleToolbar = ReturnType<typeof BubbleToolbar>;
|
|
8
|
+
export default BubbleToolbar;
|
|
9
|
+
//# sourceMappingURL=BubbleToolbar.svelte.d.ts.map
|
|
@@ -0,0 +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;AAsB1C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAqNhE,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|