@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.js CHANGED
@@ -7945,8 +7945,10 @@ import React19 from "react";
7945
7945
  var useAutoPosition = (triggerRef, popRef, enabled = true) => {
7946
7946
  const [position, setPosition] = React19.useState({
7947
7947
  position: {},
7948
- direction: "bottom"
7948
+ direction: "bottom",
7949
+ repositioning: false
7949
7950
  });
7951
+ const placedRef = React19.useRef(false);
7950
7952
  const calculatePosition = React19.useCallback(() => {
7951
7953
  if (!triggerRef.current || !popRef.current) return;
7952
7954
  const triggerRect = triggerRef.current.getBoundingClientRect();
@@ -7971,11 +7973,16 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
7971
7973
  if (left < 8) left = 8;
7972
7974
  setPosition({
7973
7975
  position: { top, left },
7974
- direction
7976
+ direction,
7977
+ repositioning: placedRef.current
7975
7978
  });
7979
+ placedRef.current = true;
7976
7980
  }, [triggerRef, popRef]);
7977
7981
  React19.useLayoutEffect(() => {
7978
- if (!enabled) return;
7982
+ if (!enabled) {
7983
+ placedRef.current = false;
7984
+ return;
7985
+ }
7979
7986
  calculatePosition();
7980
7987
  const raf = requestAnimationFrame(calculatePosition);
7981
7988
  return () => cancelAnimationFrame(raf);
@@ -7990,11 +7997,17 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
7990
7997
  }, [calculatePosition, enabled, popRef]);
7991
7998
  React19.useEffect(() => {
7992
7999
  if (!enabled) return;
7993
- window.addEventListener("resize", calculatePosition);
7994
- window.addEventListener("scroll", calculatePosition, true);
8000
+ let raf = 0;
8001
+ const onScrollOrResize = () => {
8002
+ cancelAnimationFrame(raf);
8003
+ raf = requestAnimationFrame(calculatePosition);
8004
+ };
8005
+ window.addEventListener("resize", onScrollOrResize, { passive: true });
8006
+ window.addEventListener("scroll", onScrollOrResize, { passive: true, capture: true });
7995
8007
  return () => {
7996
- window.removeEventListener("resize", calculatePosition);
7997
- window.removeEventListener("scroll", calculatePosition, true);
8008
+ cancelAnimationFrame(raf);
8009
+ window.removeEventListener("resize", onScrollOrResize);
8010
+ window.removeEventListener("scroll", onScrollOrResize, true);
7998
8011
  };
7999
8012
  }, [calculatePosition, enabled]);
8000
8013
  return position;
@@ -8035,7 +8048,7 @@ var Dropdown = (props) => {
8035
8048
  const [visible, setVisible] = React21.useState(false);
8036
8049
  const triggerRef = React21.useRef(null);
8037
8050
  const menuRef = React21.useRef(null);
8038
- const { position, direction } = useAutoPosition_default(triggerRef, menuRef, mounted);
8051
+ const { position, direction, repositioning } = useAutoPosition_default(triggerRef, menuRef, mounted);
8039
8052
  useClickOutside_default([triggerRef, menuRef], () => setIsOpen(false), isOpen);
8040
8053
  useEscapeKey(isOpen, () => {
8041
8054
  setIsOpen(false);
@@ -8070,7 +8083,7 @@ var Dropdown = (props) => {
8070
8083
  "div",
8071
8084
  {
8072
8085
  ref: menuRef,
8073
- className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }),
8086
+ className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }, repositioning && "repositioning"),
8074
8087
  style: { top: position.top, left: position.left },
8075
8088
  role: "menu",
8076
8089
  children: items.map((item) => /* @__PURE__ */ jsx322(
@@ -8096,6 +8109,111 @@ var Dropdown_default = Dropdown;
8096
8109
 
8097
8110
  // src/components/Editor/Editor.tsx
8098
8111
  import React22 from "react";
8112
+
8113
+ // src/components/Editor/highlight.ts
8114
+ var CODE_LANGUAGES = [
8115
+ { value: "plain", label: "\uC5C6\uC74C" },
8116
+ { value: "javascript", label: "JavaScript" },
8117
+ { value: "typescript", label: "TypeScript" },
8118
+ { value: "json", label: "JSON" },
8119
+ { value: "html", label: "HTML" },
8120
+ { value: "css", label: "CSS" },
8121
+ { value: "python", label: "Python" },
8122
+ { value: "sql", label: "SQL" },
8123
+ { value: "bash", label: "Bash" }
8124
+ ];
8125
+ var KEYWORDS = {
8126
+ plain: [],
8127
+ 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(" "),
8128
+ 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(" "),
8129
+ json: ["true", "false", "null"],
8130
+ html: [],
8131
+ css: [],
8132
+ 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(" "),
8133
+ 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(" "),
8134
+ 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(" ")
8135
+ };
8136
+ var LINE_COMMENT = {
8137
+ javascript: "//",
8138
+ typescript: "//",
8139
+ css: "",
8140
+ python: "#",
8141
+ sql: "--",
8142
+ bash: "#"
8143
+ };
8144
+ var escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8145
+ var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
8146
+ var highlight = (code, lang) => {
8147
+ if (lang === "plain" || !code) return escape(code);
8148
+ const keywords = new Set(KEYWORDS[lang]);
8149
+ const lineComment = LINE_COMMENT[lang];
8150
+ const caseInsensitive = lang === "sql";
8151
+ let out = "";
8152
+ let i = 0;
8153
+ while (i < code.length) {
8154
+ const rest = code.slice(i);
8155
+ if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
8156
+ const end = code.indexOf("*/", i + 2);
8157
+ const stop = end === -1 ? code.length : end + 2;
8158
+ out += wrap("comment", code.slice(i, stop));
8159
+ i = stop;
8160
+ continue;
8161
+ }
8162
+ if (lineComment && rest.startsWith(lineComment)) {
8163
+ const nl = code.indexOf("\n", i);
8164
+ const stop = nl === -1 ? code.length : nl;
8165
+ out += wrap("comment", code.slice(i, stop));
8166
+ i = stop;
8167
+ continue;
8168
+ }
8169
+ const q = rest[0];
8170
+ if (q === '"' || q === "'" || q === "`") {
8171
+ let j = 1;
8172
+ while (j < rest.length) {
8173
+ if (rest[j] === "\\") {
8174
+ j += 2;
8175
+ continue;
8176
+ }
8177
+ if (rest[j] === q) {
8178
+ j += 1;
8179
+ break;
8180
+ }
8181
+ if (rest[j] === "\n" && q !== "`") break;
8182
+ j += 1;
8183
+ }
8184
+ out += wrap("string", rest.slice(0, j));
8185
+ i += j;
8186
+ continue;
8187
+ }
8188
+ const num = /^\d+(\.\d+)?/.exec(rest);
8189
+ if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
8190
+ out += wrap("number", num[0]);
8191
+ i += num[0].length;
8192
+ continue;
8193
+ }
8194
+ const word = /^[A-Za-z_$][\w$]*/.exec(rest);
8195
+ if (word) {
8196
+ const w = word[0];
8197
+ const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
8198
+ out += hit ? wrap("keyword", w) : escape(w);
8199
+ i += w.length;
8200
+ continue;
8201
+ }
8202
+ if (lang === "html" && rest.startsWith("<")) {
8203
+ const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
8204
+ if (tag) {
8205
+ out += wrap("keyword", tag[0]);
8206
+ i += tag[0].length;
8207
+ continue;
8208
+ }
8209
+ }
8210
+ out += escape(code[i]);
8211
+ i += 1;
8212
+ }
8213
+ return out;
8214
+ };
8215
+
8216
+ // src/components/Editor/Editor.tsx
8099
8217
  import { jsx as jsx323, jsxs as jsxs207 } from "react/jsx-runtime";
8100
8218
  var DEFAULT_TOOLBAR = [
8101
8219
  "bold",
@@ -8103,14 +8221,27 @@ var DEFAULT_TOOLBAR = [
8103
8221
  "underline",
8104
8222
  "strikethrough",
8105
8223
  "code",
8106
- "heading",
8224
+ "heading-1",
8225
+ "heading-2",
8226
+ "heading-3",
8107
8227
  "list",
8108
8228
  "ordered-list",
8109
8229
  "blockquote",
8110
8230
  "code-block",
8111
8231
  "link",
8112
8232
  "image",
8113
- "divider"
8233
+ "divider",
8234
+ "checklist",
8235
+ "table",
8236
+ "align-left",
8237
+ "align-center",
8238
+ "align-right",
8239
+ "highlight",
8240
+ "superscript",
8241
+ "subscript",
8242
+ "clear-format",
8243
+ "undo",
8244
+ "redo"
8114
8245
  ];
8115
8246
  var ALLOWED_TAGS = /* @__PURE__ */ new Set([
8116
8247
  "p",
@@ -8139,11 +8270,41 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
8139
8270
  "img",
8140
8271
  "hr",
8141
8272
  "span",
8142
- "div"
8273
+ "div",
8274
+ "sub",
8275
+ "sup",
8276
+ "mark",
8277
+ "table",
8278
+ "thead",
8279
+ "tbody",
8280
+ "tr",
8281
+ "th",
8282
+ "td"
8143
8283
  ]);
8144
8284
  var ALLOWED_ATTRS = {
8145
8285
  a: ["href", "target", "rel"],
8146
- img: ["src", "alt"]
8286
+ img: ["src", "alt", "width"],
8287
+ // 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
8288
+ p: ["class"],
8289
+ h1: ["class"],
8290
+ h2: ["class"],
8291
+ h3: ["class"],
8292
+ h4: ["class"],
8293
+ h5: ["class"],
8294
+ h6: ["class"],
8295
+ ul: ["class"],
8296
+ ol: ["class"],
8297
+ li: ["class", "data-checked"],
8298
+ blockquote: ["class"],
8299
+ div: ["class"],
8300
+ span: ["class"],
8301
+ pre: ["class"],
8302
+ code: ["class"],
8303
+ mark: ["class"],
8304
+ table: ["class"],
8305
+ td: ["class"],
8306
+ th: ["class"],
8307
+ tr: ["class"]
8147
8308
  };
8148
8309
  var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
8149
8310
  "script",
@@ -8196,7 +8357,9 @@ var escapeHtml = (text) => {
8196
8357
  };
8197
8358
  var SLASH_ITEMS = [
8198
8359
  { key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
8199
- { key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
8360
+ { key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
8361
+ { key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
8362
+ { key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
8200
8363
  { key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
8201
8364
  { key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
8202
8365
  { key: "blockquote", label: "\uC778\uC6A9", hint: "" },
@@ -8239,6 +8402,8 @@ var Editor = (props) => {
8239
8402
  el.innerHTML = sanitizeHtml(value || "");
8240
8403
  if (!el.firstChild) el.innerHTML = "<p><br></p>";
8241
8404
  lastEmittedRef.current = null;
8405
+ historyRef.current = [{ html: el.innerHTML, caret: 0 }];
8406
+ historyIdxRef.current = 0;
8242
8407
  updateEmpty();
8243
8408
  }, [value]);
8244
8409
  const updateEmpty = () => {
@@ -8283,6 +8448,17 @@ var Editor = (props) => {
8283
8448
  if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
8284
8449
  const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
8285
8450
  if (block) next.add(`block:${block}`);
8451
+ if (document.queryCommandState("superscript")) next.add("superscript");
8452
+ if (document.queryCommandState("subscript")) next.add("subscript");
8453
+ } catch {
8454
+ }
8455
+ try {
8456
+ const blockEl = currentBlock();
8457
+ if (blockEl?.classList.contains("align-center")) next.add("align:center");
8458
+ else if (blockEl?.classList.contains("align-right")) next.add("align:right");
8459
+ else if (blockEl) next.add("align:left");
8460
+ if (blockEl?.closest("mark")) next.add("highlight");
8461
+ if (blockEl?.closest("ul.checklist")) next.add("checklist");
8286
8462
  } catch {
8287
8463
  }
8288
8464
  setActiveFormats(next);
@@ -8296,6 +8472,122 @@ var Editor = (props) => {
8296
8472
  const setBlock = (tag) => {
8297
8473
  exec("formatBlock", tag);
8298
8474
  };
8475
+ const toggleBlock = (tag) => {
8476
+ const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
8477
+ setBlock(current === tag ? "p" : tag);
8478
+ };
8479
+ const historyRef = React22.useRef([]);
8480
+ const historyIdxRef = React22.useRef(-1);
8481
+ const lastPushAtRef = React22.useRef(0);
8482
+ const isRestoringRef = React22.useRef(false);
8483
+ const caretOffset = (rootEl) => {
8484
+ const el = rootEl ?? editorRef.current;
8485
+ const sel = window.getSelection();
8486
+ if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
8487
+ const range = sel.getRangeAt(0).cloneRange();
8488
+ range.selectNodeContents(el);
8489
+ range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
8490
+ return range.toString().length;
8491
+ };
8492
+ const setCaretOffset = (offset, rootEl) => {
8493
+ const el = rootEl ?? editorRef.current;
8494
+ if (!el) return;
8495
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
8496
+ let remaining = offset;
8497
+ let node = null;
8498
+ while (node = walker.nextNode()) {
8499
+ const len = node.textContent?.length ?? 0;
8500
+ if (remaining <= len) {
8501
+ const range = document.createRange();
8502
+ range.setStart(node, remaining);
8503
+ range.collapse(true);
8504
+ const sel = window.getSelection();
8505
+ sel?.removeAllRanges();
8506
+ sel?.addRange(range);
8507
+ return;
8508
+ }
8509
+ remaining -= len;
8510
+ }
8511
+ placeCaretAtEnd(el);
8512
+ };
8513
+ const pushHistory = (force = false) => {
8514
+ const el = editorRef.current;
8515
+ if (!el || isRestoringRef.current) return;
8516
+ const html = el.innerHTML;
8517
+ const hist = historyRef.current;
8518
+ const cur = hist[historyIdxRef.current];
8519
+ if (cur && cur.html === html) return;
8520
+ const now = Date.now();
8521
+ const coalesce = !force && cur && now - lastPushAtRef.current < 500;
8522
+ lastPushAtRef.current = now;
8523
+ if (coalesce) {
8524
+ hist[historyIdxRef.current] = { html, caret: caretOffset() };
8525
+ return;
8526
+ }
8527
+ hist.length = historyIdxRef.current + 1;
8528
+ hist.push({ html, caret: caretOffset() });
8529
+ if (hist.length > 200) hist.shift();
8530
+ historyIdxRef.current = hist.length - 1;
8531
+ };
8532
+ const applySnapshot = (snap) => {
8533
+ const el = editorRef.current;
8534
+ if (!el) return;
8535
+ isRestoringRef.current = true;
8536
+ el.innerHTML = snap.html;
8537
+ setCaretOffset(snap.caret);
8538
+ isRestoringRef.current = false;
8539
+ lastEmittedRef.current = el.innerHTML;
8540
+ onChange?.(el.innerHTML);
8541
+ updateEmpty();
8542
+ updateActiveFormats();
8543
+ };
8544
+ const undo = () => {
8545
+ pushHistory(true);
8546
+ if (historyIdxRef.current <= 0) return;
8547
+ historyIdxRef.current -= 1;
8548
+ applySnapshot(historyRef.current[historyIdxRef.current]);
8549
+ };
8550
+ const redo = () => {
8551
+ if (historyIdxRef.current >= historyRef.current.length - 1) return;
8552
+ historyIdxRef.current += 1;
8553
+ applySnapshot(historyRef.current[historyIdxRef.current]);
8554
+ };
8555
+ const langOf = (pre) => {
8556
+ const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
8557
+ return cls?.slice(5) ?? "plain";
8558
+ };
8559
+ const [codeLang, setCodeLang] = React22.useState("plain");
8560
+ const highlightRafRef = React22.useRef(0);
8561
+ const rehighlight = (pre) => {
8562
+ if (composingRef.current) return;
8563
+ const code = pre.querySelector("code");
8564
+ if (!code) return;
8565
+ const lang = langOf(pre);
8566
+ const text = code.textContent || "";
8567
+ const next = highlight(text, lang);
8568
+ if (code.innerHTML === next) return;
8569
+ const caret = caretOffset(code);
8570
+ isRestoringRef.current = true;
8571
+ code.innerHTML = next;
8572
+ setCaretOffset(caret, code);
8573
+ isRestoringRef.current = false;
8574
+ };
8575
+ const scheduleHighlight = () => {
8576
+ const pre = closestPre();
8577
+ if (!pre) return;
8578
+ cancelAnimationFrame(highlightRafRef.current);
8579
+ highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
8580
+ };
8581
+ const changeCodeLang = (lang) => {
8582
+ const pre = closestPre();
8583
+ if (!pre) return;
8584
+ pushHistory(true);
8585
+ Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
8586
+ if (lang !== "plain") pre.classList.add(`lang-${lang}`);
8587
+ setCodeLang(lang);
8588
+ rehighlight(pre);
8589
+ handleInput();
8590
+ };
8299
8591
  const closestPre = () => {
8300
8592
  const sel = window.getSelection();
8301
8593
  const node = sel?.anchorNode;
@@ -8304,6 +8596,7 @@ var Editor = (props) => {
8304
8596
  return el?.closest("pre") ?? null;
8305
8597
  };
8306
8598
  const insertCodeBlock = () => {
8599
+ pushHistory(true);
8307
8600
  const el = editorRef.current;
8308
8601
  const sel = window.getSelection();
8309
8602
  if (!el || !sel || sel.rangeCount === 0) return;
@@ -8386,7 +8679,167 @@ var Editor = (props) => {
8386
8679
  sel.addRange(r2);
8387
8680
  editorRef.current?.focus();
8388
8681
  };
8682
+ const clearFormat = () => {
8683
+ exec("removeFormat");
8684
+ exec("unlink");
8685
+ setBlock("p");
8686
+ };
8687
+ const indentList = (outdent) => {
8688
+ exec(outdent ? "outdent" : "indent");
8689
+ };
8690
+ const inListItem = () => {
8691
+ const sel = window.getSelection();
8692
+ const node = sel?.anchorNode;
8693
+ if (!node || !editorRef.current?.contains(node)) return false;
8694
+ const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
8695
+ return !!el?.closest("li");
8696
+ };
8697
+ const currentBlock = () => {
8698
+ const sel = window.getSelection();
8699
+ const node = sel?.anchorNode;
8700
+ const root = editorRef.current;
8701
+ if (!node || !root?.contains(node)) return null;
8702
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
8703
+ while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
8704
+ el = el.parentElement;
8705
+ }
8706
+ return el && el !== root ? el : null;
8707
+ };
8708
+ const setAlign = (align) => {
8709
+ const block = currentBlock();
8710
+ if (!block) return;
8711
+ pushHistory(true);
8712
+ block.classList.remove("align-left", "align-center", "align-right");
8713
+ if (align !== "left") block.classList.add(`align-${align}`);
8714
+ handleInput();
8715
+ };
8716
+ const toggleChecklist = () => {
8717
+ pushHistory(true);
8718
+ const block = currentBlock();
8719
+ const list = block?.closest("ul");
8720
+ if (list?.classList.contains("checklist")) {
8721
+ list.classList.remove("checklist");
8722
+ list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
8723
+ handleInput();
8724
+ return;
8725
+ }
8726
+ if (list) {
8727
+ list.classList.add("checklist");
8728
+ list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
8729
+ handleInput();
8730
+ return;
8731
+ }
8732
+ exec("insertUnorderedList");
8733
+ const made = currentBlock()?.closest("ul");
8734
+ if (made) {
8735
+ made.classList.add("checklist");
8736
+ made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
8737
+ }
8738
+ handleInput();
8739
+ };
8740
+ const handleEditorClick = (e) => {
8741
+ if (readOnly) return;
8742
+ const target = e.target;
8743
+ const li = target.closest?.("li");
8744
+ if (!li || !li.closest("ul.checklist")) return;
8745
+ const rect = li.getBoundingClientRect();
8746
+ if (e.clientX - rect.left > 20) return;
8747
+ e.preventDefault();
8748
+ pushHistory(true);
8749
+ li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
8750
+ handleInput();
8751
+ };
8752
+ const toggleHighlight = () => {
8753
+ const sel = window.getSelection();
8754
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
8755
+ pushHistory(true);
8756
+ const range = sel.getRangeAt(0);
8757
+ const node = sel.anchorNode;
8758
+ const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
8759
+ const existing = el?.closest("mark");
8760
+ if (existing) {
8761
+ const parent = existing.parentNode;
8762
+ while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
8763
+ parent?.removeChild(existing);
8764
+ handleInput();
8765
+ return;
8766
+ }
8767
+ const mark = document.createElement("mark");
8768
+ try {
8769
+ range.surroundContents(mark);
8770
+ } catch {
8771
+ const frag = range.extractContents();
8772
+ mark.appendChild(frag);
8773
+ range.insertNode(mark);
8774
+ }
8775
+ handleInput();
8776
+ };
8777
+ const insertTable = (cols = 3, rows = 2) => {
8778
+ const sel = window.getSelection();
8779
+ if (!sel || sel.rangeCount === 0) return;
8780
+ pushHistory(true);
8781
+ const table = document.createElement("table");
8782
+ const thead = document.createElement("thead");
8783
+ const headRow = document.createElement("tr");
8784
+ for (let c = 0; c < cols; c++) {
8785
+ const th = document.createElement("th");
8786
+ th.innerHTML = "<br>";
8787
+ headRow.appendChild(th);
8788
+ }
8789
+ thead.appendChild(headRow);
8790
+ table.appendChild(thead);
8791
+ const tbody = document.createElement("tbody");
8792
+ for (let r2 = 0; r2 < rows; r2++) {
8793
+ const tr = document.createElement("tr");
8794
+ for (let c = 0; c < cols; c++) {
8795
+ const td = document.createElement("td");
8796
+ td.innerHTML = "<br>";
8797
+ tr.appendChild(td);
8798
+ }
8799
+ tbody.appendChild(tr);
8800
+ }
8801
+ table.appendChild(tbody);
8802
+ const range = sel.getRangeAt(0);
8803
+ range.deleteContents();
8804
+ range.insertNode(table);
8805
+ ensureParagraphAfter(table);
8806
+ const first = table.querySelector("th");
8807
+ if (first) placeCaretAtEnd(first);
8808
+ handleInput();
8809
+ };
8810
+ const moveTableCell = (backwards) => {
8811
+ const block = currentBlock();
8812
+ const cell = block?.closest("td, th");
8813
+ if (!cell) return false;
8814
+ const table = cell.closest("table");
8815
+ if (!table) return false;
8816
+ const cells = Array.from(table.querySelectorAll("td, th"));
8817
+ const i = cells.indexOf(cell);
8818
+ const next = cells[i + (backwards ? -1 : 1)];
8819
+ if (next) {
8820
+ placeCaretAtEnd(next);
8821
+ return true;
8822
+ }
8823
+ if (!backwards) {
8824
+ pushHistory(true);
8825
+ const lastRow = table.querySelector("tbody")?.lastElementChild;
8826
+ if (lastRow) {
8827
+ const tr = document.createElement("tr");
8828
+ for (let c = 0; c < lastRow.children.length; c++) {
8829
+ const td = document.createElement("td");
8830
+ td.innerHTML = "<br>";
8831
+ tr.appendChild(td);
8832
+ }
8833
+ lastRow.after(tr);
8834
+ placeCaretAtEnd(tr.firstElementChild);
8835
+ handleInput();
8836
+ return true;
8837
+ }
8838
+ }
8839
+ return false;
8840
+ };
8389
8841
  const insertDivider = () => {
8842
+ pushHistory(true);
8390
8843
  exec("insertHorizontalRule");
8391
8844
  const el = editorRef.current;
8392
8845
  if (el && el.lastElementChild?.tagName === "HR") {
@@ -8479,8 +8932,14 @@ var Editor = (props) => {
8479
8932
  case "paragraph":
8480
8933
  setBlock("p");
8481
8934
  break;
8482
- case "heading":
8483
- setBlock("h2");
8935
+ case "heading-1":
8936
+ toggleBlock("h1");
8937
+ break;
8938
+ case "heading-2":
8939
+ toggleBlock("h2");
8940
+ break;
8941
+ case "heading-3":
8942
+ toggleBlock("h3");
8484
8943
  break;
8485
8944
  case "list":
8486
8945
  exec("insertUnorderedList");
@@ -8573,6 +9032,8 @@ var Editor = (props) => {
8573
9032
  if (!el) return;
8574
9033
  const html = el.innerHTML;
8575
9034
  lastEmittedRef.current = html;
9035
+ pushHistory();
9036
+ scheduleHighlight();
8576
9037
  onChange?.(html);
8577
9038
  updateEmpty();
8578
9039
  updateActiveFormats();
@@ -8626,6 +9087,15 @@ var Editor = (props) => {
8626
9087
  return;
8627
9088
  }
8628
9089
  }
9090
+ if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
9091
+ e.preventDefault();
9092
+ return;
9093
+ }
9094
+ if (e.key === "Tab" && inListItem()) {
9095
+ e.preventDefault();
9096
+ indentList(e.shiftKey);
9097
+ return;
9098
+ }
8629
9099
  if (slashOpen) {
8630
9100
  if (e.key === "ArrowDown") {
8631
9101
  e.preventDefault();
@@ -8649,6 +9119,21 @@ var Editor = (props) => {
8649
9119
  return;
8650
9120
  }
8651
9121
  }
9122
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
9123
+ e.preventDefault();
9124
+ if (e.shiftKey) redo();
9125
+ else undo();
9126
+ return;
9127
+ }
9128
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
9129
+ plainPasteRef.current = true;
9130
+ return;
9131
+ }
9132
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
9133
+ e.preventDefault();
9134
+ redo();
9135
+ return;
9136
+ }
8652
9137
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
8653
9138
  const k = e.key.toLowerCase();
8654
9139
  if (k === "b") {
@@ -8682,10 +9167,16 @@ var Editor = (props) => {
8682
9167
  }
8683
9168
  }
8684
9169
  };
9170
+ const plainPasteRef = React22.useRef(false);
8685
9171
  const handlePaste = (e) => {
8686
9172
  if (readOnly) return;
8687
9173
  e.preventDefault();
8688
9174
  const text = e.clipboardData.getData("text/plain");
9175
+ if (plainPasteRef.current) {
9176
+ plainPasteRef.current = false;
9177
+ insertTextAtCaret(text);
9178
+ return;
9179
+ }
8689
9180
  if (closestPre()) {
8690
9181
  insertTextAtCaret(text);
8691
9182
  return;
@@ -8712,9 +9203,11 @@ var Editor = (props) => {
8712
9203
  };
8713
9204
  const handleSelectionUpdate = () => {
8714
9205
  updateActiveFormats();
9206
+ const pre = closestPre();
9207
+ setCodeLang(pre ? langOf(pre) : "plain");
8715
9208
  };
8716
9209
  const onToolbarAction = (item) => {
8717
- if (closestPre() && item !== "code-block") return;
9210
+ if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
8718
9211
  switch (item) {
8719
9212
  case "bold":
8720
9213
  exec("bold");
@@ -8743,8 +9236,14 @@ var Editor = (props) => {
8743
9236
  handleInput();
8744
9237
  break;
8745
9238
  }
8746
- case "heading":
8747
- setBlock("h2");
9239
+ case "heading-1":
9240
+ toggleBlock("h1");
9241
+ break;
9242
+ case "heading-2":
9243
+ toggleBlock("h2");
9244
+ break;
9245
+ case "heading-3":
9246
+ toggleBlock("h3");
8748
9247
  break;
8749
9248
  case "list":
8750
9249
  exec("insertUnorderedList");
@@ -8767,6 +9266,39 @@ var Editor = (props) => {
8767
9266
  case "divider":
8768
9267
  insertDivider();
8769
9268
  break;
9269
+ case "checklist":
9270
+ toggleChecklist();
9271
+ break;
9272
+ case "align-left":
9273
+ setAlign("left");
9274
+ break;
9275
+ case "align-center":
9276
+ setAlign("center");
9277
+ break;
9278
+ case "align-right":
9279
+ setAlign("right");
9280
+ break;
9281
+ case "highlight":
9282
+ toggleHighlight();
9283
+ break;
9284
+ case "superscript":
9285
+ exec("superscript");
9286
+ break;
9287
+ case "subscript":
9288
+ exec("subscript");
9289
+ break;
9290
+ case "table":
9291
+ insertTable();
9292
+ break;
9293
+ case "clear-format":
9294
+ clearFormat();
9295
+ break;
9296
+ case "undo":
9297
+ undo();
9298
+ break;
9299
+ case "redo":
9300
+ redo();
9301
+ break;
8770
9302
  }
8771
9303
  };
8772
9304
  return /* @__PURE__ */ jsxs207("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
@@ -8775,7 +9307,10 @@ var Editor = (props) => {
8775
9307
  {
8776
9308
  items: toolbar,
8777
9309
  active: activeFormats,
8778
- onAction: onToolbarAction
9310
+ onAction: onToolbarAction,
9311
+ codeLang,
9312
+ inCodeBlock: activeFormats.has("block:pre"),
9313
+ onCodeLangChange: changeCodeLang
8779
9314
  }
8780
9315
  ),
8781
9316
  /* @__PURE__ */ jsxs207("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
@@ -8793,6 +9328,7 @@ var Editor = (props) => {
8793
9328
  className: "lib-xplat-editor__editable",
8794
9329
  onInput: handleInput,
8795
9330
  onKeyDown: handleKeyDown,
9331
+ onClick: handleEditorClick,
8796
9332
  onKeyUp: handleSelectionUpdate,
8797
9333
  onMouseUp: handleSelectionUpdate,
8798
9334
  onPaste: handlePaste,
@@ -8834,14 +9370,27 @@ var TOOLBAR_LABEL = {
8834
9370
  underline: "U",
8835
9371
  strikethrough: "S",
8836
9372
  code: "<>",
8837
- heading: "H",
9373
+ "heading-1": "H1",
9374
+ "heading-2": "H2",
9375
+ "heading-3": "H3",
8838
9376
  list: "\u2022",
8839
9377
  "ordered-list": "1.",
8840
9378
  blockquote: "\u275D",
8841
9379
  "code-block": "[ ]",
8842
9380
  link: "\u{1F517}",
8843
9381
  image: "\u{1F5BC}",
8844
- divider: "\u2014"
9382
+ divider: "\u2014",
9383
+ checklist: "\u2611",
9384
+ "align-left": "\u21E4",
9385
+ "align-center": "\u2194",
9386
+ "align-right": "\u21E5",
9387
+ highlight: "\u{1F58D}",
9388
+ superscript: "x\xB2",
9389
+ subscript: "x\u2082",
9390
+ table: "\u25A6",
9391
+ "clear-format": "\u232B",
9392
+ undo: "\u21B6",
9393
+ redo: "\u21B7"
8845
9394
  };
8846
9395
  var TOOLBAR_TITLE = {
8847
9396
  bold: "\uAD75\uAC8C (\u2318B)",
@@ -8849,36 +9398,74 @@ var TOOLBAR_TITLE = {
8849
9398
  underline: "\uBC11\uC904 (\u2318U)",
8850
9399
  strikethrough: "\uCDE8\uC18C\uC120",
8851
9400
  code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
8852
- heading: "\uC81C\uBAA9",
9401
+ "heading-1": "\uC81C\uBAA9 1",
9402
+ "heading-2": "\uC81C\uBAA9 2",
9403
+ "heading-3": "\uC81C\uBAA9 3",
8853
9404
  list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
8854
9405
  "ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
8855
9406
  blockquote: "\uC778\uC6A9",
8856
9407
  "code-block": "\uCF54\uB4DC \uBE14\uB85D",
8857
9408
  link: "\uB9C1\uD06C (\u2318K)",
8858
9409
  image: "\uC774\uBBF8\uC9C0",
8859
- divider: "\uAD6C\uBD84\uC120"
9410
+ divider: "\uAD6C\uBD84\uC120",
9411
+ checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
9412
+ "align-left": "\uC67C\uCABD \uC815\uB82C",
9413
+ "align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
9414
+ "align-right": "\uC624\uB978\uCABD \uC815\uB82C",
9415
+ highlight: "\uD615\uAD11\uD39C",
9416
+ superscript: "\uC704 \uCCA8\uC790",
9417
+ subscript: "\uC544\uB798 \uCCA8\uC790",
9418
+ table: "\uD45C \uC0BD\uC785",
9419
+ "clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
9420
+ undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
9421
+ redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
8860
9422
  };
8861
9423
  var isActive = (item, active) => {
8862
- if (item === "heading") return active.has("block:h1") || active.has("block:h2") || active.has("block:h3");
9424
+ if (item === "heading-1") return active.has("block:h1");
9425
+ if (item === "heading-2") return active.has("block:h2");
9426
+ if (item === "heading-3") return active.has("block:h3");
8863
9427
  if (item === "blockquote") return active.has("block:blockquote");
8864
9428
  if (item === "code-block") return active.has("block:pre");
9429
+ if (item === "align-left") return active.has("align:left");
9430
+ if (item === "align-center") return active.has("align:center");
9431
+ if (item === "align-right") return active.has("align:right");
8865
9432
  return active.has(item);
8866
9433
  };
8867
- var EditorToolbar = ({ items, active, onAction }) => {
8868
- return /* @__PURE__ */ jsx323("div", { className: "lib-xplat-editor__toolbar", children: items.map((item) => /* @__PURE__ */ jsx323(
8869
- "button",
8870
- {
8871
- type: "button",
8872
- className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
8873
- title: TOOLBAR_TITLE[item],
8874
- onMouseDown: (e) => {
8875
- e.preventDefault();
8876
- onAction(item);
9434
+ var EditorToolbar = ({
9435
+ items,
9436
+ active,
9437
+ onAction,
9438
+ codeLang,
9439
+ inCodeBlock,
9440
+ onCodeLangChange
9441
+ }) => {
9442
+ return /* @__PURE__ */ jsxs207("div", { className: "lib-xplat-editor__toolbar", children: [
9443
+ items.map((item) => /* @__PURE__ */ jsx323(
9444
+ "button",
9445
+ {
9446
+ type: "button",
9447
+ className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
9448
+ title: TOOLBAR_TITLE[item],
9449
+ onMouseDown: (e) => {
9450
+ e.preventDefault();
9451
+ onAction(item);
9452
+ },
9453
+ children: TOOLBAR_LABEL[item]
8877
9454
  },
8878
- children: TOOLBAR_LABEL[item]
8879
- },
8880
- item
8881
- )) });
9455
+ item
9456
+ )),
9457
+ inCodeBlock && /* @__PURE__ */ jsx323(
9458
+ "select",
9459
+ {
9460
+ className: "lib-xplat-editor__lang",
9461
+ value: codeLang,
9462
+ "aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
9463
+ onMouseDown: (e) => e.stopPropagation(),
9464
+ onChange: (e) => onCodeLangChange(e.target.value),
9465
+ children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ jsx323("option", { value: l.value, children: l.label }, l.value))
9466
+ }
9467
+ )
9468
+ ] });
8882
9469
  };
8883
9470
  var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
8884
9471
  React22.useEffect(() => {
@@ -9423,6 +10010,7 @@ var PopOver = (props) => {
9423
10010
  className: clsx_default(
9424
10011
  "lib-xplat-pop-over-content",
9425
10012
  position.direction,
10013
+ position.repositioning && "repositioning",
9426
10014
  eventTrigger && "visible"
9427
10015
  ),
9428
10016
  ref: popRef,
@@ -9769,7 +10357,7 @@ var SelectRoot = (props) => {
9769
10357
  mounted && /* @__PURE__ */ jsx337(Portal_default, { children: /* @__PURE__ */ jsxs216(
9770
10358
  "div",
9771
10359
  {
9772
- className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass),
10360
+ className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass, position.repositioning && "repositioning"),
9773
10361
  ref: contentRef,
9774
10362
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
9775
10363
  role: "listbox",
@@ -10662,7 +11250,7 @@ var TimePicker = (props) => {
10662
11250
  "div",
10663
11251
  {
10664
11252
  ref: contentRef,
10665
- className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit"),
11253
+ className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit", position.repositioning && "repositioning"),
10666
11254
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
10667
11255
  role: "listbox",
10668
11256
  children: options.map((t) => /* @__PURE__ */ jsx351(