@teriusu/rich-editor 0.10.8 → 0.11.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/dist/components/BubbleToolbar.svelte +182 -54
- package/dist/components/BubbleToolbar.svelte.d.ts +3 -0
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/EmojiSuggestionMenu.svelte +291 -0
- package/dist/components/EmojiSuggestionMenu.svelte.d.ts +9 -0
- package/dist/components/EmojiSuggestionMenu.svelte.d.ts.map +1 -0
- package/dist/components/FixedToolbar.svelte +53 -47
- package/dist/components/FixedToolbar.svelte.d.ts +3 -0
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FloatingHelper.svelte +219 -0
- package/dist/components/FloatingHelper.svelte.d.ts +14 -0
- package/dist/components/FloatingHelper.svelte.d.ts.map +1 -0
- package/dist/components/InputModal.svelte +7 -3
- package/dist/components/InputModal.svelte.d.ts +3 -0
- package/dist/components/InputModal.svelte.d.ts.map +1 -1
- package/dist/components/MathModal.svelte +33 -29
- package/dist/components/MathModal.svelte.d.ts +3 -0
- package/dist/components/MathModal.svelte.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.svelte +7 -3
- package/dist/components/MediaPickerModal.svelte.d.ts +3 -0
- package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -1
- package/dist/components/SlashCommandMenu.svelte +88 -72
- package/dist/components/SlashCommandMenu.svelte.d.ts +3 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TableBubbleMenu.svelte +41 -33
- package/dist/components/TableBubbleMenu.svelte.d.ts +3 -0
- package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +239 -127
- package/dist/components/TipTapEditor.svelte.d.ts +7 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/CardBlock.d.ts.map +1 -1
- package/dist/extensions/CardBlock.js +6 -4
- package/dist/extensions/FileAttachment.d.ts.map +1 -1
- package/dist/extensions/FileAttachment.js +2 -1
- package/dist/extensions/FixedDetails.d.ts.map +1 -1
- package/dist/extensions/FixedDetails.js +5 -3
- package/dist/extensions/LegacyBlock.d.ts.map +1 -1
- package/dist/extensions/LegacyBlock.js +8 -6
- package/dist/extensions/Math.d.ts.map +1 -1
- package/dist/extensions/Math.js +5 -2
- package/dist/extensions/MbusVideo.d.ts.map +1 -1
- package/dist/extensions/MbusVideo.js +73 -9
- package/dist/extensions/MediaToolbar.d.ts +41 -0
- package/dist/extensions/MediaToolbar.d.ts.map +1 -0
- package/dist/extensions/MediaToolbar.js +196 -0
- package/dist/extensions/MediaToolbar.test.d.ts +2 -0
- package/dist/extensions/MediaToolbar.test.d.ts.map +1 -0
- package/dist/extensions/MediaToolbar.test.js +231 -0
- package/dist/extensions/OrderedListMarker.d.ts +2 -0
- package/dist/extensions/OrderedListMarker.d.ts.map +1 -0
- package/dist/extensions/OrderedListMarker.js +114 -0
- package/dist/extensions/OrderedListMarker.test.d.ts +2 -0
- package/dist/extensions/OrderedListMarker.test.d.ts.map +1 -0
- package/dist/extensions/OrderedListMarker.test.js +176 -0
- package/dist/extensions/PdfBlock.d.ts.map +1 -1
- package/dist/extensions/PdfBlock.js +25 -23
- package/dist/extensions/ResizableImage.d.ts.map +1 -1
- package/dist/extensions/ResizableImage.js +20 -1
- package/dist/extensions/TableKeymap.d.ts +16 -0
- package/dist/extensions/TableKeymap.d.ts.map +1 -0
- package/dist/extensions/TableKeymap.js +97 -0
- package/dist/extensions/TableKeymap.test.d.ts +2 -0
- package/dist/extensions/TableKeymap.test.d.ts.map +1 -0
- package/dist/extensions/TableKeymap.test.js +147 -0
- package/dist/extensions/TabsBlock.d.ts.map +1 -1
- package/dist/extensions/TabsBlock.js +12 -9
- package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
- package/dist/extensions/TiptapMidibus.js +60 -6
- package/dist/extensions/UploadSkeleton.d.ts +61 -0
- package/dist/extensions/UploadSkeleton.d.ts.map +1 -0
- package/dist/extensions/UploadSkeleton.js +233 -0
- package/dist/extensions/UploadSkeleton.test.d.ts +2 -0
- package/dist/extensions/UploadSkeleton.test.d.ts.map +1 -0
- package/dist/extensions/UploadSkeleton.test.js +128 -0
- package/dist/extensions/VideoEmbed.d.ts +8 -0
- package/dist/extensions/VideoEmbed.d.ts.map +1 -1
- package/dist/extensions/VideoEmbed.js +226 -45
- package/dist/extensions/VideoEmbed.test.js +131 -2
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +191 -0
- package/dist/i18n/es.d.ts +3 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +191 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/index.test.d.ts +2 -0
- package/dist/i18n/index.test.d.ts.map +1 -0
- package/dist/i18n/index.test.js +173 -0
- package/dist/i18n/ja.d.ts +3 -0
- package/dist/i18n/ja.d.ts.map +1 -0
- package/dist/i18n/ja.js +191 -0
- package/dist/i18n/ko.d.ts +198 -0
- package/dist/i18n/ko.d.ts.map +1 -0
- package/dist/i18n/ko.js +215 -0
- package/dist/i18n/zh-hans.d.ts +3 -0
- package/dist/i18n/zh-hans.d.ts.map +1 -0
- package/dist/i18n/zh-hans.js +191 -0
- package/dist/i18n/zh-hant.d.ts +3 -0
- package/dist/i18n/zh-hant.d.ts.map +1 -0
- package/dist/i18n/zh-hant.js +192 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/static/highlight.d.ts +13 -0
- package/dist/static/highlight.d.ts.map +1 -0
- package/dist/static/highlight.js +99 -0
- package/dist/static/index.d.ts +33 -0
- package/dist/static/index.d.ts.map +1 -0
- package/dist/static/index.js +367 -0
- package/dist/static/index.test.d.ts +2 -0
- package/dist/static/index.test.d.ts.map +1 -0
- package/dist/static/index.test.js +194 -0
- package/dist/static/tokenizer.d.ts +52 -0
- package/dist/static/tokenizer.d.ts.map +1 -0
- package/dist/static/tokenizer.js +289 -0
- package/dist/styles/editor.css +1 -1
- package/dist/utils/emoji-data.d.ts +15 -0
- package/dist/utils/emoji-suggestion.d.ts +48 -0
- package/dist/utils/emoji-suggestion.d.ts.map +1 -0
- package/dist/utils/emoji-suggestion.js +51 -0
- package/dist/utils/emoji-suggestion.test.d.ts +2 -0
- package/dist/utils/emoji-suggestion.test.d.ts.map +1 -0
- package/dist/utils/emoji-suggestion.test.js +117 -0
- package/dist/utils/floating-helper.d.ts +60 -0
- package/dist/utils/floating-helper.d.ts.map +1 -0
- package/dist/utils/floating-helper.js +63 -0
- package/dist/utils/floating-helper.test.d.ts +2 -0
- package/dist/utils/floating-helper.test.d.ts.map +1 -0
- package/dist/utils/floating-helper.test.js +141 -0
- package/dist/utils/guard-vscode-paste.d.ts +19 -0
- package/dist/utils/guard-vscode-paste.d.ts.map +1 -0
- package/dist/utils/guard-vscode-paste.js +46 -0
- package/dist/utils/guard-vscode-paste.test.d.ts +2 -0
- package/dist/utils/guard-vscode-paste.test.d.ts.map +1 -0
- package/dist/utils/guard-vscode-paste.test.js +86 -0
- package/dist/utils/media-size.d.ts +29 -0
- package/dist/utils/media-size.d.ts.map +1 -0
- package/dist/utils/media-size.js +90 -0
- package/dist/utils/resize.d.ts +12 -0
- package/dist/utils/resize.d.ts.map +1 -1
- package/dist/utils/resize.js +5 -5
- package/dist/utils/sanitize.d.ts +2 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +28 -3
- package/package.json +8 -1
|
@@ -28,15 +28,19 @@
|
|
|
28
28
|
} from "lucide-svelte";
|
|
29
29
|
import { cn } from "../utils/cn";
|
|
30
30
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
31
|
+
import { defaultTranslator, type EditorMessageKey, type EditorTranslator } from "../i18n";
|
|
31
32
|
|
|
32
33
|
let {
|
|
33
34
|
editor,
|
|
34
35
|
features,
|
|
35
36
|
onPromptLink,
|
|
37
|
+
t = defaultTranslator,
|
|
36
38
|
}: {
|
|
37
39
|
editor: Editor;
|
|
38
40
|
features: Set<ToolbarFeature>;
|
|
39
41
|
onPromptLink?: PromptHandler;
|
|
42
|
+
/** 에디터 UI 번역 함수. 미주입 시 ko. */
|
|
43
|
+
t?: EditorTranslator;
|
|
40
44
|
} = $props();
|
|
41
45
|
|
|
42
46
|
/*
|
|
@@ -65,6 +69,7 @@
|
|
|
65
69
|
|
|
66
70
|
const has = (f: ToolbarFeature) => features.has(f);
|
|
67
71
|
|
|
72
|
+
let mobile = $state(false);
|
|
68
73
|
let showBlockMenu = $state(false);
|
|
69
74
|
let showColors = $state(false);
|
|
70
75
|
let menuEl: HTMLDivElement | undefined = $state();
|
|
@@ -72,27 +77,27 @@
|
|
|
72
77
|
let colorMenuEl: HTMLDivElement | undefined = $state();
|
|
73
78
|
const iconSize = 14;
|
|
74
79
|
|
|
75
|
-
const TEXT_COLORS = [
|
|
76
|
-
{
|
|
77
|
-
{
|
|
78
|
-
{
|
|
79
|
-
{
|
|
80
|
-
{
|
|
81
|
-
{
|
|
82
|
-
{
|
|
83
|
-
{
|
|
84
|
-
{
|
|
80
|
+
const TEXT_COLORS: { labelKey: EditorMessageKey; value: string }[] = [
|
|
81
|
+
{ labelKey: "colorDefault", value: "" },
|
|
82
|
+
{ labelKey: "colorBlack", value: "#000000" },
|
|
83
|
+
{ labelKey: "colorGray", value: "#6b7280" },
|
|
84
|
+
{ labelKey: "colorRed", value: "#dc2626" },
|
|
85
|
+
{ labelKey: "colorOrange", value: "#ea580c" },
|
|
86
|
+
{ labelKey: "colorYellow", value: "#ca8a04" },
|
|
87
|
+
{ labelKey: "colorGreen", value: "#16a34a" },
|
|
88
|
+
{ labelKey: "colorBlue", value: "#2563eb" },
|
|
89
|
+
{ labelKey: "colorPurple", value: "#7c3aed" }
|
|
85
90
|
];
|
|
86
91
|
|
|
87
92
|
function getCurrentBlockLabel(): string {
|
|
88
|
-
if (isActive("heading", { level: 1 })) return "
|
|
89
|
-
if (isActive("heading", { level: 2 })) return "
|
|
90
|
-
if (isActive("heading", { level: 3 })) return "
|
|
91
|
-
if (isActive("bulletList")) return "
|
|
92
|
-
if (isActive("orderedList")) return "
|
|
93
|
-
if (isActive("taskList")) return "
|
|
94
|
-
if (isActive("blockquote")) return "
|
|
95
|
-
return "
|
|
93
|
+
if (isActive("heading", { level: 1 })) return t("heading1");
|
|
94
|
+
if (isActive("heading", { level: 2 })) return t("heading2");
|
|
95
|
+
if (isActive("heading", { level: 3 })) return t("heading3");
|
|
96
|
+
if (isActive("bulletList")) return t("bulletList");
|
|
97
|
+
if (isActive("orderedList")) return t("orderedList");
|
|
98
|
+
if (isActive("taskList")) return t("taskList");
|
|
99
|
+
if (isActive("blockquote")) return t("blockquote");
|
|
100
|
+
return t("paragraph");
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
function isParagraphActive(): boolean {
|
|
@@ -106,11 +111,18 @@
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
async function addLink() {
|
|
114
|
+
const selection = editor.state.selection.getBookmark();
|
|
115
|
+
const documentBeforePrompt = editor.state.doc;
|
|
109
116
|
const previousUrl = editor.getAttributes("link").href || "";
|
|
110
117
|
const url = onPromptLink
|
|
111
118
|
? await onPromptLink(previousUrl)
|
|
112
|
-
: window.prompt("
|
|
113
|
-
if (url === null) return;
|
|
119
|
+
: window.prompt(t("promptLinkUrl"), previousUrl);
|
|
120
|
+
if (url === null || editor.isDestroyed || editor.state.doc !== documentBeforePrompt) return;
|
|
121
|
+
try {
|
|
122
|
+
editor.view.dispatch(editor.state.tr.setSelection(selection.resolve(editor.state.doc)));
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
114
126
|
if (url === "") {
|
|
115
127
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
116
128
|
} else {
|
|
@@ -123,8 +135,13 @@
|
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
function handleDocClick(e:
|
|
138
|
+
function handleDocClick(e: Event) {
|
|
127
139
|
const target = e.target as Node;
|
|
140
|
+
// The upstream plugin treats any focus inside the menu's parent as internal.
|
|
141
|
+
// A body portal needs an explicit boundary so another input/editor dismisses it.
|
|
142
|
+
if (mobile && menuEl && !menuEl.contains(target) && !editor.view.dom.contains(target)) {
|
|
143
|
+
editor.view.dispatch(editor.state.tr.setMeta(bubbleToolbarKey, 'hide'));
|
|
144
|
+
}
|
|
128
145
|
if (showBlockMenu && blockMenuEl && !blockMenuEl.contains(target)) {
|
|
129
146
|
showBlockMenu = false;
|
|
130
147
|
}
|
|
@@ -143,12 +160,74 @@
|
|
|
143
160
|
onMount(() => {
|
|
144
161
|
if (!menuEl) return;
|
|
145
162
|
|
|
163
|
+
const element = menuEl;
|
|
164
|
+
const media = window.matchMedia('(max-width: 767px), (pointer: coarse)');
|
|
165
|
+
const viewport = window.visualViewport;
|
|
166
|
+
const anchor = document.createElement('span');
|
|
167
|
+
anchor.style.cssText = 'position:fixed;width:0;height:0;pointer-events:none;visibility:hidden';
|
|
168
|
+
anchor.setAttribute('aria-hidden', 'true');
|
|
169
|
+
document.body.appendChild(anchor);
|
|
170
|
+
mobile = media.matches;
|
|
171
|
+
|
|
172
|
+
// A portal leaves the host wrapper, where apps map their own theme to editor tokens.
|
|
173
|
+
const themeTokens = [
|
|
174
|
+
'--border', '--background', '--foreground', '--popover', '--muted',
|
|
175
|
+
'--muted-foreground', '--primary', '--primary-foreground', '--accent',
|
|
176
|
+
'--accent-foreground', '--ring', '--hce-menu-z', '--hce-menu-surface'
|
|
177
|
+
];
|
|
178
|
+
const syncTheme = () => {
|
|
179
|
+
const hostStyle = getComputedStyle(editor.view.dom);
|
|
180
|
+
for (const token of themeTokens) {
|
|
181
|
+
const value = mobile ? hostStyle.getPropertyValue(token) : '';
|
|
182
|
+
if (value) element.style.setProperty(token, value);
|
|
183
|
+
else element.style.removeProperty(token);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const syncViewport = () => {
|
|
187
|
+
syncTheme();
|
|
188
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
189
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
190
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
191
|
+
const top = viewport?.offsetTop ?? 0;
|
|
192
|
+
anchor.style.left = `${left + width / 2}px`;
|
|
193
|
+
anchor.style.top = `${top + height}px`;
|
|
194
|
+
element.style.maxWidth = mobile ? `${Math.max(0, width - 16)}px` : '';
|
|
195
|
+
element.style.setProperty('--bubble-popup-height', `${Math.max(44, height - element.getBoundingClientRect().height - 24)}px`);
|
|
196
|
+
};
|
|
197
|
+
syncViewport();
|
|
198
|
+
|
|
199
|
+
const positionOptions = () => mobile
|
|
200
|
+
? { strategy: 'fixed' as const, placement: 'top' as const, flip: false, shift: false, offset: 8 }
|
|
201
|
+
: { strategy: 'absolute' as const, placement: 'top' as const, flip: true, shift: true, offset: 8 };
|
|
202
|
+
const updatePosition = () => {
|
|
203
|
+
if (editor.isDestroyed) return;
|
|
204
|
+
syncViewport();
|
|
205
|
+
editor.view.dispatch(editor.state.tr.setMeta(bubbleToolbarKey, 'updatePosition'));
|
|
206
|
+
};
|
|
207
|
+
const updateMode = () => {
|
|
208
|
+
mobile = media.matches;
|
|
209
|
+
syncViewport();
|
|
210
|
+
editor.view.dispatch(editor.state.tr.setMeta(bubbleToolbarKey, {
|
|
211
|
+
type: 'updateOptions', options: { options: positionOptions() }
|
|
212
|
+
}));
|
|
213
|
+
if (element.isConnected) {
|
|
214
|
+
(mobile ? document.body : editor.view.dom.parentElement)?.appendChild(element);
|
|
215
|
+
}
|
|
216
|
+
updatePosition();
|
|
217
|
+
};
|
|
218
|
+
// Prevent button focus from collapsing the text selection; native OS menus remain enabled.
|
|
219
|
+
const preserveSelection = (event: MouseEvent) => {
|
|
220
|
+
if (mobile && (event.target as HTMLElement).closest('button')) event.preventDefault();
|
|
221
|
+
};
|
|
146
222
|
const plugin = BubbleMenuPlugin({
|
|
147
223
|
pluginKey: bubbleToolbarKey,
|
|
148
224
|
editor,
|
|
149
|
-
element
|
|
225
|
+
element,
|
|
226
|
+
appendTo: () => mobile ? document.body : editor.view.dom.parentElement!,
|
|
227
|
+
getReferencedVirtualElement: () => mobile ? anchor : null,
|
|
150
228
|
shouldShow: ({ editor: e, state }) => {
|
|
151
229
|
const { from, to } = state.selection;
|
|
230
|
+
if (!e.isEditable || (!e.view.hasFocus() && !element.contains(document.activeElement))) return false;
|
|
152
231
|
if (from === to) return false;
|
|
153
232
|
/*
|
|
154
233
|
* ⚠️ **덩어리를 고른 것(NodeSelection)에는 뜨지 않는다.**
|
|
@@ -193,18 +272,36 @@
|
|
|
193
272
|
* 마침 v3 기본값이 `placement: "top"` + `flip`·`shift` 활성이라 화면상 차이는 없었지만,
|
|
194
273
|
* 죽은 설정을 남겨 두면 "위치를 지정해 뒀다"고 착각하게 된다.
|
|
195
274
|
*/
|
|
196
|
-
options: {
|
|
197
|
-
placement: "top",
|
|
198
|
-
},
|
|
275
|
+
options: { ...positionOptions(), onShow: syncTheme },
|
|
199
276
|
});
|
|
200
277
|
|
|
201
278
|
editor.registerPlugin(plugin);
|
|
202
|
-
|
|
279
|
+
const themeObserver = new MutationObserver(syncTheme);
|
|
280
|
+
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
|
281
|
+
themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
|
282
|
+
const observer = new ResizeObserver(updatePosition);
|
|
283
|
+
observer.observe(element);
|
|
284
|
+
media.addEventListener('change', updateMode);
|
|
285
|
+
viewport?.addEventListener('resize', updatePosition);
|
|
286
|
+
viewport?.addEventListener('scroll', updatePosition);
|
|
287
|
+
window.addEventListener('resize', updatePosition);
|
|
288
|
+
element.addEventListener('mousedown', preserveSelection);
|
|
289
|
+
document.addEventListener("pointerdown", handleDocClick);
|
|
290
|
+
document.addEventListener("focusin", handleDocClick);
|
|
203
291
|
document.addEventListener("keydown", handleKeydown);
|
|
204
292
|
|
|
205
293
|
return () => {
|
|
206
294
|
editor.unregisterPlugin(bubbleToolbarKey);
|
|
207
|
-
|
|
295
|
+
observer.disconnect();
|
|
296
|
+
themeObserver.disconnect();
|
|
297
|
+
anchor.remove();
|
|
298
|
+
media.removeEventListener('change', updateMode);
|
|
299
|
+
viewport?.removeEventListener('resize', updatePosition);
|
|
300
|
+
viewport?.removeEventListener('scroll', updatePosition);
|
|
301
|
+
window.removeEventListener('resize', updatePosition);
|
|
302
|
+
element.removeEventListener('mousedown', preserveSelection);
|
|
303
|
+
document.removeEventListener("pointerdown", handleDocClick);
|
|
304
|
+
document.removeEventListener("focusin", handleDocClick);
|
|
208
305
|
document.removeEventListener("keydown", handleKeydown);
|
|
209
306
|
};
|
|
210
307
|
});
|
|
@@ -230,14 +327,16 @@
|
|
|
230
327
|
);
|
|
231
328
|
</script>
|
|
232
329
|
|
|
233
|
-
<div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
|
|
330
|
+
<div bind:this={menuEl} class="bubble-toolbar-container" class:mobile style="visibility: hidden">
|
|
234
331
|
<div class="flex items-center gap-0.5 px-1.5 py-1 rounded-full hce-floating-panel">
|
|
235
332
|
{#if hasBlockMenu}
|
|
236
333
|
<!-- Block type selector -->
|
|
237
|
-
<div class="relative" bind:this={blockMenuEl}>
|
|
334
|
+
<div class="relative bubble-popover-anchor" bind:this={blockMenuEl}>
|
|
238
335
|
<button
|
|
239
336
|
type="button"
|
|
240
337
|
onclick={() => (showBlockMenu = !showBlockMenu)}
|
|
338
|
+
aria-haspopup="menu"
|
|
339
|
+
aria-expanded={showBlockMenu}
|
|
241
340
|
class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
242
341
|
>
|
|
243
342
|
<Type size={12} />
|
|
@@ -246,7 +345,7 @@
|
|
|
246
345
|
</button>
|
|
247
346
|
{#if showBlockMenu}
|
|
248
347
|
<div
|
|
249
|
-
class="absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
|
|
348
|
+
class="bubble-popover absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
|
|
250
349
|
style="min-width: 140px"
|
|
251
350
|
onmousedown={(e) => e.preventDefault()}
|
|
252
351
|
role="menu"
|
|
@@ -265,9 +364,9 @@
|
|
|
265
364
|
showBlockMenu = false;
|
|
266
365
|
}}
|
|
267
366
|
>
|
|
268
|
-
<Type size={12} />
|
|
367
|
+
<Type size={12} /> {t('paragraph')}
|
|
269
368
|
</button>
|
|
270
|
-
{#each [1, 2, 3] as level}
|
|
369
|
+
{#each [1, 2, 3] as level (level)}
|
|
271
370
|
{#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
|
|
272
371
|
{@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
|
|
273
372
|
<button
|
|
@@ -287,7 +386,7 @@
|
|
|
287
386
|
showBlockMenu = false;
|
|
288
387
|
}}
|
|
289
388
|
>
|
|
290
|
-
<Icon size={12} />
|
|
389
|
+
<Icon size={12} /> {t(level === 1 ? 'heading1' : level === 2 ? 'heading2' : 'heading3')}
|
|
291
390
|
</button>
|
|
292
391
|
{/if}
|
|
293
392
|
{/each}
|
|
@@ -305,7 +404,7 @@
|
|
|
305
404
|
showBlockMenu = false;
|
|
306
405
|
}}
|
|
307
406
|
>
|
|
308
|
-
<List size={12} />
|
|
407
|
+
<List size={12} /> {t('bulletList')}
|
|
309
408
|
</button>
|
|
310
409
|
{/if}
|
|
311
410
|
{#if has('ordered-list')}
|
|
@@ -322,7 +421,7 @@
|
|
|
322
421
|
showBlockMenu = false;
|
|
323
422
|
}}
|
|
324
423
|
>
|
|
325
|
-
<ListOrdered size={12} />
|
|
424
|
+
<ListOrdered size={12} /> {t('orderedList')}
|
|
326
425
|
</button>
|
|
327
426
|
{/if}
|
|
328
427
|
{#if has('checklist')}
|
|
@@ -339,7 +438,7 @@
|
|
|
339
438
|
showBlockMenu = false;
|
|
340
439
|
}}
|
|
341
440
|
>
|
|
342
|
-
<ListChecks size={12} />
|
|
441
|
+
<ListChecks size={12} /> {t('taskList')}
|
|
343
442
|
</button>
|
|
344
443
|
{/if}
|
|
345
444
|
{#if has('blockquote')}
|
|
@@ -356,14 +455,14 @@
|
|
|
356
455
|
showBlockMenu = false;
|
|
357
456
|
}}
|
|
358
457
|
>
|
|
359
|
-
<Quote size={12} />
|
|
458
|
+
<Quote size={12} /> {t('blockquote')}
|
|
360
459
|
</button>
|
|
361
460
|
{/if}
|
|
362
461
|
</div>
|
|
363
462
|
{/if}
|
|
364
463
|
</div>
|
|
365
464
|
|
|
366
|
-
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
465
|
+
<div class="bubble-divider w-px h-5 bg-border mx-0.5"></div>
|
|
367
466
|
{/if}
|
|
368
467
|
|
|
369
468
|
<!-- Format buttons -->
|
|
@@ -371,7 +470,7 @@
|
|
|
371
470
|
<button
|
|
372
471
|
type="button"
|
|
373
472
|
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
374
|
-
aria-label=
|
|
473
|
+
aria-label={t('bold')}
|
|
375
474
|
class={cn(
|
|
376
475
|
"p-1.5 rounded-full transition-colors",
|
|
377
476
|
isActive("bold")
|
|
@@ -386,7 +485,7 @@
|
|
|
386
485
|
<button
|
|
387
486
|
type="button"
|
|
388
487
|
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
389
|
-
aria-label=
|
|
488
|
+
aria-label={t('italic')}
|
|
390
489
|
class={cn(
|
|
391
490
|
"p-1.5 rounded-full transition-colors",
|
|
392
491
|
isActive("italic")
|
|
@@ -401,7 +500,7 @@
|
|
|
401
500
|
<button
|
|
402
501
|
type="button"
|
|
403
502
|
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
|
404
|
-
aria-label=
|
|
503
|
+
aria-label={t('underline')}
|
|
405
504
|
class={cn(
|
|
406
505
|
"p-1.5 rounded-full transition-colors",
|
|
407
506
|
isActive("underline")
|
|
@@ -416,7 +515,7 @@
|
|
|
416
515
|
<button
|
|
417
516
|
type="button"
|
|
418
517
|
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
419
|
-
aria-label=
|
|
518
|
+
aria-label={t('strike')}
|
|
420
519
|
class={cn(
|
|
421
520
|
"p-1.5 rounded-full transition-colors",
|
|
422
521
|
isActive("strike")
|
|
@@ -433,7 +532,7 @@
|
|
|
433
532
|
<button
|
|
434
533
|
type="button"
|
|
435
534
|
onclick={() => editor.chain().focus().toggleCode().run()}
|
|
436
|
-
aria-label=
|
|
535
|
+
aria-label={t('code')}
|
|
437
536
|
class={cn(
|
|
438
537
|
"p-1.5 rounded-full transition-colors",
|
|
439
538
|
isActive("code")
|
|
@@ -454,7 +553,7 @@
|
|
|
454
553
|
<button
|
|
455
554
|
type="button"
|
|
456
555
|
onclick={() => editor.chain().focus().toggleMathInline().run()}
|
|
457
|
-
aria-label=
|
|
556
|
+
aria-label={t('mathInline')}
|
|
458
557
|
class={cn(
|
|
459
558
|
"p-1.5 rounded-full transition-colors",
|
|
460
559
|
isActive("math_inline")
|
|
@@ -467,14 +566,14 @@
|
|
|
467
566
|
{/if}
|
|
468
567
|
|
|
469
568
|
{#if has('highlight') || has('text-color')}
|
|
470
|
-
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
569
|
+
<div class="bubble-divider w-px h-5 bg-border mx-0.5"></div>
|
|
471
570
|
{/if}
|
|
472
571
|
|
|
473
572
|
{#if has('highlight')}
|
|
474
573
|
<button
|
|
475
574
|
type="button"
|
|
476
575
|
onclick={() => editor.chain().focus().toggleHighlight().run()}
|
|
477
|
-
aria-label=
|
|
576
|
+
aria-label={t('highlight')}
|
|
478
577
|
class={cn(
|
|
479
578
|
"p-1.5 rounded-full transition-colors",
|
|
480
579
|
isActive("highlight")
|
|
@@ -486,11 +585,13 @@
|
|
|
486
585
|
</button>
|
|
487
586
|
{/if}
|
|
488
587
|
{#if has('text-color')}
|
|
489
|
-
<div class="relative" bind:this={colorMenuEl}>
|
|
588
|
+
<div class="relative bubble-popover-anchor" bind:this={colorMenuEl}>
|
|
490
589
|
<button
|
|
491
590
|
type="button"
|
|
492
591
|
onclick={() => (showColors = !showColors)}
|
|
493
|
-
aria-
|
|
592
|
+
aria-haspopup="menu"
|
|
593
|
+
aria-expanded={showColors}
|
|
594
|
+
aria-label={t('textColor')}
|
|
494
595
|
class={cn(
|
|
495
596
|
"p-1.5 rounded-full transition-colors",
|
|
496
597
|
editor.getAttributes("textStyle").color
|
|
@@ -502,17 +603,17 @@
|
|
|
502
603
|
</button>
|
|
503
604
|
{#if showColors}
|
|
504
605
|
<div
|
|
505
|
-
class="absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl p-2"
|
|
606
|
+
class="bubble-popover absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl p-2"
|
|
506
607
|
style="min-width: 160px"
|
|
507
608
|
onmousedown={(e) => e.preventDefault()}
|
|
508
609
|
role="menu"
|
|
509
610
|
tabindex="-1"
|
|
510
611
|
>
|
|
511
612
|
<div class="grid grid-cols-3 gap-1.5">
|
|
512
|
-
{#each TEXT_COLORS as c}
|
|
613
|
+
{#each TEXT_COLORS as c (c.value)}
|
|
513
614
|
<button
|
|
514
615
|
type="button"
|
|
515
|
-
title={c.
|
|
616
|
+
title={t(c.labelKey)}
|
|
516
617
|
class="h-7 rounded-md border border-border transition-transform hover:scale-105 flex items-center justify-center text-xs font-bold"
|
|
517
618
|
style="color: {c.value || '#000'}; background: #fff"
|
|
518
619
|
onclick={() => {
|
|
@@ -531,7 +632,7 @@
|
|
|
531
632
|
<label
|
|
532
633
|
class="hce-color-divider mt-2 pt-2 flex items-center justify-between gap-2 px-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground"
|
|
533
634
|
>
|
|
534
|
-
<span
|
|
635
|
+
<span>{t('customColor')}</span>
|
|
535
636
|
<input
|
|
536
637
|
type="color"
|
|
537
638
|
class="h-6 w-10 cursor-pointer rounded border border-border bg-transparent p-0"
|
|
@@ -549,11 +650,11 @@
|
|
|
549
650
|
{/if}
|
|
550
651
|
|
|
551
652
|
{#if has('link')}
|
|
552
|
-
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
653
|
+
<div class="bubble-divider w-px h-5 bg-border mx-0.5"></div>
|
|
553
654
|
<button
|
|
554
655
|
type="button"
|
|
555
656
|
onclick={addLink}
|
|
556
|
-
aria-label=
|
|
657
|
+
aria-label={t('link')}
|
|
557
658
|
class={cn(
|
|
558
659
|
"p-1.5 rounded-full transition-colors",
|
|
559
660
|
isActive("link")
|
|
@@ -566,3 +667,30 @@
|
|
|
566
667
|
{/if}
|
|
567
668
|
</div>
|
|
568
669
|
</div>
|
|
670
|
+
|
|
671
|
+
<style>
|
|
672
|
+
.mobile .hce-floating-panel {
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-wrap: wrap;
|
|
675
|
+
justify-content: center;
|
|
676
|
+
border-radius: 12px;
|
|
677
|
+
padding: 4px;
|
|
678
|
+
padding-bottom: max(4px, env(safe-area-inset-bottom));
|
|
679
|
+
}
|
|
680
|
+
.mobile button {
|
|
681
|
+
min-width: 44px;
|
|
682
|
+
min-height: 44px;
|
|
683
|
+
display: inline-flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
justify-content: center;
|
|
686
|
+
}
|
|
687
|
+
.mobile input[type="color"] { min-width: 44px; min-height: 44px; }
|
|
688
|
+
.mobile .bubble-divider { display: none; }
|
|
689
|
+
.mobile .bubble-popover-anchor { position: static; }
|
|
690
|
+
.mobile .bubble-popover {
|
|
691
|
+
left: 0;
|
|
692
|
+
right: 0;
|
|
693
|
+
max-height: min(var(--bubble-popup-height, 50dvh), 300px);
|
|
694
|
+
overflow-y: auto;
|
|
695
|
+
}
|
|
696
|
+
</style>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
2
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
3
|
+
import { type EditorTranslator } from "../i18n";
|
|
3
4
|
type $$ComponentProps = {
|
|
4
5
|
editor: Editor;
|
|
5
6
|
features: Set<ToolbarFeature>;
|
|
6
7
|
onPromptLink?: PromptHandler;
|
|
8
|
+
/** 에디터 UI 번역 함수. 미주입 시 ko. */
|
|
9
|
+
t?: EditorTranslator;
|
|
7
10
|
};
|
|
8
11
|
declare const BubbleToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
12
|
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;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,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;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AA+hBJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|