@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.
@@ -3943,8 +3943,10 @@ var import_react19 = __toESM(require("react"), 1);
3943
3943
  var useAutoPosition = (triggerRef, popRef, enabled = true) => {
3944
3944
  const [position, setPosition] = import_react19.default.useState({
3945
3945
  position: {},
3946
- direction: "bottom"
3946
+ direction: "bottom",
3947
+ repositioning: false
3947
3948
  });
3949
+ const placedRef = import_react19.default.useRef(false);
3948
3950
  const calculatePosition = import_react19.default.useCallback(() => {
3949
3951
  if (!triggerRef.current || !popRef.current) return;
3950
3952
  const triggerRect = triggerRef.current.getBoundingClientRect();
@@ -3969,11 +3971,16 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
3969
3971
  if (left < 8) left = 8;
3970
3972
  setPosition({
3971
3973
  position: { top, left },
3972
- direction
3974
+ direction,
3975
+ repositioning: placedRef.current
3973
3976
  });
3977
+ placedRef.current = true;
3974
3978
  }, [triggerRef, popRef]);
3975
3979
  import_react19.default.useLayoutEffect(() => {
3976
- if (!enabled) return;
3980
+ if (!enabled) {
3981
+ placedRef.current = false;
3982
+ return;
3983
+ }
3977
3984
  calculatePosition();
3978
3985
  const raf = requestAnimationFrame(calculatePosition);
3979
3986
  return () => cancelAnimationFrame(raf);
@@ -3988,11 +3995,17 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
3988
3995
  }, [calculatePosition, enabled, popRef]);
3989
3996
  import_react19.default.useEffect(() => {
3990
3997
  if (!enabled) return;
3991
- window.addEventListener("resize", calculatePosition);
3992
- window.addEventListener("scroll", calculatePosition, true);
3998
+ let raf = 0;
3999
+ const onScrollOrResize = () => {
4000
+ cancelAnimationFrame(raf);
4001
+ raf = requestAnimationFrame(calculatePosition);
4002
+ };
4003
+ window.addEventListener("resize", onScrollOrResize, { passive: true });
4004
+ window.addEventListener("scroll", onScrollOrResize, { passive: true, capture: true });
3993
4005
  return () => {
3994
- window.removeEventListener("resize", calculatePosition);
3995
- window.removeEventListener("scroll", calculatePosition, true);
4006
+ cancelAnimationFrame(raf);
4007
+ window.removeEventListener("resize", onScrollOrResize);
4008
+ window.removeEventListener("scroll", onScrollOrResize, true);
3996
4009
  };
3997
4010
  }, [calculatePosition, enabled]);
3998
4011
  return position;
@@ -4033,7 +4046,7 @@ var Dropdown = (props) => {
4033
4046
  const [visible, setVisible] = import_react21.default.useState(false);
4034
4047
  const triggerRef = import_react21.default.useRef(null);
4035
4048
  const menuRef = import_react21.default.useRef(null);
4036
- const { position, direction } = useAutoPosition_default(triggerRef, menuRef, mounted);
4049
+ const { position, direction, repositioning } = useAutoPosition_default(triggerRef, menuRef, mounted);
4037
4050
  useClickOutside_default([triggerRef, menuRef], () => setIsOpen(false), isOpen);
4038
4051
  useEscapeKey(isOpen, () => {
4039
4052
  setIsOpen(false);
@@ -4068,7 +4081,7 @@ var Dropdown = (props) => {
4068
4081
  "div",
4069
4082
  {
4070
4083
  ref: menuRef,
4071
- className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }),
4084
+ className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }, repositioning && "repositioning"),
4072
4085
  style: { top: position.top, left: position.left },
4073
4086
  role: "menu",
4074
4087
  children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime322.jsx)(
@@ -4094,6 +4107,111 @@ var Dropdown_default = Dropdown;
4094
4107
 
4095
4108
  // src/components/Editor/Editor.tsx
4096
4109
  var import_react22 = __toESM(require("react"), 1);
4110
+
4111
+ // src/components/Editor/highlight.ts
4112
+ var CODE_LANGUAGES = [
4113
+ { value: "plain", label: "\uC5C6\uC74C" },
4114
+ { value: "javascript", label: "JavaScript" },
4115
+ { value: "typescript", label: "TypeScript" },
4116
+ { value: "json", label: "JSON" },
4117
+ { value: "html", label: "HTML" },
4118
+ { value: "css", label: "CSS" },
4119
+ { value: "python", label: "Python" },
4120
+ { value: "sql", label: "SQL" },
4121
+ { value: "bash", label: "Bash" }
4122
+ ];
4123
+ var KEYWORDS = {
4124
+ plain: [],
4125
+ 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(" "),
4126
+ 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(" "),
4127
+ json: ["true", "false", "null"],
4128
+ html: [],
4129
+ css: [],
4130
+ 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(" "),
4131
+ 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(" "),
4132
+ 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(" ")
4133
+ };
4134
+ var LINE_COMMENT = {
4135
+ javascript: "//",
4136
+ typescript: "//",
4137
+ css: "",
4138
+ python: "#",
4139
+ sql: "--",
4140
+ bash: "#"
4141
+ };
4142
+ var escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4143
+ var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
4144
+ var highlight = (code, lang) => {
4145
+ if (lang === "plain" || !code) return escape(code);
4146
+ const keywords = new Set(KEYWORDS[lang]);
4147
+ const lineComment = LINE_COMMENT[lang];
4148
+ const caseInsensitive = lang === "sql";
4149
+ let out = "";
4150
+ let i = 0;
4151
+ while (i < code.length) {
4152
+ const rest = code.slice(i);
4153
+ if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
4154
+ const end = code.indexOf("*/", i + 2);
4155
+ const stop = end === -1 ? code.length : end + 2;
4156
+ out += wrap("comment", code.slice(i, stop));
4157
+ i = stop;
4158
+ continue;
4159
+ }
4160
+ if (lineComment && rest.startsWith(lineComment)) {
4161
+ const nl = code.indexOf("\n", i);
4162
+ const stop = nl === -1 ? code.length : nl;
4163
+ out += wrap("comment", code.slice(i, stop));
4164
+ i = stop;
4165
+ continue;
4166
+ }
4167
+ const q = rest[0];
4168
+ if (q === '"' || q === "'" || q === "`") {
4169
+ let j = 1;
4170
+ while (j < rest.length) {
4171
+ if (rest[j] === "\\") {
4172
+ j += 2;
4173
+ continue;
4174
+ }
4175
+ if (rest[j] === q) {
4176
+ j += 1;
4177
+ break;
4178
+ }
4179
+ if (rest[j] === "\n" && q !== "`") break;
4180
+ j += 1;
4181
+ }
4182
+ out += wrap("string", rest.slice(0, j));
4183
+ i += j;
4184
+ continue;
4185
+ }
4186
+ const num = /^\d+(\.\d+)?/.exec(rest);
4187
+ if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
4188
+ out += wrap("number", num[0]);
4189
+ i += num[0].length;
4190
+ continue;
4191
+ }
4192
+ const word = /^[A-Za-z_$][\w$]*/.exec(rest);
4193
+ if (word) {
4194
+ const w = word[0];
4195
+ const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
4196
+ out += hit ? wrap("keyword", w) : escape(w);
4197
+ i += w.length;
4198
+ continue;
4199
+ }
4200
+ if (lang === "html" && rest.startsWith("<")) {
4201
+ const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
4202
+ if (tag) {
4203
+ out += wrap("keyword", tag[0]);
4204
+ i += tag[0].length;
4205
+ continue;
4206
+ }
4207
+ }
4208
+ out += escape(code[i]);
4209
+ i += 1;
4210
+ }
4211
+ return out;
4212
+ };
4213
+
4214
+ // src/components/Editor/Editor.tsx
4097
4215
  var import_jsx_runtime323 = require("react/jsx-runtime");
4098
4216
  var DEFAULT_TOOLBAR = [
4099
4217
  "bold",
@@ -4101,14 +4219,27 @@ var DEFAULT_TOOLBAR = [
4101
4219
  "underline",
4102
4220
  "strikethrough",
4103
4221
  "code",
4104
- "heading",
4222
+ "heading-1",
4223
+ "heading-2",
4224
+ "heading-3",
4105
4225
  "list",
4106
4226
  "ordered-list",
4107
4227
  "blockquote",
4108
4228
  "code-block",
4109
4229
  "link",
4110
4230
  "image",
4111
- "divider"
4231
+ "divider",
4232
+ "checklist",
4233
+ "table",
4234
+ "align-left",
4235
+ "align-center",
4236
+ "align-right",
4237
+ "highlight",
4238
+ "superscript",
4239
+ "subscript",
4240
+ "clear-format",
4241
+ "undo",
4242
+ "redo"
4112
4243
  ];
4113
4244
  var ALLOWED_TAGS = /* @__PURE__ */ new Set([
4114
4245
  "p",
@@ -4137,11 +4268,41 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
4137
4268
  "img",
4138
4269
  "hr",
4139
4270
  "span",
4140
- "div"
4271
+ "div",
4272
+ "sub",
4273
+ "sup",
4274
+ "mark",
4275
+ "table",
4276
+ "thead",
4277
+ "tbody",
4278
+ "tr",
4279
+ "th",
4280
+ "td"
4141
4281
  ]);
4142
4282
  var ALLOWED_ATTRS = {
4143
4283
  a: ["href", "target", "rel"],
4144
- img: ["src", "alt"]
4284
+ img: ["src", "alt", "width"],
4285
+ // 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
4286
+ p: ["class"],
4287
+ h1: ["class"],
4288
+ h2: ["class"],
4289
+ h3: ["class"],
4290
+ h4: ["class"],
4291
+ h5: ["class"],
4292
+ h6: ["class"],
4293
+ ul: ["class"],
4294
+ ol: ["class"],
4295
+ li: ["class", "data-checked"],
4296
+ blockquote: ["class"],
4297
+ div: ["class"],
4298
+ span: ["class"],
4299
+ pre: ["class"],
4300
+ code: ["class"],
4301
+ mark: ["class"],
4302
+ table: ["class"],
4303
+ td: ["class"],
4304
+ th: ["class"],
4305
+ tr: ["class"]
4145
4306
  };
4146
4307
  var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
4147
4308
  "script",
@@ -4194,7 +4355,9 @@ var escapeHtml = (text) => {
4194
4355
  };
4195
4356
  var SLASH_ITEMS = [
4196
4357
  { key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
4197
- { key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
4358
+ { key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
4359
+ { key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
4360
+ { key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
4198
4361
  { key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
4199
4362
  { key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
4200
4363
  { key: "blockquote", label: "\uC778\uC6A9", hint: "" },
@@ -4237,6 +4400,8 @@ var Editor = (props) => {
4237
4400
  el.innerHTML = sanitizeHtml(value || "");
4238
4401
  if (!el.firstChild) el.innerHTML = "<p><br></p>";
4239
4402
  lastEmittedRef.current = null;
4403
+ historyRef.current = [{ html: el.innerHTML, caret: 0 }];
4404
+ historyIdxRef.current = 0;
4240
4405
  updateEmpty();
4241
4406
  }, [value]);
4242
4407
  const updateEmpty = () => {
@@ -4281,6 +4446,17 @@ var Editor = (props) => {
4281
4446
  if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
4282
4447
  const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
4283
4448
  if (block) next.add(`block:${block}`);
4449
+ if (document.queryCommandState("superscript")) next.add("superscript");
4450
+ if (document.queryCommandState("subscript")) next.add("subscript");
4451
+ } catch {
4452
+ }
4453
+ try {
4454
+ const blockEl = currentBlock();
4455
+ if (blockEl?.classList.contains("align-center")) next.add("align:center");
4456
+ else if (blockEl?.classList.contains("align-right")) next.add("align:right");
4457
+ else if (blockEl) next.add("align:left");
4458
+ if (blockEl?.closest("mark")) next.add("highlight");
4459
+ if (blockEl?.closest("ul.checklist")) next.add("checklist");
4284
4460
  } catch {
4285
4461
  }
4286
4462
  setActiveFormats(next);
@@ -4294,6 +4470,122 @@ var Editor = (props) => {
4294
4470
  const setBlock = (tag) => {
4295
4471
  exec("formatBlock", tag);
4296
4472
  };
4473
+ const toggleBlock = (tag) => {
4474
+ const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
4475
+ setBlock(current === tag ? "p" : tag);
4476
+ };
4477
+ const historyRef = import_react22.default.useRef([]);
4478
+ const historyIdxRef = import_react22.default.useRef(-1);
4479
+ const lastPushAtRef = import_react22.default.useRef(0);
4480
+ const isRestoringRef = import_react22.default.useRef(false);
4481
+ const caretOffset = (rootEl) => {
4482
+ const el = rootEl ?? editorRef.current;
4483
+ const sel = window.getSelection();
4484
+ if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
4485
+ const range = sel.getRangeAt(0).cloneRange();
4486
+ range.selectNodeContents(el);
4487
+ range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
4488
+ return range.toString().length;
4489
+ };
4490
+ const setCaretOffset = (offset, rootEl) => {
4491
+ const el = rootEl ?? editorRef.current;
4492
+ if (!el) return;
4493
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
4494
+ let remaining = offset;
4495
+ let node = null;
4496
+ while (node = walker.nextNode()) {
4497
+ const len = node.textContent?.length ?? 0;
4498
+ if (remaining <= len) {
4499
+ const range = document.createRange();
4500
+ range.setStart(node, remaining);
4501
+ range.collapse(true);
4502
+ const sel = window.getSelection();
4503
+ sel?.removeAllRanges();
4504
+ sel?.addRange(range);
4505
+ return;
4506
+ }
4507
+ remaining -= len;
4508
+ }
4509
+ placeCaretAtEnd(el);
4510
+ };
4511
+ const pushHistory = (force = false) => {
4512
+ const el = editorRef.current;
4513
+ if (!el || isRestoringRef.current) return;
4514
+ const html = el.innerHTML;
4515
+ const hist = historyRef.current;
4516
+ const cur = hist[historyIdxRef.current];
4517
+ if (cur && cur.html === html) return;
4518
+ const now = Date.now();
4519
+ const coalesce = !force && cur && now - lastPushAtRef.current < 500;
4520
+ lastPushAtRef.current = now;
4521
+ if (coalesce) {
4522
+ hist[historyIdxRef.current] = { html, caret: caretOffset() };
4523
+ return;
4524
+ }
4525
+ hist.length = historyIdxRef.current + 1;
4526
+ hist.push({ html, caret: caretOffset() });
4527
+ if (hist.length > 200) hist.shift();
4528
+ historyIdxRef.current = hist.length - 1;
4529
+ };
4530
+ const applySnapshot = (snap) => {
4531
+ const el = editorRef.current;
4532
+ if (!el) return;
4533
+ isRestoringRef.current = true;
4534
+ el.innerHTML = snap.html;
4535
+ setCaretOffset(snap.caret);
4536
+ isRestoringRef.current = false;
4537
+ lastEmittedRef.current = el.innerHTML;
4538
+ onChange?.(el.innerHTML);
4539
+ updateEmpty();
4540
+ updateActiveFormats();
4541
+ };
4542
+ const undo = () => {
4543
+ pushHistory(true);
4544
+ if (historyIdxRef.current <= 0) return;
4545
+ historyIdxRef.current -= 1;
4546
+ applySnapshot(historyRef.current[historyIdxRef.current]);
4547
+ };
4548
+ const redo = () => {
4549
+ if (historyIdxRef.current >= historyRef.current.length - 1) return;
4550
+ historyIdxRef.current += 1;
4551
+ applySnapshot(historyRef.current[historyIdxRef.current]);
4552
+ };
4553
+ const langOf = (pre) => {
4554
+ const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
4555
+ return cls?.slice(5) ?? "plain";
4556
+ };
4557
+ const [codeLang, setCodeLang] = import_react22.default.useState("plain");
4558
+ const highlightRafRef = import_react22.default.useRef(0);
4559
+ const rehighlight = (pre) => {
4560
+ if (composingRef.current) return;
4561
+ const code = pre.querySelector("code");
4562
+ if (!code) return;
4563
+ const lang = langOf(pre);
4564
+ const text = code.textContent || "";
4565
+ const next = highlight(text, lang);
4566
+ if (code.innerHTML === next) return;
4567
+ const caret = caretOffset(code);
4568
+ isRestoringRef.current = true;
4569
+ code.innerHTML = next;
4570
+ setCaretOffset(caret, code);
4571
+ isRestoringRef.current = false;
4572
+ };
4573
+ const scheduleHighlight = () => {
4574
+ const pre = closestPre();
4575
+ if (!pre) return;
4576
+ cancelAnimationFrame(highlightRafRef.current);
4577
+ highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
4578
+ };
4579
+ const changeCodeLang = (lang) => {
4580
+ const pre = closestPre();
4581
+ if (!pre) return;
4582
+ pushHistory(true);
4583
+ Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
4584
+ if (lang !== "plain") pre.classList.add(`lang-${lang}`);
4585
+ setCodeLang(lang);
4586
+ rehighlight(pre);
4587
+ handleInput();
4588
+ };
4297
4589
  const closestPre = () => {
4298
4590
  const sel = window.getSelection();
4299
4591
  const node = sel?.anchorNode;
@@ -4302,6 +4594,7 @@ var Editor = (props) => {
4302
4594
  return el?.closest("pre") ?? null;
4303
4595
  };
4304
4596
  const insertCodeBlock = () => {
4597
+ pushHistory(true);
4305
4598
  const el = editorRef.current;
4306
4599
  const sel = window.getSelection();
4307
4600
  if (!el || !sel || sel.rangeCount === 0) return;
@@ -4384,7 +4677,167 @@ var Editor = (props) => {
4384
4677
  sel.addRange(r2);
4385
4678
  editorRef.current?.focus();
4386
4679
  };
4680
+ const clearFormat = () => {
4681
+ exec("removeFormat");
4682
+ exec("unlink");
4683
+ setBlock("p");
4684
+ };
4685
+ const indentList = (outdent) => {
4686
+ exec(outdent ? "outdent" : "indent");
4687
+ };
4688
+ const inListItem = () => {
4689
+ const sel = window.getSelection();
4690
+ const node = sel?.anchorNode;
4691
+ if (!node || !editorRef.current?.contains(node)) return false;
4692
+ const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
4693
+ return !!el?.closest("li");
4694
+ };
4695
+ const currentBlock = () => {
4696
+ const sel = window.getSelection();
4697
+ const node = sel?.anchorNode;
4698
+ const root = editorRef.current;
4699
+ if (!node || !root?.contains(node)) return null;
4700
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
4701
+ while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
4702
+ el = el.parentElement;
4703
+ }
4704
+ return el && el !== root ? el : null;
4705
+ };
4706
+ const setAlign = (align) => {
4707
+ const block = currentBlock();
4708
+ if (!block) return;
4709
+ pushHistory(true);
4710
+ block.classList.remove("align-left", "align-center", "align-right");
4711
+ if (align !== "left") block.classList.add(`align-${align}`);
4712
+ handleInput();
4713
+ };
4714
+ const toggleChecklist = () => {
4715
+ pushHistory(true);
4716
+ const block = currentBlock();
4717
+ const list = block?.closest("ul");
4718
+ if (list?.classList.contains("checklist")) {
4719
+ list.classList.remove("checklist");
4720
+ list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
4721
+ handleInput();
4722
+ return;
4723
+ }
4724
+ if (list) {
4725
+ list.classList.add("checklist");
4726
+ list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
4727
+ handleInput();
4728
+ return;
4729
+ }
4730
+ exec("insertUnorderedList");
4731
+ const made = currentBlock()?.closest("ul");
4732
+ if (made) {
4733
+ made.classList.add("checklist");
4734
+ made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
4735
+ }
4736
+ handleInput();
4737
+ };
4738
+ const handleEditorClick = (e) => {
4739
+ if (readOnly) return;
4740
+ const target = e.target;
4741
+ const li = target.closest?.("li");
4742
+ if (!li || !li.closest("ul.checklist")) return;
4743
+ const rect = li.getBoundingClientRect();
4744
+ if (e.clientX - rect.left > 20) return;
4745
+ e.preventDefault();
4746
+ pushHistory(true);
4747
+ li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
4748
+ handleInput();
4749
+ };
4750
+ const toggleHighlight = () => {
4751
+ const sel = window.getSelection();
4752
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
4753
+ pushHistory(true);
4754
+ const range = sel.getRangeAt(0);
4755
+ const node = sel.anchorNode;
4756
+ const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
4757
+ const existing = el?.closest("mark");
4758
+ if (existing) {
4759
+ const parent = existing.parentNode;
4760
+ while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
4761
+ parent?.removeChild(existing);
4762
+ handleInput();
4763
+ return;
4764
+ }
4765
+ const mark = document.createElement("mark");
4766
+ try {
4767
+ range.surroundContents(mark);
4768
+ } catch {
4769
+ const frag = range.extractContents();
4770
+ mark.appendChild(frag);
4771
+ range.insertNode(mark);
4772
+ }
4773
+ handleInput();
4774
+ };
4775
+ const insertTable = (cols = 3, rows = 2) => {
4776
+ const sel = window.getSelection();
4777
+ if (!sel || sel.rangeCount === 0) return;
4778
+ pushHistory(true);
4779
+ const table = document.createElement("table");
4780
+ const thead = document.createElement("thead");
4781
+ const headRow = document.createElement("tr");
4782
+ for (let c = 0; c < cols; c++) {
4783
+ const th = document.createElement("th");
4784
+ th.innerHTML = "<br>";
4785
+ headRow.appendChild(th);
4786
+ }
4787
+ thead.appendChild(headRow);
4788
+ table.appendChild(thead);
4789
+ const tbody = document.createElement("tbody");
4790
+ for (let r2 = 0; r2 < rows; r2++) {
4791
+ const tr = document.createElement("tr");
4792
+ for (let c = 0; c < cols; c++) {
4793
+ const td = document.createElement("td");
4794
+ td.innerHTML = "<br>";
4795
+ tr.appendChild(td);
4796
+ }
4797
+ tbody.appendChild(tr);
4798
+ }
4799
+ table.appendChild(tbody);
4800
+ const range = sel.getRangeAt(0);
4801
+ range.deleteContents();
4802
+ range.insertNode(table);
4803
+ ensureParagraphAfter(table);
4804
+ const first = table.querySelector("th");
4805
+ if (first) placeCaretAtEnd(first);
4806
+ handleInput();
4807
+ };
4808
+ const moveTableCell = (backwards) => {
4809
+ const block = currentBlock();
4810
+ const cell = block?.closest("td, th");
4811
+ if (!cell) return false;
4812
+ const table = cell.closest("table");
4813
+ if (!table) return false;
4814
+ const cells = Array.from(table.querySelectorAll("td, th"));
4815
+ const i = cells.indexOf(cell);
4816
+ const next = cells[i + (backwards ? -1 : 1)];
4817
+ if (next) {
4818
+ placeCaretAtEnd(next);
4819
+ return true;
4820
+ }
4821
+ if (!backwards) {
4822
+ pushHistory(true);
4823
+ const lastRow = table.querySelector("tbody")?.lastElementChild;
4824
+ if (lastRow) {
4825
+ const tr = document.createElement("tr");
4826
+ for (let c = 0; c < lastRow.children.length; c++) {
4827
+ const td = document.createElement("td");
4828
+ td.innerHTML = "<br>";
4829
+ tr.appendChild(td);
4830
+ }
4831
+ lastRow.after(tr);
4832
+ placeCaretAtEnd(tr.firstElementChild);
4833
+ handleInput();
4834
+ return true;
4835
+ }
4836
+ }
4837
+ return false;
4838
+ };
4387
4839
  const insertDivider = () => {
4840
+ pushHistory(true);
4388
4841
  exec("insertHorizontalRule");
4389
4842
  const el = editorRef.current;
4390
4843
  if (el && el.lastElementChild?.tagName === "HR") {
@@ -4477,8 +4930,14 @@ var Editor = (props) => {
4477
4930
  case "paragraph":
4478
4931
  setBlock("p");
4479
4932
  break;
4480
- case "heading":
4481
- setBlock("h2");
4933
+ case "heading-1":
4934
+ toggleBlock("h1");
4935
+ break;
4936
+ case "heading-2":
4937
+ toggleBlock("h2");
4938
+ break;
4939
+ case "heading-3":
4940
+ toggleBlock("h3");
4482
4941
  break;
4483
4942
  case "list":
4484
4943
  exec("insertUnorderedList");
@@ -4571,6 +5030,8 @@ var Editor = (props) => {
4571
5030
  if (!el) return;
4572
5031
  const html = el.innerHTML;
4573
5032
  lastEmittedRef.current = html;
5033
+ pushHistory();
5034
+ scheduleHighlight();
4574
5035
  onChange?.(html);
4575
5036
  updateEmpty();
4576
5037
  updateActiveFormats();
@@ -4624,6 +5085,15 @@ var Editor = (props) => {
4624
5085
  return;
4625
5086
  }
4626
5087
  }
5088
+ if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
5089
+ e.preventDefault();
5090
+ return;
5091
+ }
5092
+ if (e.key === "Tab" && inListItem()) {
5093
+ e.preventDefault();
5094
+ indentList(e.shiftKey);
5095
+ return;
5096
+ }
4627
5097
  if (slashOpen) {
4628
5098
  if (e.key === "ArrowDown") {
4629
5099
  e.preventDefault();
@@ -4647,6 +5117,21 @@ var Editor = (props) => {
4647
5117
  return;
4648
5118
  }
4649
5119
  }
5120
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
5121
+ e.preventDefault();
5122
+ if (e.shiftKey) redo();
5123
+ else undo();
5124
+ return;
5125
+ }
5126
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
5127
+ plainPasteRef.current = true;
5128
+ return;
5129
+ }
5130
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
5131
+ e.preventDefault();
5132
+ redo();
5133
+ return;
5134
+ }
4650
5135
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
4651
5136
  const k = e.key.toLowerCase();
4652
5137
  if (k === "b") {
@@ -4680,10 +5165,16 @@ var Editor = (props) => {
4680
5165
  }
4681
5166
  }
4682
5167
  };
5168
+ const plainPasteRef = import_react22.default.useRef(false);
4683
5169
  const handlePaste = (e) => {
4684
5170
  if (readOnly) return;
4685
5171
  e.preventDefault();
4686
5172
  const text = e.clipboardData.getData("text/plain");
5173
+ if (plainPasteRef.current) {
5174
+ plainPasteRef.current = false;
5175
+ insertTextAtCaret(text);
5176
+ return;
5177
+ }
4687
5178
  if (closestPre()) {
4688
5179
  insertTextAtCaret(text);
4689
5180
  return;
@@ -4710,9 +5201,11 @@ var Editor = (props) => {
4710
5201
  };
4711
5202
  const handleSelectionUpdate = () => {
4712
5203
  updateActiveFormats();
5204
+ const pre = closestPre();
5205
+ setCodeLang(pre ? langOf(pre) : "plain");
4713
5206
  };
4714
5207
  const onToolbarAction = (item) => {
4715
- if (closestPre() && item !== "code-block") return;
5208
+ if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
4716
5209
  switch (item) {
4717
5210
  case "bold":
4718
5211
  exec("bold");
@@ -4741,8 +5234,14 @@ var Editor = (props) => {
4741
5234
  handleInput();
4742
5235
  break;
4743
5236
  }
4744
- case "heading":
4745
- setBlock("h2");
5237
+ case "heading-1":
5238
+ toggleBlock("h1");
5239
+ break;
5240
+ case "heading-2":
5241
+ toggleBlock("h2");
5242
+ break;
5243
+ case "heading-3":
5244
+ toggleBlock("h3");
4746
5245
  break;
4747
5246
  case "list":
4748
5247
  exec("insertUnorderedList");
@@ -4765,6 +5264,39 @@ var Editor = (props) => {
4765
5264
  case "divider":
4766
5265
  insertDivider();
4767
5266
  break;
5267
+ case "checklist":
5268
+ toggleChecklist();
5269
+ break;
5270
+ case "align-left":
5271
+ setAlign("left");
5272
+ break;
5273
+ case "align-center":
5274
+ setAlign("center");
5275
+ break;
5276
+ case "align-right":
5277
+ setAlign("right");
5278
+ break;
5279
+ case "highlight":
5280
+ toggleHighlight();
5281
+ break;
5282
+ case "superscript":
5283
+ exec("superscript");
5284
+ break;
5285
+ case "subscript":
5286
+ exec("subscript");
5287
+ break;
5288
+ case "table":
5289
+ insertTable();
5290
+ break;
5291
+ case "clear-format":
5292
+ clearFormat();
5293
+ break;
5294
+ case "undo":
5295
+ undo();
5296
+ break;
5297
+ case "redo":
5298
+ redo();
5299
+ break;
4768
5300
  }
4769
5301
  };
4770
5302
  return /* @__PURE__ */ (0, import_jsx_runtime323.jsxs)("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
@@ -4773,7 +5305,10 @@ var Editor = (props) => {
4773
5305
  {
4774
5306
  items: toolbar,
4775
5307
  active: activeFormats,
4776
- onAction: onToolbarAction
5308
+ onAction: onToolbarAction,
5309
+ codeLang,
5310
+ inCodeBlock: activeFormats.has("block:pre"),
5311
+ onCodeLangChange: changeCodeLang
4777
5312
  }
4778
5313
  ),
4779
5314
  /* @__PURE__ */ (0, import_jsx_runtime323.jsxs)("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
@@ -4791,6 +5326,7 @@ var Editor = (props) => {
4791
5326
  className: "lib-xplat-editor__editable",
4792
5327
  onInput: handleInput,
4793
5328
  onKeyDown: handleKeyDown,
5329
+ onClick: handleEditorClick,
4794
5330
  onKeyUp: handleSelectionUpdate,
4795
5331
  onMouseUp: handleSelectionUpdate,
4796
5332
  onPaste: handlePaste,
@@ -4832,14 +5368,27 @@ var TOOLBAR_LABEL = {
4832
5368
  underline: "U",
4833
5369
  strikethrough: "S",
4834
5370
  code: "<>",
4835
- heading: "H",
5371
+ "heading-1": "H1",
5372
+ "heading-2": "H2",
5373
+ "heading-3": "H3",
4836
5374
  list: "\u2022",
4837
5375
  "ordered-list": "1.",
4838
5376
  blockquote: "\u275D",
4839
5377
  "code-block": "[ ]",
4840
5378
  link: "\u{1F517}",
4841
5379
  image: "\u{1F5BC}",
4842
- divider: "\u2014"
5380
+ divider: "\u2014",
5381
+ checklist: "\u2611",
5382
+ "align-left": "\u21E4",
5383
+ "align-center": "\u2194",
5384
+ "align-right": "\u21E5",
5385
+ highlight: "\u{1F58D}",
5386
+ superscript: "x\xB2",
5387
+ subscript: "x\u2082",
5388
+ table: "\u25A6",
5389
+ "clear-format": "\u232B",
5390
+ undo: "\u21B6",
5391
+ redo: "\u21B7"
4843
5392
  };
4844
5393
  var TOOLBAR_TITLE = {
4845
5394
  bold: "\uAD75\uAC8C (\u2318B)",
@@ -4847,36 +5396,74 @@ var TOOLBAR_TITLE = {
4847
5396
  underline: "\uBC11\uC904 (\u2318U)",
4848
5397
  strikethrough: "\uCDE8\uC18C\uC120",
4849
5398
  code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
4850
- heading: "\uC81C\uBAA9",
5399
+ "heading-1": "\uC81C\uBAA9 1",
5400
+ "heading-2": "\uC81C\uBAA9 2",
5401
+ "heading-3": "\uC81C\uBAA9 3",
4851
5402
  list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
4852
5403
  "ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
4853
5404
  blockquote: "\uC778\uC6A9",
4854
5405
  "code-block": "\uCF54\uB4DC \uBE14\uB85D",
4855
5406
  link: "\uB9C1\uD06C (\u2318K)",
4856
5407
  image: "\uC774\uBBF8\uC9C0",
4857
- divider: "\uAD6C\uBD84\uC120"
5408
+ divider: "\uAD6C\uBD84\uC120",
5409
+ checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
5410
+ "align-left": "\uC67C\uCABD \uC815\uB82C",
5411
+ "align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
5412
+ "align-right": "\uC624\uB978\uCABD \uC815\uB82C",
5413
+ highlight: "\uD615\uAD11\uD39C",
5414
+ superscript: "\uC704 \uCCA8\uC790",
5415
+ subscript: "\uC544\uB798 \uCCA8\uC790",
5416
+ table: "\uD45C \uC0BD\uC785",
5417
+ "clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
5418
+ undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
5419
+ redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
4858
5420
  };
4859
5421
  var isActive = (item, active) => {
4860
- if (item === "heading") return active.has("block:h1") || active.has("block:h2") || active.has("block:h3");
5422
+ if (item === "heading-1") return active.has("block:h1");
5423
+ if (item === "heading-2") return active.has("block:h2");
5424
+ if (item === "heading-3") return active.has("block:h3");
4861
5425
  if (item === "blockquote") return active.has("block:blockquote");
4862
5426
  if (item === "code-block") return active.has("block:pre");
5427
+ if (item === "align-left") return active.has("align:left");
5428
+ if (item === "align-center") return active.has("align:center");
5429
+ if (item === "align-right") return active.has("align:right");
4863
5430
  return active.has(item);
4864
5431
  };
4865
- var EditorToolbar = ({ items, active, onAction }) => {
4866
- return /* @__PURE__ */ (0, import_jsx_runtime323.jsx)("div", { className: "lib-xplat-editor__toolbar", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime323.jsx)(
4867
- "button",
4868
- {
4869
- type: "button",
4870
- className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
4871
- title: TOOLBAR_TITLE[item],
4872
- onMouseDown: (e) => {
4873
- e.preventDefault();
4874
- onAction(item);
5432
+ var EditorToolbar = ({
5433
+ items,
5434
+ active,
5435
+ onAction,
5436
+ codeLang,
5437
+ inCodeBlock,
5438
+ onCodeLangChange
5439
+ }) => {
5440
+ return /* @__PURE__ */ (0, import_jsx_runtime323.jsxs)("div", { className: "lib-xplat-editor__toolbar", children: [
5441
+ items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime323.jsx)(
5442
+ "button",
5443
+ {
5444
+ type: "button",
5445
+ className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
5446
+ title: TOOLBAR_TITLE[item],
5447
+ onMouseDown: (e) => {
5448
+ e.preventDefault();
5449
+ onAction(item);
5450
+ },
5451
+ children: TOOLBAR_LABEL[item]
4875
5452
  },
4876
- children: TOOLBAR_LABEL[item]
4877
- },
4878
- item
4879
- )) });
5453
+ item
5454
+ )),
5455
+ inCodeBlock && /* @__PURE__ */ (0, import_jsx_runtime323.jsx)(
5456
+ "select",
5457
+ {
5458
+ className: "lib-xplat-editor__lang",
5459
+ value: codeLang,
5460
+ "aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
5461
+ onMouseDown: (e) => e.stopPropagation(),
5462
+ onChange: (e) => onCodeLangChange(e.target.value),
5463
+ children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ (0, import_jsx_runtime323.jsx)("option", { value: l.value, children: l.label }, l.value))
5464
+ }
5465
+ )
5466
+ ] });
4880
5467
  };
4881
5468
  var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
4882
5469
  import_react22.default.useEffect(() => {
@@ -5421,6 +6008,7 @@ var PopOver = (props) => {
5421
6008
  className: clsx_default(
5422
6009
  "lib-xplat-pop-over-content",
5423
6010
  position.direction,
6011
+ position.repositioning && "repositioning",
5424
6012
  eventTrigger && "visible"
5425
6013
  ),
5426
6014
  ref: popRef,
@@ -5767,7 +6355,7 @@ var SelectRoot = (props) => {
5767
6355
  mounted && /* @__PURE__ */ (0, import_jsx_runtime337.jsx)(Portal_default, { children: /* @__PURE__ */ (0, import_jsx_runtime337.jsxs)(
5768
6356
  "div",
5769
6357
  {
5770
- className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass),
6358
+ className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass, position.repositioning && "repositioning"),
5771
6359
  ref: contentRef,
5772
6360
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
5773
6361
  role: "listbox",
@@ -6660,7 +7248,7 @@ var TimePicker = (props) => {
6660
7248
  "div",
6661
7249
  {
6662
7250
  ref: contentRef,
6663
- className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit"),
7251
+ className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit", position.repositioning && "repositioning"),
6664
7252
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
6665
7253
  role: "listbox",
6666
7254
  children: options.map((t) => /* @__PURE__ */ (0, import_jsx_runtime351.jsx)(