@lumir-company/editor 0.2.0

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/index.mjs ADDED
@@ -0,0 +1,427 @@
1
+ "use client";
2
+
3
+ // src/components/LumirEditor.tsx
4
+ import { useEffect, useMemo } from "react";
5
+ import {
6
+ useCreateBlockNote,
7
+ SideMenu as BlockSideMenu,
8
+ SideMenuController,
9
+ DragHandleButton,
10
+ SuggestionMenuController,
11
+ getDefaultReactSlashMenuItems
12
+ } from "@blocknote/react";
13
+ import { BlockNoteView } from "@blocknote/mantine";
14
+
15
+ // src/utils/cn.ts
16
+ function cn(...inputs) {
17
+ return inputs.filter(Boolean).join(" ");
18
+ }
19
+
20
+ // src/components/LumirEditor.tsx
21
+ import { jsx, jsxs } from "react/jsx-runtime";
22
+ var ContentUtils = class {
23
+ /**
24
+ * JSON 문자열의 유효성을 검증합니다
25
+ * @param jsonString 검증할 JSON 문자열
26
+ * @returns 유효한 JSON 문자열인지 여부
27
+ */
28
+ static isValidJSONString(jsonString) {
29
+ try {
30
+ const parsed = JSON.parse(jsonString);
31
+ return Array.isArray(parsed);
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * JSON 문자열을 DefaultPartialBlock 배열로 파싱합니다
38
+ * @param jsonString JSON 문자열
39
+ * @returns 파싱된 블록 배열 또는 null (파싱 실패 시)
40
+ */
41
+ static parseJSONContent(jsonString) {
42
+ try {
43
+ const parsed = JSON.parse(jsonString);
44
+ if (Array.isArray(parsed)) {
45
+ return parsed;
46
+ }
47
+ return null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * 기본 paragraph 블록 생성
54
+ * @returns 기본 설정이 적용된 DefaultPartialBlock
55
+ */
56
+ static createDefaultBlock() {
57
+ return {
58
+ type: "paragraph",
59
+ props: {
60
+ textColor: "default",
61
+ backgroundColor: "default",
62
+ textAlignment: "left"
63
+ },
64
+ content: [{ type: "text", text: "", styles: {} }],
65
+ children: []
66
+ };
67
+ }
68
+ /**
69
+ * 콘텐츠 유효성 검증 및 기본값 설정
70
+ * @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)
71
+ * @param emptyBlockCount 빈 블록 개수 (기본값: 3)
72
+ * @param placeholder 첫 번째 블록의 placeholder 텍스트
73
+ * @returns 검증된 콘텐츠 배열
74
+ */
75
+ static validateContent(content, emptyBlockCount = 3, placeholder) {
76
+ if (typeof content === "string") {
77
+ if (content.trim() === "") {
78
+ return this.createEmptyBlocks(emptyBlockCount, placeholder);
79
+ }
80
+ const parsedContent = this.parseJSONContent(content);
81
+ if (parsedContent && parsedContent.length > 0) {
82
+ return parsedContent;
83
+ }
84
+ return this.createEmptyBlocks(emptyBlockCount, placeholder);
85
+ }
86
+ if (!content || content.length === 0) {
87
+ return this.createEmptyBlocks(emptyBlockCount, placeholder);
88
+ }
89
+ return content;
90
+ }
91
+ /**
92
+ * 빈 블록들을 생성합니다
93
+ * @param emptyBlockCount 생성할 블록 개수
94
+ * @param placeholder 첫 번째 블록의 placeholder 텍스트
95
+ * @returns 생성된 빈 블록 배열
96
+ */
97
+ static createEmptyBlocks(emptyBlockCount, placeholder) {
98
+ return Array.from({ length: emptyBlockCount }, (_, index) => {
99
+ const block = this.createDefaultBlock();
100
+ if (index === 0 && placeholder) {
101
+ block.content = [{ type: "text", text: placeholder, styles: {} }];
102
+ }
103
+ return block;
104
+ });
105
+ }
106
+ };
107
+ var EditorConfig = class {
108
+ /**
109
+ * 테이블 설정 기본값 적용
110
+ * @param userTables 사용자 테이블 설정
111
+ * @returns 기본값이 적용된 테이블 설정
112
+ */
113
+ static getDefaultTableConfig(userTables) {
114
+ return {
115
+ splitCells: userTables?.splitCells ?? true,
116
+ cellBackgroundColor: userTables?.cellBackgroundColor ?? true,
117
+ cellTextColor: userTables?.cellTextColor ?? true,
118
+ headers: userTables?.headers ?? true
119
+ };
120
+ }
121
+ /**
122
+ * 헤딩 설정 기본값 적용
123
+ * @param userHeading 사용자 헤딩 설정
124
+ * @returns 기본값이 적용된 헤딩 설정
125
+ */
126
+ static getDefaultHeadingConfig(userHeading) {
127
+ return userHeading?.levels && userHeading.levels.length > 0 ? userHeading : { levels: [1, 2, 3, 4, 5, 6] };
128
+ }
129
+ /**
130
+ * 비활성화할 확장 기능 목록 생성
131
+ * @param userExtensions 사용자 정의 비활성 확장
132
+ * @param allowVideo 비디오 업로드 허용 여부
133
+ * @param allowAudio 오디오 업로드 허용 여부
134
+ * @returns 비활성화할 확장 기능 목록
135
+ */
136
+ static getDisabledExtensions(userExtensions, allowVideo = false, allowAudio = false) {
137
+ const set = new Set(userExtensions ?? []);
138
+ if (!allowVideo) set.add("video");
139
+ if (!allowAudio) set.add("audio");
140
+ return Array.from(set);
141
+ }
142
+ };
143
+ var createObjectUrlUploader = async (file) => {
144
+ return URL.createObjectURL(file);
145
+ };
146
+ var fileToBase64 = async (file) => await new Promise((resolve, reject) => {
147
+ const reader = new FileReader();
148
+ reader.onload = () => resolve(String(reader.result));
149
+ reader.onerror = () => reject(new Error("FileReader failed"));
150
+ reader.readAsDataURL(file);
151
+ });
152
+ function LumirEditor({
153
+ // editor options
154
+ initialContent,
155
+ initialEmptyBlocks = 3,
156
+ placeholder,
157
+ uploadFile,
158
+ pasteHandler,
159
+ tables,
160
+ heading,
161
+ animations = true,
162
+ defaultStyles = true,
163
+ disableExtensions,
164
+ domAttributes,
165
+ tabBehavior = "prefer-navigate-ui",
166
+ trailingBlock = true,
167
+ resolveFileUrl,
168
+ storeImagesAsBase64 = true,
169
+ allowVideoUpload = false,
170
+ allowAudioUpload = false,
171
+ allowFileUpload = false,
172
+ // view options
173
+ editable = true,
174
+ theme = "light",
175
+ formattingToolbar = true,
176
+ linkToolbar = true,
177
+ sideMenu = true,
178
+ slashMenu = true,
179
+ emojiPicker = true,
180
+ filePanel = true,
181
+ tableHandles = true,
182
+ comments = true,
183
+ onSelectionChange,
184
+ className = "",
185
+ includeDefaultStyles = true,
186
+ sideMenuAddButton = true,
187
+ // callbacks / refs
188
+ onContentChange,
189
+ editorRef
190
+ }) {
191
+ const validatedContent = useMemo(() => {
192
+ return ContentUtils.validateContent(
193
+ initialContent,
194
+ initialEmptyBlocks,
195
+ placeholder
196
+ );
197
+ }, [initialContent, initialEmptyBlocks, placeholder]);
198
+ const editor = useCreateBlockNote(
199
+ {
200
+ initialContent: validatedContent,
201
+ tables: EditorConfig.getDefaultTableConfig(tables),
202
+ heading: EditorConfig.getDefaultHeadingConfig(heading),
203
+ animations,
204
+ defaultStyles,
205
+ // 확장 비활성: 비디오/오디오만 제어(파일 확장은 내부 드롭 로직 의존 → 비활성화하지 않음)
206
+ disableExtensions: useMemo(() => {
207
+ return EditorConfig.getDisabledExtensions(
208
+ disableExtensions,
209
+ allowVideoUpload,
210
+ allowAudioUpload
211
+ );
212
+ }, [disableExtensions, allowVideoUpload, allowAudioUpload]),
213
+ domAttributes,
214
+ tabBehavior,
215
+ trailingBlock,
216
+ resolveFileUrl,
217
+ uploadFile: async (file) => {
218
+ const custom = uploadFile;
219
+ const fallback = storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader;
220
+ try {
221
+ if (custom) return await custom(file);
222
+ return await fallback(file);
223
+ } catch (_) {
224
+ try {
225
+ return await createObjectUrlUploader(file);
226
+ } catch {
227
+ throw new Error("Failed to process file for upload");
228
+ }
229
+ }
230
+ },
231
+ pasteHandler: (ctx) => {
232
+ const { event, editor: editor2, defaultPasteHandler } = ctx;
233
+ const fileList = event?.clipboardData?.files ?? null;
234
+ const files = fileList ? Array.from(fileList) : [];
235
+ const accepted = files.filter(
236
+ (f) => f.size > 0 && (f.type?.startsWith("image/") || !f.type && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || ""))
237
+ );
238
+ if (files.length > 0 && accepted.length === 0) {
239
+ event.preventDefault();
240
+ return true;
241
+ }
242
+ if (accepted.length === 0) return defaultPasteHandler() ?? false;
243
+ event.preventDefault();
244
+ (async () => {
245
+ const doUpload = uploadFile ?? (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);
246
+ for (const file of accepted) {
247
+ try {
248
+ const url = await doUpload(file);
249
+ editor2.pasteHTML(`<img src="${url}" alt="image" />`);
250
+ } catch (err) {
251
+ console.warn(
252
+ "Image upload failed, skipped:",
253
+ file.name || "",
254
+ err
255
+ );
256
+ continue;
257
+ }
258
+ }
259
+ })();
260
+ return true;
261
+ }
262
+ },
263
+ [
264
+ uploadFile,
265
+ pasteHandler,
266
+ storeImagesAsBase64,
267
+ allowVideoUpload,
268
+ allowAudioUpload,
269
+ allowFileUpload,
270
+ tables?.splitCells,
271
+ tables?.cellBackgroundColor,
272
+ tables?.cellTextColor,
273
+ tables?.headers,
274
+ heading?.levels?.join(","),
275
+ animations,
276
+ defaultStyles,
277
+ disableExtensions?.join(","),
278
+ domAttributes ? JSON.stringify(domAttributes) : void 0,
279
+ tabBehavior,
280
+ trailingBlock,
281
+ resolveFileUrl
282
+ ]
283
+ );
284
+ useEffect(() => {
285
+ if (!editor) return;
286
+ editor.isEditable = editable;
287
+ const el = editor.domElement;
288
+ if (!editable) {
289
+ if (el) {
290
+ el.style.userSelect = "text";
291
+ el.style.webkitUserSelect = "text";
292
+ }
293
+ }
294
+ }, [editor, editable]);
295
+ useEffect(() => {
296
+ if (!editor || !onContentChange) return;
297
+ let lastContent = "";
298
+ const handleContentChange = () => {
299
+ const topLevelBlocks = editor.topLevelBlocks;
300
+ const currentContent = JSON.stringify(topLevelBlocks);
301
+ if (lastContent === currentContent) return;
302
+ lastContent = currentContent;
303
+ onContentChange(topLevelBlocks);
304
+ };
305
+ editor.onEditorContentChange(handleContentChange);
306
+ return () => {
307
+ };
308
+ }, [editor, onContentChange]);
309
+ useEffect(() => {
310
+ if (!editorRef) return;
311
+ editorRef.current = editor ?? null;
312
+ return () => {
313
+ if (editorRef) editorRef.current = null;
314
+ };
315
+ }, [editor, editorRef]);
316
+ useEffect(() => {
317
+ const el = editor?.domElement;
318
+ if (!el) return;
319
+ const handleDragOver = (e) => {
320
+ if (e.defaultPrevented) return;
321
+ const hasFiles = e.dataTransfer?.types?.includes?.("Files");
322
+ if (hasFiles) {
323
+ e.preventDefault();
324
+ e.stopPropagation();
325
+ if (typeof e.stopImmediatePropagation === "function") {
326
+ e.stopImmediatePropagation();
327
+ }
328
+ }
329
+ };
330
+ const handleDrop = (e) => {
331
+ if (!e.dataTransfer) return;
332
+ const hasFiles = (e.dataTransfer.types ?? []).includes("Files");
333
+ if (!hasFiles) return;
334
+ e.preventDefault();
335
+ e.stopPropagation();
336
+ e.stopImmediatePropagation?.();
337
+ const items = Array.from(e.dataTransfer.items ?? []);
338
+ const files = items.filter((it) => it.kind === "file").map((it) => it.getAsFile()).filter((f) => !!f);
339
+ const accepted = files.filter(
340
+ (f) => f.size > 0 && (f.type?.startsWith("image/") || !f.type && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || ""))
341
+ );
342
+ if (accepted.length === 0) return;
343
+ (async () => {
344
+ const doUpload = uploadFile ?? (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);
345
+ for (const f of accepted) {
346
+ try {
347
+ const url = await doUpload(f);
348
+ editor?.pasteHTML(`<img src="${url}" alt="image" />`);
349
+ } catch (err) {
350
+ console.warn("Image upload failed, skipped:", f.name || "", err);
351
+ continue;
352
+ }
353
+ }
354
+ })();
355
+ };
356
+ el.addEventListener("dragover", handleDragOver, { capture: true });
357
+ el.addEventListener("drop", handleDrop, { capture: true });
358
+ return () => {
359
+ el.removeEventListener("dragover", handleDragOver, {
360
+ capture: true
361
+ });
362
+ el.removeEventListener("drop", handleDrop, { capture: true });
363
+ };
364
+ }, [
365
+ editor,
366
+ uploadFile,
367
+ storeImagesAsBase64,
368
+ allowVideoUpload,
369
+ allowAudioUpload
370
+ ]);
371
+ const computedSideMenu = sideMenuAddButton ? sideMenu : false;
372
+ const DragHandleOnlySideMenu = (props) => {
373
+ return /* @__PURE__ */ jsx(BlockSideMenu, { ...props, children: /* @__PURE__ */ jsx(DragHandleButton, { ...props }) });
374
+ };
375
+ return /* @__PURE__ */ jsxs(
376
+ BlockNoteView,
377
+ {
378
+ className: cn(
379
+ includeDefaultStyles && 'lumirEditor w-full h-full min-w-[300px] overflow-auto rounded-md border border-gray-300 focus-within:ring-2 focus-within:ring-black [&_.bn-editor]:px-[12px] [&_[data-content-type="paragraph"]]:text-[14px] bg-white',
380
+ className
381
+ ),
382
+ editor,
383
+ editable,
384
+ theme,
385
+ formattingToolbar,
386
+ linkToolbar,
387
+ sideMenu: computedSideMenu,
388
+ slashMenu: false,
389
+ emojiPicker,
390
+ filePanel,
391
+ tableHandles,
392
+ comments,
393
+ onSelectionChange,
394
+ children: [
395
+ /* @__PURE__ */ jsx(
396
+ SuggestionMenuController,
397
+ {
398
+ triggerCharacter: "/",
399
+ getItems: async (query) => {
400
+ const items = getDefaultReactSlashMenuItems(editor);
401
+ const filtered = items.filter((it) => {
402
+ const k = (it?.key || "").toString();
403
+ if (["video", "audio", "file"].includes(k)) return false;
404
+ return true;
405
+ });
406
+ if (!query) return filtered;
407
+ const q = query.toLowerCase();
408
+ return filtered.filter(
409
+ (it) => (it.title || "").toLowerCase().includes(q) || (it.aliases || []).some(
410
+ (a) => a.toLowerCase().includes(q)
411
+ )
412
+ );
413
+ }
414
+ }
415
+ ),
416
+ !sideMenuAddButton && /* @__PURE__ */ jsx(SideMenuController, { sideMenu: DragHandleOnlySideMenu })
417
+ ]
418
+ }
419
+ );
420
+ }
421
+ export {
422
+ ContentUtils,
423
+ EditorConfig,
424
+ LumirEditor,
425
+ cn
426
+ };
427
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/LumirEditor.tsx","../src/utils/cn.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo } from \"react\";\nimport {\n useCreateBlockNote,\n SideMenu as BlockSideMenu,\n SideMenuController,\n DragHandleButton,\n SuggestionMenuController,\n getDefaultReactSlashMenuItems,\n} from \"@blocknote/react\";\nimport { BlockNoteView } from \"@blocknote/mantine\";\nimport { cn } from \"../utils/cn\";\n\nimport type {\n DefaultPartialBlock,\n LumirEditorProps,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n} from \"../types\";\n\n// ==========================================\n// 유틸리티 클래스들 (안전한 리팩토링)\n// ==========================================\n\n/**\n * 콘텐츠 관리 유틸리티\n * 기본 블록 생성 및 콘텐츠 검증 로직을 담당\n */\nexport class ContentUtils {\n /**\n * JSON 문자열의 유효성을 검증합니다\n * @param jsonString 검증할 JSON 문자열\n * @returns 유효한 JSON 문자열인지 여부\n */\n static isValidJSONString(jsonString: string): boolean {\n try {\n const parsed = JSON.parse(jsonString);\n return Array.isArray(parsed);\n } catch {\n return false;\n }\n }\n\n /**\n * JSON 문자열을 DefaultPartialBlock 배열로 파싱합니다\n * @param jsonString JSON 문자열\n * @returns 파싱된 블록 배열 또는 null (파싱 실패 시)\n */\n static parseJSONContent(jsonString: string): DefaultPartialBlock[] | null {\n try {\n const parsed = JSON.parse(jsonString);\n if (Array.isArray(parsed)) {\n return parsed as DefaultPartialBlock[];\n }\n return null;\n } catch {\n return null;\n }\n }\n /**\n * 기본 paragraph 블록 생성\n * @returns 기본 설정이 적용된 DefaultPartialBlock\n */\n static createDefaultBlock(): DefaultPartialBlock {\n return {\n type: \"paragraph\",\n props: {\n textColor: \"default\",\n backgroundColor: \"default\",\n textAlignment: \"left\",\n },\n content: [{ type: \"text\", text: \"\", styles: {} }],\n children: [],\n };\n }\n\n /**\n * 콘텐츠 유효성 검증 및 기본값 설정\n * @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)\n * @param emptyBlockCount 빈 블록 개수 (기본값: 3)\n * @param placeholder 첫 번째 블록의 placeholder 텍스트\n * @returns 검증된 콘텐츠 배열\n */\n static validateContent(\n content?: DefaultPartialBlock[] | string,\n emptyBlockCount: number = 3,\n placeholder?: string\n ): DefaultPartialBlock[] {\n // 1. 문자열인 경우 JSON 파싱 시도\n if (typeof content === \"string\") {\n if (content.trim() === \"\") {\n return this.createEmptyBlocks(emptyBlockCount, placeholder);\n }\n\n const parsedContent = this.parseJSONContent(content);\n if (parsedContent && parsedContent.length > 0) {\n return parsedContent;\n }\n\n // 파싱 실패 시 빈 블록 생성\n return this.createEmptyBlocks(emptyBlockCount, placeholder);\n }\n\n // 2. 배열인 경우 기존 로직\n if (!content || content.length === 0) {\n return this.createEmptyBlocks(emptyBlockCount, placeholder);\n }\n\n return content;\n }\n\n /**\n * 빈 블록들을 생성합니다\n * @param emptyBlockCount 생성할 블록 개수\n * @param placeholder 첫 번째 블록의 placeholder 텍스트\n * @returns 생성된 빈 블록 배열\n */\n private static createEmptyBlocks(\n emptyBlockCount: number,\n placeholder?: string\n ): DefaultPartialBlock[] {\n return Array.from({ length: emptyBlockCount }, (_, index) => {\n const block = this.createDefaultBlock();\n // 첫 번째 블록에 placeholder 텍스트 적용\n if (index === 0 && placeholder) {\n block.content = [{ type: \"text\", text: placeholder, styles: {} }];\n }\n return block;\n });\n }\n}\n\n/**\n * 에디터 설정 관리 유틸리티\n * 각종 설정의 기본값과 검증 로직을 담당\n */\nexport class EditorConfig {\n /**\n * 테이블 설정 기본값 적용\n * @param userTables 사용자 테이블 설정\n * @returns 기본값이 적용된 테이블 설정\n */\n static getDefaultTableConfig(userTables?: LumirEditorProps[\"tables\"]) {\n return {\n splitCells: userTables?.splitCells ?? true,\n cellBackgroundColor: userTables?.cellBackgroundColor ?? true,\n cellTextColor: userTables?.cellTextColor ?? true,\n headers: userTables?.headers ?? true,\n };\n }\n\n /**\n * 헤딩 설정 기본값 적용\n * @param userHeading 사용자 헤딩 설정\n * @returns 기본값이 적용된 헤딩 설정\n */\n static getDefaultHeadingConfig(userHeading?: LumirEditorProps[\"heading\"]) {\n return userHeading?.levels && userHeading.levels.length > 0\n ? userHeading\n : { levels: [1, 2, 3, 4, 5, 6] as (1 | 2 | 3 | 4 | 5 | 6)[] };\n }\n\n /**\n * 비활성화할 확장 기능 목록 생성\n * @param userExtensions 사용자 정의 비활성 확장\n * @param allowVideo 비디오 업로드 허용 여부\n * @param allowAudio 오디오 업로드 허용 여부\n * @returns 비활성화할 확장 기능 목록\n */\n static getDisabledExtensions(\n userExtensions?: string[],\n allowVideo = false,\n allowAudio = false\n ): string[] {\n const set = new Set<string>(userExtensions ?? []);\n if (!allowVideo) set.add(\"video\");\n if (!allowAudio) set.add(\"audio\");\n return Array.from(set);\n }\n}\n\nconst createObjectUrlUploader = async (file: File): Promise<string> => {\n return URL.createObjectURL(file);\n};\n\nconst fileToBase64 = async (file: File): Promise<string> =>\n await new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error(\"FileReader failed\"));\n reader.readAsDataURL(file);\n });\n\nexport default function LumirEditor({\n // editor options\n initialContent,\n initialEmptyBlocks = 3,\n placeholder,\n uploadFile,\n pasteHandler,\n tables,\n heading,\n animations = true,\n defaultStyles = true,\n disableExtensions,\n domAttributes,\n tabBehavior = \"prefer-navigate-ui\",\n trailingBlock = true,\n resolveFileUrl,\n storeImagesAsBase64 = true,\n allowVideoUpload = false,\n allowAudioUpload = false,\n allowFileUpload = false,\n // view options\n editable = true,\n theme = \"light\",\n formattingToolbar = true,\n linkToolbar = true,\n sideMenu = true,\n slashMenu = true,\n emojiPicker = true,\n filePanel = true,\n tableHandles = true,\n comments = true,\n onSelectionChange,\n className = \"\",\n includeDefaultStyles = true,\n sideMenuAddButton = true,\n // callbacks / refs\n onContentChange,\n editorRef,\n}: LumirEditorProps) {\n const validatedContent = useMemo<DefaultPartialBlock[]>(() => {\n return ContentUtils.validateContent(\n initialContent,\n initialEmptyBlocks,\n placeholder\n );\n }, [initialContent, initialEmptyBlocks, placeholder]);\n\n const editor = useCreateBlockNote<\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema\n >(\n {\n initialContent: validatedContent as DefaultPartialBlock[],\n tables: EditorConfig.getDefaultTableConfig(tables),\n heading: EditorConfig.getDefaultHeadingConfig(heading),\n animations,\n defaultStyles,\n // 확장 비활성: 비디오/오디오만 제어(파일 확장은 내부 드롭 로직 의존 → 비활성화하지 않음)\n disableExtensions: useMemo(() => {\n return EditorConfig.getDisabledExtensions(\n disableExtensions,\n allowVideoUpload,\n allowAudioUpload\n );\n }, [disableExtensions, allowVideoUpload, allowAudioUpload]),\n domAttributes,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n uploadFile: async (file) => {\n const custom = uploadFile;\n const fallback = storeImagesAsBase64\n ? fileToBase64\n : createObjectUrlUploader;\n try {\n if (custom) return await custom(file);\n return await fallback(file);\n } catch (_) {\n // Fallback to ObjectURL when FileReader or custom upload fails\n try {\n return await createObjectUrlUploader(file);\n } catch {\n throw new Error(\"Failed to process file for upload\");\n }\n }\n },\n pasteHandler: (ctx) => {\n const { event, editor, defaultPasteHandler } = ctx as any;\n const fileList =\n (event?.clipboardData?.files as FileList | null) ?? null;\n const files: File[] = fileList ? Array.from(fileList) : [];\n const accepted: File[] = files.filter(\n (f: File) =>\n f.size > 0 &&\n (f.type?.startsWith(\"image/\") ||\n (!f.type &&\n /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || \"\")))\n );\n // 파일 항목이 있으나 허용되지 않으면 기본 처리도 막고 무시\n if (files.length > 0 && accepted.length === 0) {\n event.preventDefault();\n return true;\n }\n if (accepted.length === 0) return defaultPasteHandler() ?? false;\n event.preventDefault();\n (async () => {\n const doUpload =\n uploadFile ??\n (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);\n for (const file of accepted) {\n try {\n const url = await doUpload(file);\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n } catch (err) {\n // 업로드 실패 파일은 삽입하지 않음 (삭제/스킵)\n // console.warn로만 기록하여 UI 오류를 막음\n console.warn(\n \"Image upload failed, skipped:\",\n file.name || \"\",\n err\n );\n continue;\n }\n }\n })();\n return true;\n },\n },\n [\n uploadFile,\n pasteHandler,\n storeImagesAsBase64,\n allowVideoUpload,\n allowAudioUpload,\n allowFileUpload,\n tables?.splitCells,\n tables?.cellBackgroundColor,\n tables?.cellTextColor,\n tables?.headers,\n heading?.levels?.join(\",\"),\n animations,\n defaultStyles,\n disableExtensions?.join(\",\"),\n domAttributes ? JSON.stringify(domAttributes) : undefined,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n ]\n );\n\n useEffect(() => {\n if (!editor) return;\n editor.isEditable = editable;\n const el = editor.domElement as HTMLElement | undefined;\n if (!editable) {\n if (el) {\n el.style.userSelect = \"text\";\n (\n el.style as CSSStyleDeclaration & { webkitUserSelect?: string }\n ).webkitUserSelect = \"text\";\n }\n }\n }, [editor, editable]);\n\n useEffect(() => {\n if (!editor || !onContentChange) return;\n let lastContent = \"\";\n const handleContentChange = () => {\n const topLevelBlocks =\n editor.topLevelBlocks as unknown as DefaultPartialBlock[];\n const currentContent = JSON.stringify(topLevelBlocks);\n if (lastContent === currentContent) return;\n lastContent = currentContent;\n onContentChange(topLevelBlocks);\n };\n editor.onEditorContentChange(handleContentChange);\n return () => {};\n }, [editor, onContentChange]);\n\n // 외부에서 imperative API 접근을 위한 ref 연결\n useEffect(() => {\n if (!editorRef) return;\n editorRef.current = editor ?? null;\n return () => {\n if (editorRef) editorRef.current = null;\n };\n }, [editor, editorRef]);\n\n useEffect(() => {\n const el = editor?.domElement as HTMLElement | undefined;\n if (!el) return;\n const handleDragOver = (e: DragEvent) => {\n if (e.defaultPrevented) return;\n const hasFiles = (\n e.dataTransfer?.types as unknown as string[] | undefined\n )?.includes?.(\"Files\");\n if (hasFiles) {\n e.preventDefault();\n e.stopPropagation();\n // @ts-ignore\n if (typeof (e as any).stopImmediatePropagation === \"function\") {\n // @ts-ignore\n (e as any).stopImmediatePropagation();\n }\n }\n };\n\n const handleDrop = (e: DragEvent) => {\n if (!e.dataTransfer) return;\n const hasFiles = (\n (e.dataTransfer.types as unknown as string[] | undefined) ?? []\n ).includes(\"Files\");\n if (!hasFiles) return;\n\n // 기본 드롭 동작을 항상 차단해, 허용되지 않는 파일이 렌더링되지 않도록 함\n e.preventDefault();\n e.stopPropagation();\n // @ts-ignore\n (e as any).stopImmediatePropagation?.();\n\n // DataTransferItem 우선 (디렉토리/가짜 항목 배제)\n const items = Array.from(e.dataTransfer.items ?? []);\n const files = items\n .filter((it) => it.kind === \"file\")\n .map((it) => it.getAsFile())\n .filter((f): f is File => !!f);\n\n const accepted = files.filter(\n (f) =>\n f.size > 0 &&\n (f.type?.startsWith(\"image/\") ||\n (!f.type && /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || \"\")))\n );\n if (accepted.length === 0) return; // 차단만 하고 아무것도 삽입하지 않음\n\n (async () => {\n const doUpload =\n uploadFile ??\n (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);\n for (const f of accepted) {\n try {\n const url = await doUpload(f);\n editor?.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n } catch (err) {\n // 실패 파일은 삽입하지 않음\n console.warn(\"Image upload failed, skipped:\", f.name || \"\", err);\n continue;\n }\n }\n })();\n };\n el.addEventListener(\"dragover\", handleDragOver, { capture: true });\n el.addEventListener(\"drop\", handleDrop, { capture: true });\n return () => {\n el.removeEventListener(\"dragover\", handleDragOver, {\n capture: true,\n } as any);\n el.removeEventListener(\"drop\", handleDrop, { capture: true } as any);\n };\n }, [\n editor,\n uploadFile,\n storeImagesAsBase64,\n allowVideoUpload,\n allowAudioUpload,\n ]);\n\n // Add block 버튼을 끄면 기본 SideMenu를 비활성화하고 커스텀 SideMenu만 렌더(드래그 핸들 유지)\n const computedSideMenu = sideMenuAddButton ? sideMenu : false;\n\n // 공식 가이드 방식: 커스텀 SideMenu 컴포넌트 전달 (버튼 미제공 → 플러스 버튼 없음)\n // 공식 가이드 패턴: props를 전달받아 DragHandleButton만 유지\n const DragHandleOnlySideMenu = (props: any) => {\n return (\n <BlockSideMenu {...props}>\n <DragHandleButton {...props} />\n </BlockSideMenu>\n );\n };\n\n return (\n <BlockNoteView\n className={cn(\n includeDefaultStyles &&\n 'lumirEditor w-full h-full min-w-[300px] overflow-auto rounded-md border border-gray-300 focus-within:ring-2 focus-within:ring-black [&_.bn-editor]:px-[12px] [&_[data-content-type=\"paragraph\"]]:text-[14px] bg-white',\n className\n )}\n editor={editor}\n editable={editable}\n theme={theme as \"light\" | \"dark\" | undefined}\n formattingToolbar={formattingToolbar}\n linkToolbar={linkToolbar}\n sideMenu={computedSideMenu}\n slashMenu={false}\n emojiPicker={emojiPicker}\n filePanel={filePanel}\n tableHandles={tableHandles}\n comments={comments}\n onSelectionChange={onSelectionChange}\n >\n <SuggestionMenuController\n triggerCharacter=\"/\"\n getItems={async (query: string) => {\n const items = getDefaultReactSlashMenuItems(editor);\n const filtered = items.filter((it: any) => {\n const k = (it?.key || \"\").toString();\n if ([\"video\", \"audio\", \"file\"].includes(k)) return false;\n return true;\n });\n if (!query) return filtered;\n const q = query.toLowerCase();\n return filtered.filter(\n (it: any) =>\n (it.title || \"\").toLowerCase().includes(q) ||\n (it.aliases || []).some((a: string) =>\n a.toLowerCase().includes(q)\n )\n );\n }}\n />\n {!sideMenuAddButton && (\n <SideMenuController sideMenu={DragHandleOnlySideMenu} />\n )}\n </BlockNoteView>\n );\n}\n","// clsx와 tailwind-merge를 사용한 className 유틸리티\r\n// 사용자가 직접 설치하도록 권장하거나, 간단한 버전 제공\r\n\r\nexport function cn(...inputs: (string | undefined | null | false)[]) {\r\n return inputs.filter(Boolean).join(' ');\r\n}\r\n"],"mappings":"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","names":["editor"]}
package/dist/style.css ADDED
@@ -0,0 +1,13 @@
1
+ @import "@blocknote/core/fonts/inter.css";
2
+ @import "@blocknote/mantine/style.css";
3
+ @import "@blocknote/react/style.css";
4
+
5
+ /* BlockNote/Mantine 기본 폰트를 덮어씀 */
6
+ .lumirEditor {
7
+ --mantine-font-family: "Pretendard", "Noto Sans KR", Arial, Helvetica,
8
+ sans-serif;
9
+ }
10
+ .lumirEditor .bn-editor,
11
+ .lumirEditor .bn-inline-content {
12
+ font-family: "Pretendard", "Noto Sans KR", Arial, Helvetica, sans-serif !important;
13
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@lumir-company/editor",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "Advanced BlockNote-based React rich text editor with hybrid content support and flexible styling",
6
+ "keywords": [
7
+ "editor",
8
+ "blocknote",
9
+ "rich-text",
10
+ "react",
11
+ "typescript",
12
+ "tailwind",
13
+ "wysiwyg",
14
+ "markdown",
15
+ "text-editor"
16
+ ],
17
+ "main": "dist/index.js",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.ts",
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/lumir-company/editor.git"
26
+ },
27
+ "homepage": "https://github.com/lumir-company/editor#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/lumir-company/editor/issues"
30
+ },
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.mjs",
34
+ "require": "./dist/index.js"
35
+ },
36
+ "./style.css": "./dist/style.css"
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup src/index.ts --dts --format cjs,esm --sourcemap && node scripts/copy-style.js",
47
+ "dev": "tsup src/index.ts --dts --format cjs,esm --sourcemap --watch --onSuccess \"node scripts/copy-style.js\"",
48
+ "type-check": "tsc --noEmit",
49
+ "prepublishOnly": "npm run type-check && npm run build"
50
+ },
51
+ "peerDependencies": {
52
+ "react": ">=18.0.0",
53
+ "react-dom": ">=18.0.0"
54
+ },
55
+ "dependencies": {
56
+ "@blocknote/core": "^0.35.0",
57
+ "@blocknote/react": "^0.35.0",
58
+ "@blocknote/mantine": "^0.35.0"
59
+ },
60
+ "devDependencies": {
61
+ "typescript": "^5.4.0",
62
+ "tsup": "^8.0.1",
63
+ "@types/react": "^18.2.0",
64
+ "@types/react-dom": "^18.2.0",
65
+ "react": "^18.2.0",
66
+ "react-dom": "^18.2.0"
67
+ },
68
+ "sideEffects": [
69
+ "**/*.css"
70
+ ]
71
+ }