@jikjo/ui-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +10 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -164,6 +164,7 @@ const DRAG_DATA_FORMAT = "application/x-lexical-drag-block";
|
|
|
164
164
|
const BTN = 24;
|
|
165
165
|
const GAP = 4;
|
|
166
166
|
const GUTTER_LEFT = 6;
|
|
167
|
+
GUTTER_LEFT + 2 * BTN + GAP;
|
|
167
168
|
const btnBase = {
|
|
168
169
|
display: "flex",
|
|
169
170
|
alignItems: "center",
|
|
@@ -1099,9 +1100,17 @@ function EditorInner({ extensions, toolbarContent, features }) {
|
|
|
1099
1100
|
] });
|
|
1100
1101
|
}
|
|
1101
1102
|
function EditorUI({ extensions = defaultExtensions, namespace = "jikjo", toolbarContent, className, features = [...ALL_FEATURES] }) {
|
|
1103
|
+
const hasBlockToolbar = features.includes("blockHandle") || features.includes("inlineAdd");
|
|
1102
1104
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1103
1105
|
className,
|
|
1104
|
-
...
|
|
1106
|
+
...hasBlockToolbar ? { "data-has-block-toolbar": "" } : {},
|
|
1107
|
+
style: hasBlockToolbar ? {
|
|
1108
|
+
"--jikjo-content-pl": "74px",
|
|
1109
|
+
"--jikjo-placeholder-pl": "74px"
|
|
1110
|
+
} : {
|
|
1111
|
+
"--jikjo-content-pl": "16px",
|
|
1112
|
+
"--jikjo-placeholder-pl": "16px"
|
|
1113
|
+
},
|
|
1105
1114
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__jikjo_core.Editor, {
|
|
1106
1115
|
extensions,
|
|
1107
1116
|
namespace,
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/editor-ui.tsx","../src/components/bubble-menu.tsx","../src/components/slash-menu.tsx","../src/components/block-toolbar.tsx"],"sourcesContent":[],"mappings":";;;;;;;UAqGiB,aAAA;eACF;;;;AADf;;;gBAQmB,CAAA,EAAA,SAAA,GAAA,KAAA;WAMN,CAAA,EAAA,MAAA;EAAK;AA4QlB;;;UAEE,CAAA,EA9QW,KA8QX,CAAA,aAAA,GAAA,WAAA,GAAA,cAAA,GAAA,YAAA,CAAA;;AAEA,iBAJc,QAAA,CAId;EAAA,UAAA;EAAA,SAAA;EAAA,cAAA;EAAA,SAAA;EAAA;AAAA,CAAA,EAEC,aAFD,CAAA,EAEc,kBAAA,CAAA,GAAA,CAAA,OAFd;;;;UCxXe,eAAA;;UAEP;iCACuB;UACvB;ADsFV;AAA8B,iBCpBd,UAAA,CDoBc;EAAA,SAAA;EAAA,MAAA;EAAA,cAAA;EAAA;AAAA,CAAA,ECf3B,eDe2B,CAAA,ECfZ,MAAA,CAAA,WAAA,GDeY,IAAA;;;;UEzFb,cAAA;;;SAGR;UACC;EFqFO,OAAA,EAAA,GAAA,GAAA,IAAa;;AACf,iBExCC,SAAA,CFwCD;EAAA,SAAA;EAAA,KAAA;EAAA,KAAA;EAAA,MAAA;EAAA;AAAA,CAAA,EElCZ,cFkCY,CAAA,EElCE,MAAA,CAAA,WAAA,GFkCF,IAAA;;;;UGvEE,iBAAA;;;;;EHsEA,KAAA,EGjER,aHiEqB,EAAA;EAAA,MAAA,EGhEpB,aHgEoB;gBACf,CAAA,EAAA,OAAA;eAOI,CAAA,EAAA,OAAA;;EAMD,aAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EA4QF;EAAQ,SAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;;AAEtB,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/editor-ui.tsx","../src/components/bubble-menu.tsx","../src/components/slash-menu.tsx","../src/components/block-toolbar.tsx"],"sourcesContent":[],"mappings":";;;;;;;UAqGiB,aAAA;eACF;;;;AADf;;;gBAQmB,CAAA,EAAA,SAAA,GAAA,KAAA;WAMN,CAAA,EAAA,MAAA;EAAK;AA4QlB;;;UAEE,CAAA,EA9QW,KA8QX,CAAA,aAAA,GAAA,WAAA,GAAA,cAAA,GAAA,YAAA,CAAA;;AAEA,iBAJc,QAAA,CAId;EAAA,UAAA;EAAA,SAAA;EAAA,cAAA;EAAA,SAAA;EAAA;AAAA,CAAA,EAEC,aAFD,CAAA,EAEc,kBAAA,CAAA,GAAA,CAAA,OAFd;;;;UCxXe,eAAA;;UAEP;iCACuB;UACvB;ADsFV;AAA8B,iBCpBd,UAAA,CDoBc;EAAA,SAAA;EAAA,MAAA;EAAA,cAAA;EAAA;AAAA,CAAA,ECf3B,eDe2B,CAAA,ECfZ,MAAA,CAAA,WAAA,GDeY,IAAA;;;;UEzFb,cAAA;;;SAGR;UACC;EFqFO,OAAA,EAAA,GAAA,GAAA,IAAa;;AACf,iBExCC,SAAA,CFwCD;EAAA,SAAA;EAAA,KAAA;EAAA,KAAA;EAAA,MAAA;EAAA;AAAA,CAAA,EElCZ,cFkCY,CAAA,EElCE,MAAA,CAAA,WAAA,GFkCF,IAAA;;;;UGvEE,iBAAA;;;;;EHsEA,KAAA,EGjER,aHiEqB,EAAA;EAAA,MAAA,EGhEpB,aHgEoB;gBACf,CAAA,EAAA,OAAA;eAOI,CAAA,EAAA,OAAA;;EAMD,aAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EA4QF;EAAQ,SAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;;AAEtB,iBG7Rc,YAAA,CH6Rd;EAAA,SAAA;EAAA,OAAA;EAAA,cAAA;EAAA,KAAA;EAAA,MAAA;EAAA,cAAA;EAAA,aAAA;EAAA,aAAA;EAAA;AAAA,CAAA,EGnRC,iBHmRD,CAAA,EGnRkB,kBAAA,CAAA,GAAA,CAAA,OAAA,GHmRlB,IAAA"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/editor-ui.tsx","../src/components/bubble-menu.tsx","../src/components/slash-menu.tsx","../src/components/block-toolbar.tsx"],"sourcesContent":[],"mappings":";;;;;;;UAqGiB,aAAA;eACF;;;;AADf;;;gBAQmB,CAAA,EAAA,SAAA,GAAA,KAAA;WAMN,CAAA,EAAA,MAAA;EAAK;AA4QlB;;;UAEE,CAAA,EA9QW,KA8QX,CAAA,aAAA,GAAA,WAAA,GAAA,cAAA,GAAA,YAAA,CAAA;;AAEA,iBAJc,QAAA,CAId;EAAA,UAAA;EAAA,SAAA;EAAA,cAAA;EAAA,SAAA;EAAA;AAAA,CAAA,EAEC,aAFD,CAAA,EAEc,kBAAA,CAAA,GAAA,CAAA,OAFd;;;;UCxXe,eAAA;;UAEP;iCACuB;UACvB;ADsFV;AAA8B,iBCpBd,UAAA,CDoBc;EAAA,SAAA;EAAA,MAAA;EAAA,cAAA;EAAA;AAAA,CAAA,ECf3B,eDe2B,CAAA,ECfZ,MAAA,CAAA,WAAA,GDeY,IAAA;;;;UEzFb,cAAA;;;SAGR;UACC;EFqFO,OAAA,EAAA,GAAA,GAAA,IAAa;;AACf,iBExCC,SAAA,CFwCD;EAAA,SAAA;EAAA,KAAA;EAAA,KAAA;EAAA,MAAA;EAAA;AAAA,CAAA,EElCZ,cFkCY,CAAA,EElCE,MAAA,CAAA,WAAA,GFkCF,IAAA;;;;UGvEE,iBAAA;;;;;EHsEA,KAAA,EGjER,aHiEqB,EAAA;EAAA,MAAA,EGhEpB,aHgEoB;gBACf,CAAA,EAAA,OAAA;eAOI,CAAA,EAAA,OAAA;;EAMD,aAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EA4QF;EAAQ,SAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;;AAEtB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/editor-ui.tsx","../src/components/bubble-menu.tsx","../src/components/slash-menu.tsx","../src/components/block-toolbar.tsx"],"sourcesContent":[],"mappings":";;;;;;;UAqGiB,aAAA;eACF;;;;AADf;;;gBAQmB,CAAA,EAAA,SAAA,GAAA,KAAA;WAMN,CAAA,EAAA,MAAA;EAAK;AA4QlB;;;UAEE,CAAA,EA9QW,KA8QX,CAAA,aAAA,GAAA,WAAA,GAAA,cAAA,GAAA,YAAA,CAAA;;AAEA,iBAJc,QAAA,CAId;EAAA,UAAA;EAAA,SAAA;EAAA,cAAA;EAAA,SAAA;EAAA;AAAA,CAAA,EAEC,aAFD,CAAA,EAEc,kBAAA,CAAA,GAAA,CAAA,OAFd;;;;UCxXe,eAAA;;UAEP;iCACuB;UACvB;ADsFV;AAA8B,iBCpBd,UAAA,CDoBc;EAAA,SAAA;EAAA,MAAA;EAAA,cAAA;EAAA;AAAA,CAAA,ECf3B,eDe2B,CAAA,ECfZ,MAAA,CAAA,WAAA,GDeY,IAAA;;;;UEzFb,cAAA;;;SAGR;UACC;EFqFO,OAAA,EAAA,GAAA,GAAA,IAAa;;AACf,iBExCC,SAAA,CFwCD;EAAA,SAAA;EAAA,KAAA;EAAA,KAAA;EAAA,MAAA;EAAA;AAAA,CAAA,EElCZ,cFkCY,CAAA,EElCE,MAAA,CAAA,WAAA,GFkCF,IAAA;;;;UGvEE,iBAAA;;;;;EHsEA,KAAA,EGjER,aHiEqB,EAAA;EAAA,MAAA,EGhEpB,aHgEoB;gBACf,CAAA,EAAA,OAAA;eAOI,CAAA,EAAA,OAAA;;EAMD,aAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EA4QF;EAAQ,SAAA,CAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;;AAEtB,iBG7Rc,YAAA,CH6Rd;EAAA,SAAA;EAAA,OAAA;EAAA,cAAA;EAAA,KAAA;EAAA,MAAA;EAAA,cAAA;EAAA,aAAA;EAAA,aAAA;EAAA;AAAA,CAAA,EGnRC,iBHmRD,CAAA,EGnRkB,kBAAA,CAAA,GAAA,CAAA,OAAA,GHmRlB,IAAA"}
|
package/dist/index.js
CHANGED
|
@@ -164,6 +164,7 @@ const DRAG_DATA_FORMAT = "application/x-lexical-drag-block";
|
|
|
164
164
|
const BTN = 24;
|
|
165
165
|
const GAP = 4;
|
|
166
166
|
const GUTTER_LEFT = 6;
|
|
167
|
+
GUTTER_LEFT + 2 * BTN + GAP;
|
|
167
168
|
const btnBase = {
|
|
168
169
|
display: "flex",
|
|
169
170
|
alignItems: "center",
|
|
@@ -1099,9 +1100,17 @@ function EditorInner({ extensions, toolbarContent, features }) {
|
|
|
1099
1100
|
] });
|
|
1100
1101
|
}
|
|
1101
1102
|
function EditorUI({ extensions = defaultExtensions, namespace = "jikjo", toolbarContent, className, features = [...ALL_FEATURES] }) {
|
|
1103
|
+
const hasBlockToolbar = features.includes("blockHandle") || features.includes("inlineAdd");
|
|
1102
1104
|
return /* @__PURE__ */ jsx("div", {
|
|
1103
1105
|
className,
|
|
1104
|
-
...
|
|
1106
|
+
...hasBlockToolbar ? { "data-has-block-toolbar": "" } : {},
|
|
1107
|
+
style: hasBlockToolbar ? {
|
|
1108
|
+
"--jikjo-content-pl": "74px",
|
|
1109
|
+
"--jikjo-placeholder-pl": "74px"
|
|
1110
|
+
} : {
|
|
1111
|
+
"--jikjo-content-pl": "16px",
|
|
1112
|
+
"--jikjo-placeholder-pl": "16px"
|
|
1113
|
+
},
|
|
1105
1114
|
children: /* @__PURE__ */ jsx(Editor, {
|
|
1106
1115
|
extensions,
|
|
1107
1116
|
namespace,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["btnBase: CSSProperties","btnHover: CSSProperties","ICON_MAP: Record<string, React.ReactNode>","targetKey: string | null","top","dragBtnStyle: CSSProperties","addBtnStyle: CSSProperties","ICON_MAP","ICON_MAP: Record<string, ReactNode>","tbtnBase: CSSProperties","tbtnHover: CSSProperties","tbtnActive: CSSProperties","style: CSSProperties","defaultExtensions: Extension[]","key: string | null"],"sources":["../src/components/bubble-menu.tsx","../src/components/block-toolbar.tsx","../src/components/slash-menu.tsx","../src/editor-ui.tsx"],"sourcesContent":["\"use client\";\n\nimport type { SelectionFormatState } from \"@jikjo/core\";\nimport type { LexicalEditor } from \"lexical\";\nimport { Bold, Code, Italic, Strikethrough, Underline } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface BubbleMenuProps {\n isVisible: boolean;\n format: SelectionFormatState;\n onToggleFormat: (format: keyof SelectionFormatState) => void;\n editor: LexicalEditor;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\ninterface AbsolutePos {\n top: number;\n left: number;\n}\n\n/**\n * selection rect를 컨테이너 기준 absolute 좌표로 변환.\n * 컨테이너가 스크롤되어도 absolute 요소는 함께 움직인다.\n */\nfunction getSelectionAbsPos(container: HTMLElement): AbsolutePos | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n const range = sel.getRangeAt(0);\n if (range.collapsed) return null;\n\n const selRect = range.getBoundingClientRect();\n if (selRect.width === 0) return null;\n\n const cr = container.getBoundingClientRect();\n const MENU_HEIGHT = 36;\n const GAP = 6;\n\n return {\n top: selRect.top - cr.top + container.scrollTop - MENU_HEIGHT - GAP,\n left: selRect.left - cr.left + container.scrollLeft + selRect.width / 2,\n };\n}\n\n// ─── Button ───────────────────────────────────────────────────────────────────\n\nfunction Btn({\n active,\n label,\n onMouseDown,\n children,\n}: {\n active: boolean;\n label: string;\n onMouseDown: (e: React.MouseEvent) => void;\n children: React.ReactNode;\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n onMouseDown={onMouseDown}\n className={[\n \"flex items-center justify-center w-8 h-8 rounded-md\",\n \"transition-colors duration-75\",\n active\n ? \"bg-white/15 text-white\"\n : \"text-zinc-400 hover:bg-white/10 hover:text-zinc-200\",\n ].join(\" \")}\n >\n {children}\n </button>\n );\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport function BubbleMenu({\n isVisible,\n format,\n onToggleFormat,\n editor,\n}: BubbleMenuProps) {\n const [container, setContainer] = useState<HTMLElement | null>(null);\n const [pos, setPos] = useState<AbsolutePos | null>(null);\n\n // editor.getRootElement()가 준비된 후 container를 설정\n useEffect(() => {\n function attachContainer() {\n const rootEl = editor.getRootElement();\n if (!rootEl) return;\n const parent = rootEl.parentElement;\n if (parent) setContainer(parent);\n }\n attachContainer();\n return editor.registerRootListener(attachContainer);\n }, [editor]);\n\n // selection 변화 또는 isVisible 변화 시 좌표 재계산.\n // isVisible=false 될 때만 pos를 null로 초기화 (format 적용 중에는 이전 pos 유지).\n useEffect(() => {\n if (!isVisible || !container) {\n setPos(null);\n return;\n }\n const newPos = getSelectionAbsPos(container);\n // newPos가 null이면 이전 pos 유지 (format 적용 직후 DOM rect가 잠시 없을 때 대비)\n if (newPos !== null) {\n setPos(newPos);\n }\n }, [isVisible, container]);\n\n if (!container) return null;\n\n const menuStyle = pos\n ? { position: \"absolute\" as const, top: pos.top, left: pos.left, x: \"-50%\", zIndex: 50 }\n : { position: \"absolute\" as const, top: -9999, left: -9999, zIndex: 50 };\n\n return createPortal(\n <AnimatePresence>\n {isVisible && (\n <motion.div\n style={menuStyle}\n initial={{ opacity: 0, y: 6 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 6 }}\n transition={{ duration: 0.1, ease: \"easeOut\" }}\n className=\"flex items-center gap-0.5 px-1.5 py-1.5 rounded-lg bg-zinc-800 shadow-xl shadow-black/50\"\n >\n <Btn\n active={format.bold}\n label=\"Bold\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"bold\");\n }}\n >\n <Bold size={13} strokeWidth={2.5} />\n </Btn>\n <Btn\n active={format.italic}\n label=\"Italic\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"italic\");\n }}\n >\n <Italic size={13} strokeWidth={2} />\n </Btn>\n <Btn\n active={format.underline}\n label=\"Underline\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"underline\");\n }}\n >\n <Underline size={13} strokeWidth={2} />\n </Btn>\n\n <div className=\"w-px h-4 bg-zinc-600/50 mx-0.5 shrink-0\" />\n\n <Btn\n active={format.strikethrough}\n label=\"Strikethrough\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"strikethrough\");\n }}\n >\n <Strikethrough size={13} strokeWidth={2} />\n </Btn>\n <Btn\n active={format.code}\n label=\"Code\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"code\");\n }}\n >\n <Code size={13} strokeWidth={2} />\n </Btn>\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n );\n}\n","\"use client\";\n\nimport type { SlashMenuItem } from \"@jikjo/core\";\nimport type { LexicalEditor } from \"lexical\";\nimport {\n $getNearestNodeFromDOMNode,\n $getNodeByKey,\n $isElementNode,\n COMMAND_PRIORITY_HIGH,\n COMMAND_PRIORITY_LOW,\n DRAGOVER_COMMAND,\n DROP_COMMAND,\n} from \"lexical\";\nimport {\n GripVertical,\n Heading1,\n Heading2,\n Heading3,\n List,\n ListOrdered,\n ListTodo,\n Plus,\n Quote,\n Text,\n} from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface BlockToolbarProps {\n isVisible: boolean;\n nodeKey: string | null;\n /** 현재 커서(포커스)가 있는 블록의 nodeKey */\n focusedNodeKey: string | null;\n items: SlashMenuItem[];\n editor: LexicalEditor;\n showDragHandle?: boolean;\n showAddButton?: boolean;\n /** drag 시작 시 드래그 중인 블록의 nodeKey를 부모에 전달 */\n onDragStarted?: (nodeKey: string) => void;\n /** drag & drop 완료 후 이동된 블록의 nodeKey를 부모에 전달 */\n onDropped?: (nodeKey: string) => void;\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst DRAG_DATA_FORMAT = \"application/x-lexical-drag-block\";\nconst BTN = 24;\nconst GAP = 4;\nconst GUTTER_LEFT = 6;\n\n// ─── Inline styles ────────────────────────────────────────────────────────────\n\nconst btnBase: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: 4,\n border: \"none\",\n background: \"transparent\",\n color: \"#a1a1aa\",\n cursor: \"pointer\",\n padding: 0,\n transition: \"background 80ms, color 80ms\",\n};\n\nconst btnHover: CSSProperties = {\n background: \"rgba(63,63,70,0.7)\",\n color: \"#e4e4e7\",\n};\n\n// ─── Icon map ─────────────────────────────────────────────────────────────────\n\nconst ICON_MAP: Record<string, React.ReactNode> = {\n paragraph: <Text size={13} strokeWidth={1.75} />,\n heading1: <Heading1 size={13} strokeWidth={1.75} />,\n heading2: <Heading2 size={13} strokeWidth={1.75} />,\n heading3: <Heading3 size={13} strokeWidth={1.75} />,\n bulletList: <List size={13} strokeWidth={1.75} />,\n orderedList: <ListOrdered size={13} strokeWidth={1.75} />,\n taskList: <ListTodo size={13} strokeWidth={1.75} />,\n quote: <Quote size={13} strokeWidth={1.75} />,\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/** blockEl의 top을 container 기준 절대 좌표로 계산 (getBoundingClientRect 기반) */\nfunction getOffsetTopToContainer(blockEl: HTMLElement, container: HTMLElement): number {\n const blockRect = blockEl.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n // container의 scrollTop도 반영 (container가 스크롤 가능한 경우)\n return blockRect.top - containerRect.top + container.scrollTop;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport function BlockToolbar({\n isVisible,\n nodeKey,\n focusedNodeKey,\n items,\n editor,\n showDragHandle = true,\n showAddButton = true,\n onDragStarted,\n onDropped,\n}: BlockToolbarProps) {\n const [panelOpen, setPanelOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(0);\n const [container, setContainer] = useState<HTMLElement | null>(null);\n const [top, setTop] = useState<number | null>(null);\n const [dragHovered, setDragHovered] = useState(false);\n const [addHovered, setAddHovered] = useState(false);\n\n // 드래그 가이드 라인: container 기준 absolute top + 위치 (before/after)\n const [dropLine, setDropLine] = useState<{ top: number; position: \"before\" | \"after\" } | null>(null);\n // 커서 인디케이터: focusedNodeKey 블록의 위치\n const [focusedTop, setFocusedTop] = useState<number | null>(null);\n const [focusedHeight, setFocusedHeight] = useState<number>(0);\n\n const panelRef = useRef<HTMLDivElement>(null);\n const addBtnRef = useRef<HTMLButtonElement>(null);\n\n const numBtns = (showDragHandle ? 1 : 0) + (showAddButton ? 1 : 0);\n\n // ── 컨테이너 취득 ──────────────────────────────────────────────────────────\n\n useEffect(() => {\n return editor.registerRootListener((rootElement) => {\n setContainer(rootElement?.parentElement ?? null);\n });\n }, [editor]);\n\n // ── 수직 위치 계산 ─────────────────────────────────────────────────────────\n\n useEffect(() => {\n function calc() {\n if (!isVisible || !nodeKey || !container) { setTop(null); return; }\n const blockEl = editor.getElementByKey(nodeKey);\n if (!blockEl) { setTop(null); return; }\n const offset = getOffsetTopToContainer(blockEl, container);\n setTop(offset + (blockEl.offsetHeight - BTN) / 2);\n }\n calc();\n return editor.registerUpdateListener(() => calc());\n }, [isVisible, nodeKey, editor, container]);\n\n // ── 커서 인디케이터 위치 계산 ──────────────────────────────────────────────\n\n useEffect(() => {\n function calc() {\n if (!focusedNodeKey || !container) { setFocusedTop(null); return; }\n const blockEl = editor.getElementByKey(focusedNodeKey);\n if (!blockEl) { setFocusedTop(null); return; }\n setFocusedTop(getOffsetTopToContainer(blockEl, container));\n setFocusedHeight(blockEl.offsetHeight);\n }\n calc();\n const unregister = editor.registerUpdateListener(() => calc());\n return unregister;\n }, [focusedNodeKey, editor, container]);\n\n // ── 패널 닫기 ──────────────────────────────────────────────────────────────\n\n useEffect(() => { if (!panelOpen) setActiveIndex(0); }, [panelOpen]);\n useEffect(() => { if (!isVisible) setPanelOpen(false); }, [isVisible]);\n\n\n // 스크롤 시 패널 닫기\n useEffect(() => {\n if (!panelOpen) return;\n function onScroll() { setPanelOpen(false); }\n window.addEventListener(\"scroll\", onScroll, { passive: true, capture: true });\n return () => window.removeEventListener(\"scroll\", onScroll, { capture: true });\n }, [panelOpen]);\n\n useEffect(() => {\n if (!panelOpen) return;\n function onPointerDown(e: PointerEvent) {\n if (panelRef.current && !panelRef.current.contains(e.target as Node)) {\n setPanelOpen(false);\n }\n }\n window.addEventListener(\"pointerdown\", onPointerDown);\n return () => window.removeEventListener(\"pointerdown\", onPointerDown);\n }, [panelOpen]);\n\n useEffect(() => {\n if (!panelOpen) return;\n function onKeyDown(e: KeyboardEvent) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((p) => Math.min(p + 1, items.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((p) => Math.max(p - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const item = items[activeIndex];\n if (item) { editor.focus(() => { item.onSelect(editor); setPanelOpen(false); }); }\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n setPanelOpen(false);\n }\n }\n window.addEventListener(\"keydown\", onKeyDown, { capture: true });\n return () => window.removeEventListener(\"keydown\", onKeyDown, { capture: true });\n }, [panelOpen, items, activeIndex, editor]);\n\n // ── Drag & Drop (가이드 라인 포함) ─────────────────────────────────────────\n\n useEffect(() => {\n if (!showDragHandle || !container) return;\n\n const unregDragover = editor.registerCommand(\n DRAGOVER_COMMAND,\n (event) => {\n if (!event.dataTransfer?.types.includes(DRAG_DATA_FORMAT)) return false;\n event.preventDefault();\n\n const draggedKey = event.dataTransfer.getData(DRAG_DATA_FORMAT);\n const target = event.target as HTMLElement | null;\n if (!target) return true;\n\n // 드래그 중인 element가 draggedNode의 DOM 안에 있으면 가이드라인 숨김\n if (draggedKey) {\n const draggedDOM = editor.getElementByKey(draggedKey);\n if (draggedDOM?.contains(target)) { setDropLine(null); return true; }\n }\n\n let targetKey: string | null = null;\n editor.read(() => {\n const node = $getNearestNodeFromDOMNode(target);\n if (!node) return;\n // 최상위 블록으로 올라감\n const top = node.isInline() ? node.getTopLevelElement() : node;\n if (top) targetKey = top.getKey();\n });\n if (!targetKey || targetKey === draggedKey) { setDropLine(null); return true; }\n\n const targetDOM = editor.getElementByKey(targetKey);\n if (!targetDOM) { setDropLine(null); return true; }\n\n const { top: domTop, height } = targetDOM.getBoundingClientRect();\n const isBefore = event.clientY < domTop + height / 2;\n const offsetTop = getOffsetTopToContainer(targetDOM, container);\n\n setDropLine({\n top: isBefore ? offsetTop : offsetTop + targetDOM.offsetHeight,\n position: isBefore ? \"before\" : \"after\",\n });\n\n return true;\n },\n COMMAND_PRIORITY_LOW,\n );\n\n const unregDrop = editor.registerCommand(\n DROP_COMMAND,\n (event) => {\n const draggedKey = event.dataTransfer?.getData(DRAG_DATA_FORMAT);\n if (!draggedKey) return false;\n event.preventDefault();\n setDropLine(null);\n\n const target = event.target as HTMLElement | null;\n if (!target) return true;\n\n // 실제로 노드가 이동되었는지 확인하기 위해 현재 에디터 상태에서 사전 검증\n let willMove = false;\n editor.read(() => {\n const draggedNode = $getNodeByKey(draggedKey);\n if (!draggedNode) return;\n const nearestNode = $getNearestNodeFromDOMNode(target);\n if (!nearestNode) return;\n const targetNode = nearestNode.isInline() ? nearestNode.getTopLevelElement() : nearestNode;\n if (!targetNode) return;\n const targetKey = targetNode.getKey();\n if (targetKey === draggedKey) return;\n if ($isElementNode(draggedNode) && draggedNode.getChildren().some(\n (child) => child.getKey() === targetKey,\n )) return;\n willMove = true;\n });\n\n editor.update(() => {\n const draggedNode = $getNodeByKey(draggedKey);\n if (!draggedNode) return;\n\n const nearestNode = $getNearestNodeFromDOMNode(target);\n if (!nearestNode) return;\n\n // 최상위 블록으로 정규화\n const targetNode = nearestNode.isInline()\n ? nearestNode.getTopLevelElement()\n : nearestNode;\n if (!targetNode) return;\n\n const targetKey = targetNode.getKey();\n // 자기 자신 또는 자기 자손으로 drop 방지\n if (targetKey === draggedKey) return;\n if ($isElementNode(draggedNode) && draggedNode.getChildren().some(\n (child) => child.getKey() === targetKey,\n )) return;\n\n const targetDOM = editor.getElementByKey(targetKey);\n if (!targetDOM) return;\n\n const { top: targetTop, height } = targetDOM.getBoundingClientRect();\n if (event.clientY < targetTop + height / 2) {\n targetNode.insertBefore(draggedNode);\n } else {\n targetNode.insertAfter(draggedNode);\n }\n });\n\n // Lexical의 drop 이벤트 처리(DOM selection 읽기)가 완료된 뒤에 onDropped 호출\n // requestAnimationFrame은 Lexical의 microtask 큐 처리 후에 실행됨\n if (willMove && onDropped) {\n requestAnimationFrame(() => onDropped(draggedKey));\n }\n return true;\n },\n COMMAND_PRIORITY_HIGH,\n );\n\n // 드래그가 에디터 밖으로 나가면 가이드 라인 제거\n function onDragEnd() { setDropLine(null); }\n document.addEventListener(\"dragend\", onDragEnd);\n\n return () => {\n unregDragover();\n unregDrop();\n document.removeEventListener(\"dragend\", onDragEnd);\n };\n }, [editor, showDragHandle, container, onDropped]);\n\n const handleDragStart = useCallback((e: React.DragEvent) => {\n if (!nodeKey) return;\n e.dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);\n e.dataTransfer.effectAllowed = \"move\";\n const blockEl = editor.getElementByKey(nodeKey);\n if (blockEl) e.dataTransfer.setDragImage(blockEl, 0, 0);\n onDragStarted?.(nodeKey);\n }, [nodeKey, editor, onDragStarted]);\n\n // ── + 버튼 클릭 ────────────────────────────────────────────────────────────\n\n const handleAddClick = useCallback((e: React.MouseEvent) => {\n e.preventDefault();\n setPanelOpen((p) => !p);\n }, []);\n\n // ── Render ─────────────────────────────────────────────────────────────────\n\n if (!container) return null;\n\n const dragBtnStyle: CSSProperties = {\n ...btnBase,\n width: BTN,\n height: BTN,\n cursor: \"grab\",\n ...(dragHovered ? btnHover : {}),\n };\n\n const addBtnStyle: CSSProperties = {\n ...btnBase,\n width: BTN,\n height: BTN,\n ...(panelOpen || addHovered ? btnHover : {}),\n };\n\n return (\n <>\n {/* 커서 인디케이터 — 현재 포커스 블록 왼쪽에 세로 라인 */}\n {createPortal(\n <AnimatePresence>\n {focusedTop !== null && (\n <motion.div\n key={focusedNodeKey ?? \"cursor-indicator\"}\n style={{\n position: \"absolute\",\n top: focusedTop,\n left: 0,\n width: 2,\n height: focusedHeight,\n background: \"#818cf8\", // indigo-400\n borderRadius: 1,\n zIndex: 2,\n pointerEvents: \"none\",\n }}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.1 }}\n />\n )}\n </AnimatePresence>,\n container!,\n )}\n\n {/* 버튼 그룹 — container 내부, absolute */}\n {createPortal(\n <AnimatePresence>\n {isVisible && top !== null && (\n <motion.div\n key={nodeKey ?? \"toolbar\"}\n data-block-toolbar\n style={{\n position: \"absolute\",\n top,\n left: GUTTER_LEFT,\n display: \"flex\",\n alignItems: \"center\",\n gap: GAP,\n width: numBtns * BTN + Math.max(0, numBtns - 1) * GAP,\n zIndex: 2,\n userSelect: \"none\",\n }}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.08, ease: \"easeOut\" }}\n >\n {showDragHandle && (\n <div\n draggable\n data-drag-handle\n onDragStart={handleDragStart}\n aria-label=\"Drag to reorder\"\n style={dragBtnStyle}\n onMouseEnter={() => setDragHovered(true)}\n onMouseLeave={() => setDragHovered(false)}\n >\n <GripVertical size={14} strokeWidth={2} />\n </div>\n )}\n {showAddButton && (\n <button\n ref={addBtnRef}\n type=\"button\"\n aria-label=\"Add block\"\n aria-expanded={panelOpen}\n style={addBtnStyle}\n onMouseDown={handleAddClick}\n onMouseEnter={() => setAddHovered(true)}\n onMouseLeave={() => setAddHovered(false)}\n >\n <Plus size={13} strokeWidth={2.5} />\n </button>\n )}\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n )}\n\n {/* 드래그 가이드 라인 — container 내부, absolute */}\n {createPortal(\n <AnimatePresence>\n {dropLine !== null && (\n <motion.div\n key=\"drop-line\"\n style={{\n position: \"absolute\",\n top: dropLine.top,\n // 텍스트 시작점(padding-left)에 맞춰 왼쪽 여백\n left: 0,\n right: 0,\n height: 2,\n zIndex: 3,\n pointerEvents: \"none\",\n // 가이드 라인: 파란 선 + 양쪽 원형 핸들\n display: \"flex\",\n alignItems: \"center\",\n // translateY로 선을 블록 경계에 정확히 정렬\n transform: \"translateY(-1px)\",\n }}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.06 }}\n >\n {/* 왼쪽 원 */}\n <div style={{\n width: 6,\n height: 6,\n borderRadius: \"50%\",\n background: \"#818cf8\", // indigo-400\n flexShrink: 0,\n marginLeft: 4,\n }} />\n {/* 가로선 */}\n <div style={{\n flex: 1,\n height: 2,\n background: \"#818cf8\",\n marginRight: 8,\n }} />\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n )}\n\n {/* 패널 — container 내부, absolute */}\n {createPortal(\n <AnimatePresence>\n {isVisible && panelOpen && top !== null && (\n <motion.div\n ref={panelRef}\n data-block-toolbar\n style={{\n position: \"absolute\",\n top: top + BTN + GAP,\n left: GUTTER_LEFT,\n zIndex: 50,\n width: 240,\n }}\n initial={{ opacity: 0, y: -4 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -4 }}\n transition={{ duration: 0.1, ease: \"easeOut\" }}\n role=\"dialog\"\n aria-label=\"Insert block\"\n className=\"rounded-lg bg-zinc-800 shadow-xl shadow-black/50 py-1.5\"\n >\n <div role=\"listbox\" aria-label=\"Block type\" className=\"w-full px-1.5 flex flex-col\">\n {items.map((item, index) => {\n const icon = ICON_MAP[item.id] ?? item.icon;\n const isActive = index === activeIndex;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n onMouseDown={(e) => {\n e.preventDefault();\n editor.focus(() => { item.onSelect(editor); setPanelOpen(false); });\n }}\n onMouseEnter={() => setActiveIndex(index)}\n className={[\n \"w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors duration-75\",\n \"outline-none\",\n isActive\n ? \"bg-zinc-700/60 text-zinc-100\"\n : \"text-zinc-400 hover:bg-zinc-700/40 hover:text-zinc-200\",\n ].join(\" \")}\n >\n <span className=\"flex items-center justify-center shrink-0 w-4 text-current\">\n {icon}\n </span>\n <span className=\"text-sm font-normal leading-none\">{item.label}</span>\n </button>\n );\n })}\n </div>\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n )}\n </>\n );\n}\n","\"use client\";\n\nimport type { LexicalEditor } from \"lexical\";\nimport type { SlashMenuItem } from \"@jikjo/core\";\nimport { Heading1, Heading2, Heading3, Quote, Text } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface SlashMenuProps {\n isVisible: boolean;\n query: string;\n items: SlashMenuItem[];\n editor: LexicalEditor;\n onClose: () => void;\n}\n\n// ─── Icon map ─────────────────────────────────────────────────────────────────\n\nconst ICON_MAP: Record<string, ReactNode> = {\n paragraph: <Text size={14} strokeWidth={1.75} />,\n heading1: <Heading1 size={14} strokeWidth={1.75} />,\n heading2: <Heading2 size={14} strokeWidth={1.75} />,\n heading3: <Heading3 size={14} strokeWidth={1.75} />,\n quote: <Quote size={14} strokeWidth={1.75} />,\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\ninterface AbsolutePos {\n top: number;\n left: number;\n}\n\n/**\n * 캐럿(collapsed range)의 bottom 위치를 컨테이너 기준 absolute 좌표로 변환.\n */\nfunction getCaretAbsPos(container: HTMLElement): AbsolutePos | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n\n const range = sel.getRangeAt(0).cloneRange();\n range.collapse(true);\n const caretRect = range.getBoundingClientRect();\n\n // getBoundingClientRect()가 (0,0,0,0)을 반환하는 경우 방어\n if (caretRect.width === 0 && caretRect.height === 0 && caretRect.top === 0) return null;\n\n const cr = container.getBoundingClientRect();\n const GAP = 6;\n\n return {\n top: caretRect.bottom - cr.top + container.scrollTop + GAP,\n left: caretRect.left - cr.left + container.scrollLeft,\n };\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport function SlashMenu({\n isVisible,\n query,\n items,\n editor,\n onClose,\n}: SlashMenuProps) {\n const [activeIndex, setActiveIndex] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);\n const [pos, setPos] = useState<AbsolutePos | null>(null);\n\n // editor.getRootElement()가 준비된 후 portalContainer를 설정\n useEffect(() => {\n function attachContainer() {\n const rootEl = editor.getRootElement();\n if (!rootEl) return;\n const parent = rootEl.parentElement;\n if (parent) setPortalContainer(parent);\n }\n attachContainer();\n return editor.registerRootListener(attachContainer);\n }, [editor]);\n\n const filtered = items.filter(\n (item) =>\n !query ||\n item.label.toLowerCase().includes(query.toLowerCase()) ||\n item.description.toLowerCase().includes(query.toLowerCase()),\n );\n\n // isVisible 또는 query 변경 시 좌표 재계산 (캐럿이 이동/입력될 때마다)\n useEffect(() => {\n if (!isVisible || !portalContainer) {\n setPos(null);\n return;\n }\n // registerUpdateListener가 발동된 직후 호출되므로 DOM이 확정된 시점\n setPos(getCaretAbsPos(portalContainer));\n }, [isVisible, query, portalContainer]);\n\n useEffect(() => {\n setActiveIndex(0);\n }, [query]);\n\n useEffect(() => {\n if (!isVisible) return;\n function handleKeyDown(event: KeyboardEvent) {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((prev) => Math.max(prev - 1, 0));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const item = filtered[activeIndex];\n if (!item) return;\n editor.focus(() => {\n item.onSelect(editor);\n onClose();\n });\n } else if (event.key === \"Escape\") {\n onClose();\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [isVisible, filtered, activeIndex, editor, onClose]);\n\n useEffect(() => {\n const el = containerRef.current?.querySelector<HTMLElement>(\n '[data-active=\"true\"]',\n );\n el?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n if (!portalContainer) return null;\n\n const menuStyle = pos\n ? { position: \"absolute\" as const, top: pos.top, left: pos.left, zIndex: 50, width: 240 }\n : { position: \"absolute\" as const, top: -9999, left: -9999, zIndex: 50, width: 240 };\n\n return createPortal(\n <AnimatePresence>\n {isVisible && (\n <motion.div\n style={menuStyle}\n initial={{ opacity: 0, y: -4 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -4 }}\n transition={{ duration: 0.1, ease: \"easeOut\" }}\n role=\"dialog\"\n aria-label=\"Insert block\"\n className=\"rounded-lg bg-zinc-800 shadow-xl shadow-black/50 py-1.5 overflow-hidden\"\n >\n {filtered.length === 0 ? (\n <p className=\"px-4 py-2 text-xs text-zinc-500\">\n No results for “{query}”\n </p>\n ) : (\n <div\n ref={containerRef}\n role=\"listbox\"\n aria-label=\"Block type\"\n className=\"w-full max-h-72 overflow-y-auto px-1.5 flex flex-col\"\n >\n {filtered.map((item, index) => {\n const icon = ICON_MAP[item.id] ?? item.icon;\n const isActive = index === activeIndex;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n data-active={isActive}\n onMouseDown={(e) => {\n e.preventDefault();\n editor.focus(() => {\n item.onSelect(editor);\n onClose();\n });\n }}\n onMouseEnter={() => setActiveIndex(index)}\n className={[\n \"w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors duration-75\",\n \"outline-none focus-visible:outline-none\",\n isActive\n ? \"bg-zinc-700/80 text-zinc-100\"\n : \"text-zinc-400 hover:bg-zinc-700/60 hover:text-zinc-200 focus-visible:bg-zinc-700/80 focus-visible:text-zinc-100\",\n ].join(\" \")}\n >\n <span className=\"flex items-center justify-center shrink-0 w-4 text-current\">\n {icon}\n </span>\n <span className=\"text-sm font-normal leading-none\">\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n )}\n </motion.div>\n )}\n </AnimatePresence>,\n portalContainer,\n );\n}\n","\"use client\";\n\nimport {\n Editor,\n historyExtension,\n richTextExtension,\n useBlockHoverPlugin,\n useSelectionPlugin,\n useSlashCommandPlugin,\n} from \"@jikjo/core\";\nimport type { Extension, SlashMenuItem } from \"@jikjo/core\";\nimport {\n $createHeadingNode,\n $isHeadingNode,\n type HeadingTagType,\n} from \"@lexical/rich-text\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport {\n $createParagraphNode,\n $getNearestNodeFromDOMNode,\n $getSelection,\n $isRangeSelection,\n} from \"lexical\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Heading3,\n Italic,\n Strikethrough,\n Underline,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { MouseEvent } from \"react\";\nimport { BubbleMenu } from \"./components/bubble-menu\";\nimport { BlockToolbar } from \"./components/block-toolbar\";\nimport { SlashMenu } from \"./components/slash-menu\";\n\n// ─── Styles ───────────────────────────────────────────────────────────────────\n\nconst tbtnBase: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 32,\n height: 32,\n borderRadius: 6,\n border: \"none\",\n background: \"transparent\",\n color: \"#71717a\",\n cursor: \"pointer\",\n padding: 0,\n transition: \"background 100ms, color 100ms\",\n flexShrink: 0,\n};\n\nconst tbtnHover: CSSProperties = {\n background: \"rgba(39,39,42,0.9)\",\n color: \"#e4e4e7\",\n};\n\nconst tbtnActive: CSSProperties = {\n background: \"rgba(63,63,70,0.8)\",\n color: \"#f4f4f5\",\n};\n\n// ─── ToolbarButton ─────────────────────────────────────────────────────────────\n\ninterface ToolbarButtonProps {\n label: string;\n isActive?: boolean;\n onMouseDown: (e: MouseEvent) => void;\n children: ReactNode;\n}\n\nfunction ToolbarButton({ label, isActive, onMouseDown, children }: ToolbarButtonProps) {\n const [hovered, setHovered] = useState(false);\n\n const style: CSSProperties = {\n ...tbtnBase,\n ...(hovered ? tbtnHover : {}),\n ...(isActive ? tbtnActive : {}),\n };\n\n return (\n <button\n type=\"button\"\n aria-label={label}\n onMouseDown={onMouseDown}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n style={style}\n >\n {children}\n </button>\n );\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface EditorUIProps {\n extensions?: Extension[];\n namespace?: string;\n /**\n * undefined → default toolbar\n * false → no toolbar rendered at all\n * ReactNode → custom toolbar content\n */\n toolbarContent?: ReactNode | false;\n className?: string;\n /**\n * 활성화할 UI 기능 목록. 기본값: 모두 활성화.\n * \"blockHandle\" | \"inlineAdd\" | \"slashCommand\" | \"bubbleMenu\"\n */\n features?: Array<\"blockHandle\" | \"inlineAdd\" | \"slashCommand\" | \"bubbleMenu\">;\n}\n\n// ─── Default extensions ───────────────────────────────────────────────────────\n\nconst defaultExtensions: Extension[] = [richTextExtension, historyExtension];\n\n// ─── Inner component ──────────────────────────────────────────────────────────\n// Must live inside LexicalComposer so all hooks have editor context.\n\nconst ALL_FEATURES = [\"blockHandle\", \"inlineAdd\", \"slashCommand\", \"bubbleMenu\"] as const;\n\ninterface EditorInnerProps {\n extensions: Extension[];\n toolbarContent: ReactNode | false | undefined;\n features: Array<\"blockHandle\" | \"inlineAdd\" | \"slashCommand\" | \"bubbleMenu\">;\n}\n\nfunction EditorInner({\n extensions,\n toolbarContent,\n features,\n}: EditorInnerProps) {\n const [editor] = useLexicalComposerContext();\n\n const selection = useSelectionPlugin();\n const slashCommand = useSlashCommandPlugin();\n const blockHover = useBlockHoverPlugin();\n\n // heading 태그는 Lexical selection으로 추적 (toolbar 표시용)\n const [currentHeadingTag, setCurrentHeadingTag] = useState<HeadingTagType | null>(null);\n\n // focusedNodeKey: Lexical selection에서 완전히 독립.\n // - 에디터 click → DOM에서 직접 nodeKey 읽기\n // - dragstart → onDragStarted 콜백으로 nodeKey 전달\n // - drop 성공 → onDropped 콜백으로 이동된 nodeKey 전달\n // - dragend(취소) → null\n // - 에디터 바깥 클릭 → null\n const [focusedNodeKey, setFocusedNodeKey] = useState<string | null>(null);\n\n // drop 성공 플래그 (dragend 시 취소인지 성공인지 구분용)\n const dropSucceededRef = useRef(false);\n\n // ── heading tag 추적 (selection 기반, toolbar 전용) ──────────────────────\n\n useEffect(() => {\n return editor.registerUpdateListener(({ editorState }) => {\n editorState.read(() => {\n const sel = $getSelection();\n if (!$isRangeSelection(sel)) {\n setCurrentHeadingTag(null);\n return;\n }\n const topNode = sel.anchor.getNode().getTopLevelElement();\n setCurrentHeadingTag($isHeadingNode(topNode) ? topNode.getTag() : null);\n });\n });\n }, [editor]);\n\n // ── focusedNodeKey: 에디터 click 이벤트에서 DOM → nodeKey 직접 읽기 ──────\n\n useEffect(() => {\n return editor.registerRootListener((rootElement) => {\n if (!rootElement) return;\n const root = rootElement;\n\n function onClick(e: Event) {\n const target = e.target as HTMLElement | null;\n if (!target) return;\n let key: string | null = null;\n editor.read(() => {\n const node = $getNearestNodeFromDOMNode(target);\n if (!node) return;\n const top = node.isInline() ? node.getTopLevelElement() : node;\n if (top) key = top.getKey();\n });\n if (key) setFocusedNodeKey(key);\n }\n\n // 에디터/툴바 바깥 클릭 시 커서 인디케이터 제거\n function onDocClick(e: Event) {\n const target = e.target as HTMLElement | null;\n if (root.contains(target)) return;\n if (target?.closest(\"[data-block-toolbar]\") || target?.closest(\"[data-drag-handle]\")) return;\n setFocusedNodeKey(null);\n }\n\n root.addEventListener(\"click\", onClick);\n document.addEventListener(\"click\", onDocClick, true);\n\n return () => {\n root.removeEventListener(\"click\", onClick);\n document.removeEventListener(\"click\", onDocClick, true);\n };\n });\n }, [editor]);\n\n // ── drag/drop 이벤트에서 focusedNodeKey 관리 ────────────────────────────\n // Firefox: drag handle mousedown → blur → dragstart 순서로 발생.\n // blur 시 Lexical이 $setSelection(null)을 호출하지만,\n // focusedNodeKey는 Lexical selection과 독립이므로 영향받지 않음.\n // drop 성공/취소는 onDropped / dragend 이벤트로 구분.\n\n const handleDragStarted = useCallback((key: string) => {\n dropSucceededRef.current = false;\n setFocusedNodeKey(key);\n // Firefox blur로 에디터 포커스가 빠진 경우 복구\n editor.focus();\n }, [editor]);\n\n const handleDropped = useCallback((key: string) => {\n dropSucceededRef.current = true;\n setFocusedNodeKey(key);\n // drop 후 에디터 포커스 복구\n editor.focus();\n }, [editor]);\n\n useEffect(() => {\n function onDragEnd() {\n if (!dropSucceededRef.current) {\n // drag 취소: 커서 인디케이터 제거\n setFocusedNodeKey(null);\n }\n dropSucceededRef.current = false;\n }\n document.addEventListener(\"dragend\", onDragEnd);\n return () => document.removeEventListener(\"dragend\", onDragEnd);\n }, []);\n\n // Collect slash menu items from all registered extensions\n const slashMenuItems = useMemo<SlashMenuItem[]>(\n () => extensions.flatMap((ext) => ext.slashMenuItems ?? []),\n [extensions],\n );\n\n // Toggle heading: if already at this level, revert to paragraph.\n // newNode.select() preserves selection after replace to avoid Lexical error.\n const toggleHeading = useCallback(\n (tag: HeadingTagType) => {\n editor.update(() => {\n const sel = $getSelection();\n if (!$isRangeSelection(sel)) return;\n const topNode = sel.anchor.getNode().getTopLevelElementOrThrow();\n const newNode = ($isHeadingNode(topNode) && topNode.getTag() === tag)\n ? $createParagraphNode()\n : $createHeadingNode(tag);\n topNode.replace(newNode);\n newNode.select();\n });\n },\n [editor],\n );\n\n return (\n <>\n {/* ── Toolbar ──────────────────────────────────────────────────── */}\n {toolbarContent === false ? null : toolbarContent !== undefined ? (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2, padding: \"8px 16px\", borderBottom: \"1px solid rgba(39,39,42,0.5)\" }}>\n {toolbarContent}\n </div>\n ) : (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2, padding: \"8px 16px\", borderBottom: \"1px solid rgba(39,39,42,0.5)\" }}>\n {/* Format group */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2 }}>\n <ToolbarButton\n label=\"Bold\"\n isActive={selection.format.bold}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"bold\"); }}\n >\n <Bold size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Italic\"\n isActive={selection.format.italic}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"italic\"); }}\n >\n <Italic size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Underline\"\n isActive={selection.format.underline}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"underline\"); }}\n >\n <Underline size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Strikethrough\"\n isActive={selection.format.strikethrough}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"strikethrough\"); }}\n >\n <Strikethrough size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Code\"\n isActive={selection.format.code}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"code\"); }}\n >\n <Code size={14} strokeWidth={2.5} />\n </ToolbarButton>\n </div>\n\n <div style={{ width: 1, height: 16, background: \"#27272a\", margin: \"0 4px\" }} />\n\n {/* Heading group */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2 }}>\n <ToolbarButton\n label=\"Heading 1\"\n isActive={currentHeadingTag === \"h1\"}\n onMouseDown={(e) => { e.preventDefault(); toggleHeading(\"h1\"); }}\n >\n <Heading1 size={15} strokeWidth={2} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 2\"\n isActive={currentHeadingTag === \"h2\"}\n onMouseDown={(e) => { e.preventDefault(); toggleHeading(\"h2\"); }}\n >\n <Heading2 size={15} strokeWidth={2} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 3\"\n isActive={currentHeadingTag === \"h3\"}\n onMouseDown={(e) => { e.preventDefault(); toggleHeading(\"h3\"); }}\n >\n <Heading3 size={15} strokeWidth={2} />\n </ToolbarButton>\n </div>\n </div>\n )}\n\n {/* ── Floating overlays ────────────────────────────────────────── */}\n {features.includes(\"bubbleMenu\") && (\n <BubbleMenu\n isVisible={selection.isActive}\n format={selection.format}\n onToggleFormat={selection.toggleFormat}\n editor={editor}\n />\n )}\n\n {features.includes(\"slashCommand\") && (\n <SlashMenu\n isVisible={slashCommand.isActive}\n query={slashCommand.query}\n items={slashMenuItems}\n editor={editor}\n onClose={slashCommand.close}\n />\n )}\n\n {/* drag handle + + 버튼 + 커서 인디케이터 */}\n <BlockToolbar\n isVisible={blockHover.isActive && (features.includes(\"blockHandle\") || features.includes(\"inlineAdd\"))}\n nodeKey={blockHover.nodeKey}\n focusedNodeKey={focusedNodeKey}\n items={slashMenuItems}\n editor={editor}\n showDragHandle={features.includes(\"blockHandle\")}\n showAddButton={features.includes(\"inlineAdd\")}\n onDragStarted={handleDragStarted}\n onDropped={handleDropped}\n />\n </>\n );\n}\n\n// ─── Public component ─────────────────────────────────────────────────────────\n\nexport function EditorUI({\n extensions = defaultExtensions,\n namespace = \"jikjo\",\n toolbarContent,\n className,\n features = [...ALL_FEATURES],\n}: EditorUIProps) {\n const hasBlockToolbar = features.includes(\"blockHandle\") || features.includes(\"inlineAdd\");\n return (\n <div className={className} {...(hasBlockToolbar ? { \"data-has-block-toolbar\": \"\" } : {})}>\n <Editor extensions={extensions} namespace={namespace}>\n <EditorInner\n extensions={extensions}\n toolbarContent={toolbarContent}\n features={features}\n />\n </Editor>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["btnBase: CSSProperties","btnHover: CSSProperties","ICON_MAP: Record<string, React.ReactNode>","targetKey: string | null","top","dragBtnStyle: CSSProperties","addBtnStyle: CSSProperties","ICON_MAP","ICON_MAP: Record<string, ReactNode>","tbtnBase: CSSProperties","tbtnHover: CSSProperties","tbtnActive: CSSProperties","style: CSSProperties","defaultExtensions: Extension[]","key: string | null"],"sources":["../src/components/bubble-menu.tsx","../src/components/block-toolbar.tsx","../src/components/slash-menu.tsx","../src/editor-ui.tsx"],"sourcesContent":["\"use client\";\n\nimport type { SelectionFormatState } from \"@jikjo/core\";\nimport type { LexicalEditor } from \"lexical\";\nimport { Bold, Code, Italic, Strikethrough, Underline } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface BubbleMenuProps {\n isVisible: boolean;\n format: SelectionFormatState;\n onToggleFormat: (format: keyof SelectionFormatState) => void;\n editor: LexicalEditor;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\ninterface AbsolutePos {\n top: number;\n left: number;\n}\n\n/**\n * selection rect를 컨테이너 기준 absolute 좌표로 변환.\n * 컨테이너가 스크롤되어도 absolute 요소는 함께 움직인다.\n */\nfunction getSelectionAbsPos(container: HTMLElement): AbsolutePos | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n const range = sel.getRangeAt(0);\n if (range.collapsed) return null;\n\n const selRect = range.getBoundingClientRect();\n if (selRect.width === 0) return null;\n\n const cr = container.getBoundingClientRect();\n const MENU_HEIGHT = 36;\n const GAP = 6;\n\n return {\n top: selRect.top - cr.top + container.scrollTop - MENU_HEIGHT - GAP,\n left: selRect.left - cr.left + container.scrollLeft + selRect.width / 2,\n };\n}\n\n// ─── Button ───────────────────────────────────────────────────────────────────\n\nfunction Btn({\n active,\n label,\n onMouseDown,\n children,\n}: {\n active: boolean;\n label: string;\n onMouseDown: (e: React.MouseEvent) => void;\n children: React.ReactNode;\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n onMouseDown={onMouseDown}\n className={[\n \"flex items-center justify-center w-8 h-8 rounded-md\",\n \"transition-colors duration-75\",\n active\n ? \"bg-white/15 text-white\"\n : \"text-zinc-400 hover:bg-white/10 hover:text-zinc-200\",\n ].join(\" \")}\n >\n {children}\n </button>\n );\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport function BubbleMenu({\n isVisible,\n format,\n onToggleFormat,\n editor,\n}: BubbleMenuProps) {\n const [container, setContainer] = useState<HTMLElement | null>(null);\n const [pos, setPos] = useState<AbsolutePos | null>(null);\n\n // editor.getRootElement()가 준비된 후 container를 설정\n useEffect(() => {\n function attachContainer() {\n const rootEl = editor.getRootElement();\n if (!rootEl) return;\n const parent = rootEl.parentElement;\n if (parent) setContainer(parent);\n }\n attachContainer();\n return editor.registerRootListener(attachContainer);\n }, [editor]);\n\n // selection 변화 또는 isVisible 변화 시 좌표 재계산.\n // isVisible=false 될 때만 pos를 null로 초기화 (format 적용 중에는 이전 pos 유지).\n useEffect(() => {\n if (!isVisible || !container) {\n setPos(null);\n return;\n }\n const newPos = getSelectionAbsPos(container);\n // newPos가 null이면 이전 pos 유지 (format 적용 직후 DOM rect가 잠시 없을 때 대비)\n if (newPos !== null) {\n setPos(newPos);\n }\n }, [isVisible, container]);\n\n if (!container) return null;\n\n const menuStyle = pos\n ? { position: \"absolute\" as const, top: pos.top, left: pos.left, x: \"-50%\", zIndex: 50 }\n : { position: \"absolute\" as const, top: -9999, left: -9999, zIndex: 50 };\n\n return createPortal(\n <AnimatePresence>\n {isVisible && (\n <motion.div\n style={menuStyle}\n initial={{ opacity: 0, y: 6 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 6 }}\n transition={{ duration: 0.1, ease: \"easeOut\" }}\n className=\"flex items-center gap-0.5 px-1.5 py-1.5 rounded-lg bg-zinc-800 shadow-xl shadow-black/50\"\n >\n <Btn\n active={format.bold}\n label=\"Bold\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"bold\");\n }}\n >\n <Bold size={13} strokeWidth={2.5} />\n </Btn>\n <Btn\n active={format.italic}\n label=\"Italic\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"italic\");\n }}\n >\n <Italic size={13} strokeWidth={2} />\n </Btn>\n <Btn\n active={format.underline}\n label=\"Underline\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"underline\");\n }}\n >\n <Underline size={13} strokeWidth={2} />\n </Btn>\n\n <div className=\"w-px h-4 bg-zinc-600/50 mx-0.5 shrink-0\" />\n\n <Btn\n active={format.strikethrough}\n label=\"Strikethrough\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"strikethrough\");\n }}\n >\n <Strikethrough size={13} strokeWidth={2} />\n </Btn>\n <Btn\n active={format.code}\n label=\"Code\"\n onMouseDown={(e) => {\n e.preventDefault();\n onToggleFormat(\"code\");\n }}\n >\n <Code size={13} strokeWidth={2} />\n </Btn>\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n );\n}\n","\"use client\";\n\nimport type { SlashMenuItem } from \"@jikjo/core\";\nimport type { LexicalEditor } from \"lexical\";\nimport {\n $getNearestNodeFromDOMNode,\n $getNodeByKey,\n $isElementNode,\n COMMAND_PRIORITY_HIGH,\n COMMAND_PRIORITY_LOW,\n DRAGOVER_COMMAND,\n DROP_COMMAND,\n} from \"lexical\";\nimport {\n GripVertical,\n Heading1,\n Heading2,\n Heading3,\n List,\n ListOrdered,\n ListTodo,\n Plus,\n Quote,\n Text,\n} from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface BlockToolbarProps {\n isVisible: boolean;\n nodeKey: string | null;\n /** 현재 커서(포커스)가 있는 블록의 nodeKey */\n focusedNodeKey: string | null;\n items: SlashMenuItem[];\n editor: LexicalEditor;\n showDragHandle?: boolean;\n showAddButton?: boolean;\n /** drag 시작 시 드래그 중인 블록의 nodeKey를 부모에 전달 */\n onDragStarted?: (nodeKey: string) => void;\n /** drag & drop 완료 후 이동된 블록의 nodeKey를 부모에 전달 */\n onDropped?: (nodeKey: string) => void;\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst DRAG_DATA_FORMAT = \"application/x-lexical-drag-block\";\nconst BTN = 24;\nconst GAP = 4;\nconst GUTTER_LEFT = 6;\n// 버튼 그룹 전체 너비 (drag + add 기준): GUTTER_LEFT + 2*BTN + GAP\nconst TOOLBAR_WIDTH = GUTTER_LEFT + 2 * BTN + GAP; // 58px\n\n// ─── Inline styles ────────────────────────────────────────────────────────────\n\nconst btnBase: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: 4,\n border: \"none\",\n background: \"transparent\",\n color: \"#a1a1aa\",\n cursor: \"pointer\",\n padding: 0,\n transition: \"background 80ms, color 80ms\",\n};\n\nconst btnHover: CSSProperties = {\n background: \"rgba(63,63,70,0.7)\",\n color: \"#e4e4e7\",\n};\n\n// ─── Icon map ─────────────────────────────────────────────────────────────────\n\nconst ICON_MAP: Record<string, React.ReactNode> = {\n paragraph: <Text size={13} strokeWidth={1.75} />,\n heading1: <Heading1 size={13} strokeWidth={1.75} />,\n heading2: <Heading2 size={13} strokeWidth={1.75} />,\n heading3: <Heading3 size={13} strokeWidth={1.75} />,\n bulletList: <List size={13} strokeWidth={1.75} />,\n orderedList: <ListOrdered size={13} strokeWidth={1.75} />,\n taskList: <ListTodo size={13} strokeWidth={1.75} />,\n quote: <Quote size={13} strokeWidth={1.75} />,\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/** blockEl의 top을 container 기준 절대 좌표로 계산 (getBoundingClientRect 기반) */\nfunction getOffsetTopToContainer(blockEl: HTMLElement, container: HTMLElement): number {\n const blockRect = blockEl.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n // container의 scrollTop도 반영 (container가 스크롤 가능한 경우)\n return blockRect.top - containerRect.top + container.scrollTop;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport function BlockToolbar({\n isVisible,\n nodeKey,\n focusedNodeKey,\n items,\n editor,\n showDragHandle = true,\n showAddButton = true,\n onDragStarted,\n onDropped,\n}: BlockToolbarProps) {\n const [panelOpen, setPanelOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(0);\n const [container, setContainer] = useState<HTMLElement | null>(null);\n const [top, setTop] = useState<number | null>(null);\n const [dragHovered, setDragHovered] = useState(false);\n const [addHovered, setAddHovered] = useState(false);\n\n // 드래그 가이드 라인: container 기준 absolute top + 위치 (before/after)\n const [dropLine, setDropLine] = useState<{ top: number; position: \"before\" | \"after\" } | null>(null);\n // 커서 인디케이터: focusedNodeKey 블록의 위치\n const [focusedTop, setFocusedTop] = useState<number | null>(null);\n const [focusedHeight, setFocusedHeight] = useState<number>(0);\n\n const panelRef = useRef<HTMLDivElement>(null);\n const addBtnRef = useRef<HTMLButtonElement>(null);\n\n const numBtns = (showDragHandle ? 1 : 0) + (showAddButton ? 1 : 0);\n\n // ── 컨테이너 취득 ──────────────────────────────────────────────────────────\n\n useEffect(() => {\n return editor.registerRootListener((rootElement) => {\n setContainer(rootElement?.parentElement ?? null);\n });\n }, [editor]);\n\n // ── 수직 위치 계산 ─────────────────────────────────────────────────────────\n\n useEffect(() => {\n function calc() {\n if (!isVisible || !nodeKey || !container) { setTop(null); return; }\n const blockEl = editor.getElementByKey(nodeKey);\n if (!blockEl) { setTop(null); return; }\n const offset = getOffsetTopToContainer(blockEl, container);\n setTop(offset + (blockEl.offsetHeight - BTN) / 2);\n }\n calc();\n return editor.registerUpdateListener(() => calc());\n }, [isVisible, nodeKey, editor, container]);\n\n // ── 커서 인디케이터 위치 계산 ──────────────────────────────────────────────\n\n useEffect(() => {\n function calc() {\n if (!focusedNodeKey || !container) { setFocusedTop(null); return; }\n const blockEl = editor.getElementByKey(focusedNodeKey);\n if (!blockEl) { setFocusedTop(null); return; }\n setFocusedTop(getOffsetTopToContainer(blockEl, container));\n setFocusedHeight(blockEl.offsetHeight);\n }\n calc();\n const unregister = editor.registerUpdateListener(() => calc());\n return unregister;\n }, [focusedNodeKey, editor, container]);\n\n // ── 패널 닫기 ──────────────────────────────────────────────────────────────\n\n useEffect(() => { if (!panelOpen) setActiveIndex(0); }, [panelOpen]);\n useEffect(() => { if (!isVisible) setPanelOpen(false); }, [isVisible]);\n\n\n // 스크롤 시 패널 닫기\n useEffect(() => {\n if (!panelOpen) return;\n function onScroll() { setPanelOpen(false); }\n window.addEventListener(\"scroll\", onScroll, { passive: true, capture: true });\n return () => window.removeEventListener(\"scroll\", onScroll, { capture: true });\n }, [panelOpen]);\n\n useEffect(() => {\n if (!panelOpen) return;\n function onPointerDown(e: PointerEvent) {\n if (panelRef.current && !panelRef.current.contains(e.target as Node)) {\n setPanelOpen(false);\n }\n }\n window.addEventListener(\"pointerdown\", onPointerDown);\n return () => window.removeEventListener(\"pointerdown\", onPointerDown);\n }, [panelOpen]);\n\n useEffect(() => {\n if (!panelOpen) return;\n function onKeyDown(e: KeyboardEvent) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((p) => Math.min(p + 1, items.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((p) => Math.max(p - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const item = items[activeIndex];\n if (item) { editor.focus(() => { item.onSelect(editor); setPanelOpen(false); }); }\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n setPanelOpen(false);\n }\n }\n window.addEventListener(\"keydown\", onKeyDown, { capture: true });\n return () => window.removeEventListener(\"keydown\", onKeyDown, { capture: true });\n }, [panelOpen, items, activeIndex, editor]);\n\n // ── Drag & Drop (가이드 라인 포함) ─────────────────────────────────────────\n\n useEffect(() => {\n if (!showDragHandle || !container) return;\n\n const unregDragover = editor.registerCommand(\n DRAGOVER_COMMAND,\n (event) => {\n if (!event.dataTransfer?.types.includes(DRAG_DATA_FORMAT)) return false;\n event.preventDefault();\n\n const draggedKey = event.dataTransfer.getData(DRAG_DATA_FORMAT);\n const target = event.target as HTMLElement | null;\n if (!target) return true;\n\n // 드래그 중인 element가 draggedNode의 DOM 안에 있으면 가이드라인 숨김\n if (draggedKey) {\n const draggedDOM = editor.getElementByKey(draggedKey);\n if (draggedDOM?.contains(target)) { setDropLine(null); return true; }\n }\n\n let targetKey: string | null = null;\n editor.read(() => {\n const node = $getNearestNodeFromDOMNode(target);\n if (!node) return;\n // 최상위 블록으로 올라감\n const top = node.isInline() ? node.getTopLevelElement() : node;\n if (top) targetKey = top.getKey();\n });\n if (!targetKey || targetKey === draggedKey) { setDropLine(null); return true; }\n\n const targetDOM = editor.getElementByKey(targetKey);\n if (!targetDOM) { setDropLine(null); return true; }\n\n const { top: domTop, height } = targetDOM.getBoundingClientRect();\n const isBefore = event.clientY < domTop + height / 2;\n const offsetTop = getOffsetTopToContainer(targetDOM, container);\n\n setDropLine({\n top: isBefore ? offsetTop : offsetTop + targetDOM.offsetHeight,\n position: isBefore ? \"before\" : \"after\",\n });\n\n return true;\n },\n COMMAND_PRIORITY_LOW,\n );\n\n const unregDrop = editor.registerCommand(\n DROP_COMMAND,\n (event) => {\n const draggedKey = event.dataTransfer?.getData(DRAG_DATA_FORMAT);\n if (!draggedKey) return false;\n event.preventDefault();\n setDropLine(null);\n\n const target = event.target as HTMLElement | null;\n if (!target) return true;\n\n // 실제로 노드가 이동되었는지 확인하기 위해 현재 에디터 상태에서 사전 검증\n let willMove = false;\n editor.read(() => {\n const draggedNode = $getNodeByKey(draggedKey);\n if (!draggedNode) return;\n const nearestNode = $getNearestNodeFromDOMNode(target);\n if (!nearestNode) return;\n const targetNode = nearestNode.isInline() ? nearestNode.getTopLevelElement() : nearestNode;\n if (!targetNode) return;\n const targetKey = targetNode.getKey();\n if (targetKey === draggedKey) return;\n if ($isElementNode(draggedNode) && draggedNode.getChildren().some(\n (child) => child.getKey() === targetKey,\n )) return;\n willMove = true;\n });\n\n editor.update(() => {\n const draggedNode = $getNodeByKey(draggedKey);\n if (!draggedNode) return;\n\n const nearestNode = $getNearestNodeFromDOMNode(target);\n if (!nearestNode) return;\n\n // 최상위 블록으로 정규화\n const targetNode = nearestNode.isInline()\n ? nearestNode.getTopLevelElement()\n : nearestNode;\n if (!targetNode) return;\n\n const targetKey = targetNode.getKey();\n // 자기 자신 또는 자기 자손으로 drop 방지\n if (targetKey === draggedKey) return;\n if ($isElementNode(draggedNode) && draggedNode.getChildren().some(\n (child) => child.getKey() === targetKey,\n )) return;\n\n const targetDOM = editor.getElementByKey(targetKey);\n if (!targetDOM) return;\n\n const { top: targetTop, height } = targetDOM.getBoundingClientRect();\n if (event.clientY < targetTop + height / 2) {\n targetNode.insertBefore(draggedNode);\n } else {\n targetNode.insertAfter(draggedNode);\n }\n });\n\n // Lexical의 drop 이벤트 처리(DOM selection 읽기)가 완료된 뒤에 onDropped 호출\n // requestAnimationFrame은 Lexical의 microtask 큐 처리 후에 실행됨\n if (willMove && onDropped) {\n requestAnimationFrame(() => onDropped(draggedKey));\n }\n return true;\n },\n COMMAND_PRIORITY_HIGH,\n );\n\n // 드래그가 에디터 밖으로 나가면 가이드 라인 제거\n function onDragEnd() { setDropLine(null); }\n document.addEventListener(\"dragend\", onDragEnd);\n\n return () => {\n unregDragover();\n unregDrop();\n document.removeEventListener(\"dragend\", onDragEnd);\n };\n }, [editor, showDragHandle, container, onDropped]);\n\n const handleDragStart = useCallback((e: React.DragEvent) => {\n if (!nodeKey) return;\n e.dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);\n e.dataTransfer.effectAllowed = \"move\";\n const blockEl = editor.getElementByKey(nodeKey);\n if (blockEl) e.dataTransfer.setDragImage(blockEl, 0, 0);\n onDragStarted?.(nodeKey);\n }, [nodeKey, editor, onDragStarted]);\n\n // ── + 버튼 클릭 ────────────────────────────────────────────────────────────\n\n const handleAddClick = useCallback((e: React.MouseEvent) => {\n e.preventDefault();\n setPanelOpen((p) => !p);\n }, []);\n\n // ── Render ─────────────────────────────────────────────────────────────────\n\n if (!container) return null;\n\n const dragBtnStyle: CSSProperties = {\n ...btnBase,\n width: BTN,\n height: BTN,\n cursor: \"grab\",\n ...(dragHovered ? btnHover : {}),\n };\n\n const addBtnStyle: CSSProperties = {\n ...btnBase,\n width: BTN,\n height: BTN,\n ...(panelOpen || addHovered ? btnHover : {}),\n };\n\n return (\n <>\n {/* 커서 인디케이터 — 현재 포커스 블록 왼쪽에 세로 라인 */}\n {createPortal(\n <AnimatePresence>\n {focusedTop !== null && (\n <motion.div\n key={focusedNodeKey ?? \"cursor-indicator\"}\n style={{\n position: \"absolute\",\n top: focusedTop,\n left: 0,\n width: 2,\n height: focusedHeight,\n background: \"#818cf8\", // indigo-400\n borderRadius: 1,\n zIndex: 2,\n pointerEvents: \"none\",\n }}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.1 }}\n />\n )}\n </AnimatePresence>,\n container!,\n )}\n\n {/* 버튼 그룹 — container 내부, absolute */}\n {createPortal(\n <AnimatePresence>\n {isVisible && top !== null && (\n <motion.div\n key={nodeKey ?? \"toolbar\"}\n data-block-toolbar\n style={{\n position: \"absolute\",\n top,\n left: GUTTER_LEFT,\n display: \"flex\",\n alignItems: \"center\",\n gap: GAP,\n width: numBtns * BTN + Math.max(0, numBtns - 1) * GAP,\n zIndex: 2,\n userSelect: \"none\",\n }}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.08, ease: \"easeOut\" }}\n >\n {showDragHandle && (\n <div\n draggable\n data-drag-handle\n onDragStart={handleDragStart}\n aria-label=\"Drag to reorder\"\n style={dragBtnStyle}\n onMouseEnter={() => setDragHovered(true)}\n onMouseLeave={() => setDragHovered(false)}\n >\n <GripVertical size={14} strokeWidth={2} />\n </div>\n )}\n {showAddButton && (\n <button\n ref={addBtnRef}\n type=\"button\"\n aria-label=\"Add block\"\n aria-expanded={panelOpen}\n style={addBtnStyle}\n onMouseDown={handleAddClick}\n onMouseEnter={() => setAddHovered(true)}\n onMouseLeave={() => setAddHovered(false)}\n >\n <Plus size={13} strokeWidth={2.5} />\n </button>\n )}\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n )}\n\n {/* 드래그 가이드 라인 — container 내부, absolute */}\n {createPortal(\n <AnimatePresence>\n {dropLine !== null && (\n <motion.div\n key=\"drop-line\"\n style={{\n position: \"absolute\",\n top: dropLine.top,\n // 텍스트 시작점(padding-left)에 맞춰 왼쪽 여백\n left: 0,\n right: 0,\n height: 2,\n zIndex: 3,\n pointerEvents: \"none\",\n // 가이드 라인: 파란 선 + 양쪽 원형 핸들\n display: \"flex\",\n alignItems: \"center\",\n // translateY로 선을 블록 경계에 정확히 정렬\n transform: \"translateY(-1px)\",\n }}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.06 }}\n >\n {/* 왼쪽 원 */}\n <div style={{\n width: 6,\n height: 6,\n borderRadius: \"50%\",\n background: \"#818cf8\", // indigo-400\n flexShrink: 0,\n marginLeft: 4,\n }} />\n {/* 가로선 */}\n <div style={{\n flex: 1,\n height: 2,\n background: \"#818cf8\",\n marginRight: 8,\n }} />\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n )}\n\n {/* 패널 — container 내부, absolute */}\n {createPortal(\n <AnimatePresence>\n {isVisible && panelOpen && top !== null && (\n <motion.div\n ref={panelRef}\n data-block-toolbar\n style={{\n position: \"absolute\",\n top: top + BTN + GAP,\n left: GUTTER_LEFT,\n zIndex: 50,\n width: 240,\n }}\n initial={{ opacity: 0, y: -4 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -4 }}\n transition={{ duration: 0.1, ease: \"easeOut\" }}\n role=\"dialog\"\n aria-label=\"Insert block\"\n className=\"rounded-lg bg-zinc-800 shadow-xl shadow-black/50 py-1.5\"\n >\n <div role=\"listbox\" aria-label=\"Block type\" className=\"w-full px-1.5 flex flex-col\">\n {items.map((item, index) => {\n const icon = ICON_MAP[item.id] ?? item.icon;\n const isActive = index === activeIndex;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n onMouseDown={(e) => {\n e.preventDefault();\n editor.focus(() => { item.onSelect(editor); setPanelOpen(false); });\n }}\n onMouseEnter={() => setActiveIndex(index)}\n className={[\n \"w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors duration-75\",\n \"outline-none\",\n isActive\n ? \"bg-zinc-700/60 text-zinc-100\"\n : \"text-zinc-400 hover:bg-zinc-700/40 hover:text-zinc-200\",\n ].join(\" \")}\n >\n <span className=\"flex items-center justify-center shrink-0 w-4 text-current\">\n {icon}\n </span>\n <span className=\"text-sm font-normal leading-none\">{item.label}</span>\n </button>\n );\n })}\n </div>\n </motion.div>\n )}\n </AnimatePresence>,\n container,\n )}\n </>\n );\n}\n","\"use client\";\n\nimport type { LexicalEditor } from \"lexical\";\nimport type { SlashMenuItem } from \"@jikjo/core\";\nimport { Heading1, Heading2, Heading3, Quote, Text } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface SlashMenuProps {\n isVisible: boolean;\n query: string;\n items: SlashMenuItem[];\n editor: LexicalEditor;\n onClose: () => void;\n}\n\n// ─── Icon map ─────────────────────────────────────────────────────────────────\n\nconst ICON_MAP: Record<string, ReactNode> = {\n paragraph: <Text size={14} strokeWidth={1.75} />,\n heading1: <Heading1 size={14} strokeWidth={1.75} />,\n heading2: <Heading2 size={14} strokeWidth={1.75} />,\n heading3: <Heading3 size={14} strokeWidth={1.75} />,\n quote: <Quote size={14} strokeWidth={1.75} />,\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\ninterface AbsolutePos {\n top: number;\n left: number;\n}\n\n/**\n * 캐럿(collapsed range)의 bottom 위치를 컨테이너 기준 absolute 좌표로 변환.\n */\nfunction getCaretAbsPos(container: HTMLElement): AbsolutePos | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n\n const range = sel.getRangeAt(0).cloneRange();\n range.collapse(true);\n const caretRect = range.getBoundingClientRect();\n\n // getBoundingClientRect()가 (0,0,0,0)을 반환하는 경우 방어\n if (caretRect.width === 0 && caretRect.height === 0 && caretRect.top === 0) return null;\n\n const cr = container.getBoundingClientRect();\n const GAP = 6;\n\n return {\n top: caretRect.bottom - cr.top + container.scrollTop + GAP,\n left: caretRect.left - cr.left + container.scrollLeft,\n };\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport function SlashMenu({\n isVisible,\n query,\n items,\n editor,\n onClose,\n}: SlashMenuProps) {\n const [activeIndex, setActiveIndex] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);\n const [pos, setPos] = useState<AbsolutePos | null>(null);\n\n // editor.getRootElement()가 준비된 후 portalContainer를 설정\n useEffect(() => {\n function attachContainer() {\n const rootEl = editor.getRootElement();\n if (!rootEl) return;\n const parent = rootEl.parentElement;\n if (parent) setPortalContainer(parent);\n }\n attachContainer();\n return editor.registerRootListener(attachContainer);\n }, [editor]);\n\n const filtered = items.filter(\n (item) =>\n !query ||\n item.label.toLowerCase().includes(query.toLowerCase()) ||\n item.description.toLowerCase().includes(query.toLowerCase()),\n );\n\n // isVisible 또는 query 변경 시 좌표 재계산 (캐럿이 이동/입력될 때마다)\n useEffect(() => {\n if (!isVisible || !portalContainer) {\n setPos(null);\n return;\n }\n // registerUpdateListener가 발동된 직후 호출되므로 DOM이 확정된 시점\n setPos(getCaretAbsPos(portalContainer));\n }, [isVisible, query, portalContainer]);\n\n useEffect(() => {\n setActiveIndex(0);\n }, [query]);\n\n useEffect(() => {\n if (!isVisible) return;\n function handleKeyDown(event: KeyboardEvent) {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((prev) => Math.max(prev - 1, 0));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const item = filtered[activeIndex];\n if (!item) return;\n editor.focus(() => {\n item.onSelect(editor);\n onClose();\n });\n } else if (event.key === \"Escape\") {\n onClose();\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [isVisible, filtered, activeIndex, editor, onClose]);\n\n useEffect(() => {\n const el = containerRef.current?.querySelector<HTMLElement>(\n '[data-active=\"true\"]',\n );\n el?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n if (!portalContainer) return null;\n\n const menuStyle = pos\n ? { position: \"absolute\" as const, top: pos.top, left: pos.left, zIndex: 50, width: 240 }\n : { position: \"absolute\" as const, top: -9999, left: -9999, zIndex: 50, width: 240 };\n\n return createPortal(\n <AnimatePresence>\n {isVisible && (\n <motion.div\n style={menuStyle}\n initial={{ opacity: 0, y: -4 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -4 }}\n transition={{ duration: 0.1, ease: \"easeOut\" }}\n role=\"dialog\"\n aria-label=\"Insert block\"\n className=\"rounded-lg bg-zinc-800 shadow-xl shadow-black/50 py-1.5 overflow-hidden\"\n >\n {filtered.length === 0 ? (\n <p className=\"px-4 py-2 text-xs text-zinc-500\">\n No results for “{query}”\n </p>\n ) : (\n <div\n ref={containerRef}\n role=\"listbox\"\n aria-label=\"Block type\"\n className=\"w-full max-h-72 overflow-y-auto px-1.5 flex flex-col\"\n >\n {filtered.map((item, index) => {\n const icon = ICON_MAP[item.id] ?? item.icon;\n const isActive = index === activeIndex;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n data-active={isActive}\n onMouseDown={(e) => {\n e.preventDefault();\n editor.focus(() => {\n item.onSelect(editor);\n onClose();\n });\n }}\n onMouseEnter={() => setActiveIndex(index)}\n className={[\n \"w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors duration-75\",\n \"outline-none focus-visible:outline-none\",\n isActive\n ? \"bg-zinc-700/80 text-zinc-100\"\n : \"text-zinc-400 hover:bg-zinc-700/60 hover:text-zinc-200 focus-visible:bg-zinc-700/80 focus-visible:text-zinc-100\",\n ].join(\" \")}\n >\n <span className=\"flex items-center justify-center shrink-0 w-4 text-current\">\n {icon}\n </span>\n <span className=\"text-sm font-normal leading-none\">\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n )}\n </motion.div>\n )}\n </AnimatePresence>,\n portalContainer,\n );\n}\n","\"use client\";\n\nimport {\n Editor,\n historyExtension,\n richTextExtension,\n useBlockHoverPlugin,\n useSelectionPlugin,\n useSlashCommandPlugin,\n} from \"@jikjo/core\";\nimport type { Extension, SlashMenuItem } from \"@jikjo/core\";\nimport {\n $createHeadingNode,\n $isHeadingNode,\n type HeadingTagType,\n} from \"@lexical/rich-text\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport {\n $createParagraphNode,\n $getNearestNodeFromDOMNode,\n $getSelection,\n $isRangeSelection,\n} from \"lexical\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Heading3,\n Italic,\n Strikethrough,\n Underline,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { MouseEvent } from \"react\";\nimport { BubbleMenu } from \"./components/bubble-menu\";\nimport { BlockToolbar } from \"./components/block-toolbar\";\nimport { SlashMenu } from \"./components/slash-menu\";\n\n// ─── Styles ───────────────────────────────────────────────────────────────────\n\nconst tbtnBase: CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 32,\n height: 32,\n borderRadius: 6,\n border: \"none\",\n background: \"transparent\",\n color: \"#71717a\",\n cursor: \"pointer\",\n padding: 0,\n transition: \"background 100ms, color 100ms\",\n flexShrink: 0,\n};\n\nconst tbtnHover: CSSProperties = {\n background: \"rgba(39,39,42,0.9)\",\n color: \"#e4e4e7\",\n};\n\nconst tbtnActive: CSSProperties = {\n background: \"rgba(63,63,70,0.8)\",\n color: \"#f4f4f5\",\n};\n\n// ─── ToolbarButton ─────────────────────────────────────────────────────────────\n\ninterface ToolbarButtonProps {\n label: string;\n isActive?: boolean;\n onMouseDown: (e: MouseEvent) => void;\n children: ReactNode;\n}\n\nfunction ToolbarButton({ label, isActive, onMouseDown, children }: ToolbarButtonProps) {\n const [hovered, setHovered] = useState(false);\n\n const style: CSSProperties = {\n ...tbtnBase,\n ...(hovered ? tbtnHover : {}),\n ...(isActive ? tbtnActive : {}),\n };\n\n return (\n <button\n type=\"button\"\n aria-label={label}\n onMouseDown={onMouseDown}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n style={style}\n >\n {children}\n </button>\n );\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface EditorUIProps {\n extensions?: Extension[];\n namespace?: string;\n /**\n * undefined → default toolbar\n * false → no toolbar rendered at all\n * ReactNode → custom toolbar content\n */\n toolbarContent?: ReactNode | false;\n className?: string;\n /**\n * 활성화할 UI 기능 목록. 기본값: 모두 활성화.\n * \"blockHandle\" | \"inlineAdd\" | \"slashCommand\" | \"bubbleMenu\"\n */\n features?: Array<\"blockHandle\" | \"inlineAdd\" | \"slashCommand\" | \"bubbleMenu\">;\n}\n\n// ─── Default extensions ───────────────────────────────────────────────────────\n\nconst defaultExtensions: Extension[] = [richTextExtension, historyExtension];\n\n// ─── Inner component ──────────────────────────────────────────────────────────\n// Must live inside LexicalComposer so all hooks have editor context.\n\nconst ALL_FEATURES = [\"blockHandle\", \"inlineAdd\", \"slashCommand\", \"bubbleMenu\"] as const;\n\ninterface EditorInnerProps {\n extensions: Extension[];\n toolbarContent: ReactNode | false | undefined;\n features: Array<\"blockHandle\" | \"inlineAdd\" | \"slashCommand\" | \"bubbleMenu\">;\n}\n\nfunction EditorInner({\n extensions,\n toolbarContent,\n features,\n}: EditorInnerProps) {\n const [editor] = useLexicalComposerContext();\n\n const selection = useSelectionPlugin();\n const slashCommand = useSlashCommandPlugin();\n const blockHover = useBlockHoverPlugin();\n\n // heading 태그는 Lexical selection으로 추적 (toolbar 표시용)\n const [currentHeadingTag, setCurrentHeadingTag] = useState<HeadingTagType | null>(null);\n\n // focusedNodeKey: Lexical selection에서 완전히 독립.\n // - 에디터 click → DOM에서 직접 nodeKey 읽기\n // - dragstart → onDragStarted 콜백으로 nodeKey 전달\n // - drop 성공 → onDropped 콜백으로 이동된 nodeKey 전달\n // - dragend(취소) → null\n // - 에디터 바깥 클릭 → null\n const [focusedNodeKey, setFocusedNodeKey] = useState<string | null>(null);\n\n // drop 성공 플래그 (dragend 시 취소인지 성공인지 구분용)\n const dropSucceededRef = useRef(false);\n\n // ── heading tag 추적 (selection 기반, toolbar 전용) ──────────────────────\n\n useEffect(() => {\n return editor.registerUpdateListener(({ editorState }) => {\n editorState.read(() => {\n const sel = $getSelection();\n if (!$isRangeSelection(sel)) {\n setCurrentHeadingTag(null);\n return;\n }\n const topNode = sel.anchor.getNode().getTopLevelElement();\n setCurrentHeadingTag($isHeadingNode(topNode) ? topNode.getTag() : null);\n });\n });\n }, [editor]);\n\n // ── focusedNodeKey: 에디터 click 이벤트에서 DOM → nodeKey 직접 읽기 ──────\n\n useEffect(() => {\n return editor.registerRootListener((rootElement) => {\n if (!rootElement) return;\n const root = rootElement;\n\n function onClick(e: Event) {\n const target = e.target as HTMLElement | null;\n if (!target) return;\n let key: string | null = null;\n editor.read(() => {\n const node = $getNearestNodeFromDOMNode(target);\n if (!node) return;\n const top = node.isInline() ? node.getTopLevelElement() : node;\n if (top) key = top.getKey();\n });\n if (key) setFocusedNodeKey(key);\n }\n\n // 에디터/툴바 바깥 클릭 시 커서 인디케이터 제거\n function onDocClick(e: Event) {\n const target = e.target as HTMLElement | null;\n if (root.contains(target)) return;\n if (target?.closest(\"[data-block-toolbar]\") || target?.closest(\"[data-drag-handle]\")) return;\n setFocusedNodeKey(null);\n }\n\n root.addEventListener(\"click\", onClick);\n document.addEventListener(\"click\", onDocClick, true);\n\n return () => {\n root.removeEventListener(\"click\", onClick);\n document.removeEventListener(\"click\", onDocClick, true);\n };\n });\n }, [editor]);\n\n // ── drag/drop 이벤트에서 focusedNodeKey 관리 ────────────────────────────\n // Firefox: drag handle mousedown → blur → dragstart 순서로 발생.\n // blur 시 Lexical이 $setSelection(null)을 호출하지만,\n // focusedNodeKey는 Lexical selection과 독립이므로 영향받지 않음.\n // drop 성공/취소는 onDropped / dragend 이벤트로 구분.\n\n const handleDragStarted = useCallback((key: string) => {\n dropSucceededRef.current = false;\n setFocusedNodeKey(key);\n // Firefox blur로 에디터 포커스가 빠진 경우 복구\n editor.focus();\n }, [editor]);\n\n const handleDropped = useCallback((key: string) => {\n dropSucceededRef.current = true;\n setFocusedNodeKey(key);\n // drop 후 에디터 포커스 복구\n editor.focus();\n }, [editor]);\n\n useEffect(() => {\n function onDragEnd() {\n if (!dropSucceededRef.current) {\n // drag 취소: 커서 인디케이터 제거\n setFocusedNodeKey(null);\n }\n dropSucceededRef.current = false;\n }\n document.addEventListener(\"dragend\", onDragEnd);\n return () => document.removeEventListener(\"dragend\", onDragEnd);\n }, []);\n\n // Collect slash menu items from all registered extensions\n const slashMenuItems = useMemo<SlashMenuItem[]>(\n () => extensions.flatMap((ext) => ext.slashMenuItems ?? []),\n [extensions],\n );\n\n // Toggle heading: if already at this level, revert to paragraph.\n // newNode.select() preserves selection after replace to avoid Lexical error.\n const toggleHeading = useCallback(\n (tag: HeadingTagType) => {\n editor.update(() => {\n const sel = $getSelection();\n if (!$isRangeSelection(sel)) return;\n const topNode = sel.anchor.getNode().getTopLevelElementOrThrow();\n const newNode = ($isHeadingNode(topNode) && topNode.getTag() === tag)\n ? $createParagraphNode()\n : $createHeadingNode(tag);\n topNode.replace(newNode);\n newNode.select();\n });\n },\n [editor],\n );\n\n return (\n <>\n {/* ── Toolbar ──────────────────────────────────────────────────── */}\n {toolbarContent === false ? null : toolbarContent !== undefined ? (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2, padding: \"8px 16px\", borderBottom: \"1px solid rgba(39,39,42,0.5)\" }}>\n {toolbarContent}\n </div>\n ) : (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2, padding: \"8px 16px\", borderBottom: \"1px solid rgba(39,39,42,0.5)\" }}>\n {/* Format group */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2 }}>\n <ToolbarButton\n label=\"Bold\"\n isActive={selection.format.bold}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"bold\"); }}\n >\n <Bold size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Italic\"\n isActive={selection.format.italic}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"italic\"); }}\n >\n <Italic size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Underline\"\n isActive={selection.format.underline}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"underline\"); }}\n >\n <Underline size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Strikethrough\"\n isActive={selection.format.strikethrough}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"strikethrough\"); }}\n >\n <Strikethrough size={14} strokeWidth={2.5} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Code\"\n isActive={selection.format.code}\n onMouseDown={(e) => { e.preventDefault(); selection.toggleFormat(\"code\"); }}\n >\n <Code size={14} strokeWidth={2.5} />\n </ToolbarButton>\n </div>\n\n <div style={{ width: 1, height: 16, background: \"#27272a\", margin: \"0 4px\" }} />\n\n {/* Heading group */}\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 2 }}>\n <ToolbarButton\n label=\"Heading 1\"\n isActive={currentHeadingTag === \"h1\"}\n onMouseDown={(e) => { e.preventDefault(); toggleHeading(\"h1\"); }}\n >\n <Heading1 size={15} strokeWidth={2} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 2\"\n isActive={currentHeadingTag === \"h2\"}\n onMouseDown={(e) => { e.preventDefault(); toggleHeading(\"h2\"); }}\n >\n <Heading2 size={15} strokeWidth={2} />\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 3\"\n isActive={currentHeadingTag === \"h3\"}\n onMouseDown={(e) => { e.preventDefault(); toggleHeading(\"h3\"); }}\n >\n <Heading3 size={15} strokeWidth={2} />\n </ToolbarButton>\n </div>\n </div>\n )}\n\n {/* ── Floating overlays ────────────────────────────────────────── */}\n {features.includes(\"bubbleMenu\") && (\n <BubbleMenu\n isVisible={selection.isActive}\n format={selection.format}\n onToggleFormat={selection.toggleFormat}\n editor={editor}\n />\n )}\n\n {features.includes(\"slashCommand\") && (\n <SlashMenu\n isVisible={slashCommand.isActive}\n query={slashCommand.query}\n items={slashMenuItems}\n editor={editor}\n onClose={slashCommand.close}\n />\n )}\n\n {/* drag handle + + 버튼 + 커서 인디케이터 */}\n <BlockToolbar\n isVisible={blockHover.isActive && (features.includes(\"blockHandle\") || features.includes(\"inlineAdd\"))}\n nodeKey={blockHover.nodeKey}\n focusedNodeKey={focusedNodeKey}\n items={slashMenuItems}\n editor={editor}\n showDragHandle={features.includes(\"blockHandle\")}\n showAddButton={features.includes(\"inlineAdd\")}\n onDragStarted={handleDragStarted}\n onDropped={handleDropped}\n />\n </>\n );\n}\n\n// ─── Public component ─────────────────────────────────────────────────────────\n\nexport function EditorUI({\n extensions = defaultExtensions,\n namespace = \"jikjo\",\n toolbarContent,\n className,\n features = [...ALL_FEATURES],\n}: EditorUIProps) {\n const hasBlockToolbar = features.includes(\"blockHandle\") || features.includes(\"inlineAdd\");\n return (\n <div\n className={className}\n {...(hasBlockToolbar ? { \"data-has-block-toolbar\": \"\" } : {})}\n // block toolbar가 있을 때만 좌측 74px padding을 주입\n // (GUTTER_LEFT 6 + 2×BTN 48 + GAP 4 = 58px toolbar + 16px text padding)\n style={hasBlockToolbar ? ({\n \"--jikjo-content-pl\": \"74px\",\n \"--jikjo-placeholder-pl\": \"74px\",\n } as React.CSSProperties) : ({\n \"--jikjo-content-pl\": \"16px\",\n \"--jikjo-placeholder-pl\": \"16px\",\n } as React.CSSProperties)}\n >\n <Editor extensions={extensions} namespace={namespace}>\n <EditorInner\n extensions={extensions}\n toolbarContent={toolbarContent}\n features={features}\n />\n </Editor>\n </div>\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jikjo/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Styled editor UI components for jikjo",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"lucide-react": "^0.575.0",
|
|
34
34
|
"tailwind-merge": "^3.5.0",
|
|
35
35
|
"tailwind-variants": "^3.2.2",
|
|
36
|
-
"@jikjo/core": "0.
|
|
36
|
+
"@jikjo/core": "0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@base-ui/react": "^1.0.0",
|