@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.
@@ -3842,8 +3842,10 @@ import React19 from "react";
3842
3842
  var useAutoPosition = (triggerRef, popRef, enabled = true) => {
3843
3843
  const [position, setPosition] = React19.useState({
3844
3844
  position: {},
3845
- direction: "bottom"
3845
+ direction: "bottom",
3846
+ repositioning: false
3846
3847
  });
3848
+ const placedRef = React19.useRef(false);
3847
3849
  const calculatePosition = React19.useCallback(() => {
3848
3850
  if (!triggerRef.current || !popRef.current) return;
3849
3851
  const triggerRect = triggerRef.current.getBoundingClientRect();
@@ -3868,11 +3870,16 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
3868
3870
  if (left < 8) left = 8;
3869
3871
  setPosition({
3870
3872
  position: { top, left },
3871
- direction
3873
+ direction,
3874
+ repositioning: placedRef.current
3872
3875
  });
3876
+ placedRef.current = true;
3873
3877
  }, [triggerRef, popRef]);
3874
3878
  React19.useLayoutEffect(() => {
3875
- if (!enabled) return;
3879
+ if (!enabled) {
3880
+ placedRef.current = false;
3881
+ return;
3882
+ }
3876
3883
  calculatePosition();
3877
3884
  const raf = requestAnimationFrame(calculatePosition);
3878
3885
  return () => cancelAnimationFrame(raf);
@@ -3887,11 +3894,17 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
3887
3894
  }, [calculatePosition, enabled, popRef]);
3888
3895
  React19.useEffect(() => {
3889
3896
  if (!enabled) return;
3890
- window.addEventListener("resize", calculatePosition);
3891
- window.addEventListener("scroll", calculatePosition, true);
3897
+ let raf = 0;
3898
+ const onScrollOrResize = () => {
3899
+ cancelAnimationFrame(raf);
3900
+ raf = requestAnimationFrame(calculatePosition);
3901
+ };
3902
+ window.addEventListener("resize", onScrollOrResize, { passive: true });
3903
+ window.addEventListener("scroll", onScrollOrResize, { passive: true, capture: true });
3892
3904
  return () => {
3893
- window.removeEventListener("resize", calculatePosition);
3894
- window.removeEventListener("scroll", calculatePosition, true);
3905
+ cancelAnimationFrame(raf);
3906
+ window.removeEventListener("resize", onScrollOrResize);
3907
+ window.removeEventListener("scroll", onScrollOrResize, true);
3895
3908
  };
3896
3909
  }, [calculatePosition, enabled]);
3897
3910
  return position;
@@ -3932,7 +3945,7 @@ var Dropdown = (props) => {
3932
3945
  const [visible, setVisible] = React21.useState(false);
3933
3946
  const triggerRef = React21.useRef(null);
3934
3947
  const menuRef = React21.useRef(null);
3935
- const { position, direction } = useAutoPosition_default(triggerRef, menuRef, mounted);
3948
+ const { position, direction, repositioning } = useAutoPosition_default(triggerRef, menuRef, mounted);
3936
3949
  useClickOutside_default([triggerRef, menuRef], () => setIsOpen(false), isOpen);
3937
3950
  useEscapeKey(isOpen, () => {
3938
3951
  setIsOpen(false);
@@ -3967,7 +3980,7 @@ var Dropdown = (props) => {
3967
3980
  "div",
3968
3981
  {
3969
3982
  ref: menuRef,
3970
- className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }),
3983
+ className: clsx_default("lib-xplat-dropdown-menu", direction, { visible }, repositioning && "repositioning"),
3971
3984
  style: { top: position.top, left: position.left },
3972
3985
  role: "menu",
3973
3986
  children: items.map((item) => /* @__PURE__ */ jsx322(
@@ -3993,6 +4006,111 @@ var Dropdown_default = Dropdown;
3993
4006
 
3994
4007
  // src/components/Editor/Editor.tsx
3995
4008
  import React22 from "react";
4009
+
4010
+ // src/components/Editor/highlight.ts
4011
+ var CODE_LANGUAGES = [
4012
+ { value: "plain", label: "\uC5C6\uC74C" },
4013
+ { value: "javascript", label: "JavaScript" },
4014
+ { value: "typescript", label: "TypeScript" },
4015
+ { value: "json", label: "JSON" },
4016
+ { value: "html", label: "HTML" },
4017
+ { value: "css", label: "CSS" },
4018
+ { value: "python", label: "Python" },
4019
+ { value: "sql", label: "SQL" },
4020
+ { value: "bash", label: "Bash" }
4021
+ ];
4022
+ var KEYWORDS = {
4023
+ plain: [],
4024
+ 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(" "),
4025
+ 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(" "),
4026
+ json: ["true", "false", "null"],
4027
+ html: [],
4028
+ css: [],
4029
+ 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(" "),
4030
+ 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(" "),
4031
+ 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(" ")
4032
+ };
4033
+ var LINE_COMMENT = {
4034
+ javascript: "//",
4035
+ typescript: "//",
4036
+ css: "",
4037
+ python: "#",
4038
+ sql: "--",
4039
+ bash: "#"
4040
+ };
4041
+ var escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4042
+ var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
4043
+ var highlight = (code, lang) => {
4044
+ if (lang === "plain" || !code) return escape(code);
4045
+ const keywords = new Set(KEYWORDS[lang]);
4046
+ const lineComment = LINE_COMMENT[lang];
4047
+ const caseInsensitive = lang === "sql";
4048
+ let out = "";
4049
+ let i = 0;
4050
+ while (i < code.length) {
4051
+ const rest = code.slice(i);
4052
+ if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
4053
+ const end = code.indexOf("*/", i + 2);
4054
+ const stop = end === -1 ? code.length : end + 2;
4055
+ out += wrap("comment", code.slice(i, stop));
4056
+ i = stop;
4057
+ continue;
4058
+ }
4059
+ if (lineComment && rest.startsWith(lineComment)) {
4060
+ const nl = code.indexOf("\n", i);
4061
+ const stop = nl === -1 ? code.length : nl;
4062
+ out += wrap("comment", code.slice(i, stop));
4063
+ i = stop;
4064
+ continue;
4065
+ }
4066
+ const q = rest[0];
4067
+ if (q === '"' || q === "'" || q === "`") {
4068
+ let j = 1;
4069
+ while (j < rest.length) {
4070
+ if (rest[j] === "\\") {
4071
+ j += 2;
4072
+ continue;
4073
+ }
4074
+ if (rest[j] === q) {
4075
+ j += 1;
4076
+ break;
4077
+ }
4078
+ if (rest[j] === "\n" && q !== "`") break;
4079
+ j += 1;
4080
+ }
4081
+ out += wrap("string", rest.slice(0, j));
4082
+ i += j;
4083
+ continue;
4084
+ }
4085
+ const num = /^\d+(\.\d+)?/.exec(rest);
4086
+ if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
4087
+ out += wrap("number", num[0]);
4088
+ i += num[0].length;
4089
+ continue;
4090
+ }
4091
+ const word = /^[A-Za-z_$][\w$]*/.exec(rest);
4092
+ if (word) {
4093
+ const w = word[0];
4094
+ const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
4095
+ out += hit ? wrap("keyword", w) : escape(w);
4096
+ i += w.length;
4097
+ continue;
4098
+ }
4099
+ if (lang === "html" && rest.startsWith("<")) {
4100
+ const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
4101
+ if (tag) {
4102
+ out += wrap("keyword", tag[0]);
4103
+ i += tag[0].length;
4104
+ continue;
4105
+ }
4106
+ }
4107
+ out += escape(code[i]);
4108
+ i += 1;
4109
+ }
4110
+ return out;
4111
+ };
4112
+
4113
+ // src/components/Editor/Editor.tsx
3996
4114
  import { jsx as jsx323, jsxs as jsxs207 } from "react/jsx-runtime";
3997
4115
  var DEFAULT_TOOLBAR = [
3998
4116
  "bold",
@@ -4000,14 +4118,27 @@ var DEFAULT_TOOLBAR = [
4000
4118
  "underline",
4001
4119
  "strikethrough",
4002
4120
  "code",
4003
- "heading",
4121
+ "heading-1",
4122
+ "heading-2",
4123
+ "heading-3",
4004
4124
  "list",
4005
4125
  "ordered-list",
4006
4126
  "blockquote",
4007
4127
  "code-block",
4008
4128
  "link",
4009
4129
  "image",
4010
- "divider"
4130
+ "divider",
4131
+ "checklist",
4132
+ "table",
4133
+ "align-left",
4134
+ "align-center",
4135
+ "align-right",
4136
+ "highlight",
4137
+ "superscript",
4138
+ "subscript",
4139
+ "clear-format",
4140
+ "undo",
4141
+ "redo"
4011
4142
  ];
4012
4143
  var ALLOWED_TAGS = /* @__PURE__ */ new Set([
4013
4144
  "p",
@@ -4036,11 +4167,41 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
4036
4167
  "img",
4037
4168
  "hr",
4038
4169
  "span",
4039
- "div"
4170
+ "div",
4171
+ "sub",
4172
+ "sup",
4173
+ "mark",
4174
+ "table",
4175
+ "thead",
4176
+ "tbody",
4177
+ "tr",
4178
+ "th",
4179
+ "td"
4040
4180
  ]);
4041
4181
  var ALLOWED_ATTRS = {
4042
4182
  a: ["href", "target", "rel"],
4043
- img: ["src", "alt"]
4183
+ img: ["src", "alt", "width"],
4184
+ // 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
4185
+ p: ["class"],
4186
+ h1: ["class"],
4187
+ h2: ["class"],
4188
+ h3: ["class"],
4189
+ h4: ["class"],
4190
+ h5: ["class"],
4191
+ h6: ["class"],
4192
+ ul: ["class"],
4193
+ ol: ["class"],
4194
+ li: ["class", "data-checked"],
4195
+ blockquote: ["class"],
4196
+ div: ["class"],
4197
+ span: ["class"],
4198
+ pre: ["class"],
4199
+ code: ["class"],
4200
+ mark: ["class"],
4201
+ table: ["class"],
4202
+ td: ["class"],
4203
+ th: ["class"],
4204
+ tr: ["class"]
4044
4205
  };
4045
4206
  var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
4046
4207
  "script",
@@ -4093,7 +4254,9 @@ var escapeHtml = (text) => {
4093
4254
  };
4094
4255
  var SLASH_ITEMS = [
4095
4256
  { key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
4096
- { key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
4257
+ { key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
4258
+ { key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
4259
+ { key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
4097
4260
  { key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
4098
4261
  { key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
4099
4262
  { key: "blockquote", label: "\uC778\uC6A9", hint: "" },
@@ -4136,6 +4299,8 @@ var Editor = (props) => {
4136
4299
  el.innerHTML = sanitizeHtml(value || "");
4137
4300
  if (!el.firstChild) el.innerHTML = "<p><br></p>";
4138
4301
  lastEmittedRef.current = null;
4302
+ historyRef.current = [{ html: el.innerHTML, caret: 0 }];
4303
+ historyIdxRef.current = 0;
4139
4304
  updateEmpty();
4140
4305
  }, [value]);
4141
4306
  const updateEmpty = () => {
@@ -4180,6 +4345,17 @@ var Editor = (props) => {
4180
4345
  if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
4181
4346
  const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
4182
4347
  if (block) next.add(`block:${block}`);
4348
+ if (document.queryCommandState("superscript")) next.add("superscript");
4349
+ if (document.queryCommandState("subscript")) next.add("subscript");
4350
+ } catch {
4351
+ }
4352
+ try {
4353
+ const blockEl = currentBlock();
4354
+ if (blockEl?.classList.contains("align-center")) next.add("align:center");
4355
+ else if (blockEl?.classList.contains("align-right")) next.add("align:right");
4356
+ else if (blockEl) next.add("align:left");
4357
+ if (blockEl?.closest("mark")) next.add("highlight");
4358
+ if (blockEl?.closest("ul.checklist")) next.add("checklist");
4183
4359
  } catch {
4184
4360
  }
4185
4361
  setActiveFormats(next);
@@ -4193,6 +4369,122 @@ var Editor = (props) => {
4193
4369
  const setBlock = (tag) => {
4194
4370
  exec("formatBlock", tag);
4195
4371
  };
4372
+ const toggleBlock = (tag) => {
4373
+ const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
4374
+ setBlock(current === tag ? "p" : tag);
4375
+ };
4376
+ const historyRef = React22.useRef([]);
4377
+ const historyIdxRef = React22.useRef(-1);
4378
+ const lastPushAtRef = React22.useRef(0);
4379
+ const isRestoringRef = React22.useRef(false);
4380
+ const caretOffset = (rootEl) => {
4381
+ const el = rootEl ?? editorRef.current;
4382
+ const sel = window.getSelection();
4383
+ if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
4384
+ const range = sel.getRangeAt(0).cloneRange();
4385
+ range.selectNodeContents(el);
4386
+ range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
4387
+ return range.toString().length;
4388
+ };
4389
+ const setCaretOffset = (offset, rootEl) => {
4390
+ const el = rootEl ?? editorRef.current;
4391
+ if (!el) return;
4392
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
4393
+ let remaining = offset;
4394
+ let node = null;
4395
+ while (node = walker.nextNode()) {
4396
+ const len = node.textContent?.length ?? 0;
4397
+ if (remaining <= len) {
4398
+ const range = document.createRange();
4399
+ range.setStart(node, remaining);
4400
+ range.collapse(true);
4401
+ const sel = window.getSelection();
4402
+ sel?.removeAllRanges();
4403
+ sel?.addRange(range);
4404
+ return;
4405
+ }
4406
+ remaining -= len;
4407
+ }
4408
+ placeCaretAtEnd(el);
4409
+ };
4410
+ const pushHistory = (force = false) => {
4411
+ const el = editorRef.current;
4412
+ if (!el || isRestoringRef.current) return;
4413
+ const html = el.innerHTML;
4414
+ const hist = historyRef.current;
4415
+ const cur = hist[historyIdxRef.current];
4416
+ if (cur && cur.html === html) return;
4417
+ const now = Date.now();
4418
+ const coalesce = !force && cur && now - lastPushAtRef.current < 500;
4419
+ lastPushAtRef.current = now;
4420
+ if (coalesce) {
4421
+ hist[historyIdxRef.current] = { html, caret: caretOffset() };
4422
+ return;
4423
+ }
4424
+ hist.length = historyIdxRef.current + 1;
4425
+ hist.push({ html, caret: caretOffset() });
4426
+ if (hist.length > 200) hist.shift();
4427
+ historyIdxRef.current = hist.length - 1;
4428
+ };
4429
+ const applySnapshot = (snap) => {
4430
+ const el = editorRef.current;
4431
+ if (!el) return;
4432
+ isRestoringRef.current = true;
4433
+ el.innerHTML = snap.html;
4434
+ setCaretOffset(snap.caret);
4435
+ isRestoringRef.current = false;
4436
+ lastEmittedRef.current = el.innerHTML;
4437
+ onChange?.(el.innerHTML);
4438
+ updateEmpty();
4439
+ updateActiveFormats();
4440
+ };
4441
+ const undo = () => {
4442
+ pushHistory(true);
4443
+ if (historyIdxRef.current <= 0) return;
4444
+ historyIdxRef.current -= 1;
4445
+ applySnapshot(historyRef.current[historyIdxRef.current]);
4446
+ };
4447
+ const redo = () => {
4448
+ if (historyIdxRef.current >= historyRef.current.length - 1) return;
4449
+ historyIdxRef.current += 1;
4450
+ applySnapshot(historyRef.current[historyIdxRef.current]);
4451
+ };
4452
+ const langOf = (pre) => {
4453
+ const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
4454
+ return cls?.slice(5) ?? "plain";
4455
+ };
4456
+ const [codeLang, setCodeLang] = React22.useState("plain");
4457
+ const highlightRafRef = React22.useRef(0);
4458
+ const rehighlight = (pre) => {
4459
+ if (composingRef.current) return;
4460
+ const code = pre.querySelector("code");
4461
+ if (!code) return;
4462
+ const lang = langOf(pre);
4463
+ const text = code.textContent || "";
4464
+ const next = highlight(text, lang);
4465
+ if (code.innerHTML === next) return;
4466
+ const caret = caretOffset(code);
4467
+ isRestoringRef.current = true;
4468
+ code.innerHTML = next;
4469
+ setCaretOffset(caret, code);
4470
+ isRestoringRef.current = false;
4471
+ };
4472
+ const scheduleHighlight = () => {
4473
+ const pre = closestPre();
4474
+ if (!pre) return;
4475
+ cancelAnimationFrame(highlightRafRef.current);
4476
+ highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
4477
+ };
4478
+ const changeCodeLang = (lang) => {
4479
+ const pre = closestPre();
4480
+ if (!pre) return;
4481
+ pushHistory(true);
4482
+ Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
4483
+ if (lang !== "plain") pre.classList.add(`lang-${lang}`);
4484
+ setCodeLang(lang);
4485
+ rehighlight(pre);
4486
+ handleInput();
4487
+ };
4196
4488
  const closestPre = () => {
4197
4489
  const sel = window.getSelection();
4198
4490
  const node = sel?.anchorNode;
@@ -4201,6 +4493,7 @@ var Editor = (props) => {
4201
4493
  return el?.closest("pre") ?? null;
4202
4494
  };
4203
4495
  const insertCodeBlock = () => {
4496
+ pushHistory(true);
4204
4497
  const el = editorRef.current;
4205
4498
  const sel = window.getSelection();
4206
4499
  if (!el || !sel || sel.rangeCount === 0) return;
@@ -4283,7 +4576,167 @@ var Editor = (props) => {
4283
4576
  sel.addRange(r2);
4284
4577
  editorRef.current?.focus();
4285
4578
  };
4579
+ const clearFormat = () => {
4580
+ exec("removeFormat");
4581
+ exec("unlink");
4582
+ setBlock("p");
4583
+ };
4584
+ const indentList = (outdent) => {
4585
+ exec(outdent ? "outdent" : "indent");
4586
+ };
4587
+ const inListItem = () => {
4588
+ const sel = window.getSelection();
4589
+ const node = sel?.anchorNode;
4590
+ if (!node || !editorRef.current?.contains(node)) return false;
4591
+ const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
4592
+ return !!el?.closest("li");
4593
+ };
4594
+ const currentBlock = () => {
4595
+ const sel = window.getSelection();
4596
+ const node = sel?.anchorNode;
4597
+ const root = editorRef.current;
4598
+ if (!node || !root?.contains(node)) return null;
4599
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
4600
+ while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
4601
+ el = el.parentElement;
4602
+ }
4603
+ return el && el !== root ? el : null;
4604
+ };
4605
+ const setAlign = (align) => {
4606
+ const block = currentBlock();
4607
+ if (!block) return;
4608
+ pushHistory(true);
4609
+ block.classList.remove("align-left", "align-center", "align-right");
4610
+ if (align !== "left") block.classList.add(`align-${align}`);
4611
+ handleInput();
4612
+ };
4613
+ const toggleChecklist = () => {
4614
+ pushHistory(true);
4615
+ const block = currentBlock();
4616
+ const list = block?.closest("ul");
4617
+ if (list?.classList.contains("checklist")) {
4618
+ list.classList.remove("checklist");
4619
+ list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
4620
+ handleInput();
4621
+ return;
4622
+ }
4623
+ if (list) {
4624
+ list.classList.add("checklist");
4625
+ list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
4626
+ handleInput();
4627
+ return;
4628
+ }
4629
+ exec("insertUnorderedList");
4630
+ const made = currentBlock()?.closest("ul");
4631
+ if (made) {
4632
+ made.classList.add("checklist");
4633
+ made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
4634
+ }
4635
+ handleInput();
4636
+ };
4637
+ const handleEditorClick = (e) => {
4638
+ if (readOnly) return;
4639
+ const target = e.target;
4640
+ const li = target.closest?.("li");
4641
+ if (!li || !li.closest("ul.checklist")) return;
4642
+ const rect = li.getBoundingClientRect();
4643
+ if (e.clientX - rect.left > 20) return;
4644
+ e.preventDefault();
4645
+ pushHistory(true);
4646
+ li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
4647
+ handleInput();
4648
+ };
4649
+ const toggleHighlight = () => {
4650
+ const sel = window.getSelection();
4651
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
4652
+ pushHistory(true);
4653
+ const range = sel.getRangeAt(0);
4654
+ const node = sel.anchorNode;
4655
+ const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
4656
+ const existing = el?.closest("mark");
4657
+ if (existing) {
4658
+ const parent = existing.parentNode;
4659
+ while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
4660
+ parent?.removeChild(existing);
4661
+ handleInput();
4662
+ return;
4663
+ }
4664
+ const mark = document.createElement("mark");
4665
+ try {
4666
+ range.surroundContents(mark);
4667
+ } catch {
4668
+ const frag = range.extractContents();
4669
+ mark.appendChild(frag);
4670
+ range.insertNode(mark);
4671
+ }
4672
+ handleInput();
4673
+ };
4674
+ const insertTable = (cols = 3, rows = 2) => {
4675
+ const sel = window.getSelection();
4676
+ if (!sel || sel.rangeCount === 0) return;
4677
+ pushHistory(true);
4678
+ const table = document.createElement("table");
4679
+ const thead = document.createElement("thead");
4680
+ const headRow = document.createElement("tr");
4681
+ for (let c = 0; c < cols; c++) {
4682
+ const th = document.createElement("th");
4683
+ th.innerHTML = "<br>";
4684
+ headRow.appendChild(th);
4685
+ }
4686
+ thead.appendChild(headRow);
4687
+ table.appendChild(thead);
4688
+ const tbody = document.createElement("tbody");
4689
+ for (let r2 = 0; r2 < rows; r2++) {
4690
+ const tr = document.createElement("tr");
4691
+ for (let c = 0; c < cols; c++) {
4692
+ const td = document.createElement("td");
4693
+ td.innerHTML = "<br>";
4694
+ tr.appendChild(td);
4695
+ }
4696
+ tbody.appendChild(tr);
4697
+ }
4698
+ table.appendChild(tbody);
4699
+ const range = sel.getRangeAt(0);
4700
+ range.deleteContents();
4701
+ range.insertNode(table);
4702
+ ensureParagraphAfter(table);
4703
+ const first = table.querySelector("th");
4704
+ if (first) placeCaretAtEnd(first);
4705
+ handleInput();
4706
+ };
4707
+ const moveTableCell = (backwards) => {
4708
+ const block = currentBlock();
4709
+ const cell = block?.closest("td, th");
4710
+ if (!cell) return false;
4711
+ const table = cell.closest("table");
4712
+ if (!table) return false;
4713
+ const cells = Array.from(table.querySelectorAll("td, th"));
4714
+ const i = cells.indexOf(cell);
4715
+ const next = cells[i + (backwards ? -1 : 1)];
4716
+ if (next) {
4717
+ placeCaretAtEnd(next);
4718
+ return true;
4719
+ }
4720
+ if (!backwards) {
4721
+ pushHistory(true);
4722
+ const lastRow = table.querySelector("tbody")?.lastElementChild;
4723
+ if (lastRow) {
4724
+ const tr = document.createElement("tr");
4725
+ for (let c = 0; c < lastRow.children.length; c++) {
4726
+ const td = document.createElement("td");
4727
+ td.innerHTML = "<br>";
4728
+ tr.appendChild(td);
4729
+ }
4730
+ lastRow.after(tr);
4731
+ placeCaretAtEnd(tr.firstElementChild);
4732
+ handleInput();
4733
+ return true;
4734
+ }
4735
+ }
4736
+ return false;
4737
+ };
4286
4738
  const insertDivider = () => {
4739
+ pushHistory(true);
4287
4740
  exec("insertHorizontalRule");
4288
4741
  const el = editorRef.current;
4289
4742
  if (el && el.lastElementChild?.tagName === "HR") {
@@ -4376,8 +4829,14 @@ var Editor = (props) => {
4376
4829
  case "paragraph":
4377
4830
  setBlock("p");
4378
4831
  break;
4379
- case "heading":
4380
- setBlock("h2");
4832
+ case "heading-1":
4833
+ toggleBlock("h1");
4834
+ break;
4835
+ case "heading-2":
4836
+ toggleBlock("h2");
4837
+ break;
4838
+ case "heading-3":
4839
+ toggleBlock("h3");
4381
4840
  break;
4382
4841
  case "list":
4383
4842
  exec("insertUnorderedList");
@@ -4470,6 +4929,8 @@ var Editor = (props) => {
4470
4929
  if (!el) return;
4471
4930
  const html = el.innerHTML;
4472
4931
  lastEmittedRef.current = html;
4932
+ pushHistory();
4933
+ scheduleHighlight();
4473
4934
  onChange?.(html);
4474
4935
  updateEmpty();
4475
4936
  updateActiveFormats();
@@ -4523,6 +4984,15 @@ var Editor = (props) => {
4523
4984
  return;
4524
4985
  }
4525
4986
  }
4987
+ if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
4988
+ e.preventDefault();
4989
+ return;
4990
+ }
4991
+ if (e.key === "Tab" && inListItem()) {
4992
+ e.preventDefault();
4993
+ indentList(e.shiftKey);
4994
+ return;
4995
+ }
4526
4996
  if (slashOpen) {
4527
4997
  if (e.key === "ArrowDown") {
4528
4998
  e.preventDefault();
@@ -4546,6 +5016,21 @@ var Editor = (props) => {
4546
5016
  return;
4547
5017
  }
4548
5018
  }
5019
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
5020
+ e.preventDefault();
5021
+ if (e.shiftKey) redo();
5022
+ else undo();
5023
+ return;
5024
+ }
5025
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
5026
+ plainPasteRef.current = true;
5027
+ return;
5028
+ }
5029
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
5030
+ e.preventDefault();
5031
+ redo();
5032
+ return;
5033
+ }
4549
5034
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
4550
5035
  const k = e.key.toLowerCase();
4551
5036
  if (k === "b") {
@@ -4579,10 +5064,16 @@ var Editor = (props) => {
4579
5064
  }
4580
5065
  }
4581
5066
  };
5067
+ const plainPasteRef = React22.useRef(false);
4582
5068
  const handlePaste = (e) => {
4583
5069
  if (readOnly) return;
4584
5070
  e.preventDefault();
4585
5071
  const text = e.clipboardData.getData("text/plain");
5072
+ if (plainPasteRef.current) {
5073
+ plainPasteRef.current = false;
5074
+ insertTextAtCaret(text);
5075
+ return;
5076
+ }
4586
5077
  if (closestPre()) {
4587
5078
  insertTextAtCaret(text);
4588
5079
  return;
@@ -4609,9 +5100,11 @@ var Editor = (props) => {
4609
5100
  };
4610
5101
  const handleSelectionUpdate = () => {
4611
5102
  updateActiveFormats();
5103
+ const pre = closestPre();
5104
+ setCodeLang(pre ? langOf(pre) : "plain");
4612
5105
  };
4613
5106
  const onToolbarAction = (item) => {
4614
- if (closestPre() && item !== "code-block") return;
5107
+ if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
4615
5108
  switch (item) {
4616
5109
  case "bold":
4617
5110
  exec("bold");
@@ -4640,8 +5133,14 @@ var Editor = (props) => {
4640
5133
  handleInput();
4641
5134
  break;
4642
5135
  }
4643
- case "heading":
4644
- setBlock("h2");
5136
+ case "heading-1":
5137
+ toggleBlock("h1");
5138
+ break;
5139
+ case "heading-2":
5140
+ toggleBlock("h2");
5141
+ break;
5142
+ case "heading-3":
5143
+ toggleBlock("h3");
4645
5144
  break;
4646
5145
  case "list":
4647
5146
  exec("insertUnorderedList");
@@ -4664,6 +5163,39 @@ var Editor = (props) => {
4664
5163
  case "divider":
4665
5164
  insertDivider();
4666
5165
  break;
5166
+ case "checklist":
5167
+ toggleChecklist();
5168
+ break;
5169
+ case "align-left":
5170
+ setAlign("left");
5171
+ break;
5172
+ case "align-center":
5173
+ setAlign("center");
5174
+ break;
5175
+ case "align-right":
5176
+ setAlign("right");
5177
+ break;
5178
+ case "highlight":
5179
+ toggleHighlight();
5180
+ break;
5181
+ case "superscript":
5182
+ exec("superscript");
5183
+ break;
5184
+ case "subscript":
5185
+ exec("subscript");
5186
+ break;
5187
+ case "table":
5188
+ insertTable();
5189
+ break;
5190
+ case "clear-format":
5191
+ clearFormat();
5192
+ break;
5193
+ case "undo":
5194
+ undo();
5195
+ break;
5196
+ case "redo":
5197
+ redo();
5198
+ break;
4667
5199
  }
4668
5200
  };
4669
5201
  return /* @__PURE__ */ jsxs207("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
@@ -4672,7 +5204,10 @@ var Editor = (props) => {
4672
5204
  {
4673
5205
  items: toolbar,
4674
5206
  active: activeFormats,
4675
- onAction: onToolbarAction
5207
+ onAction: onToolbarAction,
5208
+ codeLang,
5209
+ inCodeBlock: activeFormats.has("block:pre"),
5210
+ onCodeLangChange: changeCodeLang
4676
5211
  }
4677
5212
  ),
4678
5213
  /* @__PURE__ */ jsxs207("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
@@ -4690,6 +5225,7 @@ var Editor = (props) => {
4690
5225
  className: "lib-xplat-editor__editable",
4691
5226
  onInput: handleInput,
4692
5227
  onKeyDown: handleKeyDown,
5228
+ onClick: handleEditorClick,
4693
5229
  onKeyUp: handleSelectionUpdate,
4694
5230
  onMouseUp: handleSelectionUpdate,
4695
5231
  onPaste: handlePaste,
@@ -4731,14 +5267,27 @@ var TOOLBAR_LABEL = {
4731
5267
  underline: "U",
4732
5268
  strikethrough: "S",
4733
5269
  code: "<>",
4734
- heading: "H",
5270
+ "heading-1": "H1",
5271
+ "heading-2": "H2",
5272
+ "heading-3": "H3",
4735
5273
  list: "\u2022",
4736
5274
  "ordered-list": "1.",
4737
5275
  blockquote: "\u275D",
4738
5276
  "code-block": "[ ]",
4739
5277
  link: "\u{1F517}",
4740
5278
  image: "\u{1F5BC}",
4741
- divider: "\u2014"
5279
+ divider: "\u2014",
5280
+ checklist: "\u2611",
5281
+ "align-left": "\u21E4",
5282
+ "align-center": "\u2194",
5283
+ "align-right": "\u21E5",
5284
+ highlight: "\u{1F58D}",
5285
+ superscript: "x\xB2",
5286
+ subscript: "x\u2082",
5287
+ table: "\u25A6",
5288
+ "clear-format": "\u232B",
5289
+ undo: "\u21B6",
5290
+ redo: "\u21B7"
4742
5291
  };
4743
5292
  var TOOLBAR_TITLE = {
4744
5293
  bold: "\uAD75\uAC8C (\u2318B)",
@@ -4746,36 +5295,74 @@ var TOOLBAR_TITLE = {
4746
5295
  underline: "\uBC11\uC904 (\u2318U)",
4747
5296
  strikethrough: "\uCDE8\uC18C\uC120",
4748
5297
  code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
4749
- heading: "\uC81C\uBAA9",
5298
+ "heading-1": "\uC81C\uBAA9 1",
5299
+ "heading-2": "\uC81C\uBAA9 2",
5300
+ "heading-3": "\uC81C\uBAA9 3",
4750
5301
  list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
4751
5302
  "ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
4752
5303
  blockquote: "\uC778\uC6A9",
4753
5304
  "code-block": "\uCF54\uB4DC \uBE14\uB85D",
4754
5305
  link: "\uB9C1\uD06C (\u2318K)",
4755
5306
  image: "\uC774\uBBF8\uC9C0",
4756
- divider: "\uAD6C\uBD84\uC120"
5307
+ divider: "\uAD6C\uBD84\uC120",
5308
+ checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
5309
+ "align-left": "\uC67C\uCABD \uC815\uB82C",
5310
+ "align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
5311
+ "align-right": "\uC624\uB978\uCABD \uC815\uB82C",
5312
+ highlight: "\uD615\uAD11\uD39C",
5313
+ superscript: "\uC704 \uCCA8\uC790",
5314
+ subscript: "\uC544\uB798 \uCCA8\uC790",
5315
+ table: "\uD45C \uC0BD\uC785",
5316
+ "clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
5317
+ undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
5318
+ redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
4757
5319
  };
4758
5320
  var isActive = (item, active) => {
4759
- if (item === "heading") return active.has("block:h1") || active.has("block:h2") || active.has("block:h3");
5321
+ if (item === "heading-1") return active.has("block:h1");
5322
+ if (item === "heading-2") return active.has("block:h2");
5323
+ if (item === "heading-3") return active.has("block:h3");
4760
5324
  if (item === "blockquote") return active.has("block:blockquote");
4761
5325
  if (item === "code-block") return active.has("block:pre");
5326
+ if (item === "align-left") return active.has("align:left");
5327
+ if (item === "align-center") return active.has("align:center");
5328
+ if (item === "align-right") return active.has("align:right");
4762
5329
  return active.has(item);
4763
5330
  };
4764
- var EditorToolbar = ({ items, active, onAction }) => {
4765
- return /* @__PURE__ */ jsx323("div", { className: "lib-xplat-editor__toolbar", children: items.map((item) => /* @__PURE__ */ jsx323(
4766
- "button",
4767
- {
4768
- type: "button",
4769
- className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
4770
- title: TOOLBAR_TITLE[item],
4771
- onMouseDown: (e) => {
4772
- e.preventDefault();
4773
- onAction(item);
5331
+ var EditorToolbar = ({
5332
+ items,
5333
+ active,
5334
+ onAction,
5335
+ codeLang,
5336
+ inCodeBlock,
5337
+ onCodeLangChange
5338
+ }) => {
5339
+ return /* @__PURE__ */ jsxs207("div", { className: "lib-xplat-editor__toolbar", children: [
5340
+ items.map((item) => /* @__PURE__ */ jsx323(
5341
+ "button",
5342
+ {
5343
+ type: "button",
5344
+ className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
5345
+ title: TOOLBAR_TITLE[item],
5346
+ onMouseDown: (e) => {
5347
+ e.preventDefault();
5348
+ onAction(item);
5349
+ },
5350
+ children: TOOLBAR_LABEL[item]
4774
5351
  },
4775
- children: TOOLBAR_LABEL[item]
4776
- },
4777
- item
4778
- )) });
5352
+ item
5353
+ )),
5354
+ inCodeBlock && /* @__PURE__ */ jsx323(
5355
+ "select",
5356
+ {
5357
+ className: "lib-xplat-editor__lang",
5358
+ value: codeLang,
5359
+ "aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
5360
+ onMouseDown: (e) => e.stopPropagation(),
5361
+ onChange: (e) => onCodeLangChange(e.target.value),
5362
+ children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ jsx323("option", { value: l.value, children: l.label }, l.value))
5363
+ }
5364
+ )
5365
+ ] });
4779
5366
  };
4780
5367
  var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
4781
5368
  React22.useEffect(() => {
@@ -5320,6 +5907,7 @@ var PopOver = (props) => {
5320
5907
  className: clsx_default(
5321
5908
  "lib-xplat-pop-over-content",
5322
5909
  position.direction,
5910
+ position.repositioning && "repositioning",
5323
5911
  eventTrigger && "visible"
5324
5912
  ),
5325
5913
  ref: popRef,
@@ -5666,7 +6254,7 @@ var SelectRoot = (props) => {
5666
6254
  mounted && /* @__PURE__ */ jsx337(Portal_default, { children: /* @__PURE__ */ jsxs216(
5667
6255
  "div",
5668
6256
  {
5669
- className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass),
6257
+ className: clsx_default("lib-xplat-select-content", size, position.direction, stateClass, position.repositioning && "repositioning"),
5670
6258
  ref: contentRef,
5671
6259
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
5672
6260
  role: "listbox",
@@ -6559,7 +7147,7 @@ var TimePicker = (props) => {
6559
7147
  "div",
6560
7148
  {
6561
7149
  ref: contentRef,
6562
- className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit"),
7150
+ className: clsx_default("lib-xplat-time-picker-content", size, visible ? "enter" : "exit", position.repositioning && "repositioning"),
6563
7151
  style: { ...position.position, width: triggerRef.current?.offsetWidth },
6564
7152
  role: "listbox",
6565
7153
  children: options.map((t) => /* @__PURE__ */ jsx351(