@x-plat/design-system 0.11.0 → 0.12.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.cjs CHANGED
@@ -8354,8 +8354,10 @@ var import_react19 = __toESM(require("react"), 1);
8354
8354
  var useAutoPosition = (triggerRef, popRef, enabled = true) => {
8355
8355
  const [position, setPosition] = import_react19.default.useState({
8356
8356
  position: {},
8357
- direction: "bottom"
8357
+ direction: "bottom",
8358
+ repositioning: false
8358
8359
  });
8360
+ const placedRef = import_react19.default.useRef(false);
8359
8361
  const calculatePosition = import_react19.default.useCallback(() => {
8360
8362
  if (!triggerRef.current || !popRef.current) return;
8361
8363
  const triggerRect = triggerRef.current.getBoundingClientRect();
@@ -8380,11 +8382,16 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
8380
8382
  if (left < 8) left = 8;
8381
8383
  setPosition({
8382
8384
  position: { top, left },
8383
- direction
8385
+ direction,
8386
+ repositioning: placedRef.current
8384
8387
  });
8388
+ placedRef.current = true;
8385
8389
  }, [triggerRef, popRef]);
8386
8390
  import_react19.default.useLayoutEffect(() => {
8387
- if (!enabled) return;
8391
+ if (!enabled) {
8392
+ placedRef.current = false;
8393
+ return;
8394
+ }
8388
8395
  calculatePosition();
8389
8396
  const raf = requestAnimationFrame(calculatePosition);
8390
8397
  return () => cancelAnimationFrame(raf);
@@ -8399,11 +8406,17 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
8399
8406
  }, [calculatePosition, enabled, popRef]);
8400
8407
  import_react19.default.useEffect(() => {
8401
8408
  if (!enabled) return;
8402
- window.addEventListener("resize", calculatePosition);
8403
- window.addEventListener("scroll", calculatePosition, true);
8409
+ let raf = 0;
8410
+ const onScrollOrResize = () => {
8411
+ cancelAnimationFrame(raf);
8412
+ raf = requestAnimationFrame(calculatePosition);
8413
+ };
8414
+ window.addEventListener("resize", onScrollOrResize, { passive: true });
8415
+ window.addEventListener("scroll", onScrollOrResize, { passive: true, capture: true });
8404
8416
  return () => {
8405
- window.removeEventListener("resize", calculatePosition);
8406
- window.removeEventListener("scroll", calculatePosition, true);
8417
+ cancelAnimationFrame(raf);
8418
+ window.removeEventListener("resize", onScrollOrResize);
8419
+ window.removeEventListener("scroll", onScrollOrResize, true);
8407
8420
  };
8408
8421
  }, [calculatePosition, enabled]);
8409
8422
  return position;
@@ -8444,7 +8457,7 @@ var Dropdown = (props) => {
8444
8457
  const [visible, setVisible] = import_react21.default.useState(false);
8445
8458
  const triggerRef = import_react21.default.useRef(null);
8446
8459
  const menuRef = import_react21.default.useRef(null);
8447
- const { position, direction } = useAutoPosition_default(triggerRef, menuRef, mounted);
8460
+ const { position, direction, repositioning } = useAutoPosition_default(triggerRef, menuRef, mounted);
8448
8461
  useClickOutside_default([triggerRef, menuRef], () => setIsOpen(false), isOpen);
8449
8462
  useEscapeKey(isOpen, () => {
8450
8463
  setIsOpen(false);
@@ -8479,7 +8492,7 @@ var Dropdown = (props) => {
8479
8492
  "div",
8480
8493
  {
8481
8494
  ref: menuRef,
8482
- className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }),
8495
+ className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }, repositioning && "repositioning"),
8483
8496
  style: { top: position.top, left: position.left },
8484
8497
  role: "menu",
8485
8498
  children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime322.jsx)(
@@ -8505,6 +8518,111 @@ var Dropdown_default = Dropdown;
8505
8518
 
8506
8519
  // src/components/Editor/Editor.tsx
8507
8520
  var import_react22 = __toESM(require("react"), 1);
8521
+
8522
+ // src/components/Editor/highlight.ts
8523
+ var CODE_LANGUAGES = [
8524
+ { value: "plain", label: "\uC5C6\uC74C" },
8525
+ { value: "javascript", label: "JavaScript" },
8526
+ { value: "typescript", label: "TypeScript" },
8527
+ { value: "json", label: "JSON" },
8528
+ { value: "html", label: "HTML" },
8529
+ { value: "css", label: "CSS" },
8530
+ { value: "python", label: "Python" },
8531
+ { value: "sql", label: "SQL" },
8532
+ { value: "bash", label: "Bash" }
8533
+ ];
8534
+ var KEYWORDS = {
8535
+ plain: [],
8536
+ 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(" "),
8537
+ 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(" "),
8538
+ json: ["true", "false", "null"],
8539
+ html: [],
8540
+ css: [],
8541
+ 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(" "),
8542
+ 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(" "),
8543
+ 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(" ")
8544
+ };
8545
+ var LINE_COMMENT = {
8546
+ javascript: "//",
8547
+ typescript: "//",
8548
+ css: "",
8549
+ python: "#",
8550
+ sql: "--",
8551
+ bash: "#"
8552
+ };
8553
+ var escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8554
+ var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
8555
+ var highlight = (code, lang) => {
8556
+ if (lang === "plain" || !code) return escape(code);
8557
+ const keywords = new Set(KEYWORDS[lang]);
8558
+ const lineComment = LINE_COMMENT[lang];
8559
+ const caseInsensitive = lang === "sql";
8560
+ let out = "";
8561
+ let i = 0;
8562
+ while (i < code.length) {
8563
+ const rest = code.slice(i);
8564
+ if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
8565
+ const end = code.indexOf("*/", i + 2);
8566
+ const stop = end === -1 ? code.length : end + 2;
8567
+ out += wrap("comment", code.slice(i, stop));
8568
+ i = stop;
8569
+ continue;
8570
+ }
8571
+ if (lineComment && rest.startsWith(lineComment)) {
8572
+ const nl = code.indexOf("\n", i);
8573
+ const stop = nl === -1 ? code.length : nl;
8574
+ out += wrap("comment", code.slice(i, stop));
8575
+ i = stop;
8576
+ continue;
8577
+ }
8578
+ const q = rest[0];
8579
+ if (q === '"' || q === "'" || q === "`") {
8580
+ let j = 1;
8581
+ while (j < rest.length) {
8582
+ if (rest[j] === "\\") {
8583
+ j += 2;
8584
+ continue;
8585
+ }
8586
+ if (rest[j] === q) {
8587
+ j += 1;
8588
+ break;
8589
+ }
8590
+ if (rest[j] === "\n" && q !== "`") break;
8591
+ j += 1;
8592
+ }
8593
+ out += wrap("string", rest.slice(0, j));
8594
+ i += j;
8595
+ continue;
8596
+ }
8597
+ const num = /^\d+(\.\d+)?/.exec(rest);
8598
+ if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
8599
+ out += wrap("number", num[0]);
8600
+ i += num[0].length;
8601
+ continue;
8602
+ }
8603
+ const word = /^[A-Za-z_$][\w$]*/.exec(rest);
8604
+ if (word) {
8605
+ const w = word[0];
8606
+ const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
8607
+ out += hit ? wrap("keyword", w) : escape(w);
8608
+ i += w.length;
8609
+ continue;
8610
+ }
8611
+ if (lang === "html" && rest.startsWith("<")) {
8612
+ const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
8613
+ if (tag) {
8614
+ out += wrap("keyword", tag[0]);
8615
+ i += tag[0].length;
8616
+ continue;
8617
+ }
8618
+ }
8619
+ out += escape(code[i]);
8620
+ i += 1;
8621
+ }
8622
+ return out;
8623
+ };
8624
+
8625
+ // src/components/Editor/Editor.tsx
8508
8626
  var import_jsx_runtime323 = require("react/jsx-runtime");
8509
8627
  var DEFAULT_TOOLBAR = [
8510
8628
  "bold",
@@ -8512,14 +8630,27 @@ var DEFAULT_TOOLBAR = [
8512
8630
  "underline",
8513
8631
  "strikethrough",
8514
8632
  "code",
8515
- "heading",
8633
+ "heading-1",
8634
+ "heading-2",
8635
+ "heading-3",
8516
8636
  "list",
8517
8637
  "ordered-list",
8518
8638
  "blockquote",
8519
8639
  "code-block",
8520
8640
  "link",
8521
8641
  "image",
8522
- "divider"
8642
+ "divider",
8643
+ "checklist",
8644
+ "table",
8645
+ "align-left",
8646
+ "align-center",
8647
+ "align-right",
8648
+ "highlight",
8649
+ "superscript",
8650
+ "subscript",
8651
+ "clear-format",
8652
+ "undo",
8653
+ "redo"
8523
8654
  ];
8524
8655
  var ALLOWED_TAGS = /* @__PURE__ */ new Set([
8525
8656
  "p",
@@ -8548,11 +8679,41 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
8548
8679
  "img",
8549
8680
  "hr",
8550
8681
  "span",
8551
- "div"
8682
+ "div",
8683
+ "sub",
8684
+ "sup",
8685
+ "mark",
8686
+ "table",
8687
+ "thead",
8688
+ "tbody",
8689
+ "tr",
8690
+ "th",
8691
+ "td"
8552
8692
  ]);
8553
8693
  var ALLOWED_ATTRS = {
8554
8694
  a: ["href", "target", "rel"],
8555
- img: ["src", "alt"]
8695
+ img: ["src", "alt", "width"],
8696
+ // 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
8697
+ p: ["class"],
8698
+ h1: ["class"],
8699
+ h2: ["class"],
8700
+ h3: ["class"],
8701
+ h4: ["class"],
8702
+ h5: ["class"],
8703
+ h6: ["class"],
8704
+ ul: ["class"],
8705
+ ol: ["class"],
8706
+ li: ["class", "data-checked"],
8707
+ blockquote: ["class"],
8708
+ div: ["class"],
8709
+ span: ["class"],
8710
+ pre: ["class"],
8711
+ code: ["class"],
8712
+ mark: ["class"],
8713
+ table: ["class"],
8714
+ td: ["class"],
8715
+ th: ["class"],
8716
+ tr: ["class"]
8556
8717
  };
8557
8718
  var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
8558
8719
  "script",
@@ -8605,7 +8766,9 @@ var escapeHtml = (text) => {
8605
8766
  };
8606
8767
  var SLASH_ITEMS = [
8607
8768
  { key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
8608
- { key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
8769
+ { key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
8770
+ { key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
8771
+ { key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
8609
8772
  { key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
8610
8773
  { key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
8611
8774
  { key: "blockquote", label: "\uC778\uC6A9", hint: "" },
@@ -8648,6 +8811,8 @@ var Editor = (props) => {
8648
8811
  el.innerHTML = sanitizeHtml(value || "");
8649
8812
  if (!el.firstChild) el.innerHTML = "<p><br></p>";
8650
8813
  lastEmittedRef.current = null;
8814
+ historyRef.current = [{ html: el.innerHTML, caret: 0 }];
8815
+ historyIdxRef.current = 0;
8651
8816
  updateEmpty();
8652
8817
  }, [value]);
8653
8818
  const updateEmpty = () => {
@@ -8692,6 +8857,17 @@ var Editor = (props) => {
8692
8857
  if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
8693
8858
  const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
8694
8859
  if (block) next.add(`block:${block}`);
8860
+ if (document.queryCommandState("superscript")) next.add("superscript");
8861
+ if (document.queryCommandState("subscript")) next.add("subscript");
8862
+ } catch {
8863
+ }
8864
+ try {
8865
+ const blockEl = currentBlock();
8866
+ if (blockEl?.classList.contains("align-center")) next.add("align:center");
8867
+ else if (blockEl?.classList.contains("align-right")) next.add("align:right");
8868
+ else if (blockEl) next.add("align:left");
8869
+ if (blockEl?.closest("mark")) next.add("highlight");
8870
+ if (blockEl?.closest("ul.checklist")) next.add("checklist");
8695
8871
  } catch {
8696
8872
  }
8697
8873
  setActiveFormats(next);
@@ -8705,6 +8881,122 @@ var Editor = (props) => {
8705
8881
  const setBlock = (tag) => {
8706
8882
  exec("formatBlock", tag);
8707
8883
  };
8884
+ const toggleBlock = (tag) => {
8885
+ const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
8886
+ setBlock(current === tag ? "p" : tag);
8887
+ };
8888
+ const historyRef = import_react22.default.useRef([]);
8889
+ const historyIdxRef = import_react22.default.useRef(-1);
8890
+ const lastPushAtRef = import_react22.default.useRef(0);
8891
+ const isRestoringRef = import_react22.default.useRef(false);
8892
+ const caretOffset = (rootEl) => {
8893
+ const el = rootEl ?? editorRef.current;
8894
+ const sel = window.getSelection();
8895
+ if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
8896
+ const range = sel.getRangeAt(0).cloneRange();
8897
+ range.selectNodeContents(el);
8898
+ range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
8899
+ return range.toString().length;
8900
+ };
8901
+ const setCaretOffset = (offset, rootEl) => {
8902
+ const el = rootEl ?? editorRef.current;
8903
+ if (!el) return;
8904
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
8905
+ let remaining = offset;
8906
+ let node = null;
8907
+ while (node = walker.nextNode()) {
8908
+ const len = node.textContent?.length ?? 0;
8909
+ if (remaining <= len) {
8910
+ const range = document.createRange();
8911
+ range.setStart(node, remaining);
8912
+ range.collapse(true);
8913
+ const sel = window.getSelection();
8914
+ sel?.removeAllRanges();
8915
+ sel?.addRange(range);
8916
+ return;
8917
+ }
8918
+ remaining -= len;
8919
+ }
8920
+ placeCaretAtEnd(el);
8921
+ };
8922
+ const pushHistory = (force = false) => {
8923
+ const el = editorRef.current;
8924
+ if (!el || isRestoringRef.current) return;
8925
+ const html = el.innerHTML;
8926
+ const hist = historyRef.current;
8927
+ const cur = hist[historyIdxRef.current];
8928
+ if (cur && cur.html === html) return;
8929
+ const now = Date.now();
8930
+ const coalesce = !force && cur && now - lastPushAtRef.current < 500;
8931
+ lastPushAtRef.current = now;
8932
+ if (coalesce) {
8933
+ hist[historyIdxRef.current] = { html, caret: caretOffset() };
8934
+ return;
8935
+ }
8936
+ hist.length = historyIdxRef.current + 1;
8937
+ hist.push({ html, caret: caretOffset() });
8938
+ if (hist.length > 200) hist.shift();
8939
+ historyIdxRef.current = hist.length - 1;
8940
+ };
8941
+ const applySnapshot = (snap) => {
8942
+ const el = editorRef.current;
8943
+ if (!el) return;
8944
+ isRestoringRef.current = true;
8945
+ el.innerHTML = snap.html;
8946
+ setCaretOffset(snap.caret);
8947
+ isRestoringRef.current = false;
8948
+ lastEmittedRef.current = el.innerHTML;
8949
+ onChange?.(el.innerHTML);
8950
+ updateEmpty();
8951
+ updateActiveFormats();
8952
+ };
8953
+ const undo = () => {
8954
+ pushHistory(true);
8955
+ if (historyIdxRef.current <= 0) return;
8956
+ historyIdxRef.current -= 1;
8957
+ applySnapshot(historyRef.current[historyIdxRef.current]);
8958
+ };
8959
+ const redo = () => {
8960
+ if (historyIdxRef.current >= historyRef.current.length - 1) return;
8961
+ historyIdxRef.current += 1;
8962
+ applySnapshot(historyRef.current[historyIdxRef.current]);
8963
+ };
8964
+ const langOf = (pre) => {
8965
+ const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
8966
+ return cls?.slice(5) ?? "plain";
8967
+ };
8968
+ const [codeLang, setCodeLang] = import_react22.default.useState("plain");
8969
+ const highlightRafRef = import_react22.default.useRef(0);
8970
+ const rehighlight = (pre) => {
8971
+ if (composingRef.current) return;
8972
+ const code = pre.querySelector("code");
8973
+ if (!code) return;
8974
+ const lang = langOf(pre);
8975
+ const text = code.textContent || "";
8976
+ const next = highlight(text, lang);
8977
+ if (code.innerHTML === next) return;
8978
+ const caret = caretOffset(code);
8979
+ isRestoringRef.current = true;
8980
+ code.innerHTML = next;
8981
+ setCaretOffset(caret, code);
8982
+ isRestoringRef.current = false;
8983
+ };
8984
+ const scheduleHighlight = () => {
8985
+ const pre = closestPre();
8986
+ if (!pre) return;
8987
+ cancelAnimationFrame(highlightRafRef.current);
8988
+ highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
8989
+ };
8990
+ const changeCodeLang = (lang) => {
8991
+ const pre = closestPre();
8992
+ if (!pre) return;
8993
+ pushHistory(true);
8994
+ Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
8995
+ if (lang !== "plain") pre.classList.add(`lang-${lang}`);
8996
+ setCodeLang(lang);
8997
+ rehighlight(pre);
8998
+ handleInput();
8999
+ };
8708
9000
  const closestPre = () => {
8709
9001
  const sel = window.getSelection();
8710
9002
  const node = sel?.anchorNode;
@@ -8713,6 +9005,7 @@ var Editor = (props) => {
8713
9005
  return el?.closest("pre") ?? null;
8714
9006
  };
8715
9007
  const insertCodeBlock = () => {
9008
+ pushHistory(true);
8716
9009
  const el = editorRef.current;
8717
9010
  const sel = window.getSelection();
8718
9011
  if (!el || !sel || sel.rangeCount === 0) return;
@@ -8795,7 +9088,167 @@ var Editor = (props) => {
8795
9088
  sel.addRange(r2);
8796
9089
  editorRef.current?.focus();
8797
9090
  };
9091
+ const clearFormat = () => {
9092
+ exec("removeFormat");
9093
+ exec("unlink");
9094
+ setBlock("p");
9095
+ };
9096
+ const indentList = (outdent) => {
9097
+ exec(outdent ? "outdent" : "indent");
9098
+ };
9099
+ const inListItem = () => {
9100
+ const sel = window.getSelection();
9101
+ const node = sel?.anchorNode;
9102
+ if (!node || !editorRef.current?.contains(node)) return false;
9103
+ const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
9104
+ return !!el?.closest("li");
9105
+ };
9106
+ const currentBlock = () => {
9107
+ const sel = window.getSelection();
9108
+ const node = sel?.anchorNode;
9109
+ const root = editorRef.current;
9110
+ if (!node || !root?.contains(node)) return null;
9111
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
9112
+ while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
9113
+ el = el.parentElement;
9114
+ }
9115
+ return el && el !== root ? el : null;
9116
+ };
9117
+ const setAlign = (align) => {
9118
+ const block = currentBlock();
9119
+ if (!block) return;
9120
+ pushHistory(true);
9121
+ block.classList.remove("align-left", "align-center", "align-right");
9122
+ if (align !== "left") block.classList.add(`align-${align}`);
9123
+ handleInput();
9124
+ };
9125
+ const toggleChecklist = () => {
9126
+ pushHistory(true);
9127
+ const block = currentBlock();
9128
+ const list = block?.closest("ul");
9129
+ if (list?.classList.contains("checklist")) {
9130
+ list.classList.remove("checklist");
9131
+ list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
9132
+ handleInput();
9133
+ return;
9134
+ }
9135
+ if (list) {
9136
+ list.classList.add("checklist");
9137
+ list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
9138
+ handleInput();
9139
+ return;
9140
+ }
9141
+ exec("insertUnorderedList");
9142
+ const made = currentBlock()?.closest("ul");
9143
+ if (made) {
9144
+ made.classList.add("checklist");
9145
+ made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
9146
+ }
9147
+ handleInput();
9148
+ };
9149
+ const handleEditorClick = (e) => {
9150
+ if (readOnly) return;
9151
+ const target = e.target;
9152
+ const li = target.closest?.("li");
9153
+ if (!li || !li.closest("ul.checklist")) return;
9154
+ const rect = li.getBoundingClientRect();
9155
+ if (e.clientX - rect.left > 20) return;
9156
+ e.preventDefault();
9157
+ pushHistory(true);
9158
+ li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
9159
+ handleInput();
9160
+ };
9161
+ const toggleHighlight = () => {
9162
+ const sel = window.getSelection();
9163
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
9164
+ pushHistory(true);
9165
+ const range = sel.getRangeAt(0);
9166
+ const node = sel.anchorNode;
9167
+ const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
9168
+ const existing = el?.closest("mark");
9169
+ if (existing) {
9170
+ const parent = existing.parentNode;
9171
+ while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
9172
+ parent?.removeChild(existing);
9173
+ handleInput();
9174
+ return;
9175
+ }
9176
+ const mark = document.createElement("mark");
9177
+ try {
9178
+ range.surroundContents(mark);
9179
+ } catch {
9180
+ const frag = range.extractContents();
9181
+ mark.appendChild(frag);
9182
+ range.insertNode(mark);
9183
+ }
9184
+ handleInput();
9185
+ };
9186
+ const insertTable = (cols = 3, rows = 2) => {
9187
+ const sel = window.getSelection();
9188
+ if (!sel || sel.rangeCount === 0) return;
9189
+ pushHistory(true);
9190
+ const table = document.createElement("table");
9191
+ const thead = document.createElement("thead");
9192
+ const headRow = document.createElement("tr");
9193
+ for (let c = 0; c < cols; c++) {
9194
+ const th = document.createElement("th");
9195
+ th.innerHTML = "<br>";
9196
+ headRow.appendChild(th);
9197
+ }
9198
+ thead.appendChild(headRow);
9199
+ table.appendChild(thead);
9200
+ const tbody = document.createElement("tbody");
9201
+ for (let r2 = 0; r2 < rows; r2++) {
9202
+ const tr = document.createElement("tr");
9203
+ for (let c = 0; c < cols; c++) {
9204
+ const td = document.createElement("td");
9205
+ td.innerHTML = "<br>";
9206
+ tr.appendChild(td);
9207
+ }
9208
+ tbody.appendChild(tr);
9209
+ }
9210
+ table.appendChild(tbody);
9211
+ const range = sel.getRangeAt(0);
9212
+ range.deleteContents();
9213
+ range.insertNode(table);
9214
+ ensureParagraphAfter(table);
9215
+ const first = table.querySelector("th");
9216
+ if (first) placeCaretAtEnd(first);
9217
+ handleInput();
9218
+ };
9219
+ const moveTableCell = (backwards) => {
9220
+ const block = currentBlock();
9221
+ const cell = block?.closest("td, th");
9222
+ if (!cell) return false;
9223
+ const table = cell.closest("table");
9224
+ if (!table) return false;
9225
+ const cells = Array.from(table.querySelectorAll("td, th"));
9226
+ const i = cells.indexOf(cell);
9227
+ const next = cells[i + (backwards ? -1 : 1)];
9228
+ if (next) {
9229
+ placeCaretAtEnd(next);
9230
+ return true;
9231
+ }
9232
+ if (!backwards) {
9233
+ pushHistory(true);
9234
+ const lastRow = table.querySelector("tbody")?.lastElementChild;
9235
+ if (lastRow) {
9236
+ const tr = document.createElement("tr");
9237
+ for (let c = 0; c < lastRow.children.length; c++) {
9238
+ const td = document.createElement("td");
9239
+ td.innerHTML = "<br>";
9240
+ tr.appendChild(td);
9241
+ }
9242
+ lastRow.after(tr);
9243
+ placeCaretAtEnd(tr.firstElementChild);
9244
+ handleInput();
9245
+ return true;
9246
+ }
9247
+ }
9248
+ return false;
9249
+ };
8798
9250
  const insertDivider = () => {
9251
+ pushHistory(true);
8799
9252
  exec("insertHorizontalRule");
8800
9253
  const el = editorRef.current;
8801
9254
  if (el && el.lastElementChild?.tagName === "HR") {
@@ -8888,8 +9341,14 @@ var Editor = (props) => {
8888
9341
  case "paragraph":
8889
9342
  setBlock("p");
8890
9343
  break;
8891
- case "heading":
8892
- setBlock("h2");
9344
+ case "heading-1":
9345
+ toggleBlock("h1");
9346
+ break;
9347
+ case "heading-2":
9348
+ toggleBlock("h2");
9349
+ break;
9350
+ case "heading-3":
9351
+ toggleBlock("h3");
8893
9352
  break;
8894
9353
  case "list":
8895
9354
  exec("insertUnorderedList");
@@ -8982,6 +9441,8 @@ var Editor = (props) => {
8982
9441
  if (!el) return;
8983
9442
  const html = el.innerHTML;
8984
9443
  lastEmittedRef.current = html;
9444
+ pushHistory();
9445
+ scheduleHighlight();
8985
9446
  onChange?.(html);
8986
9447
  updateEmpty();
8987
9448
  updateActiveFormats();
@@ -9035,6 +9496,15 @@ var Editor = (props) => {
9035
9496
  return;
9036
9497
  }
9037
9498
  }
9499
+ if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
9500
+ e.preventDefault();
9501
+ return;
9502
+ }
9503
+ if (e.key === "Tab" && inListItem()) {
9504
+ e.preventDefault();
9505
+ indentList(e.shiftKey);
9506
+ return;
9507
+ }
9038
9508
  if (slashOpen) {
9039
9509
  if (e.key === "ArrowDown") {
9040
9510
  e.preventDefault();
@@ -9058,6 +9528,21 @@ var Editor = (props) => {
9058
9528
  return;
9059
9529
  }
9060
9530
  }
9531
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
9532
+ e.preventDefault();
9533
+ if (e.shiftKey) redo();
9534
+ else undo();
9535
+ return;
9536
+ }
9537
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
9538
+ plainPasteRef.current = true;
9539
+ return;
9540
+ }
9541
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
9542
+ e.preventDefault();
9543
+ redo();
9544
+ return;
9545
+ }
9061
9546
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
9062
9547
  const k = e.key.toLowerCase();
9063
9548
  if (k === "b") {
@@ -9091,10 +9576,16 @@ var Editor = (props) => {
9091
9576
  }
9092
9577
  }
9093
9578
  };
9579
+ const plainPasteRef = import_react22.default.useRef(false);
9094
9580
  const handlePaste = (e) => {
9095
9581
  if (readOnly) return;
9096
9582
  e.preventDefault();
9097
9583
  const text = e.clipboardData.getData("text/plain");
9584
+ if (plainPasteRef.current) {
9585
+ plainPasteRef.current = false;
9586
+ insertTextAtCaret(text);
9587
+ return;
9588
+ }
9098
9589
  if (closestPre()) {
9099
9590
  insertTextAtCaret(text);
9100
9591
  return;
@@ -9121,9 +9612,11 @@ var Editor = (props) => {
9121
9612
  };
9122
9613
  const handleSelectionUpdate = () => {
9123
9614
  updateActiveFormats();
9615
+ const pre = closestPre();
9616
+ setCodeLang(pre ? langOf(pre) : "plain");
9124
9617
  };
9125
9618
  const onToolbarAction = (item) => {
9126
- if (closestPre() && item !== "code-block") return;
9619
+ if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
9127
9620
  switch (item) {
9128
9621
  case "bold":
9129
9622
  exec("bold");
@@ -9152,8 +9645,14 @@ var Editor = (props) => {
9152
9645
  handleInput();
9153
9646
  break;
9154
9647
  }
9155
- case "heading":
9156
- setBlock("h2");
9648
+ case "heading-1":
9649
+ toggleBlock("h1");
9650
+ break;
9651
+ case "heading-2":
9652
+ toggleBlock("h2");
9653
+ break;
9654
+ case "heading-3":
9655
+ toggleBlock("h3");
9157
9656
  break;
9158
9657
  case "list":
9159
9658
  exec("insertUnorderedList");
@@ -9176,6 +9675,39 @@ var Editor = (props) => {
9176
9675
  case "divider":
9177
9676
  insertDivider();
9178
9677
  break;
9678
+ case "checklist":
9679
+ toggleChecklist();
9680
+ break;
9681
+ case "align-left":
9682
+ setAlign("left");
9683
+ break;
9684
+ case "align-center":
9685
+ setAlign("center");
9686
+ break;
9687
+ case "align-right":
9688
+ setAlign("right");
9689
+ break;
9690
+ case "highlight":
9691
+ toggleHighlight();
9692
+ break;
9693
+ case "superscript":
9694
+ exec("superscript");
9695
+ break;
9696
+ case "subscript":
9697
+ exec("subscript");
9698
+ break;
9699
+ case "table":
9700
+ insertTable();
9701
+ break;
9702
+ case "clear-format":
9703
+ clearFormat();
9704
+ break;
9705
+ case "undo":
9706
+ undo();
9707
+ break;
9708
+ case "redo":
9709
+ redo();
9710
+ break;
9179
9711
  }
9180
9712
  };
9181
9713
  return /* @__PURE__ */ (0, import_jsx_runtime323.jsxs)("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
@@ -9184,7 +9716,10 @@ var Editor = (props) => {
9184
9716
  {
9185
9717
  items: toolbar,
9186
9718
  active: activeFormats,
9187
- onAction: onToolbarAction
9719
+ onAction: onToolbarAction,
9720
+ codeLang,
9721
+ inCodeBlock: activeFormats.has("block:pre"),
9722
+ onCodeLangChange: changeCodeLang
9188
9723
  }
9189
9724
  ),
9190
9725
  /* @__PURE__ */ (0, import_jsx_runtime323.jsxs)("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
@@ -9202,6 +9737,7 @@ var Editor = (props) => {
9202
9737
  className: "lib-xplat-editor__editable",
9203
9738
  onInput: handleInput,
9204
9739
  onKeyDown: handleKeyDown,
9740
+ onClick: handleEditorClick,
9205
9741
  onKeyUp: handleSelectionUpdate,
9206
9742
  onMouseUp: handleSelectionUpdate,
9207
9743
  onPaste: handlePaste,
@@ -9243,14 +9779,27 @@ var TOOLBAR_LABEL = {
9243
9779
  underline: "U",
9244
9780
  strikethrough: "S",
9245
9781
  code: "<>",
9246
- heading: "H",
9782
+ "heading-1": "H1",
9783
+ "heading-2": "H2",
9784
+ "heading-3": "H3",
9247
9785
  list: "\u2022",
9248
9786
  "ordered-list": "1.",
9249
9787
  blockquote: "\u275D",
9250
9788
  "code-block": "[ ]",
9251
9789
  link: "\u{1F517}",
9252
9790
  image: "\u{1F5BC}",
9253
- divider: "\u2014"
9791
+ divider: "\u2014",
9792
+ checklist: "\u2611",
9793
+ "align-left": "\u21E4",
9794
+ "align-center": "\u2194",
9795
+ "align-right": "\u21E5",
9796
+ highlight: "\u{1F58D}",
9797
+ superscript: "x\xB2",
9798
+ subscript: "x\u2082",
9799
+ table: "\u25A6",
9800
+ "clear-format": "\u232B",
9801
+ undo: "\u21B6",
9802
+ redo: "\u21B7"
9254
9803
  };
9255
9804
  var TOOLBAR_TITLE = {
9256
9805
  bold: "\uAD75\uAC8C (\u2318B)",
@@ -9258,36 +9807,74 @@ var TOOLBAR_TITLE = {
9258
9807
  underline: "\uBC11\uC904 (\u2318U)",
9259
9808
  strikethrough: "\uCDE8\uC18C\uC120",
9260
9809
  code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
9261
- heading: "\uC81C\uBAA9",
9810
+ "heading-1": "\uC81C\uBAA9 1",
9811
+ "heading-2": "\uC81C\uBAA9 2",
9812
+ "heading-3": "\uC81C\uBAA9 3",
9262
9813
  list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
9263
9814
  "ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
9264
9815
  blockquote: "\uC778\uC6A9",
9265
9816
  "code-block": "\uCF54\uB4DC \uBE14\uB85D",
9266
9817
  link: "\uB9C1\uD06C (\u2318K)",
9267
9818
  image: "\uC774\uBBF8\uC9C0",
9268
- divider: "\uAD6C\uBD84\uC120"
9819
+ divider: "\uAD6C\uBD84\uC120",
9820
+ checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
9821
+ "align-left": "\uC67C\uCABD \uC815\uB82C",
9822
+ "align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
9823
+ "align-right": "\uC624\uB978\uCABD \uC815\uB82C",
9824
+ highlight: "\uD615\uAD11\uD39C",
9825
+ superscript: "\uC704 \uCCA8\uC790",
9826
+ subscript: "\uC544\uB798 \uCCA8\uC790",
9827
+ table: "\uD45C \uC0BD\uC785",
9828
+ "clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
9829
+ undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
9830
+ redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
9269
9831
  };
9270
9832
  var isActive = (item, active) => {
9271
- if (item === "heading") return active.has("block:h1") || active.has("block:h2") || active.has("block:h3");
9833
+ if (item === "heading-1") return active.has("block:h1");
9834
+ if (item === "heading-2") return active.has("block:h2");
9835
+ if (item === "heading-3") return active.has("block:h3");
9272
9836
  if (item === "blockquote") return active.has("block:blockquote");
9273
9837
  if (item === "code-block") return active.has("block:pre");
9838
+ if (item === "align-left") return active.has("align:left");
9839
+ if (item === "align-center") return active.has("align:center");
9840
+ if (item === "align-right") return active.has("align:right");
9274
9841
  return active.has(item);
9275
9842
  };
9276
- var EditorToolbar = ({ items, active, onAction }) => {
9277
- return /* @__PURE__ */ (0, import_jsx_runtime323.jsx)("div", { className: "lib-xplat-editor__toolbar", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime323.jsx)(
9278
- "button",
9279
- {
9280
- type: "button",
9281
- className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
9282
- title: TOOLBAR_TITLE[item],
9283
- onMouseDown: (e) => {
9284
- e.preventDefault();
9285
- onAction(item);
9843
+ var EditorToolbar = ({
9844
+ items,
9845
+ active,
9846
+ onAction,
9847
+ codeLang,
9848
+ inCodeBlock,
9849
+ onCodeLangChange
9850
+ }) => {
9851
+ return /* @__PURE__ */ (0, import_jsx_runtime323.jsxs)("div", { className: "lib-xplat-editor__toolbar", children: [
9852
+ items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime323.jsx)(
9853
+ "button",
9854
+ {
9855
+ type: "button",
9856
+ className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
9857
+ title: TOOLBAR_TITLE[item],
9858
+ onMouseDown: (e) => {
9859
+ e.preventDefault();
9860
+ onAction(item);
9861
+ },
9862
+ children: TOOLBAR_LABEL[item]
9286
9863
  },
9287
- children: TOOLBAR_LABEL[item]
9288
- },
9289
- item
9290
- )) });
9864
+ item
9865
+ )),
9866
+ inCodeBlock && /* @__PURE__ */ (0, import_jsx_runtime323.jsx)(
9867
+ "select",
9868
+ {
9869
+ className: "lib-xplat-editor__lang",
9870
+ value: codeLang,
9871
+ "aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
9872
+ onMouseDown: (e) => e.stopPropagation(),
9873
+ onChange: (e) => onCodeLangChange(e.target.value),
9874
+ children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ (0, import_jsx_runtime323.jsx)("option", { value: l.value, children: l.label }, l.value))
9875
+ }
9876
+ )
9877
+ ] });
9291
9878
  };
9292
9879
  var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
9293
9880
  import_react22.default.useEffect(() => {
@@ -9832,6 +10419,7 @@ var PopOver = (props) => {
9832
10419
  className: clsx_default(
9833
10420
  "lib-xplat-pop-over-content",
9834
10421
  position.direction,
10422
+ position.repositioning && "repositioning",
9835
10423
  eventTrigger && "visible"
9836
10424
  ),
9837
10425
  ref: popRef,
@@ -10178,7 +10766,7 @@ var SelectRoot = (props) => {
10178
10766
  mounted && /* @__PURE__ */ (0, import_jsx_runtime337.jsx)(Portal_default, { children: /* @__PURE__ */ (0, import_jsx_runtime337.jsxs)(
10179
10767
  "div",
10180
10768
  {
10181
- className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass),
10769
+ className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass, position.repositioning && "repositioning"),
10182
10770
  ref: contentRef,
10183
10771
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
10184
10772
  role: "listbox",
@@ -11071,7 +11659,7 @@ var TimePicker = (props) => {
11071
11659
  "div",
11072
11660
  {
11073
11661
  ref: contentRef,
11074
- className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit"),
11662
+ className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit", position.repositioning && "repositioning"),
11075
11663
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
11076
11664
  role: "listbox",
11077
11665
  children: options.map((t) => /* @__PURE__ */ (0, import_jsx_runtime351.jsx)(