@x-plat/design-system 0.5.48 → 0.5.50

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.
@@ -0,0 +1,760 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/components/Editor/index.ts
31
+ var Editor_exports = {};
32
+ __export(Editor_exports, {
33
+ default: () => Editor_default
34
+ });
35
+ module.exports = __toCommonJS(Editor_exports);
36
+
37
+ // src/components/Editor/Editor.tsx
38
+ var import_react = __toESM(require("react"), 1);
39
+
40
+ // ../../node_modules/clsx/dist/clsx.mjs
41
+ function r(e) {
42
+ var t, f, n = "";
43
+ if ("string" == typeof e || "number" == typeof e) n += e;
44
+ else if ("object" == typeof e) if (Array.isArray(e)) {
45
+ var o = e.length;
46
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
47
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
48
+ return n;
49
+ }
50
+ function clsx() {
51
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
52
+ return n;
53
+ }
54
+ var clsx_default = clsx;
55
+
56
+ // src/components/Editor/Editor.tsx
57
+ var import_jsx_runtime = require("react/jsx-runtime");
58
+ var DEFAULT_TOOLBAR = [
59
+ "bold",
60
+ "italic",
61
+ "underline",
62
+ "strikethrough",
63
+ "code",
64
+ "heading",
65
+ "list",
66
+ "ordered-list",
67
+ "blockquote",
68
+ "code-block",
69
+ "link",
70
+ "image",
71
+ "divider"
72
+ ];
73
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
74
+ "p",
75
+ "br",
76
+ "h1",
77
+ "h2",
78
+ "h3",
79
+ "h4",
80
+ "h5",
81
+ "h6",
82
+ "ul",
83
+ "ol",
84
+ "li",
85
+ "blockquote",
86
+ "pre",
87
+ "code",
88
+ "strong",
89
+ "b",
90
+ "em",
91
+ "i",
92
+ "u",
93
+ "s",
94
+ "strike",
95
+ "del",
96
+ "a",
97
+ "img",
98
+ "hr",
99
+ "span",
100
+ "div"
101
+ ]);
102
+ var ALLOWED_ATTRS = {
103
+ a: ["href", "target", "rel"],
104
+ img: ["src", "alt"]
105
+ };
106
+ var sanitizeHtml = (input) => {
107
+ const doc = new DOMParser().parseFromString(`<div>${input}</div>`, "text/html");
108
+ const root = doc.body.firstElementChild;
109
+ if (!root) return "";
110
+ const walk = (node) => {
111
+ const children = Array.from(node.children);
112
+ for (const child of children) {
113
+ const tag = child.tagName.toLowerCase();
114
+ if (!ALLOWED_TAGS.has(tag)) {
115
+ while (child.firstChild) node.insertBefore(child.firstChild, child);
116
+ node.removeChild(child);
117
+ continue;
118
+ }
119
+ const allowed = ALLOWED_ATTRS[tag] || [];
120
+ for (const attr of Array.from(child.attributes)) {
121
+ if (!allowed.includes(attr.name)) {
122
+ child.removeAttribute(attr.name);
123
+ }
124
+ if (attr.name === "href" || attr.name === "src") {
125
+ const val = attr.value.trim().toLowerCase();
126
+ if (val.startsWith("javascript:") || val.startsWith("data:text/html")) {
127
+ child.removeAttribute(attr.name);
128
+ }
129
+ }
130
+ }
131
+ walk(child);
132
+ }
133
+ };
134
+ walk(root);
135
+ return root.innerHTML;
136
+ };
137
+ var escapeHtml = (text) => {
138
+ const div = document.createElement("div");
139
+ div.textContent = text;
140
+ return div.innerHTML.replace(/\n/g, "<br>");
141
+ };
142
+ var SLASH_ITEMS = [
143
+ { key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
144
+ { key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
145
+ { key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
146
+ { key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
147
+ { key: "blockquote", label: "\uC778\uC6A9", hint: "" },
148
+ { key: "code-block", label: "\uCF54\uB4DC \uBE14\uB85D", hint: "" },
149
+ { key: "divider", label: "\uAD6C\uBD84\uC120", hint: "\u2500" },
150
+ { key: "image", label: "\uC774\uBBF8\uC9C0", hint: "" }
151
+ ];
152
+ var Editor = (props) => {
153
+ const {
154
+ value = "",
155
+ onChange,
156
+ placeholder = "\uB0B4\uC6A9\uC744 \uC785\uB825\uD558\uC138\uC694",
157
+ readOnly = false,
158
+ toolbar = DEFAULT_TOOLBAR,
159
+ enableSlashCommand = true,
160
+ enableMarkdownShortcuts = true,
161
+ onImageUpload,
162
+ minHeight = 200
163
+ } = props;
164
+ const editorRef = import_react.default.useRef(null);
165
+ const lastRangeRef = import_react.default.useRef(null);
166
+ const composingRef = import_react.default.useRef(false);
167
+ const [isFocused, setIsFocused] = import_react.default.useState(false);
168
+ const [isEmpty, setIsEmpty] = import_react.default.useState(!value);
169
+ const [activeFormats, setActiveFormats] = import_react.default.useState(/* @__PURE__ */ new Set());
170
+ const [slashOpen, setSlashOpen] = import_react.default.useState(false);
171
+ const [slashFilter, setSlashFilter] = import_react.default.useState("");
172
+ const [slashIdx, setSlashIdx] = import_react.default.useState(0);
173
+ const [slashPos, setSlashPos] = import_react.default.useState({ top: 0, left: 0 });
174
+ const slashStartRangeRef = import_react.default.useRef(null);
175
+ const [linkOpen, setLinkOpen] = import_react.default.useState(false);
176
+ const [linkValue, setLinkValue] = import_react.default.useState("");
177
+ const [linkPos, setLinkPos] = import_react.default.useState({ top: 0, left: 0 });
178
+ import_react.default.useEffect(() => {
179
+ if (!editorRef.current) return;
180
+ if (editorRef.current.innerHTML !== value) {
181
+ editorRef.current.innerHTML = sanitizeHtml(value || "");
182
+ updateEmpty();
183
+ }
184
+ }, [value]);
185
+ const updateEmpty = () => {
186
+ const el = editorRef.current;
187
+ if (!el) return;
188
+ const text = el.textContent || "";
189
+ const childCount = el.children.length;
190
+ setIsEmpty(text.length === 0 && childCount <= 1);
191
+ };
192
+ const saveSelection = () => {
193
+ const sel = window.getSelection();
194
+ if (sel && sel.rangeCount > 0 && editorRef.current?.contains(sel.anchorNode)) {
195
+ lastRangeRef.current = sel.getRangeAt(0).cloneRange();
196
+ }
197
+ };
198
+ const restoreSelection = () => {
199
+ if (!lastRangeRef.current) return;
200
+ const sel = window.getSelection();
201
+ sel?.removeAllRanges();
202
+ sel?.addRange(lastRangeRef.current);
203
+ };
204
+ const updateActiveFormats = () => {
205
+ if (!editorRef.current) return;
206
+ const sel = window.getSelection();
207
+ if (!sel || !editorRef.current.contains(sel.anchorNode)) return;
208
+ const next = /* @__PURE__ */ new Set();
209
+ try {
210
+ if (document.queryCommandState("bold")) next.add("bold");
211
+ if (document.queryCommandState("italic")) next.add("italic");
212
+ if (document.queryCommandState("underline")) next.add("underline");
213
+ if (document.queryCommandState("strikethrough")) next.add("strikethrough");
214
+ if (document.queryCommandState("insertUnorderedList")) next.add("list");
215
+ if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
216
+ const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
217
+ if (block) next.add(`block:${block}`);
218
+ } catch {
219
+ }
220
+ setActiveFormats(next);
221
+ };
222
+ const exec = (cmd, val) => {
223
+ document.execCommand(cmd, false, val);
224
+ editorRef.current?.focus();
225
+ updateActiveFormats();
226
+ handleInput();
227
+ };
228
+ const setBlock = (tag) => {
229
+ exec("formatBlock", tag);
230
+ };
231
+ const insertDivider = () => {
232
+ exec("insertHorizontalRule");
233
+ const el = editorRef.current;
234
+ if (el && el.lastElementChild?.tagName === "HR") {
235
+ const p = document.createElement("p");
236
+ p.innerHTML = "<br>";
237
+ el.appendChild(p);
238
+ handleInput();
239
+ }
240
+ };
241
+ const insertImageByUrl = (url) => {
242
+ if (!url) return;
243
+ exec("insertImage", url);
244
+ };
245
+ const triggerImageUpload = async () => {
246
+ if (!onImageUpload) {
247
+ const url = window.prompt("\uC774\uBBF8\uC9C0 URL");
248
+ if (url) {
249
+ restoreSelection();
250
+ insertImageByUrl(url);
251
+ }
252
+ return;
253
+ }
254
+ const input = document.createElement("input");
255
+ input.type = "file";
256
+ input.accept = "image/*";
257
+ input.onchange = async () => {
258
+ const file = input.files?.[0];
259
+ if (!file) return;
260
+ try {
261
+ const url = await onImageUpload(file);
262
+ restoreSelection();
263
+ insertImageByUrl(url);
264
+ } catch (err) {
265
+ console.error("\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC \uC2E4\uD328:", err);
266
+ }
267
+ };
268
+ input.click();
269
+ };
270
+ const openSlashMenu = () => {
271
+ if (!enableSlashCommand) return;
272
+ const sel = window.getSelection();
273
+ if (!sel || sel.rangeCount === 0) return;
274
+ const range = sel.getRangeAt(0);
275
+ slashStartRangeRef.current = range.cloneRange();
276
+ const rect = range.getBoundingClientRect();
277
+ const editorRect = editorRef.current.getBoundingClientRect();
278
+ setSlashPos({
279
+ top: rect.bottom - editorRect.top + 4,
280
+ left: rect.left - editorRect.left
281
+ });
282
+ setSlashFilter("");
283
+ setSlashIdx(0);
284
+ setSlashOpen(true);
285
+ };
286
+ const closeSlashMenu = () => {
287
+ setSlashOpen(false);
288
+ setSlashFilter("");
289
+ setSlashIdx(0);
290
+ slashStartRangeRef.current = null;
291
+ };
292
+ const filteredSlashItems = import_react.default.useMemo(() => {
293
+ if (!slashFilter) return SLASH_ITEMS;
294
+ const f = slashFilter.toLowerCase();
295
+ return SLASH_ITEMS.filter((it) => it.label.toLowerCase().includes(f) || it.key.includes(f));
296
+ }, [slashFilter]);
297
+ const removeSlashTrigger = () => {
298
+ const sel = window.getSelection();
299
+ if (!sel || sel.rangeCount === 0) return;
300
+ const range = sel.getRangeAt(0);
301
+ const node = range.startContainer;
302
+ if (node.nodeType === Node.TEXT_NODE) {
303
+ const text = node.textContent || "";
304
+ const idx = text.lastIndexOf("/");
305
+ if (idx >= 0) {
306
+ node.textContent = text.slice(0, idx) + text.slice(range.startOffset);
307
+ const newRange = document.createRange();
308
+ newRange.setStart(node, idx);
309
+ newRange.collapse(true);
310
+ sel.removeAllRanges();
311
+ sel.addRange(newRange);
312
+ }
313
+ }
314
+ };
315
+ const applySlashItem = (item) => {
316
+ removeSlashTrigger();
317
+ closeSlashMenu();
318
+ switch (item.key) {
319
+ case "paragraph":
320
+ setBlock("p");
321
+ break;
322
+ case "heading":
323
+ setBlock("h2");
324
+ break;
325
+ case "list":
326
+ exec("insertUnorderedList");
327
+ break;
328
+ case "ordered-list":
329
+ exec("insertOrderedList");
330
+ break;
331
+ case "blockquote":
332
+ setBlock("blockquote");
333
+ break;
334
+ case "code-block":
335
+ setBlock("pre");
336
+ break;
337
+ case "divider":
338
+ insertDivider();
339
+ break;
340
+ case "image":
341
+ triggerImageUpload();
342
+ break;
343
+ default:
344
+ break;
345
+ }
346
+ };
347
+ const openLinkEditor = () => {
348
+ saveSelection();
349
+ const sel = window.getSelection();
350
+ if (!sel || sel.rangeCount === 0) return;
351
+ const range = sel.getRangeAt(0);
352
+ const rect = range.getBoundingClientRect();
353
+ const editorRect = editorRef.current.getBoundingClientRect();
354
+ setLinkPos({
355
+ top: rect.bottom - editorRect.top + 4,
356
+ left: rect.left - editorRect.left
357
+ });
358
+ const anchor = sel.anchorNode?.parentElement?.closest("a");
359
+ setLinkValue(anchor?.getAttribute("href") || "");
360
+ setLinkOpen(true);
361
+ };
362
+ const applyLink = () => {
363
+ restoreSelection();
364
+ if (linkValue) {
365
+ exec("createLink", linkValue);
366
+ const sel = window.getSelection();
367
+ const anchor = sel?.anchorNode?.parentElement?.closest("a");
368
+ if (anchor) {
369
+ anchor.setAttribute("target", "_blank");
370
+ anchor.setAttribute("rel", "noopener noreferrer");
371
+ }
372
+ } else {
373
+ exec("unlink");
374
+ }
375
+ setLinkOpen(false);
376
+ };
377
+ const tryMarkdownShortcut = () => {
378
+ if (!enableMarkdownShortcuts) return false;
379
+ const sel = window.getSelection();
380
+ if (!sel || sel.rangeCount === 0) return false;
381
+ const range = sel.getRangeAt(0);
382
+ const node = range.startContainer;
383
+ if (node.nodeType !== Node.TEXT_NODE) return false;
384
+ const text = (node.textContent || "").slice(0, range.startOffset);
385
+ const patterns = [
386
+ [/^###\s$/, () => setBlock("h3")],
387
+ [/^##\s$/, () => setBlock("h2")],
388
+ [/^#\s$/, () => setBlock("h1")],
389
+ [/^-\s$/, () => exec("insertUnorderedList")],
390
+ [/^\*\s$/, () => exec("insertUnorderedList")],
391
+ [/^1\.\s$/, () => exec("insertOrderedList")],
392
+ [/^>\s$/, () => setBlock("blockquote")]
393
+ ];
394
+ for (const [re, fn] of patterns) {
395
+ if (re.test(text)) {
396
+ node.textContent = (node.textContent || "").slice(range.startOffset);
397
+ const newRange = document.createRange();
398
+ newRange.setStart(node, 0);
399
+ newRange.collapse(true);
400
+ sel.removeAllRanges();
401
+ sel.addRange(newRange);
402
+ fn();
403
+ return true;
404
+ }
405
+ }
406
+ return false;
407
+ };
408
+ const handleInput = () => {
409
+ if (composingRef.current) return;
410
+ const el = editorRef.current;
411
+ if (!el) return;
412
+ onChange?.(el.innerHTML);
413
+ updateEmpty();
414
+ updateActiveFormats();
415
+ if (slashOpen) {
416
+ const sel = window.getSelection();
417
+ if (sel && sel.rangeCount > 0) {
418
+ const range = sel.getRangeAt(0);
419
+ const node = range.startContainer;
420
+ if (node.nodeType === Node.TEXT_NODE) {
421
+ const text = node.textContent || "";
422
+ const lastSlash = text.lastIndexOf("/", range.startOffset - 1);
423
+ if (lastSlash >= 0) {
424
+ const filter = text.slice(lastSlash + 1, range.startOffset);
425
+ if (filter.includes(" ") || filter.includes("\n")) {
426
+ closeSlashMenu();
427
+ } else {
428
+ setSlashFilter(filter);
429
+ setSlashIdx(0);
430
+ }
431
+ } else {
432
+ closeSlashMenu();
433
+ }
434
+ }
435
+ }
436
+ }
437
+ };
438
+ const handleKeyDown = (e) => {
439
+ if (readOnly) return;
440
+ if (slashOpen) {
441
+ if (e.key === "ArrowDown") {
442
+ e.preventDefault();
443
+ setSlashIdx((i) => Math.min(i + 1, filteredSlashItems.length - 1));
444
+ return;
445
+ }
446
+ if (e.key === "ArrowUp") {
447
+ e.preventDefault();
448
+ setSlashIdx((i) => Math.max(i - 1, 0));
449
+ return;
450
+ }
451
+ if (e.key === "Enter") {
452
+ e.preventDefault();
453
+ const item = filteredSlashItems[slashIdx];
454
+ if (item) applySlashItem(item);
455
+ return;
456
+ }
457
+ if (e.key === "Escape") {
458
+ e.preventDefault();
459
+ closeSlashMenu();
460
+ return;
461
+ }
462
+ }
463
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
464
+ const k = e.key.toLowerCase();
465
+ if (k === "b") {
466
+ e.preventDefault();
467
+ exec("bold");
468
+ return;
469
+ }
470
+ if (k === "i") {
471
+ e.preventDefault();
472
+ exec("italic");
473
+ return;
474
+ }
475
+ if (k === "u") {
476
+ e.preventDefault();
477
+ exec("underline");
478
+ return;
479
+ }
480
+ if (k === "k") {
481
+ e.preventDefault();
482
+ openLinkEditor();
483
+ return;
484
+ }
485
+ }
486
+ if (enableSlashCommand && e.key === "/") {
487
+ setTimeout(openSlashMenu, 0);
488
+ }
489
+ if (enableMarkdownShortcuts && e.key === " ") {
490
+ if (tryMarkdownShortcut()) {
491
+ e.preventDefault();
492
+ }
493
+ }
494
+ };
495
+ const handlePaste = (e) => {
496
+ if (readOnly) return;
497
+ e.preventDefault();
498
+ const html = e.clipboardData.getData("text/html");
499
+ const text = e.clipboardData.getData("text/plain");
500
+ const clean = html ? sanitizeHtml(html) : escapeHtml(text);
501
+ document.execCommand("insertHTML", false, clean);
502
+ handleInput();
503
+ };
504
+ const handleCompositionStart = () => {
505
+ composingRef.current = true;
506
+ };
507
+ const handleCompositionEnd = () => {
508
+ composingRef.current = false;
509
+ handleInput();
510
+ };
511
+ const handleFocus = () => {
512
+ setIsFocused(true);
513
+ updateActiveFormats();
514
+ };
515
+ const handleBlur = () => {
516
+ setIsFocused(false);
517
+ saveSelection();
518
+ };
519
+ const handleSelectionUpdate = () => {
520
+ updateActiveFormats();
521
+ };
522
+ const onToolbarAction = (item) => {
523
+ switch (item) {
524
+ case "bold":
525
+ exec("bold");
526
+ break;
527
+ case "italic":
528
+ exec("italic");
529
+ break;
530
+ case "underline":
531
+ exec("underline");
532
+ break;
533
+ case "strikethrough":
534
+ exec("strikethrough");
535
+ break;
536
+ case "code": {
537
+ const sel = window.getSelection();
538
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
539
+ const range = sel.getRangeAt(0);
540
+ const code = document.createElement("code");
541
+ try {
542
+ range.surroundContents(code);
543
+ } catch {
544
+ const frag = range.extractContents();
545
+ code.appendChild(frag);
546
+ range.insertNode(code);
547
+ }
548
+ handleInput();
549
+ break;
550
+ }
551
+ case "heading":
552
+ setBlock("h2");
553
+ break;
554
+ case "list":
555
+ exec("insertUnorderedList");
556
+ break;
557
+ case "ordered-list":
558
+ exec("insertOrderedList");
559
+ break;
560
+ case "blockquote":
561
+ setBlock("blockquote");
562
+ break;
563
+ case "code-block":
564
+ setBlock("pre");
565
+ break;
566
+ case "link":
567
+ openLinkEditor();
568
+ break;
569
+ case "image":
570
+ triggerImageUpload();
571
+ break;
572
+ case "divider":
573
+ insertDivider();
574
+ break;
575
+ }
576
+ };
577
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
578
+ !readOnly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
579
+ EditorToolbar,
580
+ {
581
+ items: toolbar,
582
+ active: activeFormats,
583
+ onAction: onToolbarAction
584
+ }
585
+ ),
586
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
587
+ isEmpty && !isFocused && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lib-xplat-editor__placeholder", children: placeholder }),
588
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
589
+ "div",
590
+ {
591
+ ref: editorRef,
592
+ contentEditable: !readOnly,
593
+ suppressContentEditableWarning: true,
594
+ className: "lib-xplat-editor__editable",
595
+ onInput: handleInput,
596
+ onKeyDown: handleKeyDown,
597
+ onKeyUp: handleSelectionUpdate,
598
+ onMouseUp: handleSelectionUpdate,
599
+ onPaste: handlePaste,
600
+ onCompositionStart: handleCompositionStart,
601
+ onCompositionEnd: handleCompositionEnd,
602
+ onFocus: handleFocus,
603
+ onBlur: handleBlur,
604
+ spellCheck: true
605
+ }
606
+ ),
607
+ slashOpen && filteredSlashItems.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
608
+ SlashMenu,
609
+ {
610
+ position: slashPos,
611
+ items: filteredSlashItems,
612
+ activeIndex: slashIdx,
613
+ onSelect: applySlashItem,
614
+ onClose: closeSlashMenu
615
+ }
616
+ ),
617
+ linkOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
618
+ LinkPopover,
619
+ {
620
+ position: linkPos,
621
+ value: linkValue,
622
+ onChange: setLinkValue,
623
+ onConfirm: applyLink,
624
+ onClose: () => setLinkOpen(false)
625
+ }
626
+ )
627
+ ] })
628
+ ] });
629
+ };
630
+ Editor.displayName = "Editor";
631
+ var Editor_default = Editor;
632
+ var TOOLBAR_LABEL = {
633
+ bold: "B",
634
+ italic: "I",
635
+ underline: "U",
636
+ strikethrough: "S",
637
+ code: "<>",
638
+ heading: "H",
639
+ list: "\u2022",
640
+ "ordered-list": "1.",
641
+ blockquote: "\u275D",
642
+ "code-block": "[ ]",
643
+ link: "\u{1F517}",
644
+ image: "\u{1F5BC}",
645
+ divider: "\u2014"
646
+ };
647
+ var TOOLBAR_TITLE = {
648
+ bold: "\uAD75\uAC8C (\u2318B)",
649
+ italic: "\uAE30\uC6B8\uC784 (\u2318I)",
650
+ underline: "\uBC11\uC904 (\u2318U)",
651
+ strikethrough: "\uCDE8\uC18C\uC120",
652
+ code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
653
+ heading: "\uC81C\uBAA9",
654
+ list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
655
+ "ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
656
+ blockquote: "\uC778\uC6A9",
657
+ "code-block": "\uCF54\uB4DC \uBE14\uB85D",
658
+ link: "\uB9C1\uD06C (\u2318K)",
659
+ image: "\uC774\uBBF8\uC9C0",
660
+ divider: "\uAD6C\uBD84\uC120"
661
+ };
662
+ var isActive = (item, active) => {
663
+ if (item === "heading") return active.has("block:h1") || active.has("block:h2") || active.has("block:h3");
664
+ if (item === "blockquote") return active.has("block:blockquote");
665
+ if (item === "code-block") return active.has("block:pre");
666
+ return active.has(item);
667
+ };
668
+ var EditorToolbar = ({ items, active, onAction }) => {
669
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lib-xplat-editor__toolbar", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
670
+ "button",
671
+ {
672
+ type: "button",
673
+ className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
674
+ title: TOOLBAR_TITLE[item],
675
+ onMouseDown: (e) => {
676
+ e.preventDefault();
677
+ onAction(item);
678
+ },
679
+ children: TOOLBAR_LABEL[item]
680
+ },
681
+ item
682
+ )) });
683
+ };
684
+ var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
685
+ import_react.default.useEffect(() => {
686
+ const handleClickOutside = (e) => {
687
+ const target = e.target;
688
+ const menu = document.querySelector(".lib-xplat-editor__slash-menu");
689
+ if (menu && !menu.contains(target)) onClose();
690
+ };
691
+ window.addEventListener("mousedown", handleClickOutside);
692
+ return () => window.removeEventListener("mousedown", handleClickOutside);
693
+ }, [onClose]);
694
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
695
+ "div",
696
+ {
697
+ className: "lib-xplat-editor__slash-menu",
698
+ style: { top: position.top, left: position.left },
699
+ children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ className: clsx_default("lib-xplat-editor__slash-item", i === activeIndex && "active"),
704
+ onMouseDown: (e) => {
705
+ e.preventDefault();
706
+ onSelect(item);
707
+ },
708
+ children: [
709
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "label", children: item.label }),
710
+ item.hint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hint", children: item.hint })
711
+ ]
712
+ },
713
+ item.key
714
+ ))
715
+ }
716
+ );
717
+ };
718
+ var LinkPopover = ({ position, value, onChange, onConfirm, onClose }) => {
719
+ const inputRef = import_react.default.useRef(null);
720
+ import_react.default.useEffect(() => {
721
+ inputRef.current?.focus();
722
+ }, []);
723
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
724
+ "div",
725
+ {
726
+ className: "lib-xplat-editor__link-popover",
727
+ style: { top: position.top, left: position.left },
728
+ children: [
729
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
730
+ "input",
731
+ {
732
+ ref: inputRef,
733
+ type: "url",
734
+ placeholder: "https://",
735
+ value,
736
+ onChange: (e) => onChange(e.target.value),
737
+ onKeyDown: (e) => {
738
+ if (e.key === "Enter") {
739
+ e.preventDefault();
740
+ onConfirm();
741
+ }
742
+ if (e.key === "Escape") {
743
+ e.preventDefault();
744
+ onClose();
745
+ }
746
+ }
747
+ }
748
+ ),
749
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onMouseDown: (e) => {
750
+ e.preventDefault();
751
+ onConfirm();
752
+ }, children: "\uC801\uC6A9" }),
753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onMouseDown: (e) => {
754
+ e.preventDefault();
755
+ onClose();
756
+ }, children: "\uCDE8\uC18C" })
757
+ ]
758
+ }
759
+ );
760
+ };