@x-plat/design-system 0.11.0 → 0.13.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.
Files changed (63) hide show
  1. package/dist/components/AutoResizeTextArea/index.cjs +2 -2
  2. package/dist/components/AutoResizeTextArea/index.js +2 -2
  3. package/dist/components/ChatInput/index.cjs +2 -2
  4. package/dist/components/ChatInput/index.js +2 -2
  5. package/dist/components/CheckBox/index.cjs +2 -2
  6. package/dist/components/CheckBox/index.js +2 -2
  7. package/dist/components/DatePicker/index.cjs +109 -75
  8. package/dist/components/DatePicker/index.d.cts +19 -0
  9. package/dist/components/DatePicker/index.d.ts +19 -0
  10. package/dist/components/DatePicker/index.js +109 -75
  11. package/dist/components/Dropdown/index.cjs +22 -9
  12. package/dist/components/Dropdown/index.css +3 -0
  13. package/dist/components/Dropdown/index.js +22 -9
  14. package/dist/components/Editor/index.cjs +602 -30
  15. package/dist/components/Editor/index.css +103 -0
  16. package/dist/components/Editor/index.d.cts +1 -1
  17. package/dist/components/Editor/index.d.ts +1 -1
  18. package/dist/components/Editor/index.js +602 -30
  19. package/dist/components/FieldMessage/index.cjs +2 -2
  20. package/dist/components/FieldMessage/index.d.cts +8 -0
  21. package/dist/components/FieldMessage/index.d.ts +8 -0
  22. package/dist/components/FieldMessage/index.js +2 -2
  23. package/dist/components/FileUpload/index.cjs +10 -6
  24. package/dist/components/FileUpload/index.d.cts +15 -2
  25. package/dist/components/FileUpload/index.d.ts +15 -2
  26. package/dist/components/FileUpload/index.js +10 -6
  27. package/dist/components/ImageSelector/index.cjs +7 -6
  28. package/dist/components/ImageSelector/index.d.cts +15 -2
  29. package/dist/components/ImageSelector/index.d.ts +15 -2
  30. package/dist/components/ImageSelector/index.js +7 -6
  31. package/dist/components/Input/index.cjs +2 -2
  32. package/dist/components/Input/index.d.cts +4 -0
  33. package/dist/components/Input/index.d.ts +4 -0
  34. package/dist/components/Input/index.js +2 -2
  35. package/dist/components/PopOver/index.cjs +21 -7
  36. package/dist/components/PopOver/index.css +3 -0
  37. package/dist/components/PopOver/index.js +21 -7
  38. package/dist/components/Radio/index.cjs +2 -2
  39. package/dist/components/Radio/index.js +2 -2
  40. package/dist/components/Select/index.cjs +23 -10
  41. package/dist/components/Select/index.css +3 -0
  42. package/dist/components/Select/index.d.cts +1 -2
  43. package/dist/components/Select/index.d.ts +1 -2
  44. package/dist/components/Select/index.js +23 -10
  45. package/dist/components/Switch/index.cjs +2 -2
  46. package/dist/components/Switch/index.js +2 -2
  47. package/dist/components/TextArea/index.cjs +2 -2
  48. package/dist/components/TextArea/index.js +2 -2
  49. package/dist/components/TimePicker/index.cjs +23 -10
  50. package/dist/components/TimePicker/index.css +3 -0
  51. package/dist/components/TimePicker/index.js +23 -10
  52. package/dist/components/index.cjs +751 -124
  53. package/dist/components/index.css +113 -0
  54. package/dist/components/index.js +751 -124
  55. package/dist/index.cjs +751 -124
  56. package/dist/index.css +113 -0
  57. package/dist/index.js +751 -124
  58. package/guidelines/Guidelines.md +12 -0
  59. package/guidelines/MIGRATION.md +270 -0
  60. package/guidelines/components/datepicker.md +26 -0
  61. package/guidelines/components/editor.md +58 -3
  62. package/guidelines/components/file-media.md +21 -4
  63. package/package.json +1 -1
@@ -17,6 +17,109 @@ function clsx() {
17
17
  }
18
18
  var clsx_default = clsx;
19
19
 
20
+ // src/components/Editor/highlight.ts
21
+ var CODE_LANGUAGES = [
22
+ { value: "plain", label: "\uC5C6\uC74C" },
23
+ { value: "javascript", label: "JavaScript" },
24
+ { value: "typescript", label: "TypeScript" },
25
+ { value: "json", label: "JSON" },
26
+ { value: "html", label: "HTML" },
27
+ { value: "css", label: "CSS" },
28
+ { value: "python", label: "Python" },
29
+ { value: "sql", label: "SQL" },
30
+ { value: "bash", label: "Bash" }
31
+ ];
32
+ var KEYWORDS = {
33
+ plain: [],
34
+ javascript: "const let var function return if else for while class new this import export from default async await try catch throw typeof instanceof null undefined true false break continue switch case do delete in of yield".split(" "),
35
+ typescript: "const let var function return if else for while class new this import export from default async await try catch throw typeof instanceof null undefined true false break continue switch case do delete in of yield interface type enum implements extends public private protected readonly as satisfies keyof infer never unknown any".split(" "),
36
+ json: ["true", "false", "null"],
37
+ html: [],
38
+ css: [],
39
+ python: "def class return if elif else for while import from as try except finally raise with lambda None True False and or not in is pass break continue global nonlocal yield async await".split(" "),
40
+ sql: "SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE ALTER DROP JOIN LEFT RIGHT INNER OUTER ON GROUP BY ORDER HAVING LIMIT OFFSET AS AND OR NOT NULL DISTINCT UNION INDEX PRIMARY KEY FOREIGN REFERENCES DEFAULT".split(" "),
41
+ bash: "if then else fi for while do done case esac function return export local echo cd ls rm cp mv mkdir sudo source set unset exit".split(" ")
42
+ };
43
+ var LINE_COMMENT = {
44
+ javascript: "//",
45
+ typescript: "//",
46
+ css: "",
47
+ python: "#",
48
+ sql: "--",
49
+ bash: "#"
50
+ };
51
+ var escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
52
+ var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
53
+ var highlight = (code, lang) => {
54
+ if (lang === "plain" || !code) return escape(code);
55
+ const keywords = new Set(KEYWORDS[lang]);
56
+ const lineComment = LINE_COMMENT[lang];
57
+ const caseInsensitive = lang === "sql";
58
+ let out = "";
59
+ let i = 0;
60
+ while (i < code.length) {
61
+ const rest = code.slice(i);
62
+ if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
63
+ const end = code.indexOf("*/", i + 2);
64
+ const stop = end === -1 ? code.length : end + 2;
65
+ out += wrap("comment", code.slice(i, stop));
66
+ i = stop;
67
+ continue;
68
+ }
69
+ if (lineComment && rest.startsWith(lineComment)) {
70
+ const nl = code.indexOf("\n", i);
71
+ const stop = nl === -1 ? code.length : nl;
72
+ out += wrap("comment", code.slice(i, stop));
73
+ i = stop;
74
+ continue;
75
+ }
76
+ const q = rest[0];
77
+ if (q === '"' || q === "'" || q === "`") {
78
+ let j = 1;
79
+ while (j < rest.length) {
80
+ if (rest[j] === "\\") {
81
+ j += 2;
82
+ continue;
83
+ }
84
+ if (rest[j] === q) {
85
+ j += 1;
86
+ break;
87
+ }
88
+ if (rest[j] === "\n" && q !== "`") break;
89
+ j += 1;
90
+ }
91
+ out += wrap("string", rest.slice(0, j));
92
+ i += j;
93
+ continue;
94
+ }
95
+ const num = /^\d+(\.\d+)?/.exec(rest);
96
+ if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
97
+ out += wrap("number", num[0]);
98
+ i += num[0].length;
99
+ continue;
100
+ }
101
+ const word = /^[A-Za-z_$][\w$]*/.exec(rest);
102
+ if (word) {
103
+ const w = word[0];
104
+ const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
105
+ out += hit ? wrap("keyword", w) : escape(w);
106
+ i += w.length;
107
+ continue;
108
+ }
109
+ if (lang === "html" && rest.startsWith("<")) {
110
+ const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
111
+ if (tag) {
112
+ out += wrap("keyword", tag[0]);
113
+ i += tag[0].length;
114
+ continue;
115
+ }
116
+ }
117
+ out += escape(code[i]);
118
+ i += 1;
119
+ }
120
+ return out;
121
+ };
122
+
20
123
  // src/components/Editor/Editor.tsx
21
124
  import { jsx, jsxs } from "react/jsx-runtime";
22
125
  var DEFAULT_TOOLBAR = [
@@ -25,14 +128,27 @@ var DEFAULT_TOOLBAR = [
25
128
  "underline",
26
129
  "strikethrough",
27
130
  "code",
28
- "heading",
131
+ "heading-1",
132
+ "heading-2",
133
+ "heading-3",
29
134
  "list",
30
135
  "ordered-list",
31
136
  "blockquote",
32
137
  "code-block",
33
138
  "link",
34
139
  "image",
35
- "divider"
140
+ "divider",
141
+ "checklist",
142
+ "table",
143
+ "align-left",
144
+ "align-center",
145
+ "align-right",
146
+ "highlight",
147
+ "superscript",
148
+ "subscript",
149
+ "clear-format",
150
+ "undo",
151
+ "redo"
36
152
  ];
37
153
  var ALLOWED_TAGS = /* @__PURE__ */ new Set([
38
154
  "p",
@@ -61,11 +177,41 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
61
177
  "img",
62
178
  "hr",
63
179
  "span",
64
- "div"
180
+ "div",
181
+ "sub",
182
+ "sup",
183
+ "mark",
184
+ "table",
185
+ "thead",
186
+ "tbody",
187
+ "tr",
188
+ "th",
189
+ "td"
65
190
  ]);
66
191
  var ALLOWED_ATTRS = {
67
192
  a: ["href", "target", "rel"],
68
- img: ["src", "alt"]
193
+ img: ["src", "alt", "width"],
194
+ // 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
195
+ p: ["class"],
196
+ h1: ["class"],
197
+ h2: ["class"],
198
+ h3: ["class"],
199
+ h4: ["class"],
200
+ h5: ["class"],
201
+ h6: ["class"],
202
+ ul: ["class"],
203
+ ol: ["class"],
204
+ li: ["class", "data-checked"],
205
+ blockquote: ["class"],
206
+ div: ["class"],
207
+ span: ["class"],
208
+ pre: ["class"],
209
+ code: ["class"],
210
+ mark: ["class"],
211
+ table: ["class"],
212
+ td: ["class"],
213
+ th: ["class"],
214
+ tr: ["class"]
69
215
  };
70
216
  var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
71
217
  "script",
@@ -118,7 +264,9 @@ var escapeHtml = (text) => {
118
264
  };
119
265
  var SLASH_ITEMS = [
120
266
  { key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
121
- { key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
267
+ { key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
268
+ { key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
269
+ { key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
122
270
  { key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
123
271
  { key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
124
272
  { key: "blockquote", label: "\uC778\uC6A9", hint: "" },
@@ -161,6 +309,8 @@ var Editor = (props) => {
161
309
  el.innerHTML = sanitizeHtml(value || "");
162
310
  if (!el.firstChild) el.innerHTML = "<p><br></p>";
163
311
  lastEmittedRef.current = null;
312
+ historyRef.current = [{ html: el.innerHTML, caret: 0 }];
313
+ historyIdxRef.current = 0;
164
314
  updateEmpty();
165
315
  }, [value]);
166
316
  const updateEmpty = () => {
@@ -205,6 +355,17 @@ var Editor = (props) => {
205
355
  if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
206
356
  const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
207
357
  if (block) next.add(`block:${block}`);
358
+ if (document.queryCommandState("superscript")) next.add("superscript");
359
+ if (document.queryCommandState("subscript")) next.add("subscript");
360
+ } catch {
361
+ }
362
+ try {
363
+ const blockEl = currentBlock();
364
+ if (blockEl?.classList.contains("align-center")) next.add("align:center");
365
+ else if (blockEl?.classList.contains("align-right")) next.add("align:right");
366
+ else if (blockEl) next.add("align:left");
367
+ if (blockEl?.closest("mark")) next.add("highlight");
368
+ if (blockEl?.closest("ul.checklist")) next.add("checklist");
208
369
  } catch {
209
370
  }
210
371
  setActiveFormats(next);
@@ -218,6 +379,122 @@ var Editor = (props) => {
218
379
  const setBlock = (tag) => {
219
380
  exec("formatBlock", tag);
220
381
  };
382
+ const toggleBlock = (tag) => {
383
+ const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
384
+ setBlock(current === tag ? "p" : tag);
385
+ };
386
+ const historyRef = React.useRef([]);
387
+ const historyIdxRef = React.useRef(-1);
388
+ const lastPushAtRef = React.useRef(0);
389
+ const isRestoringRef = React.useRef(false);
390
+ const caretOffset = (rootEl) => {
391
+ const el = rootEl ?? editorRef.current;
392
+ const sel = window.getSelection();
393
+ if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
394
+ const range = sel.getRangeAt(0).cloneRange();
395
+ range.selectNodeContents(el);
396
+ range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
397
+ return range.toString().length;
398
+ };
399
+ const setCaretOffset = (offset, rootEl) => {
400
+ const el = rootEl ?? editorRef.current;
401
+ if (!el) return;
402
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
403
+ let remaining = offset;
404
+ let node = null;
405
+ while (node = walker.nextNode()) {
406
+ const len = node.textContent?.length ?? 0;
407
+ if (remaining <= len) {
408
+ const range = document.createRange();
409
+ range.setStart(node, remaining);
410
+ range.collapse(true);
411
+ const sel = window.getSelection();
412
+ sel?.removeAllRanges();
413
+ sel?.addRange(range);
414
+ return;
415
+ }
416
+ remaining -= len;
417
+ }
418
+ placeCaretAtEnd(el);
419
+ };
420
+ const pushHistory = (force = false) => {
421
+ const el = editorRef.current;
422
+ if (!el || isRestoringRef.current) return;
423
+ const html = el.innerHTML;
424
+ const hist = historyRef.current;
425
+ const cur = hist[historyIdxRef.current];
426
+ if (cur && cur.html === html) return;
427
+ const now = Date.now();
428
+ const coalesce = !force && cur && now - lastPushAtRef.current < 500;
429
+ lastPushAtRef.current = now;
430
+ if (coalesce) {
431
+ hist[historyIdxRef.current] = { html, caret: caretOffset() };
432
+ return;
433
+ }
434
+ hist.length = historyIdxRef.current + 1;
435
+ hist.push({ html, caret: caretOffset() });
436
+ if (hist.length > 200) hist.shift();
437
+ historyIdxRef.current = hist.length - 1;
438
+ };
439
+ const applySnapshot = (snap) => {
440
+ const el = editorRef.current;
441
+ if (!el) return;
442
+ isRestoringRef.current = true;
443
+ el.innerHTML = snap.html;
444
+ setCaretOffset(snap.caret);
445
+ isRestoringRef.current = false;
446
+ lastEmittedRef.current = el.innerHTML;
447
+ onChange?.(el.innerHTML);
448
+ updateEmpty();
449
+ updateActiveFormats();
450
+ };
451
+ const undo = () => {
452
+ pushHistory(true);
453
+ if (historyIdxRef.current <= 0) return;
454
+ historyIdxRef.current -= 1;
455
+ applySnapshot(historyRef.current[historyIdxRef.current]);
456
+ };
457
+ const redo = () => {
458
+ if (historyIdxRef.current >= historyRef.current.length - 1) return;
459
+ historyIdxRef.current += 1;
460
+ applySnapshot(historyRef.current[historyIdxRef.current]);
461
+ };
462
+ const langOf = (pre) => {
463
+ const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
464
+ return cls?.slice(5) ?? "plain";
465
+ };
466
+ const [codeLang, setCodeLang] = React.useState("plain");
467
+ const highlightRafRef = React.useRef(0);
468
+ const rehighlight = (pre) => {
469
+ if (composingRef.current) return;
470
+ const code = pre.querySelector("code");
471
+ if (!code) return;
472
+ const lang = langOf(pre);
473
+ const text = code.textContent || "";
474
+ const next = highlight(text, lang);
475
+ if (code.innerHTML === next) return;
476
+ const caret = caretOffset(code);
477
+ isRestoringRef.current = true;
478
+ code.innerHTML = next;
479
+ setCaretOffset(caret, code);
480
+ isRestoringRef.current = false;
481
+ };
482
+ const scheduleHighlight = () => {
483
+ const pre = closestPre();
484
+ if (!pre) return;
485
+ cancelAnimationFrame(highlightRafRef.current);
486
+ highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
487
+ };
488
+ const changeCodeLang = (lang) => {
489
+ const pre = closestPre();
490
+ if (!pre) return;
491
+ pushHistory(true);
492
+ Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
493
+ if (lang !== "plain") pre.classList.add(`lang-${lang}`);
494
+ setCodeLang(lang);
495
+ rehighlight(pre);
496
+ handleInput();
497
+ };
221
498
  const closestPre = () => {
222
499
  const sel = window.getSelection();
223
500
  const node = sel?.anchorNode;
@@ -226,6 +503,7 @@ var Editor = (props) => {
226
503
  return el?.closest("pre") ?? null;
227
504
  };
228
505
  const insertCodeBlock = () => {
506
+ pushHistory(true);
229
507
  const el = editorRef.current;
230
508
  const sel = window.getSelection();
231
509
  if (!el || !sel || sel.rangeCount === 0) return;
@@ -308,7 +586,167 @@ var Editor = (props) => {
308
586
  sel.addRange(r2);
309
587
  editorRef.current?.focus();
310
588
  };
589
+ const clearFormat = () => {
590
+ exec("removeFormat");
591
+ exec("unlink");
592
+ setBlock("p");
593
+ };
594
+ const indentList = (outdent) => {
595
+ exec(outdent ? "outdent" : "indent");
596
+ };
597
+ const inListItem = () => {
598
+ const sel = window.getSelection();
599
+ const node = sel?.anchorNode;
600
+ if (!node || !editorRef.current?.contains(node)) return false;
601
+ const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
602
+ return !!el?.closest("li");
603
+ };
604
+ const currentBlock = () => {
605
+ const sel = window.getSelection();
606
+ const node = sel?.anchorNode;
607
+ const root = editorRef.current;
608
+ if (!node || !root?.contains(node)) return null;
609
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
610
+ while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
611
+ el = el.parentElement;
612
+ }
613
+ return el && el !== root ? el : null;
614
+ };
615
+ const setAlign = (align) => {
616
+ const block = currentBlock();
617
+ if (!block) return;
618
+ pushHistory(true);
619
+ block.classList.remove("align-left", "align-center", "align-right");
620
+ if (align !== "left") block.classList.add(`align-${align}`);
621
+ handleInput();
622
+ };
623
+ const toggleChecklist = () => {
624
+ pushHistory(true);
625
+ const block = currentBlock();
626
+ const list = block?.closest("ul");
627
+ if (list?.classList.contains("checklist")) {
628
+ list.classList.remove("checklist");
629
+ list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
630
+ handleInput();
631
+ return;
632
+ }
633
+ if (list) {
634
+ list.classList.add("checklist");
635
+ list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
636
+ handleInput();
637
+ return;
638
+ }
639
+ exec("insertUnorderedList");
640
+ const made = currentBlock()?.closest("ul");
641
+ if (made) {
642
+ made.classList.add("checklist");
643
+ made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
644
+ }
645
+ handleInput();
646
+ };
647
+ const handleEditorClick = (e) => {
648
+ if (readOnly) return;
649
+ const target = e.target;
650
+ const li = target.closest?.("li");
651
+ if (!li || !li.closest("ul.checklist")) return;
652
+ const rect = li.getBoundingClientRect();
653
+ if (e.clientX - rect.left > 20) return;
654
+ e.preventDefault();
655
+ pushHistory(true);
656
+ li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
657
+ handleInput();
658
+ };
659
+ const toggleHighlight = () => {
660
+ const sel = window.getSelection();
661
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
662
+ pushHistory(true);
663
+ const range = sel.getRangeAt(0);
664
+ const node = sel.anchorNode;
665
+ const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
666
+ const existing = el?.closest("mark");
667
+ if (existing) {
668
+ const parent = existing.parentNode;
669
+ while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
670
+ parent?.removeChild(existing);
671
+ handleInput();
672
+ return;
673
+ }
674
+ const mark = document.createElement("mark");
675
+ try {
676
+ range.surroundContents(mark);
677
+ } catch {
678
+ const frag = range.extractContents();
679
+ mark.appendChild(frag);
680
+ range.insertNode(mark);
681
+ }
682
+ handleInput();
683
+ };
684
+ const insertTable = (cols = 3, rows = 2) => {
685
+ const sel = window.getSelection();
686
+ if (!sel || sel.rangeCount === 0) return;
687
+ pushHistory(true);
688
+ const table = document.createElement("table");
689
+ const thead = document.createElement("thead");
690
+ const headRow = document.createElement("tr");
691
+ for (let c = 0; c < cols; c++) {
692
+ const th = document.createElement("th");
693
+ th.innerHTML = "<br>";
694
+ headRow.appendChild(th);
695
+ }
696
+ thead.appendChild(headRow);
697
+ table.appendChild(thead);
698
+ const tbody = document.createElement("tbody");
699
+ for (let r2 = 0; r2 < rows; r2++) {
700
+ const tr = document.createElement("tr");
701
+ for (let c = 0; c < cols; c++) {
702
+ const td = document.createElement("td");
703
+ td.innerHTML = "<br>";
704
+ tr.appendChild(td);
705
+ }
706
+ tbody.appendChild(tr);
707
+ }
708
+ table.appendChild(tbody);
709
+ const range = sel.getRangeAt(0);
710
+ range.deleteContents();
711
+ range.insertNode(table);
712
+ ensureParagraphAfter(table);
713
+ const first = table.querySelector("th");
714
+ if (first) placeCaretAtEnd(first);
715
+ handleInput();
716
+ };
717
+ const moveTableCell = (backwards) => {
718
+ const block = currentBlock();
719
+ const cell = block?.closest("td, th");
720
+ if (!cell) return false;
721
+ const table = cell.closest("table");
722
+ if (!table) return false;
723
+ const cells = Array.from(table.querySelectorAll("td, th"));
724
+ const i = cells.indexOf(cell);
725
+ const next = cells[i + (backwards ? -1 : 1)];
726
+ if (next) {
727
+ placeCaretAtEnd(next);
728
+ return true;
729
+ }
730
+ if (!backwards) {
731
+ pushHistory(true);
732
+ const lastRow = table.querySelector("tbody")?.lastElementChild;
733
+ if (lastRow) {
734
+ const tr = document.createElement("tr");
735
+ for (let c = 0; c < lastRow.children.length; c++) {
736
+ const td = document.createElement("td");
737
+ td.innerHTML = "<br>";
738
+ tr.appendChild(td);
739
+ }
740
+ lastRow.after(tr);
741
+ placeCaretAtEnd(tr.firstElementChild);
742
+ handleInput();
743
+ return true;
744
+ }
745
+ }
746
+ return false;
747
+ };
311
748
  const insertDivider = () => {
749
+ pushHistory(true);
312
750
  exec("insertHorizontalRule");
313
751
  const el = editorRef.current;
314
752
  if (el && el.lastElementChild?.tagName === "HR") {
@@ -401,8 +839,14 @@ var Editor = (props) => {
401
839
  case "paragraph":
402
840
  setBlock("p");
403
841
  break;
404
- case "heading":
405
- setBlock("h2");
842
+ case "heading-1":
843
+ toggleBlock("h1");
844
+ break;
845
+ case "heading-2":
846
+ toggleBlock("h2");
847
+ break;
848
+ case "heading-3":
849
+ toggleBlock("h3");
406
850
  break;
407
851
  case "list":
408
852
  exec("insertUnorderedList");
@@ -495,6 +939,8 @@ var Editor = (props) => {
495
939
  if (!el) return;
496
940
  const html = el.innerHTML;
497
941
  lastEmittedRef.current = html;
942
+ pushHistory();
943
+ scheduleHighlight();
498
944
  onChange?.(html);
499
945
  updateEmpty();
500
946
  updateActiveFormats();
@@ -548,6 +994,15 @@ var Editor = (props) => {
548
994
  return;
549
995
  }
550
996
  }
997
+ if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
998
+ e.preventDefault();
999
+ return;
1000
+ }
1001
+ if (e.key === "Tab" && inListItem()) {
1002
+ e.preventDefault();
1003
+ indentList(e.shiftKey);
1004
+ return;
1005
+ }
551
1006
  if (slashOpen) {
552
1007
  if (e.key === "ArrowDown") {
553
1008
  e.preventDefault();
@@ -571,6 +1026,21 @@ var Editor = (props) => {
571
1026
  return;
572
1027
  }
573
1028
  }
1029
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
1030
+ e.preventDefault();
1031
+ if (e.shiftKey) redo();
1032
+ else undo();
1033
+ return;
1034
+ }
1035
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
1036
+ plainPasteRef.current = true;
1037
+ return;
1038
+ }
1039
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
1040
+ e.preventDefault();
1041
+ redo();
1042
+ return;
1043
+ }
574
1044
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
575
1045
  const k = e.key.toLowerCase();
576
1046
  if (k === "b") {
@@ -604,10 +1074,16 @@ var Editor = (props) => {
604
1074
  }
605
1075
  }
606
1076
  };
1077
+ const plainPasteRef = React.useRef(false);
607
1078
  const handlePaste = (e) => {
608
1079
  if (readOnly) return;
609
1080
  e.preventDefault();
610
1081
  const text = e.clipboardData.getData("text/plain");
1082
+ if (plainPasteRef.current) {
1083
+ plainPasteRef.current = false;
1084
+ insertTextAtCaret(text);
1085
+ return;
1086
+ }
611
1087
  if (closestPre()) {
612
1088
  insertTextAtCaret(text);
613
1089
  return;
@@ -634,9 +1110,11 @@ var Editor = (props) => {
634
1110
  };
635
1111
  const handleSelectionUpdate = () => {
636
1112
  updateActiveFormats();
1113
+ const pre = closestPre();
1114
+ setCodeLang(pre ? langOf(pre) : "plain");
637
1115
  };
638
1116
  const onToolbarAction = (item) => {
639
- if (closestPre() && item !== "code-block") return;
1117
+ if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
640
1118
  switch (item) {
641
1119
  case "bold":
642
1120
  exec("bold");
@@ -665,8 +1143,14 @@ var Editor = (props) => {
665
1143
  handleInput();
666
1144
  break;
667
1145
  }
668
- case "heading":
669
- setBlock("h2");
1146
+ case "heading-1":
1147
+ toggleBlock("h1");
1148
+ break;
1149
+ case "heading-2":
1150
+ toggleBlock("h2");
1151
+ break;
1152
+ case "heading-3":
1153
+ toggleBlock("h3");
670
1154
  break;
671
1155
  case "list":
672
1156
  exec("insertUnorderedList");
@@ -689,6 +1173,39 @@ var Editor = (props) => {
689
1173
  case "divider":
690
1174
  insertDivider();
691
1175
  break;
1176
+ case "checklist":
1177
+ toggleChecklist();
1178
+ break;
1179
+ case "align-left":
1180
+ setAlign("left");
1181
+ break;
1182
+ case "align-center":
1183
+ setAlign("center");
1184
+ break;
1185
+ case "align-right":
1186
+ setAlign("right");
1187
+ break;
1188
+ case "highlight":
1189
+ toggleHighlight();
1190
+ break;
1191
+ case "superscript":
1192
+ exec("superscript");
1193
+ break;
1194
+ case "subscript":
1195
+ exec("subscript");
1196
+ break;
1197
+ case "table":
1198
+ insertTable();
1199
+ break;
1200
+ case "clear-format":
1201
+ clearFormat();
1202
+ break;
1203
+ case "undo":
1204
+ undo();
1205
+ break;
1206
+ case "redo":
1207
+ redo();
1208
+ break;
692
1209
  }
693
1210
  };
694
1211
  return /* @__PURE__ */ jsxs("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
@@ -697,7 +1214,10 @@ var Editor = (props) => {
697
1214
  {
698
1215
  items: toolbar,
699
1216
  active: activeFormats,
700
- onAction: onToolbarAction
1217
+ onAction: onToolbarAction,
1218
+ codeLang,
1219
+ inCodeBlock: activeFormats.has("block:pre"),
1220
+ onCodeLangChange: changeCodeLang
701
1221
  }
702
1222
  ),
703
1223
  /* @__PURE__ */ jsxs("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
@@ -715,6 +1235,7 @@ var Editor = (props) => {
715
1235
  className: "lib-xplat-editor__editable",
716
1236
  onInput: handleInput,
717
1237
  onKeyDown: handleKeyDown,
1238
+ onClick: handleEditorClick,
718
1239
  onKeyUp: handleSelectionUpdate,
719
1240
  onMouseUp: handleSelectionUpdate,
720
1241
  onPaste: handlePaste,
@@ -756,14 +1277,27 @@ var TOOLBAR_LABEL = {
756
1277
  underline: "U",
757
1278
  strikethrough: "S",
758
1279
  code: "<>",
759
- heading: "H",
1280
+ "heading-1": "H1",
1281
+ "heading-2": "H2",
1282
+ "heading-3": "H3",
760
1283
  list: "\u2022",
761
1284
  "ordered-list": "1.",
762
1285
  blockquote: "\u275D",
763
1286
  "code-block": "[ ]",
764
1287
  link: "\u{1F517}",
765
1288
  image: "\u{1F5BC}",
766
- divider: "\u2014"
1289
+ divider: "\u2014",
1290
+ checklist: "\u2611",
1291
+ "align-left": "\u21E4",
1292
+ "align-center": "\u2194",
1293
+ "align-right": "\u21E5",
1294
+ highlight: "\u{1F58D}",
1295
+ superscript: "x\xB2",
1296
+ subscript: "x\u2082",
1297
+ table: "\u25A6",
1298
+ "clear-format": "\u232B",
1299
+ undo: "\u21B6",
1300
+ redo: "\u21B7"
767
1301
  };
768
1302
  var TOOLBAR_TITLE = {
769
1303
  bold: "\uAD75\uAC8C (\u2318B)",
@@ -771,36 +1305,74 @@ var TOOLBAR_TITLE = {
771
1305
  underline: "\uBC11\uC904 (\u2318U)",
772
1306
  strikethrough: "\uCDE8\uC18C\uC120",
773
1307
  code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
774
- heading: "\uC81C\uBAA9",
1308
+ "heading-1": "\uC81C\uBAA9 1",
1309
+ "heading-2": "\uC81C\uBAA9 2",
1310
+ "heading-3": "\uC81C\uBAA9 3",
775
1311
  list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
776
1312
  "ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
777
1313
  blockquote: "\uC778\uC6A9",
778
1314
  "code-block": "\uCF54\uB4DC \uBE14\uB85D",
779
1315
  link: "\uB9C1\uD06C (\u2318K)",
780
1316
  image: "\uC774\uBBF8\uC9C0",
781
- divider: "\uAD6C\uBD84\uC120"
1317
+ divider: "\uAD6C\uBD84\uC120",
1318
+ checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
1319
+ "align-left": "\uC67C\uCABD \uC815\uB82C",
1320
+ "align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
1321
+ "align-right": "\uC624\uB978\uCABD \uC815\uB82C",
1322
+ highlight: "\uD615\uAD11\uD39C",
1323
+ superscript: "\uC704 \uCCA8\uC790",
1324
+ subscript: "\uC544\uB798 \uCCA8\uC790",
1325
+ table: "\uD45C \uC0BD\uC785",
1326
+ "clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
1327
+ undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
1328
+ redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
782
1329
  };
783
1330
  var isActive = (item, active) => {
784
- if (item === "heading") return active.has("block:h1") || active.has("block:h2") || active.has("block:h3");
1331
+ if (item === "heading-1") return active.has("block:h1");
1332
+ if (item === "heading-2") return active.has("block:h2");
1333
+ if (item === "heading-3") return active.has("block:h3");
785
1334
  if (item === "blockquote") return active.has("block:blockquote");
786
1335
  if (item === "code-block") return active.has("block:pre");
1336
+ if (item === "align-left") return active.has("align:left");
1337
+ if (item === "align-center") return active.has("align:center");
1338
+ if (item === "align-right") return active.has("align:right");
787
1339
  return active.has(item);
788
1340
  };
789
- var EditorToolbar = ({ items, active, onAction }) => {
790
- return /* @__PURE__ */ jsx("div", { className: "lib-xplat-editor__toolbar", children: items.map((item) => /* @__PURE__ */ jsx(
791
- "button",
792
- {
793
- type: "button",
794
- className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
795
- title: TOOLBAR_TITLE[item],
796
- onMouseDown: (e) => {
797
- e.preventDefault();
798
- onAction(item);
1341
+ var EditorToolbar = ({
1342
+ items,
1343
+ active,
1344
+ onAction,
1345
+ codeLang,
1346
+ inCodeBlock,
1347
+ onCodeLangChange
1348
+ }) => {
1349
+ return /* @__PURE__ */ jsxs("div", { className: "lib-xplat-editor__toolbar", children: [
1350
+ items.map((item) => /* @__PURE__ */ jsx(
1351
+ "button",
1352
+ {
1353
+ type: "button",
1354
+ className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
1355
+ title: TOOLBAR_TITLE[item],
1356
+ onMouseDown: (e) => {
1357
+ e.preventDefault();
1358
+ onAction(item);
1359
+ },
1360
+ children: TOOLBAR_LABEL[item]
799
1361
  },
800
- children: TOOLBAR_LABEL[item]
801
- },
802
- item
803
- )) });
1362
+ item
1363
+ )),
1364
+ inCodeBlock && /* @__PURE__ */ jsx(
1365
+ "select",
1366
+ {
1367
+ className: "lib-xplat-editor__lang",
1368
+ value: codeLang,
1369
+ "aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
1370
+ onMouseDown: (e) => e.stopPropagation(),
1371
+ onChange: (e) => onCodeLangChange(e.target.value),
1372
+ children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ jsx("option", { value: l.value, children: l.label }, l.value))
1373
+ }
1374
+ )
1375
+ ] });
804
1376
  };
805
1377
  var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
806
1378
  React.useEffect(() => {