@buildingbite/blocks 0.1.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.js ADDED
@@ -0,0 +1,2514 @@
1
+ 'use strict';
2
+
3
+ var React9 = require('react');
4
+ var lucideReact = require('lucide-react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var core = require('@dnd-kit/core');
7
+ var sortable = require('@dnd-kit/sortable');
8
+ var utilities = require('@dnd-kit/utilities');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var React9__default = /*#__PURE__*/_interopDefault(React9);
13
+
14
+ // src/BlockEditor.tsx
15
+
16
+ // src/types/blocks.ts
17
+ function createEmptyParagraphBlock() {
18
+ return {
19
+ id: generateBlockId(),
20
+ type: "paragraph",
21
+ content: [{ text: "" }]
22
+ };
23
+ }
24
+ function generateBlockId() {
25
+ return `block_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
26
+ }
27
+ function textNodesToPlainText(nodes) {
28
+ return nodes.map((node) => node.text).join("");
29
+ }
30
+ function generatePreviewFromBlocks(blocks, maxLength = 150) {
31
+ const textParts = [];
32
+ for (const block of blocks) {
33
+ if (textParts.join(" ").length >= maxLength) break;
34
+ switch (block.type) {
35
+ case "paragraph":
36
+ case "heading":
37
+ case "quote":
38
+ textParts.push(textNodesToPlainText(block.content));
39
+ break;
40
+ case "dialogue":
41
+ for (const line of block.lines) {
42
+ textParts.push(`${line.speaker}: ${textNodesToPlainText(line.content)}`);
43
+ }
44
+ break;
45
+ case "list":
46
+ textParts.push(textNodesToPlainText(block.content));
47
+ break;
48
+ }
49
+ }
50
+ const fullText = textParts.join(" ").replace(/\s+/g, " ").trim();
51
+ return fullText.slice(0, maxLength);
52
+ }
53
+ function countBlocksCharacters(blocks) {
54
+ let count = 0;
55
+ for (const block of blocks) {
56
+ switch (block.type) {
57
+ case "paragraph":
58
+ case "heading":
59
+ case "quote":
60
+ count += textNodesToPlainText(block.content).length;
61
+ break;
62
+ case "dialogue":
63
+ for (const line of block.lines) {
64
+ count += line.speaker.length + textNodesToPlainText(line.content).length;
65
+ }
66
+ break;
67
+ case "list":
68
+ count += textNodesToPlainText(block.content).length;
69
+ break;
70
+ }
71
+ }
72
+ return count;
73
+ }
74
+ var menuItems = [
75
+ {
76
+ type: "paragraph",
77
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Type, { size: 20 }),
78
+ label: "\uBCF8\uBB38",
79
+ description: "\uC77C\uBC18 \uD14D\uC2A4\uD2B8",
80
+ category: "basic"
81
+ },
82
+ {
83
+ type: "heading",
84
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading2, { size: 20 }),
85
+ label: "\uC81C\uBAA9",
86
+ description: "\uC139\uC158 \uC81C\uBAA9",
87
+ category: "basic"
88
+ },
89
+ {
90
+ type: "quote",
91
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Quote, { size: 20 }),
92
+ label: "\uC778\uC6A9\uBB38",
93
+ description: "\uC778\uC6A9 \uD14D\uC2A4\uD2B8",
94
+ category: "basic"
95
+ },
96
+ {
97
+ type: "list",
98
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { size: 20 }),
99
+ label: "\uBAA9\uB85D",
100
+ description: "\uAE00\uBA38\uB9AC \uBAA9\uB85D",
101
+ category: "basic"
102
+ },
103
+ {
104
+ type: "divider",
105
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minus, { size: 20 }),
106
+ label: "\uAD6C\uBD84\uC120",
107
+ description: "\uC139\uC158 \uAD6C\uBD84",
108
+ category: "basic"
109
+ },
110
+ {
111
+ type: "image",
112
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { size: 20 }),
113
+ label: "\uC774\uBBF8\uC9C0",
114
+ description: "\uB2E8\uC77C \uC774\uBBF8\uC9C0",
115
+ category: "basic"
116
+ },
117
+ {
118
+ type: "image-gallery",
119
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Images, { size: 20 }),
120
+ label: "\uC774\uBBF8\uC9C0 \uAC24\uB7EC\uB9AC",
121
+ description: "\uC5EC\uB7EC \uC774\uBBF8\uC9C0 \uADF8\uB9AC\uB4DC/\uC2AC\uB77C\uC774\uB4DC",
122
+ category: "advanced"
123
+ },
124
+ {
125
+ type: "link-card",
126
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { size: 20 }),
127
+ label: "\uB9C1\uD06C \uCE74\uB4DC",
128
+ description: "OG \uBBF8\uB9AC\uBCF4\uAE30 \uCE74\uB4DC",
129
+ category: "advanced"
130
+ },
131
+ {
132
+ type: "link-embed",
133
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Youtube, { size: 20 }),
134
+ label: "\uC784\uBCA0\uB4DC",
135
+ description: "YouTube, Twitter \uB4F1",
136
+ category: "advanced"
137
+ },
138
+ {
139
+ type: "dialogue",
140
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, { size: 20 }),
141
+ label: "\uB300\uC0AC",
142
+ description: "\uCE90\uB9AD\uD130 \uB300\uD654",
143
+ category: "advanced"
144
+ }
145
+ ];
146
+ function BlockMenu({ position, onClose, onSelect }) {
147
+ const menuRef = React9.useRef(null);
148
+ React9.useEffect(() => {
149
+ const handleClickOutside = (e) => {
150
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
151
+ onClose();
152
+ }
153
+ };
154
+ const handleKeyDown = (e) => {
155
+ if (e.key === "Escape") {
156
+ onClose();
157
+ }
158
+ };
159
+ document.addEventListener("mousedown", handleClickOutside);
160
+ document.addEventListener("keydown", handleKeyDown);
161
+ return () => {
162
+ document.removeEventListener("mousedown", handleClickOutside);
163
+ document.removeEventListener("keydown", handleKeyDown);
164
+ };
165
+ }, [onClose]);
166
+ const handleSelect = (item) => {
167
+ onSelect(item.type);
168
+ };
169
+ const adjustedPosition = {
170
+ x: Math.min(position.x, typeof window !== "undefined" ? window.innerWidth - 280 : position.x),
171
+ y: Math.min(position.y, typeof window !== "undefined" ? window.innerHeight - 400 : position.y)
172
+ };
173
+ const basicItems = menuItems.filter((item) => item.category === "basic");
174
+ const advancedItems = menuItems.filter((item) => item.category === "advanced");
175
+ return /* @__PURE__ */ jsxRuntime.jsxs(
176
+ "div",
177
+ {
178
+ ref: menuRef,
179
+ className: "fixed z-50 bg-white rounded-xl shadow-xl border border-gray-200 py-2 w-64 max-h-96 overflow-y-auto",
180
+ style: {
181
+ left: adjustedPosition.x,
182
+ top: adjustedPosition.y
183
+ },
184
+ children: [
185
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-gray-400 uppercase", children: "\uBE14\uB85D \uCD94\uAC00" }) }),
186
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "divide-y divide-gray-100", children: [
187
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-1", children: [
188
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-3 py-1 text-xs text-gray-400", children: "\uAE30\uBCF8" }),
189
+ basicItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
190
+ "button",
191
+ {
192
+ onClick: () => handleSelect(item),
193
+ className: "w-full px-3 py-2 flex items-center gap-3 hover:bg-gray-50 transition-colors",
194
+ children: [
195
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: item.icon }),
196
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-left", children: [
197
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-900", children: item.label }),
198
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: item.description })
199
+ ] })
200
+ ]
201
+ },
202
+ item.type
203
+ ))
204
+ ] }),
205
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-1", children: [
206
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-3 py-1 text-xs text-gray-400", children: "\uACE0\uAE09" }),
207
+ advancedItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
208
+ "button",
209
+ {
210
+ onClick: () => handleSelect(item),
211
+ className: "w-full px-3 py-2 flex items-center gap-3 hover:bg-gray-50 transition-colors",
212
+ children: [
213
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: item.icon }),
214
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-left", children: [
215
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-900", children: item.label }),
216
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: item.description })
217
+ ] })
218
+ ]
219
+ },
220
+ item.type
221
+ ))
222
+ ] })
223
+ ] })
224
+ ]
225
+ }
226
+ );
227
+ }
228
+ var EditorContext = React9.createContext(null);
229
+ function EditorProvider({ services, children }) {
230
+ return /* @__PURE__ */ jsxRuntime.jsx(EditorContext.Provider, { value: services, children });
231
+ }
232
+ function useEditorServices() {
233
+ const context = React9.useContext(EditorContext);
234
+ if (!context) {
235
+ throw new Error("useEditorServices must be used within an EditorProvider");
236
+ }
237
+ return context;
238
+ }
239
+ function useOptionalEditorServices() {
240
+ return React9.useContext(EditorContext);
241
+ }
242
+ function DefaultImage({
243
+ src,
244
+ alt,
245
+ fill,
246
+ width,
247
+ height,
248
+ className,
249
+ style
250
+ }) {
251
+ if (fill) {
252
+ return /* @__PURE__ */ jsxRuntime.jsx(
253
+ "img",
254
+ {
255
+ src,
256
+ alt,
257
+ className,
258
+ style: {
259
+ ...style,
260
+ position: "absolute",
261
+ width: "100%",
262
+ height: "100%",
263
+ objectFit: "cover"
264
+ }
265
+ }
266
+ );
267
+ }
268
+ return /* @__PURE__ */ jsxRuntime.jsx(
269
+ "img",
270
+ {
271
+ src,
272
+ alt,
273
+ width,
274
+ height,
275
+ className,
276
+ style
277
+ }
278
+ );
279
+ }
280
+ function textNodesToHtml(nodes) {
281
+ return nodes.map((node) => {
282
+ let html = node.text;
283
+ html = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
284
+ if (node.bold) html = `<strong>${html}</strong>`;
285
+ if (node.italic) html = `<em>${html}</em>`;
286
+ if (node.underline) html = `<u>${html}</u>`;
287
+ if (node.strikethrough) html = `<s>${html}</s>`;
288
+ if (node.link) html = `<a href="${node.link}" class="text-blue-600 underline">${html}</a>`;
289
+ return html;
290
+ }).join("");
291
+ }
292
+ function htmlToTextNodes(html) {
293
+ const div = document.createElement("div");
294
+ div.innerHTML = html;
295
+ const text = div.textContent || "";
296
+ return [{ text }];
297
+ }
298
+ function TextBlockEditor({ block, onUpdate, onMergeWithPrevious, onSplitBlock, onPasteBlocks, onDeleteEmptyBlock, onFocusPrevious, onFocusNext, isOnlyBlock }) {
299
+ const editorRef = React9.useRef(null);
300
+ const [uploading, setUploading] = React9.useState(false);
301
+ const isComposingRef = React9.useRef(false);
302
+ const lastContentRef = React9.useRef("");
303
+ const services = useEditorServices();
304
+ const ImageComponent = services.ImageComponent || DefaultImage;
305
+ const isCursorAtStart = React9.useCallback(() => {
306
+ const selection = window.getSelection();
307
+ if (!selection || selection.rangeCount === 0) return false;
308
+ const range = selection.getRangeAt(0);
309
+ if (!range.collapsed) return false;
310
+ const editor = editorRef.current;
311
+ if (!editor) return false;
312
+ if (range.startOffset === 0) {
313
+ const startNode = range.startContainer;
314
+ if (startNode === editor || startNode === editor.firstChild) {
315
+ return true;
316
+ }
317
+ if (startNode.nodeType === Node.TEXT_NODE) {
318
+ let node = startNode;
319
+ while (node.previousSibling === null && node.parentNode && node.parentNode !== editor) {
320
+ node = node.parentNode;
321
+ }
322
+ if (node.previousSibling === null) return true;
323
+ }
324
+ }
325
+ return false;
326
+ }, []);
327
+ const isCursorAtEnd = React9.useCallback(() => {
328
+ const selection = window.getSelection();
329
+ if (!selection || selection.rangeCount === 0) return false;
330
+ const range = selection.getRangeAt(0);
331
+ if (!range.collapsed) return false;
332
+ const editor = editorRef.current;
333
+ if (!editor) return false;
334
+ const endNode = range.endContainer;
335
+ const endOffset = range.endOffset;
336
+ if (endNode.nodeType === Node.TEXT_NODE) {
337
+ if (endOffset !== endNode.textContent?.length) return false;
338
+ let node = endNode;
339
+ while (node.nextSibling === null && node.parentNode && node.parentNode !== editor) {
340
+ node = node.parentNode;
341
+ }
342
+ if (node.nextSibling === null) return true;
343
+ } else if (endNode === editor) {
344
+ return endOffset === editor.childNodes.length;
345
+ }
346
+ return false;
347
+ }, []);
348
+ if (block.type === "image") {
349
+ const imageBlock = block;
350
+ const handleUpload = async (e) => {
351
+ const file = e.target.files?.[0];
352
+ if (!file) return;
353
+ try {
354
+ setUploading(true);
355
+ const url = await services.uploadImage(file, "editor");
356
+ onUpdate({
357
+ image: { ...imageBlock.image, url }
358
+ });
359
+ } catch (error) {
360
+ console.error("Image upload error:", error);
361
+ alert("\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
362
+ } finally {
363
+ setUploading(false);
364
+ }
365
+ };
366
+ const handleRemove = () => {
367
+ onUpdate({ image: { url: "", alt: "", caption: "" } });
368
+ };
369
+ const handleAlignmentChange = (alignment) => {
370
+ onUpdate({ alignment });
371
+ };
372
+ if (!imageBlock.image.url) {
373
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col items-center justify-center w-full h-40 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-gray-400 hover:bg-gray-50 transition-colors", children: [
374
+ /* @__PURE__ */ jsxRuntime.jsx(
375
+ "input",
376
+ {
377
+ type: "file",
378
+ accept: "image/*",
379
+ onChange: handleUpload,
380
+ className: "hidden",
381
+ disabled: uploading
382
+ }
383
+ ),
384
+ uploading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
385
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 border-2 border-gray-400 border-t-transparent rounded-full animate-spin mx-auto mb-2" }),
386
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: "\uC5C5\uB85C\uB4DC \uC911..." })
387
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
388
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { size: 24, className: "mx-auto mb-2 text-gray-400" }),
389
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: "\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC" })
390
+ ] })
391
+ ] });
392
+ }
393
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
394
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsxs(
395
+ "div",
396
+ {
397
+ className: `relative ${imageBlock.alignment === "full-width" ? "w-full" : "max-w-md"} ${imageBlock.alignment === "left" ? "mr-auto" : imageBlock.alignment === "right" ? "ml-auto" : "mx-auto"}`,
398
+ children: [
399
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative aspect-video", children: /* @__PURE__ */ jsxRuntime.jsx(
400
+ ImageComponent,
401
+ {
402
+ src: imageBlock.image.url,
403
+ alt: imageBlock.image.alt || "",
404
+ fill: true,
405
+ className: "object-contain rounded-lg"
406
+ }
407
+ ) }),
408
+ /* @__PURE__ */ jsxRuntime.jsx(
409
+ "button",
410
+ {
411
+ onClick: handleRemove,
412
+ className: "absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600",
413
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
414
+ }
415
+ )
416
+ ]
417
+ }
418
+ ) }),
419
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1", children: [
420
+ /* @__PURE__ */ jsxRuntime.jsx(
421
+ "button",
422
+ {
423
+ onClick: () => handleAlignmentChange("left"),
424
+ className: `p-1.5 rounded ${imageBlock.alignment === "left" ? "bg-gray-200" : "hover:bg-gray-100"}`,
425
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignLeft, { size: 14 })
426
+ }
427
+ ),
428
+ /* @__PURE__ */ jsxRuntime.jsx(
429
+ "button",
430
+ {
431
+ onClick: () => handleAlignmentChange("center"),
432
+ className: `p-1.5 rounded ${imageBlock.alignment === "center" ? "bg-gray-200" : "hover:bg-gray-100"}`,
433
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignCenter, { size: 14 })
434
+ }
435
+ ),
436
+ /* @__PURE__ */ jsxRuntime.jsx(
437
+ "button",
438
+ {
439
+ onClick: () => handleAlignmentChange("right"),
440
+ className: `p-1.5 rounded ${imageBlock.alignment === "right" ? "bg-gray-200" : "hover:bg-gray-100"}`,
441
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignRight, { size: 14 })
442
+ }
443
+ ),
444
+ /* @__PURE__ */ jsxRuntime.jsx(
445
+ "button",
446
+ {
447
+ onClick: () => handleAlignmentChange("full-width"),
448
+ className: `p-1.5 rounded ${imageBlock.alignment === "full-width" ? "bg-gray-200" : "hover:bg-gray-100"}`,
449
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, { size: 14 })
450
+ }
451
+ )
452
+ ] }),
453
+ /* @__PURE__ */ jsxRuntime.jsx(
454
+ "input",
455
+ {
456
+ type: "text",
457
+ value: imageBlock.image.caption || "",
458
+ onChange: (e) => onUpdate({ image: { ...imageBlock.image, caption: e.target.value } }),
459
+ placeholder: "\uCEA1\uC158 (\uC120\uD0DD)",
460
+ className: "w-full text-center text-sm text-gray-500 border-none outline-none"
461
+ }
462
+ )
463
+ ] });
464
+ }
465
+ const isListBlock = block.type === "list";
466
+ const textBlock = block;
467
+ const content = textBlock.content;
468
+ React9.useEffect(() => {
469
+ if (editorRef.current && !isComposingRef.current) {
470
+ const html = textNodesToHtml(content);
471
+ const currentHtml = editorRef.current.innerHTML;
472
+ if (html !== lastContentRef.current && currentHtml !== html) {
473
+ editorRef.current.innerHTML = html || "";
474
+ lastContentRef.current = html;
475
+ }
476
+ }
477
+ }, [content]);
478
+ const handlePaste = React9.useCallback((e) => {
479
+ const text = e.clipboardData.getData("text/plain");
480
+ const lines = text.split("\n");
481
+ if (lines.length <= 1 || !onPasteBlocks) return;
482
+ e.preventDefault();
483
+ const selection = window.getSelection();
484
+ if (!selection || selection.rangeCount === 0) return;
485
+ const range = selection.getRangeAt(0);
486
+ const editor = editorRef.current;
487
+ if (!editor) return;
488
+ if (!range.collapsed) {
489
+ range.deleteContents();
490
+ }
491
+ const beforeRange = document.createRange();
492
+ beforeRange.setStart(editor, 0);
493
+ beforeRange.setEnd(range.startContainer, range.startOffset);
494
+ const beforeText = beforeRange.toString();
495
+ const afterRange = document.createRange();
496
+ afterRange.setStart(range.endContainer, range.endOffset);
497
+ afterRange.setEnd(editor, editor.childNodes.length);
498
+ const afterText = afterRange.toString();
499
+ const firstLineText = beforeText + lines[0];
500
+ const firstLineHtml = textNodesToHtml([{ text: firstLineText }]);
501
+ editor.innerHTML = firstLineHtml;
502
+ lastContentRef.current = firstLineHtml;
503
+ onPasteBlocks(
504
+ [{ text: firstLineText }],
505
+ lines.slice(1, -1),
506
+ [{ text: lines[lines.length - 1] + afterText }]
507
+ );
508
+ }, [onPasteBlocks]);
509
+ const handleKeyDown = React9.useCallback((e) => {
510
+ if (e.key === "Delete" || e.key === "Backspace") {
511
+ const sel = window.getSelection();
512
+ if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
513
+ const range = sel.getRangeAt(0);
514
+ const editor = editorRef.current;
515
+ if (editor && (!editor.contains(range.startContainer) || !editor.contains(range.endContainer))) {
516
+ return;
517
+ }
518
+ }
519
+ }
520
+ if ((e.ctrlKey || e.metaKey) && e.key === "b") {
521
+ e.preventDefault();
522
+ document.execCommand("bold");
523
+ return;
524
+ }
525
+ if ((e.ctrlKey || e.metaKey) && e.key === "i") {
526
+ e.preventDefault();
527
+ document.execCommand("italic");
528
+ return;
529
+ }
530
+ if ((e.ctrlKey || e.metaKey) && e.key === "u") {
531
+ e.preventDefault();
532
+ document.execCommand("underline");
533
+ return;
534
+ }
535
+ if (e.key === "ArrowUp" && onFocusPrevious && isCursorAtStart()) {
536
+ e.preventDefault();
537
+ onFocusPrevious();
538
+ return;
539
+ }
540
+ if (e.key === "ArrowDown" && onFocusNext && isCursorAtEnd()) {
541
+ e.preventDefault();
542
+ onFocusNext();
543
+ return;
544
+ }
545
+ if (e.key === "Enter" && !e.shiftKey && onSplitBlock) {
546
+ if (isComposingRef.current) return;
547
+ e.preventDefault();
548
+ const selection = window.getSelection();
549
+ if (!selection || selection.rangeCount === 0) return;
550
+ const range = selection.getRangeAt(0);
551
+ const editor = editorRef.current;
552
+ if (!editor) return;
553
+ if (!range.collapsed) {
554
+ range.deleteContents();
555
+ }
556
+ const beforeRange = document.createRange();
557
+ beforeRange.setStart(editor, 0);
558
+ beforeRange.setEnd(range.startContainer, range.startOffset);
559
+ const afterRange = document.createRange();
560
+ afterRange.setStart(range.endContainer, range.endOffset);
561
+ afterRange.setEnd(editor, editor.childNodes.length);
562
+ const beforeText = beforeRange.toString();
563
+ const afterText = afterRange.toString();
564
+ const beforeHtml = textNodesToHtml([{ text: beforeText }]);
565
+ editor.innerHTML = beforeHtml;
566
+ lastContentRef.current = beforeHtml;
567
+ onSplitBlock(
568
+ [{ text: beforeText }],
569
+ [{ text: afterText }]
570
+ );
571
+ } else if (e.key === "Backspace" && onMergeWithPrevious) {
572
+ const selection = window.getSelection();
573
+ if (!selection || selection.rangeCount === 0) return;
574
+ const range = selection.getRangeAt(0);
575
+ if (range.collapsed && range.startOffset === 0) {
576
+ const editor = editorRef.current;
577
+ if (editor && (range.startContainer === editor.firstChild || range.startContainer === editor)) {
578
+ e.preventDefault();
579
+ onMergeWithPrevious();
580
+ }
581
+ }
582
+ } else if (e.key === "Delete" && onDeleteEmptyBlock) {
583
+ const editor = editorRef.current;
584
+ const text = editor?.textContent || "";
585
+ if (text.length === 0) {
586
+ e.preventDefault();
587
+ onDeleteEmptyBlock();
588
+ }
589
+ }
590
+ }, [onSplitBlock, onMergeWithPrevious, onDeleteEmptyBlock, onFocusPrevious, onFocusNext, isCursorAtStart, isCursorAtEnd]);
591
+ const handleInput = React9.useCallback(() => {
592
+ if (editorRef.current) {
593
+ const html = editorRef.current.innerHTML;
594
+ const nodes = htmlToTextNodes(html);
595
+ lastContentRef.current = textNodesToHtml(nodes);
596
+ onUpdate({ content: nodes });
597
+ }
598
+ }, [onUpdate]);
599
+ const handleCompositionStart = React9.useCallback(() => {
600
+ isComposingRef.current = true;
601
+ }, []);
602
+ const handleCompositionEnd = React9.useCallback(() => {
603
+ isComposingRef.current = false;
604
+ }, []);
605
+ const getBlockStyle = () => {
606
+ switch (block.type) {
607
+ case "heading":
608
+ const headingBlock = block;
609
+ switch (headingBlock.level) {
610
+ case 1:
611
+ return "text-3xl font-bold";
612
+ case 2:
613
+ return "text-2xl font-semibold";
614
+ case 3:
615
+ return "text-xl font-semibold";
616
+ default:
617
+ return "text-2xl font-semibold";
618
+ }
619
+ case "quote":
620
+ return "border-l-4 border-gray-300 pl-4 text-gray-600 italic";
621
+ default:
622
+ return "text-base";
623
+ }
624
+ };
625
+ const getPlaceholder = () => {
626
+ switch (block.type) {
627
+ case "heading":
628
+ return "\uC81C\uBAA9\uC744 \uC785\uB825\uD558\uC138\uC694...";
629
+ case "quote":
630
+ return "\uC778\uC6A9\uBB38\uC744 \uC785\uB825\uD558\uC138\uC694...";
631
+ case "list":
632
+ return "\uBAA9\uB85D \uD56D\uBAA9...";
633
+ default:
634
+ return isOnlyBlock ? "\uB0B4\uC6A9\uC744 \uC785\uB825\uD558\uC138\uC694..." : "";
635
+ }
636
+ };
637
+ if (isListBlock) {
638
+ const listBlock = block;
639
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
640
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 select-none text-gray-500 mt-0.5", children: listBlock.listType === "ordered" ? "1." : "\u2022" }),
641
+ /* @__PURE__ */ jsxRuntime.jsx(
642
+ "div",
643
+ {
644
+ ref: editorRef,
645
+ contentEditable: true,
646
+ suppressContentEditableWarning: true,
647
+ onInput: handleInput,
648
+ onKeyDown: handleKeyDown,
649
+ onPaste: handlePaste,
650
+ onCompositionStart: handleCompositionStart,
651
+ onCompositionEnd: handleCompositionEnd,
652
+ "data-placeholder": getPlaceholder(),
653
+ className: "flex-1 outline-none min-h-[1.5em] empty:before:content-[attr(data-placeholder)] empty:before:text-gray-300"
654
+ }
655
+ )
656
+ ] });
657
+ }
658
+ return /* @__PURE__ */ jsxRuntime.jsx(
659
+ "div",
660
+ {
661
+ ref: editorRef,
662
+ contentEditable: true,
663
+ suppressContentEditableWarning: true,
664
+ onInput: handleInput,
665
+ onKeyDown: handleKeyDown,
666
+ onPaste: handlePaste,
667
+ onCompositionStart: handleCompositionStart,
668
+ onCompositionEnd: handleCompositionEnd,
669
+ "data-placeholder": getPlaceholder(),
670
+ className: `outline-none min-h-[1.5em] ${getBlockStyle()} empty:before:content-[attr(data-placeholder)] empty:before:text-gray-300`
671
+ }
672
+ );
673
+ }
674
+ function ImageGalleryEditor({ block, onUpdate }) {
675
+ const [uploading, setUploading] = React9.useState(false);
676
+ const services = useEditorServices();
677
+ const ImageComponent = services.ImageComponent || DefaultImage;
678
+ const handleUpload = async (e) => {
679
+ const files = e.target.files;
680
+ if (!files || files.length === 0) return;
681
+ try {
682
+ setUploading(true);
683
+ const uploadedImages = [];
684
+ for (const file of Array.from(files)) {
685
+ const url = await services.uploadImage(file, "gallery");
686
+ uploadedImages.push({
687
+ url,
688
+ alt: "",
689
+ caption: ""
690
+ });
691
+ }
692
+ onUpdate({
693
+ images: [...block.images, ...uploadedImages]
694
+ });
695
+ } catch (error) {
696
+ console.error("Image upload error:", error);
697
+ alert("\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
698
+ } finally {
699
+ setUploading(false);
700
+ }
701
+ };
702
+ const handleRemoveImage = (index) => {
703
+ const newImages = block.images.filter((_, i) => i !== index);
704
+ onUpdate({ images: newImages });
705
+ };
706
+ const handleLayoutChange = (layout) => {
707
+ onUpdate({ layout });
708
+ };
709
+ const handleColumnsChange = (columns) => {
710
+ onUpdate({ columns });
711
+ };
712
+ const handleImageCaptionChange = (index, caption) => {
713
+ const newImages = [...block.images];
714
+ newImages[index] = { ...newImages[index], caption };
715
+ onUpdate({ images: newImages });
716
+ };
717
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
718
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
719
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 bg-gray-100 rounded-lg p-1", children: [
720
+ /* @__PURE__ */ jsxRuntime.jsxs(
721
+ "button",
722
+ {
723
+ onClick: () => handleLayoutChange("grid"),
724
+ className: `p-2 rounded flex items-center gap-1 text-sm ${block.layout === "grid" ? "bg-white shadow" : "hover:bg-gray-200"}`,
725
+ title: "\uADF8\uB9AC\uB4DC",
726
+ children: [
727
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Grid, { size: 16 }),
728
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uADF8\uB9AC\uB4DC" })
729
+ ]
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsxRuntime.jsxs(
733
+ "button",
734
+ {
735
+ onClick: () => handleLayoutChange("horizontal-scroll"),
736
+ className: `p-2 rounded flex items-center gap-1 text-sm ${block.layout === "horizontal-scroll" ? "bg-white shadow" : "hover:bg-gray-200"}`,
737
+ title: "\uAC00\uB85C \uC2A4\uD06C\uB864",
738
+ children: [
739
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoveHorizontal, { size: 16 }),
740
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uAC00\uB85C \uC2A4\uD06C\uB864" })
741
+ ]
742
+ }
743
+ ),
744
+ /* @__PURE__ */ jsxRuntime.jsxs(
745
+ "button",
746
+ {
747
+ onClick: () => handleLayoutChange("masonry"),
748
+ className: `p-2 rounded flex items-center gap-1 text-sm ${block.layout === "masonry" ? "bg-white shadow" : "hover:bg-gray-200"}`,
749
+ title: "\uB9E4\uC18C\uB2C8",
750
+ children: [
751
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutGrid, { size: 16 }),
752
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uB9E4\uC18C\uB2C8" })
753
+ ]
754
+ }
755
+ )
756
+ ] }),
757
+ (block.layout === "grid" || block.layout === "masonry") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
758
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: "\uC5F4:" }),
759
+ /* @__PURE__ */ jsxRuntime.jsxs(
760
+ "select",
761
+ {
762
+ value: block.columns || 2,
763
+ onChange: (e) => handleColumnsChange(parseInt(e.target.value)),
764
+ className: "px-2 py-1 border border-gray-200 rounded text-sm",
765
+ children: [
766
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 2, children: "2\uC5F4" }),
767
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 3, children: "3\uC5F4" }),
768
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 4, children: "4\uC5F4" })
769
+ ]
770
+ }
771
+ )
772
+ ] })
773
+ ] }),
774
+ block.images.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
775
+ "div",
776
+ {
777
+ className: `grid gap-2 ${block.layout === "horizontal-scroll" ? "grid-flow-col auto-cols-[200px] overflow-x-auto pb-2" : block.columns === 2 ? "grid-cols-2" : block.columns === 3 ? "grid-cols-3" : "grid-cols-4"}`,
778
+ children: block.images.map((image, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group", children: [
779
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative aspect-square", children: [
780
+ /* @__PURE__ */ jsxRuntime.jsx(
781
+ ImageComponent,
782
+ {
783
+ src: image.url,
784
+ alt: image.alt || "",
785
+ fill: true,
786
+ className: "object-cover rounded-lg"
787
+ }
788
+ ),
789
+ /* @__PURE__ */ jsxRuntime.jsx(
790
+ "button",
791
+ {
792
+ onClick: () => handleRemoveImage(index),
793
+ className: "absolute top-1 right-1 p-1 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity",
794
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 })
795
+ }
796
+ )
797
+ ] }),
798
+ /* @__PURE__ */ jsxRuntime.jsx(
799
+ "input",
800
+ {
801
+ type: "text",
802
+ value: image.caption || "",
803
+ onChange: (e) => handleImageCaptionChange(index, e.target.value),
804
+ placeholder: "\uCEA1\uC158",
805
+ className: "w-full mt-1 px-2 py-1 text-xs border border-gray-200 rounded"
806
+ }
807
+ )
808
+ ] }, index))
809
+ }
810
+ ),
811
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-gray-400 hover:bg-gray-50 transition-colors", children: [
812
+ /* @__PURE__ */ jsxRuntime.jsx(
813
+ "input",
814
+ {
815
+ type: "file",
816
+ accept: "image/*",
817
+ multiple: true,
818
+ onChange: handleUpload,
819
+ className: "hidden",
820
+ disabled: uploading
821
+ }
822
+ ),
823
+ uploading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
824
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 border-2 border-gray-400 border-t-transparent rounded-full animate-spin mx-auto mb-2" }),
825
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: "\uC5C5\uB85C\uB4DC \uC911..." })
826
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
827
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { size: 24, className: "mx-auto mb-2 text-gray-400" }),
828
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: "\uC774\uBBF8\uC9C0 \uCD94\uAC00 (\uC5EC\uB7EC \uAC1C \uC120\uD0DD \uAC00\uB2A5)" })
829
+ ] })
830
+ ] })
831
+ ] });
832
+ }
833
+ function LinkCardEditor({ block, onUpdate }) {
834
+ const [urlInput, setUrlInput] = React9.useState(block.url || "");
835
+ const [loading, setLoading] = React9.useState(false);
836
+ const [error, setError] = React9.useState(null);
837
+ const services = useEditorServices();
838
+ const ImageComponent = services.ImageComponent || DefaultImage;
839
+ const fetchOGData = async (url) => {
840
+ try {
841
+ setLoading(true);
842
+ setError(null);
843
+ const ogData = await services.fetchOGData(url);
844
+ onUpdate({ url, ogData });
845
+ } catch (err) {
846
+ console.error("OG fetch error:", err);
847
+ setError("\uB9C1\uD06C \uC815\uBCF4\uB97C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
848
+ onUpdate({ url, ogData: void 0 });
849
+ } finally {
850
+ setLoading(false);
851
+ }
852
+ };
853
+ const handleSubmit = (e) => {
854
+ e.preventDefault();
855
+ if (urlInput.trim()) {
856
+ fetchOGData(urlInput.trim());
857
+ }
858
+ };
859
+ const handleRefresh = () => {
860
+ if (block.url) {
861
+ fetchOGData(block.url);
862
+ }
863
+ };
864
+ if (!block.url || !block.ogData && !loading) {
865
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "space-y-3", children: [
866
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
867
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 relative", children: [
868
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { size: 18, className: "absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" }),
869
+ /* @__PURE__ */ jsxRuntime.jsx(
870
+ "input",
871
+ {
872
+ type: "url",
873
+ value: urlInput,
874
+ onChange: (e) => setUrlInput(e.target.value),
875
+ placeholder: "https://example.com",
876
+ className: "w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200",
877
+ disabled: loading
878
+ }
879
+ )
880
+ ] }),
881
+ /* @__PURE__ */ jsxRuntime.jsx(
882
+ "button",
883
+ {
884
+ type: "submit",
885
+ disabled: loading || !urlInput.trim(),
886
+ className: "px-4 py-2.5 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed",
887
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 18, className: "animate-spin" }) : "\uD655\uC778"
888
+ }
889
+ )
890
+ ] }),
891
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-red-500", children: error })
892
+ ] });
893
+ }
894
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
895
+ /* @__PURE__ */ jsxRuntime.jsx(
896
+ "a",
897
+ {
898
+ href: block.url,
899
+ target: "_blank",
900
+ rel: "noopener noreferrer",
901
+ className: "block border border-gray-200 rounded-lg overflow-hidden hover:border-gray-300 transition-colors",
902
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
903
+ block.ogData?.image && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative w-40 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsxRuntime.jsx(
904
+ ImageComponent,
905
+ {
906
+ src: block.ogData.image,
907
+ alt: "",
908
+ fill: true,
909
+ className: "object-cover"
910
+ }
911
+ ) }) }),
912
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 p-4", children: [
913
+ block.ogData?.siteName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mb-1", children: block.ogData.siteName }),
914
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-medium text-gray-900 line-clamp-2 mb-1", children: block.ogData?.title || block.url }),
915
+ block.ogData?.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 line-clamp-2", children: block.ogData.description }),
916
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 mt-2 text-xs text-gray-400", children: [
917
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { size: 12 }),
918
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: new URL(block.url).hostname })
919
+ ] })
920
+ ] })
921
+ ] })
922
+ }
923
+ ),
924
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
925
+ /* @__PURE__ */ jsxRuntime.jsxs(
926
+ "button",
927
+ {
928
+ onClick: handleRefresh,
929
+ disabled: loading,
930
+ className: "flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700",
931
+ children: [
932
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 14, className: loading ? "animate-spin" : "" }),
933
+ "\uC0C8\uB85C\uACE0\uCE68"
934
+ ]
935
+ }
936
+ ),
937
+ /* @__PURE__ */ jsxRuntime.jsx(
938
+ "button",
939
+ {
940
+ onClick: () => {
941
+ setUrlInput("");
942
+ onUpdate({ url: "", ogData: void 0 });
943
+ },
944
+ className: "text-sm text-red-500 hover:text-red-600",
945
+ children: "\uBCC0\uACBD"
946
+ }
947
+ )
948
+ ] })
949
+ ] });
950
+ }
951
+ function extractYouTubeId(url) {
952
+ const patterns = [
953
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
954
+ /youtube\.com\/shorts\/([^&\n?#]+)/
955
+ ];
956
+ for (const pattern of patterns) {
957
+ const match = url.match(pattern);
958
+ if (match) return match[1];
959
+ }
960
+ return null;
961
+ }
962
+ function detectEmbedType(url) {
963
+ if (url.includes("youtube.com") || url.includes("youtu.be")) {
964
+ return "youtube";
965
+ }
966
+ if (url.includes("twitter.com") || url.includes("x.com")) {
967
+ return "twitter";
968
+ }
969
+ return "generic";
970
+ }
971
+ function LinkEmbedEditor({ block, onUpdate }) {
972
+ const [urlInput, setUrlInput] = React9.useState(block.url || "");
973
+ const [error, setError] = React9.useState(null);
974
+ const handleSubmit = (e) => {
975
+ e.preventDefault();
976
+ const url = urlInput.trim();
977
+ if (!url) return;
978
+ const embedType = detectEmbedType(url);
979
+ setError(null);
980
+ if (embedType === "youtube") {
981
+ const videoId = extractYouTubeId(url);
982
+ if (videoId) {
983
+ onUpdate({ url, embedType, videoId });
984
+ } else {
985
+ setError("\uC62C\uBC14\uB978 YouTube URL\uC774 \uC544\uB2D9\uB2C8\uB2E4.");
986
+ }
987
+ } else if (embedType === "twitter") {
988
+ onUpdate({ url, embedType });
989
+ } else {
990
+ onUpdate({ url, embedType: "generic" });
991
+ }
992
+ };
993
+ const renderEmbed = () => {
994
+ switch (block.embedType) {
995
+ case "youtube":
996
+ if (block.videoId) {
997
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative w-full aspect-video rounded-lg overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
998
+ "iframe",
999
+ {
1000
+ src: `https://www.youtube.com/embed/${block.videoId}`,
1001
+ title: "YouTube video",
1002
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
1003
+ allowFullScreen: true,
1004
+ className: "absolute inset-0 w-full h-full"
1005
+ }
1006
+ ) });
1007
+ }
1008
+ break;
1009
+ case "twitter":
1010
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-gray-200 rounded-lg p-4 bg-gray-50", children: [
1011
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [
1012
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Twitter, { size: 20 }),
1013
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: "Twitter \uC784\uBCA0\uB4DC" })
1014
+ ] }),
1015
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-600 truncate", children: block.url })
1016
+ ] });
1017
+ case "generic":
1018
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-gray-200 rounded-lg p-4 bg-gray-50", children: [
1019
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [
1020
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { size: 20 }),
1021
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: "\uC678\uBD80 \uB9C1\uD06C" })
1022
+ ] }),
1023
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-600 truncate", children: block.url })
1024
+ ] });
1025
+ }
1026
+ return null;
1027
+ };
1028
+ if (!block.url) {
1029
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "space-y-3", children: [
1030
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1031
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 relative", children: [
1032
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Youtube, { size: 18, className: "absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" }),
1033
+ /* @__PURE__ */ jsxRuntime.jsx(
1034
+ "input",
1035
+ {
1036
+ type: "url",
1037
+ value: urlInput,
1038
+ onChange: (e) => setUrlInput(e.target.value),
1039
+ placeholder: "YouTube, Twitter URL\uC744 \uC785\uB825\uD558\uC138\uC694",
1040
+ className: "w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200"
1041
+ }
1042
+ )
1043
+ ] }),
1044
+ /* @__PURE__ */ jsxRuntime.jsx(
1045
+ "button",
1046
+ {
1047
+ type: "submit",
1048
+ disabled: !urlInput.trim(),
1049
+ className: "px-4 py-2.5 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed",
1050
+ children: "\uD655\uC778"
1051
+ }
1052
+ )
1053
+ ] }),
1054
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-red-500", children: error }),
1055
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 text-xs text-gray-400", children: [
1056
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
1057
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Youtube, { size: 14 }),
1058
+ " YouTube"
1059
+ ] }),
1060
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
1061
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Twitter, { size: 14 }),
1062
+ " Twitter/X"
1063
+ ] })
1064
+ ] })
1065
+ ] });
1066
+ }
1067
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1068
+ renderEmbed(),
1069
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
1070
+ "button",
1071
+ {
1072
+ onClick: () => {
1073
+ setUrlInput("");
1074
+ onUpdate({ url: "", videoId: void 0 });
1075
+ },
1076
+ className: "text-sm text-red-500 hover:text-red-600",
1077
+ children: "\uBCC0\uACBD"
1078
+ }
1079
+ ) })
1080
+ ] });
1081
+ }
1082
+ var BUBBLE_COLORS = [
1083
+ "#FEE500",
1084
+ // 카카오 노란색
1085
+ "#A8D8EA",
1086
+ // 하늘색
1087
+ "#FFB7B2",
1088
+ // 연분홍
1089
+ "#BAFFC9",
1090
+ // 연두색
1091
+ "#E2D1F9",
1092
+ // 연보라
1093
+ "#FFFFFF"
1094
+ // 흰색
1095
+ ];
1096
+ function DialogueEditor({ block, onUpdate }) {
1097
+ const fileInputRefs = React9.useRef([]);
1098
+ const [uploadingIndices, setUploadingIndices] = React9.useState(/* @__PURE__ */ new Set());
1099
+ const [characters, setCharacters] = React9.useState([]);
1100
+ const [showProfileMenu, setShowProfileMenu] = React9.useState(null);
1101
+ const services = useEditorServices();
1102
+ const ImageComponent = services.ImageComponent || DefaultImage;
1103
+ React9.useEffect(() => {
1104
+ if (services.getCharacters) {
1105
+ services.getCharacters().then(setCharacters).catch(console.error);
1106
+ }
1107
+ }, [services]);
1108
+ React9.useEffect(() => {
1109
+ if (showProfileMenu === null) return;
1110
+ const handleClickOutside = (e) => {
1111
+ const target = e.target;
1112
+ if (!target.closest(".profile-menu-container")) {
1113
+ setShowProfileMenu(null);
1114
+ }
1115
+ };
1116
+ document.addEventListener("click", handleClickOutside);
1117
+ return () => document.removeEventListener("click", handleClickOutside);
1118
+ }, [showProfileMenu]);
1119
+ const handleSelectCharacter = (index, character) => {
1120
+ handleUpdateLine(index, {
1121
+ speaker: character.name,
1122
+ profileImage: character.profileImage || void 0,
1123
+ speakerColor: character.defaultColor
1124
+ });
1125
+ setShowProfileMenu(null);
1126
+ };
1127
+ const handleAddLine = () => {
1128
+ const newLine = {
1129
+ speaker: "",
1130
+ content: [{ text: "" }],
1131
+ speakerColor: block.style === "chat" ? "#FEE500" : void 0,
1132
+ alignment: "left"
1133
+ };
1134
+ onUpdate({
1135
+ lines: [...block.lines, newLine]
1136
+ });
1137
+ };
1138
+ const handleRemoveLine = (index) => {
1139
+ if (block.lines.length <= 1) return;
1140
+ const newLines = block.lines.filter((_, i) => i !== index);
1141
+ onUpdate({ lines: newLines });
1142
+ };
1143
+ const handleUpdateLine = (index, updates) => {
1144
+ const newLines = block.lines.map(
1145
+ (line, i) => i === index ? { ...line, ...updates } : line
1146
+ );
1147
+ onUpdate({ lines: newLines });
1148
+ };
1149
+ const handleSpeakerChange = (index, speaker) => {
1150
+ const existingLine = block.lines.find(
1151
+ (line, i) => i !== index && line.speaker.trim().toLowerCase() === speaker.trim().toLowerCase() && line.speaker.trim() !== ""
1152
+ );
1153
+ if (existingLine && speaker.trim() !== "") {
1154
+ handleUpdateLine(index, {
1155
+ speaker,
1156
+ profileImage: existingLine.profileImage,
1157
+ speakerColor: existingLine.speakerColor
1158
+ });
1159
+ } else {
1160
+ handleUpdateLine(index, { speaker });
1161
+ }
1162
+ };
1163
+ const handleContentChange = (index, text) => {
1164
+ handleUpdateLine(index, { content: [{ text }] });
1165
+ };
1166
+ const handleColorChange = (index, color) => {
1167
+ handleUpdateLine(index, { speakerColor: color || void 0 });
1168
+ };
1169
+ const handleAlignmentChange = (index, alignment) => {
1170
+ handleUpdateLine(index, { alignment });
1171
+ };
1172
+ const handleProfileImageUpload = async (index, file) => {
1173
+ setUploadingIndices((prev) => new Set(prev).add(index));
1174
+ try {
1175
+ const url = await services.uploadImage(file, "profile");
1176
+ handleUpdateLine(index, { profileImage: url });
1177
+ } catch (error) {
1178
+ console.error("Profile image upload error:", error);
1179
+ alert("\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
1180
+ } finally {
1181
+ setUploadingIndices((prev) => {
1182
+ const next = new Set(prev);
1183
+ next.delete(index);
1184
+ return next;
1185
+ });
1186
+ }
1187
+ };
1188
+ const handleRemoveProfileImage = (index) => {
1189
+ handleUpdateLine(index, { profileImage: void 0 });
1190
+ };
1191
+ const handleStyleChange = (style) => {
1192
+ const updatedLines = block.lines.map((line) => ({
1193
+ ...line,
1194
+ speakerColor: style === "chat" ? line.speakerColor || "#FEE500" : void 0,
1195
+ alignment: style === "chat" ? line.alignment || "left" : void 0
1196
+ }));
1197
+ onUpdate({ style, lines: updatedLines });
1198
+ };
1199
+ const getLineText = (nodes) => {
1200
+ return nodes.map((node) => node.text).join("");
1201
+ };
1202
+ const renderChatEditor = (line, index) => {
1203
+ const isUploading = uploadingIndices.has(index);
1204
+ const isRight = line.alignment === "right";
1205
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-gray-50 rounded-lg space-y-2", children: [
1206
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1207
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-gray-200 rounded p-0.5", children: [
1208
+ /* @__PURE__ */ jsxRuntime.jsx(
1209
+ "button",
1210
+ {
1211
+ onClick: () => handleAlignmentChange(index, "left"),
1212
+ className: `p-1 rounded ${!isRight ? "bg-white shadow" : "hover:bg-gray-300"}`,
1213
+ title: "\uC67C\uCABD \uC815\uB82C",
1214
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignLeft, { size: 14 })
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ jsxRuntime.jsx(
1218
+ "button",
1219
+ {
1220
+ onClick: () => handleAlignmentChange(index, "right"),
1221
+ className: `p-1 rounded ${isRight ? "bg-white shadow" : "hover:bg-gray-300"}`,
1222
+ title: "\uC624\uB978\uCABD \uC815\uB82C",
1223
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignRight, { size: 14 })
1224
+ }
1225
+ )
1226
+ ] }),
1227
+ /* @__PURE__ */ jsxRuntime.jsx(
1228
+ "input",
1229
+ {
1230
+ type: "text",
1231
+ value: line.speaker,
1232
+ onChange: (e) => handleSpeakerChange(index, e.target.value),
1233
+ placeholder: "\uD654\uC790 \uC774\uB984",
1234
+ className: "flex-1 px-2 py-1 text-sm font-medium border border-gray-200 rounded focus:outline-none focus:ring-2 focus:ring-gray-200"
1235
+ }
1236
+ ),
1237
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1", children: BUBBLE_COLORS.map((color) => /* @__PURE__ */ jsxRuntime.jsx(
1238
+ "button",
1239
+ {
1240
+ onClick: () => handleColorChange(index, color),
1241
+ className: `w-5 h-5 rounded-full border-2 transition-transform ${line.speakerColor === color ? "border-gray-600 scale-110" : "border-gray-300"}`,
1242
+ style: { backgroundColor: color },
1243
+ title: color
1244
+ },
1245
+ color
1246
+ )) }),
1247
+ /* @__PURE__ */ jsxRuntime.jsx(
1248
+ "button",
1249
+ {
1250
+ onClick: () => handleRemoveLine(index),
1251
+ disabled: block.lines.length <= 1,
1252
+ className: "p-1 text-gray-400 hover:text-red-500 disabled:opacity-30 disabled:cursor-not-allowed",
1253
+ title: "\uB300\uC0AC \uC0AD\uC81C",
1254
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16 })
1255
+ }
1256
+ )
1257
+ ] }),
1258
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-start gap-2 ${isRight ? "flex-row-reverse" : ""}`, children: [
1259
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-shrink-0 relative profile-menu-container", children: [
1260
+ /* @__PURE__ */ jsxRuntime.jsx(
1261
+ "input",
1262
+ {
1263
+ type: "file",
1264
+ accept: "image/*",
1265
+ ref: (el) => {
1266
+ fileInputRefs.current[index] = el;
1267
+ },
1268
+ onChange: (e) => {
1269
+ const file = e.target.files?.[0];
1270
+ if (file) handleProfileImageUpload(index, file);
1271
+ e.target.value = "";
1272
+ setShowProfileMenu(null);
1273
+ },
1274
+ className: "hidden"
1275
+ }
1276
+ ),
1277
+ isUploading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "text-gray-400 animate-spin" }) }) : line.profileImage ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-10 h-10", children: [
1278
+ /* @__PURE__ */ jsxRuntime.jsx(
1279
+ ImageComponent,
1280
+ {
1281
+ src: line.profileImage,
1282
+ alt: line.speaker || "\uD504\uB85C\uD544",
1283
+ fill: true,
1284
+ className: "rounded-full object-cover cursor-pointer"
1285
+ }
1286
+ ),
1287
+ /* @__PURE__ */ jsxRuntime.jsx(
1288
+ "button",
1289
+ {
1290
+ onClick: () => handleRemoveProfileImage(index),
1291
+ className: "absolute -top-1 -right-1 p-0.5 bg-red-500 text-white rounded-full hover:bg-red-600 z-10",
1292
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 10 })
1293
+ }
1294
+ )
1295
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
1296
+ "button",
1297
+ {
1298
+ onClick: () => setShowProfileMenu(showProfileMenu === index ? null : index),
1299
+ className: "w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full hover:bg-gray-300 transition-colors",
1300
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 16, className: "text-gray-400" })
1301
+ }
1302
+ ),
1303
+ showProfileMenu === index && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-full mt-1 left-0 z-50 bg-white border border-gray-200 rounded-lg shadow-lg py-1 min-w-[160px]", children: [
1304
+ /* @__PURE__ */ jsxRuntime.jsxs(
1305
+ "button",
1306
+ {
1307
+ onClick: () => {
1308
+ fileInputRefs.current[index]?.click();
1309
+ },
1310
+ className: "w-full flex items-center gap-2 px-3 py-2 hover:bg-gray-100 text-left text-sm",
1311
+ children: [
1312
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 16, className: "text-gray-500" }),
1313
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uD504\uB85C\uD544 \uC0AC\uC9C4 \uC124\uC815" })
1314
+ ]
1315
+ }
1316
+ ),
1317
+ characters.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1318
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-gray-100 my-1" }),
1319
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-1 text-xs text-gray-500", children: "\uCE90\uB9AD\uD130 \uC120\uD0DD" }),
1320
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-40 overflow-y-auto", children: characters.map((char) => /* @__PURE__ */ jsxRuntime.jsxs(
1321
+ "button",
1322
+ {
1323
+ onClick: () => handleSelectCharacter(index, char),
1324
+ className: "w-full flex items-center gap-2 px-3 py-1.5 hover:bg-gray-100 text-left",
1325
+ children: [
1326
+ char.profileImage ? /* @__PURE__ */ jsxRuntime.jsx(
1327
+ ImageComponent,
1328
+ {
1329
+ src: char.profileImage,
1330
+ alt: char.name,
1331
+ width: 20,
1332
+ height: 20,
1333
+ className: "rounded-full object-cover"
1334
+ }
1335
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1336
+ "div",
1337
+ {
1338
+ className: "w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold",
1339
+ style: { backgroundColor: char.defaultColor },
1340
+ children: char.name.charAt(0).toUpperCase()
1341
+ }
1342
+ ),
1343
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm truncate flex-1", children: char.name }),
1344
+ /* @__PURE__ */ jsxRuntime.jsx(
1345
+ "div",
1346
+ {
1347
+ className: "w-3 h-3 rounded-full border border-gray-300 flex-shrink-0",
1348
+ style: { backgroundColor: char.defaultColor }
1349
+ }
1350
+ )
1351
+ ]
1352
+ },
1353
+ char.id
1354
+ )) })
1355
+ ] })
1356
+ ] })
1357
+ ] }),
1358
+ /* @__PURE__ */ jsxRuntime.jsx(
1359
+ "div",
1360
+ {
1361
+ className: `flex-1 px-3 py-2 rounded-lg ${isRight ? "ml-8" : "mr-8"}`,
1362
+ style: { backgroundColor: line.speakerColor || "#FEE500" },
1363
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1364
+ "textarea",
1365
+ {
1366
+ value: getLineText(line.content),
1367
+ onChange: (e) => handleContentChange(index, e.target.value),
1368
+ placeholder: "\uB300\uC0AC\uB97C \uC785\uB825\uD558\uC138\uC694...",
1369
+ rows: 2,
1370
+ className: "w-full bg-transparent text-sm resize-none focus:outline-none",
1371
+ style: { color: isLightColor(line.speakerColor || "#FEE500") ? "#000" : "#fff" }
1372
+ }
1373
+ )
1374
+ }
1375
+ )
1376
+ ] })
1377
+ ] }, index);
1378
+ };
1379
+ const renderSimpleEditor = (line, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [
1380
+ /* @__PURE__ */ jsxRuntime.jsx(
1381
+ "input",
1382
+ {
1383
+ type: "text",
1384
+ value: line.speaker,
1385
+ onChange: (e) => handleSpeakerChange(index, e.target.value),
1386
+ placeholder: "\uD654\uC790",
1387
+ className: "w-24 px-2 py-1.5 text-sm font-bold border border-gray-200 rounded focus:outline-none focus:ring-2 focus:ring-gray-200"
1388
+ }
1389
+ ),
1390
+ /* @__PURE__ */ jsxRuntime.jsx(
1391
+ "textarea",
1392
+ {
1393
+ value: getLineText(line.content),
1394
+ onChange: (e) => handleContentChange(index, e.target.value),
1395
+ placeholder: "\uB300\uC0AC\uB97C \uC785\uB825\uD558\uC138\uC694...",
1396
+ rows: 1,
1397
+ className: "flex-1 px-2 py-1.5 text-sm border border-gray-200 rounded focus:outline-none focus:ring-2 focus:ring-gray-200 resize-none"
1398
+ }
1399
+ ),
1400
+ /* @__PURE__ */ jsxRuntime.jsx(
1401
+ "button",
1402
+ {
1403
+ onClick: () => handleRemoveLine(index),
1404
+ disabled: block.lines.length <= 1,
1405
+ className: "p-1 text-gray-400 hover:text-red-500 disabled:opacity-30 disabled:cursor-not-allowed",
1406
+ title: "\uB300\uC0AC \uC0AD\uC81C",
1407
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16 })
1408
+ }
1409
+ )
1410
+ ] }, index);
1411
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
1412
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1413
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: "\uC2A4\uD0C0\uC77C:" }),
1414
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 bg-gray-100 rounded-lg p-1", children: [
1415
+ /* @__PURE__ */ jsxRuntime.jsxs(
1416
+ "button",
1417
+ {
1418
+ onClick: () => handleStyleChange("chat"),
1419
+ className: `p-1.5 rounded flex items-center gap-1 text-sm ${block.style === "chat" ? "bg-white shadow" : "hover:bg-gray-200"}`,
1420
+ title: "\uCC44\uD305 \uC2A4\uD0C0\uC77C",
1421
+ children: [
1422
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageCircle, { size: 14 }),
1423
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uCC44\uD305" })
1424
+ ]
1425
+ }
1426
+ ),
1427
+ /* @__PURE__ */ jsxRuntime.jsxs(
1428
+ "button",
1429
+ {
1430
+ onClick: () => handleStyleChange("play"),
1431
+ className: `p-1.5 rounded flex items-center gap-1 text-sm ${block.style === "play" ? "bg-white shadow" : "hover:bg-gray-200"}`,
1432
+ title: "\uD76C\uADF9 \uC2A4\uD0C0\uC77C",
1433
+ children: [
1434
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Theater, { size: 14 }),
1435
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uD76C\uADF9" })
1436
+ ]
1437
+ }
1438
+ )
1439
+ ] })
1440
+ ] }),
1441
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: block.lines.map(
1442
+ (line, index) => block.style === "chat" ? renderChatEditor(line, index) : renderSimpleEditor(line, index)
1443
+ ) }),
1444
+ /* @__PURE__ */ jsxRuntime.jsxs(
1445
+ "button",
1446
+ {
1447
+ onClick: handleAddLine,
1448
+ className: "w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-gray-400 hover:text-gray-500 transition-colors flex items-center justify-center gap-1",
1449
+ children: [
1450
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { size: 16 }),
1451
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: "\uB300\uC0AC \uCD94\uAC00" })
1452
+ ]
1453
+ }
1454
+ )
1455
+ ] });
1456
+ }
1457
+ function isLightColor(hexColor) {
1458
+ const hex = hexColor.replace("#", "");
1459
+ const r = parseInt(hex.substr(0, 2), 16);
1460
+ const g = parseInt(hex.substr(2, 2), 16);
1461
+ const b = parseInt(hex.substr(4, 2), 16);
1462
+ const brightness = (r * 299 + g * 587 + b * 114) / 1e3;
1463
+ return brightness > 128;
1464
+ }
1465
+ function DividerEditor({ block, onUpdate }) {
1466
+ const [uploading, setUploading] = React9.useState(false);
1467
+ const services = useEditorServices();
1468
+ const ImageComponent = services.ImageComponent || DefaultImage;
1469
+ const handleVariantChange = (variant) => {
1470
+ const alignment = variant === "line" ? void 0 : block.alignment || "center";
1471
+ onUpdate({ variant, customImage: variant === "custom-image" ? block.customImage : void 0, alignment });
1472
+ };
1473
+ const handleAlignmentChange = (alignment) => {
1474
+ onUpdate({ alignment });
1475
+ };
1476
+ const handleImageUpload = async (e) => {
1477
+ const file = e.target.files?.[0];
1478
+ if (!file) return;
1479
+ try {
1480
+ setUploading(true);
1481
+ const url = await services.uploadImage(file, "divider");
1482
+ onUpdate({ variant: "custom-image", customImage: url, alignment: block.alignment || "center" });
1483
+ } catch (error) {
1484
+ console.error("Image upload error:", error);
1485
+ alert("\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
1486
+ } finally {
1487
+ setUploading(false);
1488
+ }
1489
+ };
1490
+ const handleRemoveImage = () => {
1491
+ onUpdate({ variant: "line", customImage: void 0, alignment: void 0 });
1492
+ };
1493
+ const renderDivider = () => {
1494
+ const alignClass = block.alignment === "left" ? "justify-start" : block.alignment === "right" ? "justify-end" : "justify-center";
1495
+ switch (block.variant) {
1496
+ case "line":
1497
+ return /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "border-t border-black" });
1498
+ case "short-line":
1499
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex ${alignClass}`, children: /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "border-t border-black w-1/5" }) });
1500
+ case "dots":
1501
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center ${alignClass} gap-2`, children: [
1502
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-1 h-1 bg-black rounded-full" }),
1503
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-1 h-1 bg-black rounded-full" }),
1504
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-1 h-1 bg-black rounded-full" })
1505
+ ] });
1506
+ case "custom-image":
1507
+ if (block.customImage) {
1508
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex ${alignClass}`, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative w-12 h-12", children: /* @__PURE__ */ jsxRuntime.jsx(
1509
+ ImageComponent,
1510
+ {
1511
+ src: block.customImage,
1512
+ alt: "\uAD6C\uBD84\uC120",
1513
+ fill: true,
1514
+ className: "object-contain"
1515
+ }
1516
+ ) }) });
1517
+ }
1518
+ return /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "border-t border-black" });
1519
+ default:
1520
+ return /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "border-t border-black" });
1521
+ }
1522
+ };
1523
+ const showAlignment = block.variant !== "line";
1524
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1525
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1", children: [
1526
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center bg-gray-100 rounded-lg p-0.5", children: [
1527
+ /* @__PURE__ */ jsxRuntime.jsx(
1528
+ "button",
1529
+ {
1530
+ onClick: () => handleVariantChange("line"),
1531
+ className: `px-2 py-1 rounded text-xs transition-colors ${block.variant === "line" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1532
+ title: "\uC9C1\uC120",
1533
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minus, { size: 14 })
1534
+ }
1535
+ ),
1536
+ /* @__PURE__ */ jsxRuntime.jsx(
1537
+ "button",
1538
+ {
1539
+ onClick: () => handleVariantChange("short-line"),
1540
+ className: `px-2 py-1 rounded text-xs transition-colors ${block.variant === "short-line" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1541
+ title: "\uC9E7\uC740 \uC120",
1542
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block w-3 border-t border-current" })
1543
+ }
1544
+ ),
1545
+ /* @__PURE__ */ jsxRuntime.jsx(
1546
+ "button",
1547
+ {
1548
+ onClick: () => handleVariantChange("dots"),
1549
+ className: `px-2 py-1 rounded text-xs transition-colors ${block.variant === "dots" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1550
+ title: "\uC810",
1551
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { size: 14 })
1552
+ }
1553
+ ),
1554
+ /* @__PURE__ */ jsxRuntime.jsxs(
1555
+ "label",
1556
+ {
1557
+ className: `px-2 py-1 rounded text-xs transition-colors cursor-pointer ${block.variant === "custom-image" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1558
+ title: "\uCEE4\uC2A4\uD140 \uC774\uBBF8\uC9C0",
1559
+ children: [
1560
+ /* @__PURE__ */ jsxRuntime.jsx(
1561
+ "input",
1562
+ {
1563
+ type: "file",
1564
+ accept: "image/*",
1565
+ onChange: handleImageUpload,
1566
+ className: "hidden",
1567
+ disabled: uploading
1568
+ }
1569
+ ),
1570
+ uploading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3.5 h-3.5 border border-gray-400 border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { size: 14 })
1571
+ ]
1572
+ }
1573
+ )
1574
+ ] }),
1575
+ showAlignment && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center bg-gray-100 rounded-lg p-0.5 ml-2", children: [
1576
+ /* @__PURE__ */ jsxRuntime.jsx(
1577
+ "button",
1578
+ {
1579
+ onClick: () => handleAlignmentChange("left"),
1580
+ className: `px-1.5 py-1 rounded text-xs transition-colors ${block.alignment === "left" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1581
+ title: "\uC67C\uCABD",
1582
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignLeft, { size: 12 })
1583
+ }
1584
+ ),
1585
+ /* @__PURE__ */ jsxRuntime.jsx(
1586
+ "button",
1587
+ {
1588
+ onClick: () => handleAlignmentChange("center"),
1589
+ className: `px-1.5 py-1 rounded text-xs transition-colors ${!block.alignment || block.alignment === "center" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1590
+ title: "\uAC00\uC6B4\uB370",
1591
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignCenter, { size: 12 })
1592
+ }
1593
+ ),
1594
+ /* @__PURE__ */ jsxRuntime.jsx(
1595
+ "button",
1596
+ {
1597
+ onClick: () => handleAlignmentChange("right"),
1598
+ className: `px-1.5 py-1 rounded text-xs transition-colors ${block.alignment === "right" ? "bg-white shadow text-gray-900" : "text-gray-500 hover:text-gray-700"}`,
1599
+ title: "\uC624\uB978\uCABD",
1600
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignRight, { size: 12 })
1601
+ }
1602
+ )
1603
+ ] })
1604
+ ] }),
1605
+ block.variant === "custom-image" && block.customImage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs(
1606
+ "button",
1607
+ {
1608
+ onClick: handleRemoveImage,
1609
+ className: "text-xs text-gray-400 hover:text-red-500 flex items-center gap-1",
1610
+ title: "\uC774\uBBF8\uC9C0 \uC0AD\uC81C",
1611
+ children: [
1612
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12 }),
1613
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\uC774\uBBF8\uC9C0 \uC81C\uAC70" })
1614
+ ]
1615
+ }
1616
+ ) }),
1617
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-4", children: renderDivider() })
1618
+ ] });
1619
+ }
1620
+ function BlockItem({
1621
+ block,
1622
+ blockIndex,
1623
+ onUpdate,
1624
+ onDelete,
1625
+ onAddBlock,
1626
+ onMergeWithPrevious,
1627
+ onSplitBlock,
1628
+ onPasteBlocks,
1629
+ onDeleteEmptyBlock,
1630
+ onFocusPrevious,
1631
+ onFocusNext,
1632
+ isOnlyBlock,
1633
+ isSelected,
1634
+ onSelect
1635
+ }) {
1636
+ const {
1637
+ attributes,
1638
+ listeners,
1639
+ setNodeRef,
1640
+ transform,
1641
+ transition,
1642
+ isDragging
1643
+ } = sortable.useSortable({ id: block.id });
1644
+ const style = {
1645
+ transform: utilities.CSS.Transform.toString(transform),
1646
+ transition,
1647
+ opacity: isDragging ? 0.5 : 1
1648
+ };
1649
+ const renderBlockContent = () => {
1650
+ switch (block.type) {
1651
+ case "paragraph":
1652
+ case "heading":
1653
+ case "quote":
1654
+ return /* @__PURE__ */ jsxRuntime.jsx(
1655
+ TextBlockEditor,
1656
+ {
1657
+ block,
1658
+ onUpdate,
1659
+ onMergeWithPrevious,
1660
+ onSplitBlock,
1661
+ onPasteBlocks,
1662
+ onDeleteEmptyBlock,
1663
+ onFocusPrevious,
1664
+ onFocusNext,
1665
+ isOnlyBlock
1666
+ }
1667
+ );
1668
+ case "list":
1669
+ return /* @__PURE__ */ jsxRuntime.jsx(
1670
+ TextBlockEditor,
1671
+ {
1672
+ block,
1673
+ onUpdate,
1674
+ onMergeWithPrevious,
1675
+ onSplitBlock,
1676
+ onPasteBlocks,
1677
+ onDeleteEmptyBlock,
1678
+ onFocusPrevious,
1679
+ onFocusNext,
1680
+ isOnlyBlock
1681
+ }
1682
+ );
1683
+ case "image":
1684
+ return /* @__PURE__ */ jsxRuntime.jsx(TextBlockEditor, { block, onUpdate });
1685
+ case "image-gallery":
1686
+ return /* @__PURE__ */ jsxRuntime.jsx(
1687
+ ImageGalleryEditor,
1688
+ {
1689
+ block,
1690
+ onUpdate
1691
+ }
1692
+ );
1693
+ case "link-card":
1694
+ return /* @__PURE__ */ jsxRuntime.jsx(LinkCardEditor, { block, onUpdate });
1695
+ case "link-embed":
1696
+ return /* @__PURE__ */ jsxRuntime.jsx(
1697
+ LinkEmbedEditor,
1698
+ {
1699
+ block,
1700
+ onUpdate
1701
+ }
1702
+ );
1703
+ case "dialogue":
1704
+ return /* @__PURE__ */ jsxRuntime.jsx(DialogueEditor, { block, onUpdate });
1705
+ case "divider":
1706
+ return /* @__PURE__ */ jsxRuntime.jsx(DividerEditor, { block, onUpdate });
1707
+ default:
1708
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-400", children: "Unknown block type" });
1709
+ }
1710
+ };
1711
+ const setBlockRef = React9.useCallback((el) => {
1712
+ setNodeRef(el);
1713
+ }, [setNodeRef]);
1714
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1715
+ "div",
1716
+ {
1717
+ ref: setBlockRef,
1718
+ style,
1719
+ className: `group relative flex items-start gap-2 ${isSelected ? "bg-blue-50 rounded" : ""}`,
1720
+ "data-block-id": block.id,
1721
+ onClick: onSelect,
1722
+ children: [
1723
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -left-10 top-1 opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntime.jsx(
1724
+ "button",
1725
+ {
1726
+ onClick: onAddBlock,
1727
+ className: "rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600",
1728
+ title: "\uBE14\uB85D \uCD94\uAC00",
1729
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { size: 18 })
1730
+ }
1731
+ ) }),
1732
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderBlockContent() }),
1733
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute -right-16 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [
1734
+ /* @__PURE__ */ jsxRuntime.jsx(
1735
+ "button",
1736
+ {
1737
+ onClick: onDelete,
1738
+ className: "p-1 rounded hover:bg-red-50 text-gray-300 hover:text-red-500",
1739
+ title: "\uBE14\uB85D \uC0AD\uC81C",
1740
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: 16 })
1741
+ }
1742
+ ),
1743
+ /* @__PURE__ */ jsxRuntime.jsx(
1744
+ "button",
1745
+ {
1746
+ ...attributes,
1747
+ ...listeners,
1748
+ className: "p-1 rounded hover:bg-gray-100 text-gray-300 hover:text-gray-500 cursor-grab active:cursor-grabbing",
1749
+ title: "\uB4DC\uB798\uADF8\uD558\uC5EC \uC774\uB3D9",
1750
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { size: 16 })
1751
+ }
1752
+ )
1753
+ ] })
1754
+ ]
1755
+ }
1756
+ );
1757
+ }
1758
+ function findBlockElement(node, container) {
1759
+ let current = node;
1760
+ while (current && current !== container) {
1761
+ if (current instanceof HTMLElement && current.hasAttribute("data-block-id")) {
1762
+ return current;
1763
+ }
1764
+ current = current.parentNode;
1765
+ }
1766
+ return null;
1767
+ }
1768
+ function getCharacterOffsetWithin(rootEl, targetNode, targetOffset) {
1769
+ const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT, null);
1770
+ let offset = 0;
1771
+ let node;
1772
+ while (node = walker.nextNode()) {
1773
+ if (node === targetNode) {
1774
+ return offset + targetOffset;
1775
+ }
1776
+ offset += node.textContent?.length || 0;
1777
+ }
1778
+ return offset;
1779
+ }
1780
+ function getCrossBlockSelection(blocks, containerEl) {
1781
+ const sel = window.getSelection();
1782
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
1783
+ const range = sel.getRangeAt(0);
1784
+ const startBlockEl = findBlockElement(range.startContainer, containerEl);
1785
+ const endBlockEl = findBlockElement(range.endContainer, containerEl);
1786
+ if (!startBlockEl || !endBlockEl) return null;
1787
+ const startBlockId = startBlockEl.getAttribute("data-block-id");
1788
+ const endBlockId = endBlockEl.getAttribute("data-block-id");
1789
+ if (!startBlockId || !endBlockId) return null;
1790
+ if (startBlockId === endBlockId) return null;
1791
+ const startBlockIndex = blocks.findIndex((b) => b.id === startBlockId);
1792
+ const endBlockIndex = blocks.findIndex((b) => b.id === endBlockId);
1793
+ if (startBlockIndex === -1 || endBlockIndex === -1) return null;
1794
+ const startEditable = startBlockEl.querySelector('[contenteditable="true"]');
1795
+ const endEditable = endBlockEl.querySelector('[contenteditable="true"]');
1796
+ const startOffset = startEditable ? getCharacterOffsetWithin(startEditable, range.startContainer, range.startOffset) : 0;
1797
+ const endOffset = endEditable ? getCharacterOffsetWithin(endEditable, range.endContainer, range.endOffset) : 0;
1798
+ return {
1799
+ startBlockId,
1800
+ startBlockIndex,
1801
+ startOffset,
1802
+ endBlockId,
1803
+ endBlockIndex,
1804
+ endOffset
1805
+ };
1806
+ }
1807
+ function BlockEditor({
1808
+ initialBlocks = [],
1809
+ onChange,
1810
+ onCharCountChange
1811
+ }) {
1812
+ const [blocks, setBlocks] = React9.useState(
1813
+ initialBlocks.length > 0 ? initialBlocks : [createEmptyParagraphBlock()]
1814
+ );
1815
+ const [showBlockMenu, setShowBlockMenu] = React9.useState(false);
1816
+ const [blockMenuPosition, setBlockMenuPosition] = React9.useState({ x: 0, y: 0 });
1817
+ const [insertIndex, setInsertIndex] = React9.useState(0);
1818
+ const [selectedBlocks, setSelectedBlocks] = React9.useState(/* @__PURE__ */ new Set());
1819
+ const [lastSelectedIndex, setLastSelectedIndex] = React9.useState(null);
1820
+ const blockRefs = React9__default.default.useRef(/* @__PURE__ */ new Map());
1821
+ const editorContainerRef = React9.useRef(null);
1822
+ const pendingFocusRef = React9.useRef(null);
1823
+ const selStartRef = React9.useRef(null);
1824
+ const focusBlockById = React9.useCallback((blockId, position = "start") => {
1825
+ setTimeout(() => {
1826
+ const blockEl = blockRefs.current.get(blockId);
1827
+ if (!blockEl) return;
1828
+ const editable = blockEl.querySelector('[contenteditable="true"]');
1829
+ if (editable) {
1830
+ editable.focus();
1831
+ const range = document.createRange();
1832
+ const selection = window.getSelection();
1833
+ if (position === "end" && editable.lastChild) {
1834
+ const lastChild = editable.lastChild;
1835
+ if (lastChild.nodeType === Node.TEXT_NODE) {
1836
+ range.setStart(lastChild, lastChild.textContent?.length || 0);
1837
+ } else {
1838
+ range.setStartAfter(lastChild);
1839
+ }
1840
+ } else {
1841
+ range.setStart(editable, 0);
1842
+ }
1843
+ range.collapse(true);
1844
+ selection?.removeAllRanges();
1845
+ selection?.addRange(range);
1846
+ } else {
1847
+ const input = blockEl.querySelector('input:not([type="file"]), textarea');
1848
+ if (input) {
1849
+ input.focus();
1850
+ try {
1851
+ if (position === "end") {
1852
+ input.selectionStart = input.selectionEnd = input.value.length;
1853
+ } else {
1854
+ input.selectionStart = input.selectionEnd = 0;
1855
+ }
1856
+ } catch (e) {
1857
+ }
1858
+ }
1859
+ }
1860
+ }, 0);
1861
+ }, []);
1862
+ const focusBlock = React9.useCallback((index, position = "start") => {
1863
+ const targetBlock = blocks[index];
1864
+ if (!targetBlock) return;
1865
+ setTimeout(() => {
1866
+ const blockEl = blockRefs.current.get(targetBlock.id);
1867
+ if (!blockEl) return;
1868
+ const editable = blockEl.querySelector('[contenteditable="true"]');
1869
+ if (editable) {
1870
+ editable.focus();
1871
+ const range = document.createRange();
1872
+ const selection = window.getSelection();
1873
+ if (position === "end" && editable.lastChild) {
1874
+ const lastChild = editable.lastChild;
1875
+ if (lastChild.nodeType === Node.TEXT_NODE) {
1876
+ range.setStart(lastChild, lastChild.textContent?.length || 0);
1877
+ } else {
1878
+ range.setStartAfter(lastChild);
1879
+ }
1880
+ } else {
1881
+ range.setStart(editable, 0);
1882
+ }
1883
+ range.collapse(true);
1884
+ selection?.removeAllRanges();
1885
+ selection?.addRange(range);
1886
+ } else {
1887
+ const input = blockEl.querySelector('input:not([type="file"]), textarea');
1888
+ if (input) {
1889
+ input.focus();
1890
+ try {
1891
+ if (position === "end") {
1892
+ input.selectionStart = input.selectionEnd = input.value.length;
1893
+ } else {
1894
+ input.selectionStart = input.selectionEnd = 0;
1895
+ }
1896
+ } catch (e) {
1897
+ }
1898
+ }
1899
+ }
1900
+ }, 0);
1901
+ }, [blocks]);
1902
+ React9.useEffect(() => {
1903
+ if (pendingFocusRef.current) {
1904
+ const { blockId, position } = pendingFocusRef.current;
1905
+ pendingFocusRef.current = null;
1906
+ focusBlockById(blockId, position);
1907
+ }
1908
+ }, [blocks, focusBlockById]);
1909
+ const sensors = core.useSensors(
1910
+ core.useSensor(core.PointerSensor, {
1911
+ activationConstraint: {
1912
+ distance: 8
1913
+ }
1914
+ }),
1915
+ core.useSensor(core.KeyboardSensor, {
1916
+ coordinateGetter: sortable.sortableKeyboardCoordinates
1917
+ })
1918
+ );
1919
+ React9.useEffect(() => {
1920
+ onChange(blocks);
1921
+ if (onCharCountChange) {
1922
+ onCharCountChange(countBlocksCharacters(blocks));
1923
+ }
1924
+ }, [blocks]);
1925
+ const handleUpdateBlock = React9.useCallback(
1926
+ (id, updates) => {
1927
+ setBlocks(
1928
+ (prev) => prev.map(
1929
+ (block) => block.id === id ? { ...block, ...updates } : block
1930
+ )
1931
+ );
1932
+ },
1933
+ []
1934
+ );
1935
+ const handleDeleteBlock = React9.useCallback((id) => {
1936
+ setBlocks((prev) => {
1937
+ const filtered = prev.filter((block) => block.id !== id);
1938
+ if (filtered.length === 0) {
1939
+ return [createEmptyParagraphBlock()];
1940
+ }
1941
+ return filtered;
1942
+ });
1943
+ }, []);
1944
+ const handleShowBlockMenu = React9.useCallback(
1945
+ (e, index) => {
1946
+ e.preventDefault();
1947
+ e.stopPropagation();
1948
+ const rect = e.target.getBoundingClientRect();
1949
+ setBlockMenuPosition({ x: rect.left, y: rect.bottom + 5 });
1950
+ setInsertIndex(index);
1951
+ setShowBlockMenu(true);
1952
+ },
1953
+ []
1954
+ );
1955
+ const handleAddBlock = React9.useCallback(
1956
+ (type) => {
1957
+ let newBlock;
1958
+ const id = generateBlockId();
1959
+ switch (type) {
1960
+ case "paragraph":
1961
+ newBlock = {
1962
+ id,
1963
+ type: "paragraph",
1964
+ content: [{ text: "" }]
1965
+ };
1966
+ break;
1967
+ case "heading":
1968
+ newBlock = {
1969
+ id,
1970
+ type: "heading",
1971
+ level: 2,
1972
+ content: [{ text: "" }]
1973
+ };
1974
+ break;
1975
+ case "quote":
1976
+ newBlock = {
1977
+ id,
1978
+ type: "quote",
1979
+ content: [{ text: "" }]
1980
+ };
1981
+ break;
1982
+ case "list":
1983
+ newBlock = {
1984
+ id,
1985
+ type: "list",
1986
+ listType: "unordered",
1987
+ content: [{ text: "" }]
1988
+ };
1989
+ break;
1990
+ case "divider":
1991
+ newBlock = { id, type: "divider", variant: "line" };
1992
+ break;
1993
+ case "image":
1994
+ newBlock = {
1995
+ id,
1996
+ type: "image",
1997
+ image: { url: "" },
1998
+ alignment: "center"
1999
+ };
2000
+ break;
2001
+ case "image-gallery":
2002
+ newBlock = {
2003
+ id,
2004
+ type: "image-gallery",
2005
+ images: [],
2006
+ layout: "grid",
2007
+ columns: 2
2008
+ };
2009
+ break;
2010
+ case "link-card":
2011
+ newBlock = { id, type: "link-card", url: "" };
2012
+ break;
2013
+ case "link-embed":
2014
+ newBlock = {
2015
+ id,
2016
+ type: "link-embed",
2017
+ url: "",
2018
+ embedType: "youtube"
2019
+ };
2020
+ break;
2021
+ case "dialogue":
2022
+ newBlock = {
2023
+ id,
2024
+ type: "dialogue",
2025
+ lines: [{ speaker: "", content: [{ text: "" }] }],
2026
+ style: "chat"
2027
+ };
2028
+ break;
2029
+ default:
2030
+ return;
2031
+ }
2032
+ setBlocks((prev) => {
2033
+ const newBlocks = [...prev];
2034
+ newBlocks.splice(insertIndex + 1, 0, newBlock);
2035
+ return newBlocks;
2036
+ });
2037
+ setShowBlockMenu(false);
2038
+ },
2039
+ [insertIndex]
2040
+ );
2041
+ const handleMergeWithPrevious = React9.useCallback((index) => {
2042
+ if (index <= 0) return;
2043
+ const currentBlock = blocks[index];
2044
+ const previousBlock = blocks[index - 1];
2045
+ if (!currentBlock || !previousBlock) return;
2046
+ const isTextType = (t) => t === "paragraph" || t === "heading" || t === "quote" || t === "list";
2047
+ if (!isTextType(currentBlock.type) || !isTextType(previousBlock.type)) return;
2048
+ const prevBlockId = previousBlock.id;
2049
+ setBlocks((prev) => {
2050
+ const merged = [...prev];
2051
+ const prevContent = prev[index - 1].content;
2052
+ const currContent = prev[index].content;
2053
+ merged[index - 1] = {
2054
+ ...prev[index - 1],
2055
+ content: [...prevContent, ...currContent]
2056
+ };
2057
+ merged.splice(index, 1);
2058
+ return merged;
2059
+ });
2060
+ pendingFocusRef.current = { blockId: prevBlockId, position: "end" };
2061
+ }, [blocks]);
2062
+ const handleDeleteEmptyBlock = React9.useCallback((index) => {
2063
+ if (index <= 0) return;
2064
+ const prevBlockId = blocks[index - 1]?.id;
2065
+ if (!prevBlockId) return;
2066
+ setBlocks((prev) => {
2067
+ if (prev.length <= 1) return prev;
2068
+ const newBlocks = [...prev];
2069
+ newBlocks.splice(index, 1);
2070
+ return newBlocks;
2071
+ });
2072
+ pendingFocusRef.current = { blockId: prevBlockId, position: "end" };
2073
+ }, [blocks]);
2074
+ const handleSplitBlock = React9.useCallback(
2075
+ (index, beforeText, afterText) => {
2076
+ const newBlockId = generateBlockId();
2077
+ setBlocks((prev) => {
2078
+ const newBlocks = [...prev];
2079
+ const currentBlock = newBlocks[index];
2080
+ newBlocks[index] = {
2081
+ ...currentBlock,
2082
+ content: beforeText
2083
+ };
2084
+ let newBlock;
2085
+ if (currentBlock.type === "list") {
2086
+ newBlock = {
2087
+ id: newBlockId,
2088
+ type: "list",
2089
+ listType: currentBlock.listType,
2090
+ content: afterText.length > 0 ? afterText : [{ text: "" }]
2091
+ };
2092
+ } else {
2093
+ newBlock = {
2094
+ id: newBlockId,
2095
+ type: "paragraph",
2096
+ content: afterText.length > 0 ? afterText : [{ text: "" }]
2097
+ };
2098
+ }
2099
+ newBlocks.splice(index + 1, 0, newBlock);
2100
+ return newBlocks;
2101
+ });
2102
+ setTimeout(() => {
2103
+ focusBlockById(newBlockId, "start");
2104
+ }, 10);
2105
+ },
2106
+ [focusBlockById]
2107
+ );
2108
+ const handlePasteBlocks = React9.useCallback(
2109
+ (index, beforeText, pastedLines, afterText) => {
2110
+ const lastBlockId = generateBlockId();
2111
+ setBlocks((prev) => {
2112
+ const newBlocks = [...prev];
2113
+ const currentBlock = newBlocks[index];
2114
+ newBlocks[index] = {
2115
+ ...currentBlock,
2116
+ content: beforeText
2117
+ };
2118
+ const middleBlocks = pastedLines.map((line) => ({
2119
+ id: generateBlockId(),
2120
+ type: "paragraph",
2121
+ content: [{ text: line }]
2122
+ }));
2123
+ const lastBlock = {
2124
+ id: lastBlockId,
2125
+ type: "paragraph",
2126
+ content: afterText
2127
+ };
2128
+ newBlocks.splice(index + 1, 0, ...middleBlocks, lastBlock);
2129
+ return newBlocks;
2130
+ });
2131
+ setTimeout(() => {
2132
+ focusBlockById(lastBlockId, "end");
2133
+ }, 10);
2134
+ },
2135
+ [focusBlockById]
2136
+ );
2137
+ const handleDragEnd = React9.useCallback((event) => {
2138
+ const { active, over } = event;
2139
+ if (over && active.id !== over.id) {
2140
+ setBlocks((prev) => {
2141
+ const oldIndex = prev.findIndex((block) => block.id === active.id);
2142
+ const newIndex = prev.findIndex((block) => block.id === over.id);
2143
+ if (oldIndex !== -1 && newIndex !== -1) {
2144
+ const newBlocks = [...prev];
2145
+ const [removed] = newBlocks.splice(oldIndex, 1);
2146
+ newBlocks.splice(newIndex, 0, removed);
2147
+ return newBlocks;
2148
+ }
2149
+ return prev;
2150
+ });
2151
+ }
2152
+ }, []);
2153
+ const handleBlockSelect = React9.useCallback((index, e) => {
2154
+ const target = e.target;
2155
+ if (target.closest('[contenteditable="true"]') || target.closest("input") || target.closest("textarea")) {
2156
+ if (selectedBlocks.size > 0) {
2157
+ setSelectedBlocks(/* @__PURE__ */ new Set());
2158
+ }
2159
+ setLastSelectedIndex(index);
2160
+ return;
2161
+ }
2162
+ if (e.shiftKey && lastSelectedIndex !== null) {
2163
+ const start = Math.min(lastSelectedIndex, index);
2164
+ const end = Math.max(lastSelectedIndex, index);
2165
+ const newSelected = /* @__PURE__ */ new Set();
2166
+ for (let i = start; i <= end; i++) {
2167
+ newSelected.add(blocks[i].id);
2168
+ }
2169
+ setSelectedBlocks(newSelected);
2170
+ } else if (e.metaKey || e.ctrlKey) {
2171
+ const newSelected = new Set(selectedBlocks);
2172
+ if (newSelected.has(blocks[index].id)) {
2173
+ newSelected.delete(blocks[index].id);
2174
+ } else {
2175
+ newSelected.add(blocks[index].id);
2176
+ }
2177
+ setSelectedBlocks(newSelected);
2178
+ setLastSelectedIndex(index);
2179
+ } else {
2180
+ setSelectedBlocks(/* @__PURE__ */ new Set());
2181
+ setLastSelectedIndex(index);
2182
+ }
2183
+ }, [blocks, selectedBlocks, lastSelectedIndex]);
2184
+ const handleDeleteSelected = React9.useCallback(() => {
2185
+ if (selectedBlocks.size === 0) return;
2186
+ setBlocks((prev) => {
2187
+ const filtered = prev.filter((block) => !selectedBlocks.has(block.id));
2188
+ if (filtered.length === 0) {
2189
+ return [createEmptyParagraphBlock()];
2190
+ }
2191
+ return filtered;
2192
+ });
2193
+ setSelectedBlocks(/* @__PURE__ */ new Set());
2194
+ }, [selectedBlocks]);
2195
+ const handleCrossBlockDelete = React9.useCallback((crossSel, insertChar) => {
2196
+ const { startBlockIndex, endBlockIndex, startOffset, endOffset } = crossSel;
2197
+ setBlocks((prev) => {
2198
+ const startBlock = prev[startBlockIndex];
2199
+ const endBlock = prev[endBlockIndex];
2200
+ let beforeText = "";
2201
+ if (startBlock.type === "paragraph" || startBlock.type === "heading" || startBlock.type === "quote" || startBlock.type === "list") {
2202
+ const fullText = startBlock.content.map((n) => n.text).join("");
2203
+ beforeText = fullText.slice(0, startOffset);
2204
+ }
2205
+ let afterText = "";
2206
+ if (endBlock.type === "paragraph" || endBlock.type === "heading" || endBlock.type === "quote" || endBlock.type === "list") {
2207
+ const fullText = endBlock.content.map((n) => n.text).join("");
2208
+ afterText = fullText.slice(endOffset);
2209
+ }
2210
+ const mergedText = beforeText + (insertChar || "") + afterText;
2211
+ const mergedBlock = {
2212
+ ...startBlock,
2213
+ content: [{ text: mergedText }]
2214
+ };
2215
+ const newBlocks = [
2216
+ ...prev.slice(0, startBlockIndex),
2217
+ mergedBlock,
2218
+ ...prev.slice(endBlockIndex + 1)
2219
+ ];
2220
+ if (newBlocks.length === 0) {
2221
+ return [createEmptyParagraphBlock()];
2222
+ }
2223
+ return newBlocks;
2224
+ });
2225
+ const cursorPos = startOffset + (insertChar?.length || 0);
2226
+ setTimeout(() => {
2227
+ const blockEl = blockRefs.current.get(crossSel.startBlockId);
2228
+ if (!blockEl) return;
2229
+ const editable = blockEl.querySelector('[contenteditable="true"]');
2230
+ if (!editable) return;
2231
+ editable.focus();
2232
+ requestAnimationFrame(() => {
2233
+ const walker = document.createTreeWalker(editable, NodeFilter.SHOW_TEXT, null);
2234
+ let offset = 0;
2235
+ let targetNode = null;
2236
+ let targetOffset = 0;
2237
+ let node;
2238
+ while (node = walker.nextNode()) {
2239
+ const len = node.textContent?.length || 0;
2240
+ if (offset + len >= cursorPos) {
2241
+ targetNode = node;
2242
+ targetOffset = cursorPos - offset;
2243
+ break;
2244
+ }
2245
+ offset += len;
2246
+ }
2247
+ if (targetNode) {
2248
+ const range = document.createRange();
2249
+ range.setStart(targetNode, targetOffset);
2250
+ range.collapse(true);
2251
+ const sel = window.getSelection();
2252
+ sel?.removeAllRanges();
2253
+ sel?.addRange(range);
2254
+ }
2255
+ });
2256
+ }, 0);
2257
+ }, []);
2258
+ React9.useEffect(() => {
2259
+ const container = editorContainerRef.current;
2260
+ const handleKeyDown = (e) => {
2261
+ if (selectedBlocks.size > 0 && (e.key === "Delete" || e.key === "Backspace")) {
2262
+ const activeEl = document.activeElement;
2263
+ if (!activeEl?.closest('[contenteditable="true"]') && !(activeEl instanceof HTMLInputElement) && !(activeEl instanceof HTMLTextAreaElement)) {
2264
+ e.preventDefault();
2265
+ handleDeleteSelected();
2266
+ return;
2267
+ }
2268
+ }
2269
+ if ((e.key === "Delete" || e.key === "Backspace") && !e.isComposing && container) {
2270
+ const crossSel = getCrossBlockSelection(blocks, container);
2271
+ if (crossSel) {
2272
+ e.preventDefault();
2273
+ handleCrossBlockDelete(crossSel);
2274
+ return;
2275
+ }
2276
+ }
2277
+ if (e.key === "Escape" && selectedBlocks.size > 0) {
2278
+ setSelectedBlocks(/* @__PURE__ */ new Set());
2279
+ }
2280
+ };
2281
+ const handleBeforeInput = (e) => {
2282
+ if (!container) return;
2283
+ const crossSel = getCrossBlockSelection(blocks, container);
2284
+ if (crossSel) {
2285
+ if ((e.inputType === "insertText" || e.inputType === "insertCompositionText") && !e.isComposing && e.data) {
2286
+ e.preventDefault();
2287
+ handleCrossBlockDelete(crossSel, e.data);
2288
+ } else if (e.inputType.startsWith("delete")) {
2289
+ e.preventDefault();
2290
+ handleCrossBlockDelete(crossSel);
2291
+ } else {
2292
+ e.preventDefault();
2293
+ }
2294
+ return;
2295
+ }
2296
+ if (e.target === container) {
2297
+ e.preventDefault();
2298
+ }
2299
+ };
2300
+ document.addEventListener("keydown", handleKeyDown);
2301
+ if (container) {
2302
+ container.addEventListener("beforeinput", handleBeforeInput);
2303
+ }
2304
+ return () => {
2305
+ document.removeEventListener("keydown", handleKeyDown);
2306
+ if (container) {
2307
+ container.removeEventListener("beforeinput", handleBeforeInput);
2308
+ }
2309
+ };
2310
+ }, [selectedBlocks, handleDeleteSelected, blocks, handleCrossBlockDelete]);
2311
+ React9.useEffect(() => {
2312
+ const container = editorContainerRef.current;
2313
+ if (!container) return;
2314
+ let docMoveHandler = null;
2315
+ let docUpHandler = null;
2316
+ const handleMouseDown = (e) => {
2317
+ const target = e.target;
2318
+ const editable = target.closest('[contenteditable="true"]');
2319
+ if (!editable || editable === container) return;
2320
+ if (e.shiftKey && selStartRef.current) {
2321
+ const caretRange = document.caretRangeFromPoint(e.clientX, e.clientY);
2322
+ if (!caretRange) return;
2323
+ const caretBlockEl = findBlockElement(caretRange.startContainer, container);
2324
+ if (!caretBlockEl) return;
2325
+ const caretBlockId = caretBlockEl.getAttribute("data-block-id");
2326
+ if (caretBlockId && caretBlockId !== selStartRef.current.blockId) {
2327
+ e.preventDefault();
2328
+ const sel = window.getSelection();
2329
+ if (sel) {
2330
+ try {
2331
+ sel.setBaseAndExtent(
2332
+ selStartRef.current.node,
2333
+ selStartRef.current.offset,
2334
+ caretRange.startContainer,
2335
+ caretRange.startOffset
2336
+ );
2337
+ } catch (_) {
2338
+ }
2339
+ }
2340
+ }
2341
+ return;
2342
+ }
2343
+ requestAnimationFrame(() => {
2344
+ const sel = window.getSelection();
2345
+ if (!sel || sel.rangeCount === 0) return;
2346
+ const range = sel.getRangeAt(0);
2347
+ const blockEl = findBlockElement(range.startContainer, container);
2348
+ if (!blockEl) return;
2349
+ selStartRef.current = {
2350
+ blockId: blockEl.getAttribute("data-block-id"),
2351
+ node: range.startContainer,
2352
+ offset: range.startOffset
2353
+ };
2354
+ });
2355
+ docMoveHandler = (moveE) => {
2356
+ if (!selStartRef.current) return;
2357
+ const caretRange = document.caretRangeFromPoint(moveE.clientX, moveE.clientY);
2358
+ if (!caretRange) return;
2359
+ const caretBlockEl = findBlockElement(caretRange.startContainer, container);
2360
+ if (!caretBlockEl) return;
2361
+ const caretBlockId = caretBlockEl.getAttribute("data-block-id");
2362
+ if (!caretBlockId || caretBlockId === selStartRef.current.blockId) return;
2363
+ requestAnimationFrame(() => {
2364
+ if (!selStartRef.current) return;
2365
+ const sel = window.getSelection();
2366
+ if (!sel) return;
2367
+ try {
2368
+ sel.setBaseAndExtent(
2369
+ selStartRef.current.node,
2370
+ selStartRef.current.offset,
2371
+ caretRange.startContainer,
2372
+ caretRange.startOffset
2373
+ );
2374
+ } catch (_) {
2375
+ }
2376
+ });
2377
+ };
2378
+ docUpHandler = () => {
2379
+ if (docMoveHandler) document.removeEventListener("mousemove", docMoveHandler);
2380
+ if (docUpHandler) document.removeEventListener("mouseup", docUpHandler);
2381
+ docMoveHandler = null;
2382
+ docUpHandler = null;
2383
+ };
2384
+ document.addEventListener("mousemove", docMoveHandler);
2385
+ document.addEventListener("mouseup", docUpHandler);
2386
+ };
2387
+ container.addEventListener("mousedown", handleMouseDown);
2388
+ return () => {
2389
+ container.removeEventListener("mousedown", handleMouseDown);
2390
+ if (docMoveHandler) document.removeEventListener("mousemove", docMoveHandler);
2391
+ if (docUpHandler) document.removeEventListener("mouseup", docUpHandler);
2392
+ };
2393
+ }, []);
2394
+ React9.useEffect(() => {
2395
+ const container = editorContainerRef.current;
2396
+ if (!container) return;
2397
+ const handleSelectionChange = () => {
2398
+ container.querySelectorAll("[data-cross-selected]").forEach((el) => {
2399
+ el.removeAttribute("data-cross-selected");
2400
+ });
2401
+ const crossSel = getCrossBlockSelection(blocks, container);
2402
+ if (!crossSel) return;
2403
+ const minIdx = Math.min(crossSel.startBlockIndex, crossSel.endBlockIndex);
2404
+ const maxIdx = Math.max(crossSel.startBlockIndex, crossSel.endBlockIndex);
2405
+ for (let i = minIdx; i <= maxIdx; i++) {
2406
+ const wrapper = blockRefs.current.get(blocks[i].id);
2407
+ if (wrapper) {
2408
+ wrapper.setAttribute("data-cross-selected", "true");
2409
+ }
2410
+ }
2411
+ };
2412
+ document.addEventListener("selectionchange", handleSelectionChange);
2413
+ return () => document.removeEventListener("selectionchange", handleSelectionChange);
2414
+ }, [blocks]);
2415
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "block-editor relative select-text", children: [
2416
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
2417
+ [data-cross-selected] [contenteditable]:empty::after {
2418
+ content: '\\00a0';
2419
+ background-color: rgb(191 219 254);
2420
+ border-radius: 1px;
2421
+ }
2422
+ ` }),
2423
+ /* @__PURE__ */ jsxRuntime.jsx(
2424
+ core.DndContext,
2425
+ {
2426
+ sensors,
2427
+ collisionDetection: core.closestCenter,
2428
+ onDragEnd: handleDragEnd,
2429
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2430
+ sortable.SortableContext,
2431
+ {
2432
+ items: blocks.map((b) => b.id),
2433
+ strategy: sortable.verticalListSortingStrategy,
2434
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2435
+ "div",
2436
+ {
2437
+ ref: editorContainerRef,
2438
+ contentEditable: true,
2439
+ suppressContentEditableWarning: true,
2440
+ className: "space-y-1 selection:bg-blue-200 outline-none",
2441
+ children: blocks.map((block, index) => /* @__PURE__ */ jsxRuntime.jsx(
2442
+ "div",
2443
+ {
2444
+ contentEditable: false,
2445
+ ref: (el) => {
2446
+ if (el) blockRefs.current.set(block.id, el);
2447
+ else blockRefs.current.delete(block.id);
2448
+ },
2449
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2450
+ BlockItem,
2451
+ {
2452
+ block,
2453
+ blockIndex: index,
2454
+ onUpdate: (updates) => handleUpdateBlock(block.id, updates),
2455
+ onDelete: () => handleDeleteBlock(block.id),
2456
+ onAddBlock: (e) => {
2457
+ if (e) {
2458
+ handleShowBlockMenu(e, index);
2459
+ } else {
2460
+ setInsertIndex(index);
2461
+ setShowBlockMenu(true);
2462
+ setBlockMenuPosition({ x: 100, y: 100 });
2463
+ }
2464
+ },
2465
+ onMergeWithPrevious: index > 0 ? () => handleMergeWithPrevious(index) : void 0,
2466
+ onSplitBlock: (before, after) => handleSplitBlock(index, before, after),
2467
+ onPasteBlocks: (before, lines, after) => handlePasteBlocks(index, before, lines, after),
2468
+ onDeleteEmptyBlock: index > 0 ? () => handleDeleteEmptyBlock(index) : void 0,
2469
+ onFocusPrevious: index > 0 ? () => focusBlock(index - 1, "end") : void 0,
2470
+ onFocusNext: index < blocks.length - 1 ? () => focusBlock(index + 1, "start") : void 0,
2471
+ isOnlyBlock: blocks.length === 1,
2472
+ isSelected: selectedBlocks.has(block.id),
2473
+ onSelect: (e) => handleBlockSelect(index, e)
2474
+ }
2475
+ )
2476
+ },
2477
+ block.id
2478
+ ))
2479
+ }
2480
+ )
2481
+ }
2482
+ )
2483
+ }
2484
+ ),
2485
+ showBlockMenu && /* @__PURE__ */ jsxRuntime.jsx(
2486
+ BlockMenu,
2487
+ {
2488
+ position: blockMenuPosition,
2489
+ onClose: () => setShowBlockMenu(false),
2490
+ onSelect: handleAddBlock
2491
+ }
2492
+ )
2493
+ ] });
2494
+ }
2495
+
2496
+ exports.BlockEditor = BlockEditor;
2497
+ exports.BlockMenu = BlockMenu;
2498
+ exports.DefaultImage = DefaultImage;
2499
+ exports.DialogueEditor = DialogueEditor;
2500
+ exports.DividerEditor = DividerEditor;
2501
+ exports.EditorProvider = EditorProvider;
2502
+ exports.ImageGalleryEditor = ImageGalleryEditor;
2503
+ exports.LinkCardEditor = LinkCardEditor;
2504
+ exports.LinkEmbedEditor = LinkEmbedEditor;
2505
+ exports.TextBlockEditor = TextBlockEditor;
2506
+ exports.countBlocksCharacters = countBlocksCharacters;
2507
+ exports.createEmptyParagraphBlock = createEmptyParagraphBlock;
2508
+ exports.generateBlockId = generateBlockId;
2509
+ exports.generatePreviewFromBlocks = generatePreviewFromBlocks;
2510
+ exports.textNodesToPlainText = textNodesToPlainText;
2511
+ exports.useEditorServices = useEditorServices;
2512
+ exports.useOptionalEditorServices = useOptionalEditorServices;
2513
+ //# sourceMappingURL=index.js.map
2514
+ //# sourceMappingURL=index.js.map