@teriusu/rich-editor 0.7.12 → 0.8.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.
@@ -20,6 +20,7 @@
20
20
  Columns3,
21
21
  Type,
22
22
  Tv,
23
+ SquareDashed,
23
24
  } from "lucide-svelte";
24
25
  import type { SlashMenuItem, ToolbarFeature, PromptHandler } from "../types";
25
26
  import type { Component } from "svelte";
@@ -36,6 +37,7 @@
36
37
  h2: "기본",
37
38
  h3: "기본",
38
39
  mbus: "미디어",
40
+ card: "블록",
39
41
  "bullet-list": "리스트",
40
42
  "ordered-list": "리스트",
41
43
  checklist: "리스트",
@@ -184,6 +186,15 @@
184
186
  if (url) editor.chain().focus().setMbusVideo({ src: url }).run();
185
187
  },
186
188
  },
189
+ {
190
+ feature: "card",
191
+ label: "카드",
192
+ keywords: "card 카드 상자 박스 강조 배경",
193
+ icon: SquareDashed,
194
+ command: (editor) => {
195
+ editor.chain().focus().setCard().run();
196
+ },
197
+ },
187
198
  {
188
199
  feature: "link",
189
200
  label: "링크",
@@ -1 +1 @@
1
- {"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AAiWJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAuB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AA2WJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -20,7 +20,7 @@
20
20
  import SuperscriptExt from "@tiptap/extension-superscript";
21
21
  import Typography from "@tiptap/extension-typography";
22
22
  import CharacterCount from "@tiptap/extension-character-count";
23
- import { Table } from "@tiptap/extension-table";
23
+ import { Table, TableView } from "@tiptap/extension-table";
24
24
  import { TableRow } from "@tiptap/extension-table-row";
25
25
  import { TableHeader } from "@tiptap/extension-table-header";
26
26
  import { TableCell } from "@tiptap/extension-table-cell";
@@ -36,6 +36,7 @@
36
36
  import { Indent } from "../extensions/Indent";
37
37
  import { FileAttachment } from "../extensions/FileAttachment";
38
38
  import { MbusVideo } from "../extensions/MbusVideo";
39
+ import { CardBlock } from "../extensions/CardBlock";
39
40
  import FixedToolbar from "./FixedToolbar.svelte";
40
41
  import BubbleToolbar from "./BubbleToolbar.svelte";
41
42
  import SlashCommandMenu from "./SlashCommandMenu.svelte";
@@ -116,6 +117,7 @@
116
117
  onPromptLink,
117
118
  onPromptImage,
118
119
  onPromptMbus,
120
+ onPromptCardBackground,
119
121
  extensions: extraExtensions = [],
120
122
  editable = true,
121
123
  toolbar = 'full',
@@ -130,6 +132,8 @@
130
132
  onPromptLink?: PromptHandler;
131
133
  onPromptImage?: PromptHandler;
132
134
  onPromptMbus?: PromptHandler;
135
+ /** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
136
+ onPromptCardBackground?: PromptHandler;
133
137
  extensions?: AnyExtension[];
134
138
  editable?: boolean;
135
139
  toolbar?: ToolbarMode;
@@ -312,6 +316,20 @@
312
316
  onMount(() => {
313
317
  if (!editorElement) return;
314
318
 
319
+ // 읽기(비편집) 모드에서도 표 가로 스크롤 래퍼(.tableWrapper)를 만든다.
320
+ // 편집 모드는 columnResizing 플러그인의 TableView가 래퍼를 생성하지만,
321
+ // 비편집 에디터에는 그 플러그인이 없어 표가 맨 <table>로 렌더돼 카드를 넘친다.
322
+ // 비편집 인스턴스에만 addNodeView로 동일한 TableView를 달아 래퍼를 생성한다.
323
+ // (편집 인스턴스에 달면 columnResizing의 plugin nodeView를 섀도잉해 리사이즈가 깨진다.)
324
+ const TableExt = editable
325
+ ? Table
326
+ : Table.extend({
327
+ addNodeView() {
328
+ return ({ node }) =>
329
+ new TableView(node, this.options.cellMinWidth);
330
+ },
331
+ });
332
+
315
333
  editor = new Editor({
316
334
  element: editorElement,
317
335
  extensions: [
@@ -356,7 +374,7 @@
356
374
  SuperscriptExt,
357
375
  Typography,
358
376
  CharacterCount,
359
- Table.configure({ resizable: true, allowTableNodeSelection: true }),
377
+ TableExt.configure({ resizable: true, allowTableNodeSelection: true }),
360
378
  TableRow,
361
379
  CustomTableHeader,
362
380
  CustomTableCell,
@@ -366,6 +384,7 @@
366
384
  ...(fileDownloadBaseUrl ? { downloadBaseUrl: fileDownloadBaseUrl } : {}),
367
385
  }),
368
386
  MbusVideo,
387
+ CardBlock.configure({ promptBackground: onPromptCardBackground ?? null }),
369
388
  Columns,
370
389
  Column,
371
390
  CodeBlockTopEscape,
@@ -447,6 +466,9 @@
447
466
  : []),
448
467
  ],
449
468
  content: transformLegacyHtml(content),
469
+ // 저장된 HTML의 블록 선두 일반 공백(U+0020)이 재파싱 시 collapse되지 않도록 보존.
470
+ // true = 공백 보존, 블록 사이 개행은 여전히 collapse (선두 공백 버그 표적 수정).
471
+ parseOptions: { preserveWhitespace: true },
450
472
  editable,
451
473
  onUpdate: ({ editor: e }) => {
452
474
  // 빈 paragraph는 <p></p>로 저장 (ProseMirror가 편집기 DOM에 넣는 trailing <br>는 출력에서 제거).
@@ -515,7 +537,11 @@
515
537
  // 에디터 자체 onChange에서 나온 값이면 무시 (무한 루프 방지)
516
538
  if (content === lastEmittedHtml) return;
517
539
  const transformed = transformLegacyHtml(content);
518
- editor.commands.setContent(transformed, { emitUpdate: false });
540
+ editor.commands.setContent(transformed, {
541
+ emitUpdate: false,
542
+ // 외부 sync 경로에서도 블록 선두 공백 보존 (편집 재로드 경로와 동일하게)
543
+ parseOptions: { preserveWhitespace: true }
544
+ });
519
545
  lastEmittedHtml = content;
520
546
  editor.commands.fixTables();
521
547
  });
@@ -11,6 +11,8 @@ type $$ComponentProps = {
11
11
  onPromptLink?: PromptHandler;
12
12
  onPromptImage?: PromptHandler;
13
13
  onPromptMbus?: PromptHandler;
14
+ /** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
15
+ onPromptCardBackground?: PromptHandler;
14
16
  extensions?: AnyExtension[];
15
17
  editable?: boolean;
16
18
  toolbar?: ToolbarMode;
@@ -1 +1 @@
1
- {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AAmkBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAc5D,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AA2lBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { Node } from "@tiptap/core";
2
+ /** 배경 문자열을 받아 새 값(취소면 null)을 돌려주는 호스트 훅. */
3
+ export type CardBackgroundPrompt = (current: string) => Promise<string | null>;
4
+ export interface CardBlockOptions {
5
+ HTMLAttributes: Record<string, unknown>;
6
+ promptBackground: CardBackgroundPrompt | null;
7
+ }
8
+ export declare const CardBlock: Node<CardBlockOptions, any>;
9
+ declare module "@tiptap/core" {
10
+ interface Commands<ReturnType> {
11
+ card: {
12
+ setCard: (attrs?: {
13
+ title?: string;
14
+ background?: string;
15
+ height?: string;
16
+ }) => ReturnType;
17
+ };
18
+ }
19
+ }
20
+ //# sourceMappingURL=CardBlock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CardBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/CardBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAiCrD,4CAA4C;AAC5C,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAE/E,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,gBAAgB,EAAE,oBAAoB,GAAG,IAAI,CAAC;CAC9C;AAiBD,eAAO,MAAM,SAAS,6BAwNpB,CAAC;AAEH,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,IAAI,EAAE;YACL,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE;gBACjB,KAAK,CAAC,EAAE,MAAM,CAAC;gBACf,UAAU,CAAC,EAAE,MAAM,CAAC;gBACpB,MAAM,CAAC,EAAE,MAAM,CAAC;aAChB,KAAK,UAAU,CAAC;SACjB,CAAC;KACF;CACD"}
@@ -0,0 +1,236 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { attachResize } from "../utils/resize";
3
+ /**
4
+ * 카드 블록 — 본문 중간에 넣는 **배경 있는 상자**.
5
+ *
6
+ * 제목 한 줄 + 자유 내용, 배경은 그라디언트·단색·이미지 어느 쪽이든 CSS `background` 문자열로
7
+ * 받는다. 원래 정올(`apps/jungol/src/components/ui/tiptap/card/`)에만 있던 것을 패키지로 올렸다.
8
+ *
9
+ * ## 이식하면서 달라진 것
10
+ *
11
+ * - **NodeView 를 vanilla JS 로 다시 썼다.** 원본은 `SvelteNodeViewRenderer` 였는데, 이 패키지의
12
+ * 확장은 전부 vanilla 다(`.claude/rules/rich-editor.md`). Svelte 런타임을 노드뷰에 끌어들이면
13
+ * 호스트 앱과 Svelte 사본이 갈릴 때 조용히 깨진다.
14
+ * - **배경 고르기 UI 를 패키지가 갖지 않는다.** 원본 피커는 Unsplash 검색·파일 업로드·스낵바까지
15
+ * 앱에 묶여 있었다. 대신 `onPromptLink` 와 같은 **핸들러 규약**으로 뺐다 —
16
+ * 호스트가 `editor.storage.card.promptBackground` 를 채우면 그걸 부르고, 없으면 `window.prompt`.
17
+ * - **높이 조절은 공용 `attachResize`** 를 쓴다. 원본은 seorii 패키지의 `MediaResize` 가
18
+ * `data-resize-*` 플래그를 보고 붙여 주던 것이라, 그 패키지를 떠나면 같이 사라졌다.
19
+ *
20
+ * ## 저장 형식
21
+ *
22
+ * 새로 저장하는 것은 `div[data-type="card"]` 다. 옛 정올 문서의 `<tiptap-card>` 는
23
+ * `parseHTML` 이 직접 받고, 서버에 남은 문자열은 `transformLegacyHtml` 이 바꾼다
24
+ * (file·collapsable·midibus 와 같은 경로).
25
+ */
26
+ const DEFAULT_BACKGROUND = "linear-gradient(135deg, #f8fafc 0%, #e0f2fe 55%, #fef3c7 100%)";
27
+ const MIN_HEIGHT = 120;
28
+ const MAX_HEIGHT = 640;
29
+ const DEFAULT_HEIGHT = 190;
30
+ const clampHeight = (raw) => {
31
+ const n = Number.parseFloat(String(raw ?? ""));
32
+ if (!Number.isFinite(n))
33
+ return DEFAULT_HEIGHT;
34
+ return Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, Math.round(n)));
35
+ };
36
+ /**
37
+ * 배경 값은 그대로 `style.background` 에 들어간다. 사용자가 넣은 문자열이 그 선언을 닫고
38
+ * 다른 선언을 여는 것을 막으려면 `;` 를 지워야 한다(원본 정올 코드도 같은 방어를 한다).
39
+ */
40
+ const safeBackground = (raw) => {
41
+ const value = raw.trim().replace(/;/g, "");
42
+ return value || DEFAULT_BACKGROUND;
43
+ };
44
+ export const CardBlock = Node.create({
45
+ name: "card",
46
+ group: "block",
47
+ content: "block*",
48
+ draggable: true,
49
+ addOptions() {
50
+ return {
51
+ HTMLAttributes: {},
52
+ promptBackground: null
53
+ };
54
+ },
55
+ addStorage() {
56
+ return {
57
+ promptBackground: this.options.promptBackground
58
+ };
59
+ },
60
+ addAttributes() {
61
+ return {
62
+ title: {
63
+ default: "",
64
+ parseHTML: (el) => el.getAttribute("data-card-title") ?? el.getAttribute("title") ?? "",
65
+ renderHTML: (attrs) => attrs.title ? { "data-card-title": attrs.title } : {}
66
+ },
67
+ background: {
68
+ default: "",
69
+ parseHTML: (el) => el.getAttribute("data-card-background") ?? el.getAttribute("background") ?? "",
70
+ renderHTML: (attrs) => attrs.background
71
+ ? { "data-card-background": attrs.background }
72
+ : {}
73
+ },
74
+ height: {
75
+ default: String(DEFAULT_HEIGHT),
76
+ parseHTML: (el) => el.getAttribute("data-card-height") ?? el.getAttribute("height") ?? String(DEFAULT_HEIGHT),
77
+ renderHTML: (attrs) => ({ "data-card-height": String(attrs.height ?? DEFAULT_HEIGHT) })
78
+ }
79
+ };
80
+ },
81
+ parseHTML() {
82
+ return [
83
+ { tag: 'div[data-type="card"]' },
84
+ // 옛 정올 스키마. `transformLegacyHtml` 이 못 잡고 지나간 것도 여기서 받는다.
85
+ { tag: "tiptap-card" }
86
+ ];
87
+ },
88
+ renderHTML({ HTMLAttributes }) {
89
+ return [
90
+ "div",
91
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
92
+ "data-type": "card"
93
+ }),
94
+ 0
95
+ ];
96
+ },
97
+ addNodeView() {
98
+ return ({ node, editor, getPos }) => {
99
+ let currentNode = node;
100
+ let detachResize = null;
101
+ const dom = document.createElement("div");
102
+ dom.setAttribute("data-type", "card");
103
+ dom.setAttribute("data-node-view-wrapper", "");
104
+ dom.className = "hce-card";
105
+ dom.style.position = "relative";
106
+ const frame = document.createElement("section");
107
+ frame.className = "hce-card-frame";
108
+ frame.setAttribute("data-card-frame", "");
109
+ dom.appendChild(frame);
110
+ const content = document.createElement("div");
111
+ content.className = "hce-card-content";
112
+ frame.appendChild(content);
113
+ const header = document.createElement("header");
114
+ header.className = "hce-card-header";
115
+ content.appendChild(header);
116
+ const body = document.createElement("div");
117
+ body.className = "hce-card-body";
118
+ content.appendChild(body);
119
+ /** 편집 모드에서만 만드는 제목 입력칸. 읽기 모드에서는 `<h2>`. */
120
+ let titleInput = null;
121
+ let titleHeading = null;
122
+ const applyAttrs = (n) => {
123
+ frame.style.height = `${clampHeight(n.attrs.height)}px`;
124
+ frame.style.background = safeBackground(String(n.attrs.background ?? ""));
125
+ const title = String(n.attrs.title ?? "");
126
+ if (titleInput) {
127
+ if (titleInput.value !== title)
128
+ titleInput.value = title;
129
+ }
130
+ else if (titleHeading) {
131
+ titleHeading.textContent = title;
132
+ titleHeading.hidden = !title;
133
+ }
134
+ };
135
+ const setAttr = (patch) => {
136
+ const pos = getPos();
137
+ if (pos == null)
138
+ return;
139
+ editor.view.dispatch(editor.view.state.tr.setNodeMarkup(pos, undefined, {
140
+ ...currentNode.attrs,
141
+ ...patch
142
+ }));
143
+ };
144
+ if (editor.isEditable) {
145
+ titleInput = document.createElement("input");
146
+ titleInput.type = "text";
147
+ titleInput.className = "hce-card-title-input";
148
+ titleInput.placeholder = "카드 제목";
149
+ titleInput.contentEditable = "false";
150
+ // ProseMirror 가 입력칸의 키/포인터를 가져가지 않도록 막는다.
151
+ titleInput.setAttribute("data-card-control", "");
152
+ titleInput.addEventListener("input", () => {
153
+ setAttr({ title: titleInput?.value ?? "" });
154
+ });
155
+ header.appendChild(titleInput);
156
+ const pick = document.createElement("button");
157
+ pick.type = "button";
158
+ pick.className = "hce-card-bg-button";
159
+ pick.setAttribute("data-card-control", "");
160
+ pick.contentEditable = "false";
161
+ pick.textContent = "배경 변경";
162
+ pick.addEventListener("click", async (e) => {
163
+ e.preventDefault();
164
+ e.stopPropagation();
165
+ // 이 패키지는 `Storage` 타입 증강을 하지 않아 인덱스 접근이 막힌다
166
+ // (`editor.storage.fileAttachment` 도 같은 이유로 이미 에러가 나 있다).
167
+ // 여기서만 unknown 을 거쳐 내려간다 — 새 타입 에러를 만들지 않기 위해서다.
168
+ const storage = editor.storage;
169
+ const prompt = storage.card
170
+ ?.promptBackground;
171
+ const current = String(currentNode.attrs.background ?? "");
172
+ const next = prompt
173
+ ? await prompt(current)
174
+ : window.prompt("카드 배경 (CSS background 값)", current);
175
+ if (next == null)
176
+ return;
177
+ setAttr({ background: next });
178
+ });
179
+ frame.appendChild(pick);
180
+ detachResize = attachResize({
181
+ dom: frame,
182
+ editor,
183
+ getPos,
184
+ getNode: () => currentNode,
185
+ axis: "y",
186
+ attr: "height",
187
+ min: MIN_HEIGHT,
188
+ max: MAX_HEIGHT,
189
+ label: "카드 높이 조절",
190
+ // 원본과 같은 저장 형식 — 단위 없는 숫자 문자열.
191
+ format: (v) => String(Math.round(v))
192
+ });
193
+ }
194
+ else {
195
+ titleHeading = document.createElement("h2");
196
+ titleHeading.className = "hce-card-title";
197
+ header.appendChild(titleHeading);
198
+ }
199
+ applyAttrs(node);
200
+ return {
201
+ dom,
202
+ contentDOM: body,
203
+ update: (updated) => {
204
+ if (updated.type.name !== "card")
205
+ return false;
206
+ currentNode = updated;
207
+ applyAttrs(updated);
208
+ return true;
209
+ },
210
+ // 제목 입력칸·배경 버튼 위의 이벤트는 ProseMirror 가 가로채면 안 된다.
211
+ stopEvent: (event) => {
212
+ const target = event.target;
213
+ return target instanceof Element && !!target.closest("[data-card-control]");
214
+ },
215
+ destroy: () => {
216
+ detachResize?.();
217
+ }
218
+ };
219
+ };
220
+ },
221
+ addCommands() {
222
+ return {
223
+ setCard: (attrs) => ({ chain }) => chain()
224
+ .insertContent({
225
+ type: this.name,
226
+ attrs: {
227
+ title: attrs?.title ?? "",
228
+ background: attrs?.background ?? "",
229
+ height: attrs?.height ?? String(DEFAULT_HEIGHT)
230
+ },
231
+ content: [{ type: "paragraph" }]
232
+ })
233
+ .run()
234
+ };
235
+ }
236
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=CardBlock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CardBlock.test.d.ts","sourceRoot":"","sources":["../../src/extensions/CardBlock.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { Editor } from "@tiptap/core";
3
+ import StarterKit from "@tiptap/starter-kit";
4
+ import { CardBlock } from "./CardBlock";
5
+ import { transformLegacyHtml } from "../utils/sanitize";
6
+ let editor = null;
7
+ const make = (content) => {
8
+ editor = new Editor({
9
+ element: document.createElement("div"),
10
+ extensions: [StarterKit, CardBlock],
11
+ content
12
+ });
13
+ return editor;
14
+ };
15
+ afterEach(() => {
16
+ editor?.destroy();
17
+ editor = null;
18
+ });
19
+ describe("CardBlock", () => {
20
+ it("새 스키마(div[data-type=card])를 카드로 읽는다", () => {
21
+ const e = make('<div data-type="card" data-card-title="제목" data-card-background="red" data-card-height="240"><p>본문</p></div>');
22
+ const node = e.getJSON().content?.[0];
23
+ expect(node?.type).toBe("card");
24
+ expect(node?.attrs?.title).toBe("제목");
25
+ expect(node?.attrs?.background).toBe("red");
26
+ expect(node?.attrs?.height).toBe("240");
27
+ expect(node?.content?.[0]?.type).toBe("paragraph");
28
+ });
29
+ it("옛 정올 스키마(<tiptap-card>)도 그대로 읽는다", () => {
30
+ const e = make('<tiptap-card title="옛제목" background="blue" height="300"><p>본문</p></tiptap-card>');
31
+ const node = e.getJSON().content?.[0];
32
+ expect(node?.type).toBe("card");
33
+ expect(node?.attrs?.title).toBe("옛제목");
34
+ expect(node?.attrs?.background).toBe("blue");
35
+ expect(node?.attrs?.height).toBe("300");
36
+ });
37
+ it("내용을 가진 컨테이너다 — 자식 블록이 보존된다", () => {
38
+ const e = make('<div data-type="card"><p>첫째</p><p>둘째</p></div>');
39
+ const node = e.getJSON().content?.[0];
40
+ expect(node?.content).toHaveLength(2);
41
+ });
42
+ it("새 스키마로 다시 쓴다", () => {
43
+ const e = make('<tiptap-card title="T" height="200"><p>x</p></tiptap-card>');
44
+ const html = e.getHTML();
45
+ expect(html).toContain('data-type="card"');
46
+ expect(html).toContain('data-card-title="T"');
47
+ expect(html).toContain('data-card-height="200"');
48
+ // 옛 태그로는 저장하지 않는다
49
+ expect(html).not.toContain("<tiptap-card");
50
+ });
51
+ it("height 기본값은 190", () => {
52
+ const e = make('<div data-type="card"><p>x</p></div>');
53
+ expect(e.getJSON().content?.[0]?.attrs?.height).toBe("190");
54
+ });
55
+ it("setCard 명령이 빈 문단 하나를 가진 카드를 넣는다", () => {
56
+ const e = make("<p></p>");
57
+ e.commands.setCard();
58
+ const card = e.getJSON().content?.find((n) => n.type === "card");
59
+ expect(card).toBeTruthy();
60
+ expect(card?.content?.[0]?.type).toBe("paragraph");
61
+ });
62
+ });
63
+ describe("transformLegacyHtml — tiptap-card", () => {
64
+ it("속성 순서와 무관하게 바꾼다", () => {
65
+ const a = transformLegacyHtml('<tiptap-card title="T" background="B" height="200"><p>x</p></tiptap-card>');
66
+ const b = transformLegacyHtml('<tiptap-card height="200" background="B" title="T"><p>x</p></tiptap-card>');
67
+ for (const out of [a, b]) {
68
+ expect(out).toContain('data-type="card"');
69
+ expect(out).toContain('data-card-title="T"');
70
+ expect(out).toContain('data-card-background="B"');
71
+ expect(out).toContain('data-card-height="200"');
72
+ expect(out).toContain("<p>x</p>");
73
+ }
74
+ });
75
+ it("속성이 없어도 기본 높이를 붙인다", () => {
76
+ const out = transformLegacyHtml("<tiptap-card><p>x</p></tiptap-card>");
77
+ expect(out).toContain('data-card-height="190"');
78
+ expect(out).not.toContain("data-card-title");
79
+ });
80
+ it("중첩 블록이 있는 본문을 통째로 보존한다", () => {
81
+ const out = transformLegacyHtml('<tiptap-card title="T"><p>하나</p><ul><li>둘</li></ul></tiptap-card>');
82
+ expect(out).toContain("<ul><li>둘</li></ul>");
83
+ });
84
+ it("카드가 둘 이상이어도 각각 바꾼다", () => {
85
+ const out = transformLegacyHtml('<tiptap-card title="A"><p>1</p></tiptap-card><tiptap-card title="B"><p>2</p></tiptap-card>');
86
+ expect(out.match(/data-type="card"/g)).toHaveLength(2);
87
+ expect(out).toContain('data-card-title="A"');
88
+ expect(out).toContain('data-card-title="B"');
89
+ });
90
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BA0KpB,CAAC"}
1
+ {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAGrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BAwHpB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { attachResize } from "../utils/resize";
2
3
  export const MbusVideo = Node.create({
3
4
  name: "mbusVideo",
4
5
  group: "block",
@@ -39,6 +40,9 @@ export const MbusVideo = Node.create({
39
40
  },
40
41
  addNodeView() {
41
42
  return ({ node, editor, getPos }) => {
43
+ // 리사이즈가 attrs 를 되쓰므로 **항상 최신 노드**여야 한다(복붙 시절의 stale 버그).
44
+ let currentNode = node;
45
+ let detachResize = null;
42
46
  const dom = document.createElement("div");
43
47
  dom.setAttribute("data-type", "mbusVideo");
44
48
  dom.setAttribute("data-node-view-wrapper", "");
@@ -72,80 +76,28 @@ export const MbusVideo = Node.create({
72
76
  }
73
77
  });
74
78
  dom.appendChild(del);
75
- const resizeHandle = document.createElement("button");
76
- resizeHandle.type = "button";
77
- resizeHandle.contentEditable = "false";
78
- resizeHandle.setAttribute("aria-label", "영상 너비 조절");
79
- resizeHandle.style.cssText =
80
- "position:absolute;right:-12px;top:0;bottom:0;width:14px;padding:0;margin:0;border:0;background:transparent;cursor:ew-resize;z-index:2;display:flex;align-items:center;justify-content:center;";
81
- const bar = document.createElement("span");
82
- bar.style.cssText =
83
- "display:block;width:3px;height:48px;background:var(--border, #d1d5db);border-radius:2px;transition:background 0.15s;pointer-events:none;";
84
- resizeHandle.appendChild(bar);
85
- resizeHandle.addEventListener("mouseenter", () => {
86
- bar.style.background = "var(--primary, #3382f2)";
79
+ detachResize = attachResize({
80
+ dom,
81
+ editor,
82
+ getPos: () => (typeof getPos === "function" ? getPos() : undefined),
83
+ getNode: () => currentNode,
84
+ axis: "x",
85
+ label: "영상 너비 조절"
87
86
  });
88
- resizeHandle.addEventListener("mouseleave", () => {
89
- if (!resizing)
90
- bar.style.background = "var(--border, #d1d5db)";
91
- });
92
- const MIN_W = 240;
93
- const MAX_W = 1600;
94
- let resizing = false;
95
- let startX = 0;
96
- let startWidth = 0;
97
- resizeHandle.addEventListener("pointerdown", (e) => {
98
- e.preventDefault();
99
- e.stopPropagation();
100
- resizing = true;
101
- startX = e.clientX;
102
- startWidth = dom.getBoundingClientRect().width;
103
- bar.style.background = "var(--primary, #3382f2)";
104
- resizeHandle.setPointerCapture(e.pointerId);
105
- });
106
- resizeHandle.addEventListener("pointermove", (e) => {
107
- if (!resizing)
108
- return;
109
- const delta = e.clientX - startX;
110
- const next = Math.max(MIN_W, Math.min(MAX_W, startWidth + delta));
111
- dom.style.width = `${next}px`;
112
- });
113
- const endResize = (e) => {
114
- if (!resizing)
115
- return;
116
- resizing = false;
117
- bar.style.background = "var(--border, #d1d5db)";
118
- try {
119
- resizeHandle.releasePointerCapture(e.pointerId);
120
- }
121
- catch {
122
- // ignore
123
- }
124
- const finalWidth = dom.style.width;
125
- if (!finalWidth)
126
- return;
127
- const pos = typeof getPos === "function" ? getPos() : null;
128
- if (pos == null)
129
- return;
130
- editor.view.dispatch(editor.view.state.tr.setNodeMarkup(pos, undefined, {
131
- ...node.attrs,
132
- width: finalWidth
133
- }));
134
- };
135
- resizeHandle.addEventListener("pointerup", endResize);
136
- resizeHandle.addEventListener("pointercancel", endResize);
137
- dom.appendChild(resizeHandle);
138
87
  }
139
88
  return {
140
89
  dom,
141
90
  update: (updated) => {
142
- if (updated.type !== node.type)
91
+ if (updated.type !== currentNode.type)
143
92
  return false;
144
93
  const w = updated.attrs.width;
145
94
  dom.style.width = w || "";
95
+ currentNode = updated;
146
96
  return true;
147
97
  },
148
- destroy: () => { }
98
+ destroy: () => {
99
+ detachResize?.();
100
+ }
149
101
  };
150
102
  };
151
103
  },
@@ -1 +1 @@
1
- {"version":3,"file":"PdfBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/PdfBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,UAAU,EAAmB,MAAM,cAAc,CAAC;AAInE,eAAO,MAAM,QAAQ,sBA4bnB,CAAC"}
1
+ {"version":3,"file":"PdfBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/PdfBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,UAAU,EAAmB,MAAM,cAAc,CAAC;AAKnE,eAAO,MAAM,QAAQ,sBAyYnB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Node as TiptapNode, mergeAttributes } from "@tiptap/core";
2
2
  import { getPdfJs } from "../utils/pdf";
3
+ import { attachResize } from "../utils/resize";
3
4
  export const PdfBlock = TiptapNode.create({
4
5
  name: "pdfBlock",
5
6
  group: "block",
@@ -79,6 +80,10 @@ export const PdfBlock = TiptapNode.create({
79
80
  let destroyed = false;
80
81
  let resizeObserver = null;
81
82
  let resizeTimeout;
83
+ // 리사이즈가 `setNodeMarkup` 으로 attrs 를 되쓰기 때문에 **항상 최신 노드**여야 한다.
84
+ // 클로저의 `node` 를 쓰면 그 사이의 다른 속성 변경을 리사이즈가 되돌린다.
85
+ let currentNode = node;
86
+ let detachResize = null;
82
87
  const dom = document.createElement("div");
83
88
  dom.classList.add("my-4");
84
89
  dom.contentEditable = "false";
@@ -97,69 +102,14 @@ export const PdfBlock = TiptapNode.create({
97
102
  dom.appendChild(wrapper);
98
103
  // 리사이즈 핸들 (편집 가능 모드에서만)
99
104
  if (editor.isEditable) {
100
- const resizeHandle = document.createElement("button");
101
- resizeHandle.type = "button";
102
- resizeHandle.contentEditable = "false";
103
- resizeHandle.setAttribute("aria-label", "PDF 너비 조절");
104
- resizeHandle.style.cssText =
105
- "position:absolute;right:-12px;top:0;bottom:0;width:14px;padding:0;margin:0;border:0;background:transparent;cursor:ew-resize;z-index:2;display:flex;align-items:center;justify-content:center;";
106
- const bar = document.createElement("span");
107
- bar.style.cssText =
108
- "display:block;width:3px;height:48px;background:var(--border, #d1d5db);border-radius:2px;transition:background 0.15s;pointer-events:none;";
109
- resizeHandle.appendChild(bar);
110
- resizeHandle.addEventListener("mouseenter", () => {
111
- bar.style.background = "var(--primary, #3382f2)";
105
+ detachResize = attachResize({
106
+ dom,
107
+ editor,
108
+ getPos,
109
+ getNode: () => currentNode,
110
+ axis: "x",
111
+ label: "PDF 너비 조절",
112
112
  });
113
- resizeHandle.addEventListener("mouseleave", () => {
114
- if (!resizing)
115
- bar.style.background = "var(--border, #d1d5db)";
116
- });
117
- const MIN_W = 240;
118
- const MAX_W = 1600;
119
- let resizing = false;
120
- let startX = 0;
121
- let startWidth = 0;
122
- resizeHandle.addEventListener("pointerdown", (e) => {
123
- e.preventDefault();
124
- e.stopPropagation();
125
- resizing = true;
126
- startX = e.clientX;
127
- startWidth = dom.getBoundingClientRect().width;
128
- bar.style.background = "var(--primary, #3382f2)";
129
- resizeHandle.setPointerCapture(e.pointerId);
130
- });
131
- resizeHandle.addEventListener("pointermove", (e) => {
132
- if (!resizing)
133
- return;
134
- const delta = e.clientX - startX;
135
- const next = Math.max(MIN_W, Math.min(MAX_W, startWidth + delta));
136
- dom.style.width = `${next}px`;
137
- });
138
- const endResize = (e) => {
139
- if (!resizing)
140
- return;
141
- resizing = false;
142
- bar.style.background = "var(--border, #d1d5db)";
143
- try {
144
- resizeHandle.releasePointerCapture(e.pointerId);
145
- }
146
- catch {
147
- // ignore release errors (handle may have lost capture already)
148
- }
149
- const finalWidth = dom.style.width;
150
- if (!finalWidth)
151
- return;
152
- const pos = getPos();
153
- if (pos == null)
154
- return;
155
- editor.view.dispatch(editor.view.state.tr.setNodeMarkup(pos, undefined, {
156
- ...node.attrs,
157
- width: finalWidth,
158
- }));
159
- };
160
- resizeHandle.addEventListener("pointerup", endResize);
161
- resizeHandle.addEventListener("pointercancel", endResize);
162
- dom.appendChild(resizeHandle);
163
113
  }
164
114
  // Header
165
115
  const header = document.createElement("div");
@@ -405,9 +355,10 @@ export const PdfBlock = TiptapNode.create({
405
355
  if (updatedNode.type.name !== "pdfBlock")
406
356
  return false;
407
357
  const newWidth = updatedNode.attrs.width;
408
- if (newWidth !== node.attrs.width) {
358
+ if (newWidth !== currentNode.attrs.width) {
409
359
  dom.style.width = newWidth || "";
410
360
  }
361
+ currentNode = updatedNode;
411
362
  return true;
412
363
  },
413
364
  selectNode: () => { },
@@ -416,6 +367,7 @@ export const PdfBlock = TiptapNode.create({
416
367
  destroyed = true;
417
368
  clearTimeout(resizeTimeout);
418
369
  resizeObserver?.disconnect();
370
+ detachResize?.();
419
371
  },
420
372
  };
421
373
  };
package/dist/index.d.ts CHANGED
@@ -11,11 +11,15 @@ export { FixedDetails } from "./extensions/FixedDetails";
11
11
  export { FileAttachment } from "./extensions/FileAttachment";
12
12
  export type { FileResolver, FileResolveResult } from "./extensions/FileAttachment";
13
13
  export { MbusVideo } from "./extensions/MbusVideo";
14
+ export { CardBlock } from "./extensions/CardBlock";
15
+ export type { CardBackgroundPrompt, CardBlockOptions } from "./extensions/CardBlock";
14
16
  export { Columns } from "./extensions/Columns";
15
17
  export { Column } from "./extensions/Column";
16
18
  export { MathInline, MathDisplay } from "./extensions/Math";
17
19
  export { sanitizeHtml, stripHtmlToExcerpt, transformLegacyHtml } from "./utils/sanitize";
18
20
  export { configurePdfJs, getPdfJs } from "./utils/pdf";
21
+ export { attachResize } from "./utils/resize";
22
+ export type { AttachResizeOptions, ResizeAxis } from "./utils/resize";
19
23
  export { cn } from "./utils/cn";
20
24
  export type { TipTapEditorProps, UploadHandler, ToolbarMode, ToolbarFeature, FixedToolbarProps, BlockHandleProps, SlashCommandMenuProps, PdfViewerProps, SlashMenuItem, } from "./types";
21
25
  export { TOOLBAR_PRESETS, resolveFeatures } from "./types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAGhC,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAGhC,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -12,11 +12,13 @@ export { Indent } from "./extensions/Indent";
12
12
  export { FixedDetails } from "./extensions/FixedDetails";
13
13
  export { FileAttachment } from "./extensions/FileAttachment";
14
14
  export { MbusVideo } from "./extensions/MbusVideo";
15
+ export { CardBlock } from "./extensions/CardBlock";
15
16
  export { Columns } from "./extensions/Columns";
16
17
  export { Column } from "./extensions/Column";
17
18
  export { MathInline, MathDisplay } from "./extensions/Math";
18
19
  // 유틸리티
19
20
  export { sanitizeHtml, stripHtmlToExcerpt, transformLegacyHtml } from "./utils/sanitize";
20
21
  export { configurePdfJs, getPdfJs } from "./utils/pdf";
22
+ export { attachResize } from "./utils/resize";
21
23
  export { cn } from "./utils/cn";
22
24
  export { TOOLBAR_PRESETS, resolveFeatures } from "./types";
@@ -1 +1 @@
1
- .pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.top-full{top:100%}.z-30{z-index:30}.z-50{z-index:50}.mx-0\.5{margin-left:.125rem;margin-right:.125rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-px{height:1px}.w-10{width:2.5rem}.w-4{width:1rem}.w-\[360px\]{width:360px}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-px{width:1px}.min-w-\[96px\]{min-width:96px}.max-w-\[90vw\]{max-width:90vw}.flex-shrink{flex-shrink:1}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.columns-2{-moz-columns:2;column-count:2}.columns-3{-moz-columns:3;column-count:3}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:var(--radius-md,4px)}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius-xl,8px)}.rounded-md{border-radius:var(--radius-md,6px)}.rounded-xl{border-radius:var(--radius-2xl,12px)}.rounded-t-xl{border-top-left-radius:var(--radius-2xl,12px);border-top-right-radius:var(--radius-2xl,12px)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-border{border-color:var(--border)}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.border-white\/20{border-color:hsla(0,0%,100%,.2)}.bg-background{background-color:var(--background)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-border{background-color:var(--border)}.bg-foreground{background-color:var(--foreground)}.bg-muted{background-color:var(--muted)}.bg-popover{background-color:var(--popover)}.bg-primary{background-color:var(--primary)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-bottom:.5rem}.pt-2,.py-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/80{color:hsla(0,0%,100%,.8)}.underline{text-decoration-line:underline}.opacity-30{opacity:.3}.shadow-lg{--tw-shadow:var(--shadow-dropdown,0 10px 15px -3px rgba(0,0,0,.1));--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:var(--shadow-dropdown,0 1px 2px 0 rgba(0,0,0,.05));--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:var(--shadow-popup,0 20px 25px -5px rgba(0,0,0,.1));--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hce-editor-wrapper{overflow:visible!important}.hce-color-divider{border-top:1px solid hsla(0,0%,100%,.15)}.bubble-toolbar-container button,.hce-editor-wrapper button,.slash-menu button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;cursor:pointer;box-sizing:border-box}.bubble-toolbar-container button:disabled,.hce-editor-wrapper button:disabled,.slash-menu button:disabled{cursor:default}.tiptap{font-size:1.0625rem;line-height:1.8;color:var(--foreground);padding:1.5rem}.tiptap>*+*{margin-top:.25em}.tiptap p{margin:.5em 0!important;line-height:1.5!important}.tiptap p:first-child{margin-top:0!important}.tiptap p:last-child{margin-bottom:0!important}.tiptap p:empty,.tiptap p:has(>br:only-child){min-height:1.5em}.tiptap h1,.tiptap h2,.tiptap h3,.tiptap li,.tiptap p{white-space:pre-wrap}.tiptap h1{font-size:2rem;font-weight:700;margin:1.5em 0 .5em;line-height:1.25}.tiptap h2{font-size:1.5rem;font-weight:600;margin:1.2em 0 .4em;line-height:1.3}.tiptap h3{font-size:1.25rem;font-weight:600;margin:1em 0 .3em;line-height:1.35}.tiptap ul{list-style:disc}.tiptap ol,.tiptap ul{padding-left:1.5em;margin:.5em 0}.tiptap ol{list-style:decimal}.tiptap ul ul{list-style:circle}.tiptap ul ul ul{list-style:square}.tiptap ol ol{list-style:lower-alpha}.tiptap ol ol ol{list-style:lower-roman}.tiptap li{margin:.25em 0}.tiptap ul[data-type=taskList]{list-style:none;padding-left:0}.tiptap ul[data-type=taskList] li{display:flex;align-items:baseline;gap:.5rem;margin:.1em 0;list-style:none;list-style-type:none}.tiptap ul[data-type=taskList] li>label{flex-shrink:0;display:flex;align-items:center;height:1.5em}.tiptap ul[data-type=taskList] li>label input[type=checkbox]{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:1.1em;height:1.1em;margin:0;padding:0;cursor:pointer;background-color:var(--background,#fff);border:1.5px solid var(--border);border-radius:4px;transition:background-color .12s ease,border-color .12s ease,box-shadow .12s ease;display:inline-block;vertical-align:middle;flex-shrink:0}.tiptap ul[data-type=taskList] li>label input[type=checkbox]:hover{border-color:var(--muted-foreground)}.tiptap ul[data-type=taskList] li>label input[type=checkbox]:focus-visible{outline:none;box-shadow:0 0 0 2px var(--background,#fff),0 0 0 4px var(--primary)}.tiptap ul[data-type=taskList] li>label input[type=checkbox]:checked{background-color:var(--primary);border-color:var(--primary);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' viewBox='0 0 16 16'%3E%3Cpath d='m3.5 8.5 3 3 6-6.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.tiptap ul[data-type=taskList] li>div{flex:1}.tiptap ul[data-type=taskList] li[data-checked=true]>div{text-decoration:line-through;opacity:.6}.tiptap blockquote{border-left:3px solid var(--border);padding:.5rem 1rem;margin:.75em 0;color:var(--muted-foreground);font-style:italic}.tiptap hr{border:none;border-top:2px solid color-mix(in srgb,var(--foreground) 38%,transparent);margin:1.5em 0}.tiptap code{background:var(--muted);border-radius:4px;padding:.15em .35em;font-size:.9em}.tiptap pre{background:#282c34;color:#abb2bf;border-radius:8px;padding:1em;margin:.75em 0;overflow-x:auto;font-family:JetBrains Mono,Fira Code,Consolas,monospace;font-size:1em;line-height:1.6}.tiptap pre code{background:none;padding:0;border-radius:0;color:#abb2bf;font-size:inherit}.tiptap pre code .hljs-keyword,.tiptap pre code .hljs-operator{color:#c678dd}.tiptap pre code .hljs-addition,.tiptap pre code .hljs-doctag,.tiptap pre code .hljs-regexp,.tiptap pre code .hljs-string{color:#98c379}.tiptap pre code .hljs-attr,.tiptap pre code .hljs-number,.tiptap pre code .hljs-selector-class,.tiptap pre code .hljs-template-variable,.tiptap pre code .hljs-type,.tiptap pre code .hljs-variable{color:#d19a66}.tiptap pre code .hljs-function,.tiptap pre code .hljs-section,.tiptap pre code .hljs-title,.tiptap pre code .hljs-title.class_{color:#61aeee}.tiptap pre code .hljs-params,.tiptap pre code .hljs-punctuation{color:#abb2bf}.tiptap pre code .hljs-comment,.tiptap pre code .hljs-quote{color:#5c6370;font-style:italic}.tiptap pre code .hljs-built_in,.tiptap pre code .hljs-formula{color:#e6c07b}.tiptap pre code .hljs-meta,.tiptap pre code .hljs-meta .hljs-keyword{color:#61aeee}.tiptap pre code .hljs-meta .hljs-string{color:#98c379}.tiptap pre code .hljs-attribute,.tiptap pre code .hljs-deletion,.tiptap pre code .hljs-name,.tiptap pre code .hljs-property,.tiptap pre code .hljs-selector-tag,.tiptap pre code .hljs-subst,.tiptap pre code .hljs-tag{color:#e06c75}.tiptap pre code .hljs-literal{color:#56b6c2}.tiptap pre code .hljs-selector-attr,.tiptap pre code .hljs-selector-id,.tiptap pre code .hljs-selector-pseudo{color:#61aeee}.tiptap pre code .hljs-link{color:#61aeee;text-decoration:underline}.tiptap pre code .hljs-bullet,.tiptap pre code .hljs-symbol{color:#61aeee}.tiptap pre code .hljs-emphasis{font-style:italic}.tiptap pre code .hljs-strong{font-weight:700}.tiptap img{max-width:100%;height:auto;margin:.75em 0;cursor:pointer;transition:box-shadow .15s}.tiptap [data-type=pdfBlock].ProseMirror-selectednode>div,.tiptap img{border-radius:8px}.tiptap a{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.tiptap a:hover{opacity:.8}.tiptap mark{background-color:#fef08a;border-radius:2px;padding:.05em .1em}.tiptap [data-type=details]{border:none;border-radius:6px;padding:0;margin:.5em 0;background:transparent;display:grid;grid-template-columns:26px 1fr;align-items:start;transition:background .12s}.tiptap [data-type=details]:hover{background:var(--muted)}.tiptap [data-type=details]>button{background:none;border:none;cursor:pointer;padding:0;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted-foreground);border-radius:4px;transition:transform .2s,color .15s;flex-shrink:0;margin-top:13px;margin-left:4px}.tiptap [data-type=details]>button:after{content:"";display:block;width:0;height:0;border-left:5px solid;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:2px}.tiptap [data-type=details]:hover>button{color:var(--foreground)}.tiptap [data-type=details].is-open>button{transform:rotate(90deg)}.tiptap [data-type=details]>div{flex:1;min-width:0}.tiptap [data-type=details] summary{font-weight:600;cursor:pointer;padding:10px 8px 10px 0;display:block;border-bottom:1px solid transparent;transition:border-color .15s}.tiptap [data-type=details].is-open>div>summary{border-bottom-color:var(--border,#e5e7eb)}.tiptap [data-type=detailsContent]{padding:8px 8px 8px 0;border-left:none}.tiptap [data-type=detailsContent][hidden]{display:none}.tiptap details{border:none;border-radius:6px;padding:0;margin:.5em 0;background:transparent;transition:background .12s}.tiptap details:hover{background:var(--muted)}.tiptap details summary{font-weight:600;cursor:pointer;padding:10px 8px 10px 26px;-webkit-user-select:none;-moz-user-select:none;user-select:none;border-bottom:1px solid transparent;position:relative;list-style:none;transition:border-color .15s}.tiptap details[open]>summary{border-bottom-color:var(--border,#e5e7eb)}.tiptap details summary::-webkit-details-marker{display:none}.tiptap details summary::marker{content:""}.tiptap details summary:before{content:"";position:absolute;left:8px;top:50%;width:0;height:0;border-left:5px solid var(--muted-foreground);border-top:4px solid transparent;border-bottom:4px solid transparent;transform:translateY(-50%);transition:transform .2s,border-left-color .15s}.tiptap details:hover summary:before{border-left-color:var(--foreground)}.tiptap details[open]>summary:before{transform:translateY(-50%) rotate(90deg)}.tiptap details[open]>:not(summary){margin-top:8px;padding-left:26px;padding-right:8px}.tiptap table{border-collapse:collapse;margin:1em 0;table-layout:fixed;border:2px solid var(--border);max-width:100%}.tiptap .tableWrapper{overflow-x:auto;max-width:100%}.tiptap td,.tiptap th{border:1px solid var(--border);padding:.6em .8em;min-width:60px;vertical-align:middle;position:relative;box-sizing:border-box;overflow:hidden;word-break:break-word;line-height:1.5}.tiptap th{background:var(--muted);font-weight:600;text-align:left}.tiptap td{background:var(--background)}.tiptap .selectedCell:after{content:"";position:absolute;inset:0;background:var(--primary);opacity:.12;pointer-events:none;z-index:1}.tiptap .column-resize-handle{position:absolute;right:-3px;top:0;bottom:0;width:6px;background:var(--primary);opacity:0;cursor:col-resize;z-index:20;border-radius:2px;transition:opacity .15s}.tiptap table:hover .column-resize-handle{opacity:.4}.tiptap .column-resize-handle:hover{opacity:1}.tiptap.resize-cursor{cursor:col-resize}.tableWrapper{overflow-x:auto;overflow-y:hidden;margin:1em 0;max-width:100%;padding-right:4px}.tiptap table .grip-column,.tiptap table .grip-row,.tiptap table .grip-table{display:none!important}.tiptap .is-empty:before{content:attr(data-placeholder);float:left;color:var(--muted-foreground);opacity:.5;pointer-events:none;height:0}.tiptap h1.is-empty:before{font-size:2rem;font-weight:700}.tiptap h2.is-empty:before{font-size:1.5rem;font-weight:600}.tiptap h3.is-empty:before{font-size:1.25rem;font-weight:600}.ProseMirror-gapcursor{display:none;pointer-events:none;position:relative}.ProseMirror-gapcursor:after{content:"";display:block;position:absolute;top:-2px;width:20px;border-top:1px solid var(--foreground);animation:ProseMirror-cursor-blink 1.1s steps(2,start) infinite}@keyframes ProseMirror-cursor-blink{to{visibility:hidden}}.ProseMirror-focused .ProseMirror-gapcursor{display:block}.ProseMirror-dropcursor{border-top:2px solid var(--primary);margin:-1px 0}.tiptap ::-moz-selection{background-color:color-mix(in srgb,var(--primary) 15%,transparent)}.tiptap ::selection{background-color:color-mix(in srgb,var(--primary) 15%,transparent)}.ProseMirror-noderangeselection ::-moz-selection{background:transparent}.ProseMirror-noderangeselection ::selection{background:transparent}.ProseMirror-noderangeselection *{caret-color:transparent}.ProseMirror-selectednode,.ProseMirror-selectednoderange{position:relative}.ProseMirror-selectednode:not(img):not([data-type=pdfBlock]):before,.ProseMirror-selectednoderange:not([data-type=pdfBlock]):before{content:"";position:absolute;inset:-.25rem;background-color:color-mix(in srgb,var(--primary) 10%,transparent);border:2px solid color-mix(in srgb,var(--primary) 30%,transparent);border-radius:.5rem;pointer-events:none;z-index:-1}.bubble-toolbar-container{z-index:50}.hce-floating-plus{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;border:1px solid var(--border);background:var(--background);color:var(--muted-foreground);cursor:pointer;transition:all .15s}.hce-floating-plus:hover{color:var(--foreground);border-color:var(--foreground);background:var(--muted)}.hce-drag-handle{display:flex;align-items:center;gap:2px}.hce-drag-handle__plus{display:flex;align-items:center;justify-content:center;padding:4px;border-radius:4px;border:none;background:transparent;color:var(--muted-foreground);opacity:.5;cursor:pointer;transition:opacity .15s,background-color .15s}.hce-drag-handle__plus:hover{opacity:1;background:var(--muted)}.hce-drag-handle__grip{display:flex;align-items:center;justify-content:center;padding:2px;border-radius:4px;color:var(--muted-foreground);opacity:.5;cursor:grab;transition:opacity .15s,background-color .15s}.hce-drag-handle__grip:hover{opacity:1;background:var(--muted)}.hce-drag-handle__grip:active{cursor:grabbing}.tiptap div[data-type=columns]{display:grid;gap:16px;margin:1em 0}.tiptap div[data-type=columns][data-columns="2"]{grid-template-columns:1fr 1fr}.tiptap div[data-type=columns][data-columns="3"]{grid-template-columns:1fr 1fr 1fr}.tiptap div[data-type=column]{min-width:0;padding:4px 6px;border:1px dashed var(--border);border-radius:8px}.tiptap[contenteditable=true] div[data-type=column]:focus-within{border-color:var(--primary);border-style:solid}.tiptap div[data-type=columns].ProseMirror-selectednode{outline:2px solid color-mix(in srgb,var(--primary) 30%,transparent);border-radius:8px}@media (max-width:640px){.tiptap div[data-type=columns]{grid-template-columns:1fr!important}}.hce-editor-wrapper button[data-tooltip]{position:relative}.hce-editor-wrapper button[data-tooltip]:hover:after{content:attr(data-tooltip);position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;padding:4px 8px;font-size:11px;line-height:1.2;white-space:nowrap;color:#fff;background:#1e293b;border-radius:6px;pointer-events:none;z-index:60}.hce-table-btn-wrap{position:relative;display:inline-flex}.hce-table-btn-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);margin-bottom:6px;padding:4px 8px;font-size:11px;line-height:1.2;white-space:nowrap;color:#fff;background:#1e293b;border-radius:6px;pointer-events:none;opacity:0;transition:opacity .15s;z-index:60}.hce-table-btn-wrap:hover .hce-table-btn-tooltip{opacity:1}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:bg-muted:hover{background-color:var(--muted)}.hover\:bg-white\/10:hover{background-color:hsla(0,0%,100%,.1)}.hover\:text-destructive:hover{color:var(--destructive)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-primary:focus{border-color:var(--primary)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.disabled\:opacity-30:disabled{opacity:.3}
1
+ .pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.top-full{top:100%}.z-30{z-index:30}.z-50{z-index:50}.mx-0\.5{margin-left:.125rem;margin-right:.125rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-px{height:1px}.w-10{width:2.5rem}.w-4{width:1rem}.w-\[360px\]{width:360px}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-px{width:1px}.min-w-\[96px\]{min-width:96px}.max-w-\[90vw\]{max-width:90vw}.flex-shrink{flex-shrink:1}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.columns-2{-moz-columns:2;column-count:2}.columns-3{-moz-columns:3;column-count:3}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:var(--radius-md,4px)}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius-xl,8px)}.rounded-md{border-radius:var(--radius-md,6px)}.rounded-xl{border-radius:var(--radius-2xl,12px)}.rounded-t-xl{border-top-left-radius:var(--radius-2xl,12px);border-top-right-radius:var(--radius-2xl,12px)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-border{border-color:var(--border)}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.border-white\/20{border-color:hsla(0,0%,100%,.2)}.bg-background{background-color:var(--background)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-border{background-color:var(--border)}.bg-foreground{background-color:var(--foreground)}.bg-muted{background-color:var(--muted)}.bg-popover{background-color:var(--popover)}.bg-primary{background-color:var(--primary)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-bottom:.5rem}.pt-2,.py-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/80{color:hsla(0,0%,100%,.8)}.underline{text-decoration-line:underline}.opacity-30{opacity:.3}.shadow-lg{--tw-shadow:var(--shadow-dropdown,0 10px 15px -3px rgba(0,0,0,.1));--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:var(--shadow-dropdown,0 1px 2px 0 rgba(0,0,0,.05));--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:var(--shadow-popup,0 20px 25px -5px rgba(0,0,0,.1));--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hce-editor-wrapper{overflow:visible!important}.hce-color-divider{border-top:1px solid hsla(0,0%,100%,.15)}.bubble-toolbar-container button,.hce-editor-wrapper button,.slash-menu button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;cursor:pointer;box-sizing:border-box}.bubble-toolbar-container button:disabled,.hce-editor-wrapper button:disabled,.slash-menu button:disabled{cursor:default}.tiptap{font-size:1.0625rem;line-height:1.8;color:var(--foreground);padding:1.5rem}.tiptap>*+*{margin-top:.25em}.tiptap p{margin:.5em 0!important;line-height:1.5}.tiptap p:first-child{margin-top:0!important}.tiptap p:last-child{margin-bottom:0!important}.tiptap p:empty,.tiptap p:has(>br:only-child){min-height:1.5em}.tiptap h1,.tiptap h2,.tiptap h3,.tiptap li,.tiptap p{white-space:pre-wrap}.tiptap h1{font-size:2rem;font-weight:700;margin:1.5em 0 .5em;line-height:1.25}.tiptap h2{font-size:1.5rem;font-weight:600;margin:1.2em 0 .4em;line-height:1.3}.tiptap h3{font-size:1.25rem;font-weight:600;margin:1em 0 .3em;line-height:1.35}.tiptap ul{list-style:disc}.tiptap ol,.tiptap ul{padding-left:1.5em;margin:.5em 0}.tiptap ol{list-style:decimal}.tiptap ul ul{list-style:circle}.tiptap ul ul ul{list-style:square}.tiptap ol ol{list-style:lower-alpha}.tiptap ol ol ol{list-style:lower-roman}.tiptap li{margin:.4em 0;line-height:1.7}.tiptap li>p{margin:0!important;line-height:inherit!important}.tiptap ul[data-type=taskList]{list-style:none;padding-left:0}.tiptap ul[data-type=taskList] li{display:flex;align-items:baseline;gap:.5rem;margin:.3em 0;line-height:1.7;list-style:none;list-style-type:none}.tiptap ul[data-type=taskList] li>label{flex-shrink:0;display:flex;align-items:center;height:1.5em}.tiptap ul[data-type=taskList] li>label input[type=checkbox]{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:1.1em;height:1.1em;margin:0;padding:0;cursor:pointer;background-color:var(--background,#fff);border:1.5px solid var(--border);border-radius:4px;transition:background-color .12s ease,border-color .12s ease,box-shadow .12s ease;display:inline-block;vertical-align:middle;flex-shrink:0}.tiptap ul[data-type=taskList] li>label input[type=checkbox]:hover{border-color:var(--muted-foreground)}.tiptap ul[data-type=taskList] li>label input[type=checkbox]:focus-visible{outline:none;box-shadow:0 0 0 2px var(--background,#fff),0 0 0 4px var(--primary)}.tiptap ul[data-type=taskList] li>label input[type=checkbox]:checked{background-color:var(--primary);border-color:var(--primary);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5' viewBox='0 0 16 16'%3E%3Cpath d='m3.5 8.5 3 3 6-6.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.tiptap ul[data-type=taskList] li>div{flex:1}.tiptap ul[data-type=taskList] li[data-checked=true]>div{text-decoration:line-through;opacity:.6}.tiptap blockquote{border-left:3px solid var(--border);padding:.5rem 1rem;margin:.75em 0;color:var(--muted-foreground);font-style:italic}.tiptap hr{border:none;border-top:2px solid color-mix(in srgb,var(--foreground) 38%,transparent);margin:1.5em 0}.tiptap code{background:var(--muted);border-radius:4px;padding:.15em .35em;font-size:.9em}.tiptap pre{background:#282c34;color:#abb2bf;border-radius:8px;padding:1em;margin:.75em 0;overflow-x:auto;font-family:JetBrains Mono,Fira Code,Consolas,monospace;font-size:1em;line-height:1.6}.tiptap pre code{background:none;padding:0;border-radius:0;color:#abb2bf;font-size:inherit}.tiptap pre code .hljs-keyword,.tiptap pre code .hljs-operator{color:#c678dd}.tiptap pre code .hljs-addition,.tiptap pre code .hljs-doctag,.tiptap pre code .hljs-regexp,.tiptap pre code .hljs-string{color:#98c379}.tiptap pre code .hljs-attr,.tiptap pre code .hljs-number,.tiptap pre code .hljs-selector-class,.tiptap pre code .hljs-template-variable,.tiptap pre code .hljs-type,.tiptap pre code .hljs-variable{color:#d19a66}.tiptap pre code .hljs-function,.tiptap pre code .hljs-section,.tiptap pre code .hljs-title,.tiptap pre code .hljs-title.class_{color:#61aeee}.tiptap pre code .hljs-params,.tiptap pre code .hljs-punctuation{color:#abb2bf}.tiptap pre code .hljs-comment,.tiptap pre code .hljs-quote{color:#5c6370;font-style:italic}.tiptap pre code .hljs-built_in,.tiptap pre code .hljs-formula{color:#e6c07b}.tiptap pre code .hljs-meta,.tiptap pre code .hljs-meta .hljs-keyword{color:#61aeee}.tiptap pre code .hljs-meta .hljs-string{color:#98c379}.tiptap pre code .hljs-attribute,.tiptap pre code .hljs-deletion,.tiptap pre code .hljs-name,.tiptap pre code .hljs-property,.tiptap pre code .hljs-selector-tag,.tiptap pre code .hljs-subst,.tiptap pre code .hljs-tag{color:#e06c75}.tiptap pre code .hljs-literal{color:#56b6c2}.tiptap pre code .hljs-selector-attr,.tiptap pre code .hljs-selector-id,.tiptap pre code .hljs-selector-pseudo{color:#61aeee}.tiptap pre code .hljs-link{color:#61aeee;text-decoration:underline}.tiptap pre code .hljs-bullet,.tiptap pre code .hljs-symbol{color:#61aeee}.tiptap pre code .hljs-emphasis{font-style:italic}.tiptap pre code .hljs-strong{font-weight:700}.tiptap img{max-width:100%;height:auto;margin:.75em 0;cursor:pointer;transition:box-shadow .15s}.tiptap [data-type=pdfBlock].ProseMirror-selectednode>div,.tiptap img{border-radius:8px}.tiptap a{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.tiptap a:hover{opacity:.8}.tiptap mark{background-color:#fef08a;border-radius:2px;padding:.05em .1em}.tiptap [data-type=details]{border:none;border-radius:6px;padding:0;margin:.5em 0;background:transparent;display:grid;grid-template-columns:26px 1fr;align-items:start;transition:background .12s}.tiptap [data-type=details]:hover{background:var(--muted)}.tiptap [data-type=details]>button{background:none;border:none;cursor:pointer;padding:0;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted-foreground);border-radius:4px;transition:transform .2s,color .15s;flex-shrink:0;margin-top:13px;margin-left:4px}.tiptap [data-type=details]>button:after{content:"";display:block;width:0;height:0;border-left:5px solid;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:2px}.tiptap [data-type=details]:hover>button{color:var(--foreground)}.tiptap [data-type=details].is-open>button{transform:rotate(90deg)}.tiptap [data-type=details]>div{flex:1;min-width:0}.tiptap [data-type=details] summary{font-weight:600;cursor:pointer;padding:10px 8px 10px 0;display:block;border-bottom:1px solid transparent;transition:border-color .15s}.tiptap [data-type=details].is-open>div>summary{border-bottom-color:var(--border,#e5e7eb)}.tiptap [data-type=detailsContent]{padding:8px 8px 8px 0;border-left:none}.tiptap [data-type=detailsContent][hidden]{display:none}.tiptap details{border:none;border-radius:6px;padding:0;margin:.5em 0;background:transparent;transition:background .12s}.tiptap details:hover{background:var(--muted)}.tiptap details summary{font-weight:600;cursor:pointer;padding:10px 8px 10px 26px;-webkit-user-select:none;-moz-user-select:none;user-select:none;border-bottom:1px solid transparent;position:relative;list-style:none;transition:border-color .15s}.tiptap details[open]>summary{border-bottom-color:var(--border,#e5e7eb)}.tiptap details summary::-webkit-details-marker{display:none}.tiptap details summary::marker{content:""}.tiptap details summary:before{content:"";position:absolute;left:8px;top:50%;width:0;height:0;border-left:5px solid var(--muted-foreground);border-top:4px solid transparent;border-bottom:4px solid transparent;transform:translateY(-50%);transition:transform .2s,border-left-color .15s}.tiptap details:hover summary:before{border-left-color:var(--foreground)}.tiptap details[open]>summary:before{transform:translateY(-50%) rotate(90deg)}.tiptap details[open]>:not(summary){margin-top:8px;padding-left:26px;padding-right:8px}.tiptap table{border-collapse:collapse;margin:1em 0;table-layout:fixed;border:2px solid var(--border);max-width:100%}.tiptap .tableWrapper{overflow-x:auto;max-width:100%}.tiptap td,.tiptap th{border:1px solid var(--border);padding:.6em .8em;min-width:60px;vertical-align:middle;position:relative;box-sizing:border-box;overflow:hidden;word-break:break-word;line-height:1.5}.tiptap th{background:var(--muted);font-weight:600;text-align:left}.tiptap td{background:var(--background)}.tiptap .selectedCell:after{content:"";position:absolute;inset:0;background:var(--primary);opacity:.12;pointer-events:none;z-index:1}.tiptap .column-resize-handle{position:absolute;right:-3px;top:0;bottom:0;width:6px;background:var(--primary);opacity:0;cursor:col-resize;z-index:20;border-radius:2px;transition:opacity .15s}.tiptap table:hover .column-resize-handle{opacity:.4}.tiptap .column-resize-handle:hover{opacity:1}.tiptap.resize-cursor{cursor:col-resize}.tableWrapper{overflow-x:auto;overflow-y:hidden;margin:1em 0;max-width:100%;padding-right:4px}.tiptap table .grip-column,.tiptap table .grip-row,.tiptap table .grip-table{display:none!important}.tiptap .is-empty:before{content:attr(data-placeholder);float:left;color:var(--muted-foreground);opacity:.5;pointer-events:none;height:0}.tiptap h1.is-empty:before{font-size:2rem;font-weight:700}.tiptap h2.is-empty:before{font-size:1.5rem;font-weight:600}.tiptap h3.is-empty:before{font-size:1.25rem;font-weight:600}.ProseMirror-gapcursor{display:none;pointer-events:none;position:relative}.ProseMirror-gapcursor:after{content:"";display:block;position:absolute;top:-2px;width:20px;border-top:1px solid var(--foreground);animation:ProseMirror-cursor-blink 1.1s steps(2,start) infinite}@keyframes ProseMirror-cursor-blink{to{visibility:hidden}}.ProseMirror-focused .ProseMirror-gapcursor{display:block}.ProseMirror-dropcursor{border-top:2px solid var(--primary);margin:-1px 0}.tiptap ::-moz-selection{background-color:color-mix(in srgb,var(--primary) 15%,transparent)}.tiptap ::selection{background-color:color-mix(in srgb,var(--primary) 15%,transparent)}.ProseMirror-noderangeselection ::-moz-selection{background:transparent}.ProseMirror-noderangeselection ::selection{background:transparent}.ProseMirror-noderangeselection *{caret-color:transparent}.ProseMirror-selectednode,.ProseMirror-selectednoderange{position:relative}.ProseMirror-selectednode:not(img):not([data-type=pdfBlock]):before,.ProseMirror-selectednoderange:not([data-type=pdfBlock]):before{content:"";position:absolute;inset:-.25rem;background-color:color-mix(in srgb,var(--primary) 10%,transparent);border:2px solid color-mix(in srgb,var(--primary) 30%,transparent);border-radius:.5rem;pointer-events:none;z-index:-1}.bubble-toolbar-container{z-index:50}.hce-floating-plus{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;border:1px solid var(--border);background:var(--background);color:var(--muted-foreground);cursor:pointer;transition:all .15s}.hce-floating-plus:hover{color:var(--foreground);border-color:var(--foreground);background:var(--muted)}.hce-drag-handle{display:flex;align-items:center;gap:2px}.hce-drag-handle__plus{display:flex;align-items:center;justify-content:center;padding:4px;border-radius:4px;border:none;background:transparent;color:var(--muted-foreground);opacity:.5;cursor:pointer;transition:opacity .15s,background-color .15s}.hce-drag-handle__plus:hover{opacity:1;background:var(--muted)}.hce-drag-handle__grip{display:flex;align-items:center;justify-content:center;padding:2px;border-radius:4px;color:var(--muted-foreground);opacity:.5;cursor:grab;transition:opacity .15s,background-color .15s}.hce-drag-handle__grip:hover{opacity:1;background:var(--muted)}.hce-drag-handle__grip:active{cursor:grabbing}.tiptap div[data-type=columns]{display:grid;gap:16px;margin:1em 0}.tiptap div[data-type=columns][data-columns="2"]{grid-template-columns:1fr 1fr}.tiptap div[data-type=columns][data-columns="3"]{grid-template-columns:1fr 1fr 1fr}.tiptap div[data-type=column]{min-width:0;padding:4px 6px;border:1px dashed var(--border);border-radius:8px}.tiptap[contenteditable=true] div[data-type=column]:focus-within{border-color:var(--primary);border-style:solid}.tiptap div[data-type=columns].ProseMirror-selectednode{outline:2px solid color-mix(in srgb,var(--primary) 30%,transparent);border-radius:8px}@media (max-width:640px){.tiptap div[data-type=columns]{grid-template-columns:1fr!important}}.hce-editor-wrapper button[data-tooltip]{position:relative}.hce-editor-wrapper button[data-tooltip]:hover:after{content:attr(data-tooltip);position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;padding:4px 8px;font-size:11px;line-height:1.2;white-space:nowrap;color:#fff;background:#1e293b;border-radius:6px;pointer-events:none;z-index:60}.hce-table-btn-wrap{position:relative;display:inline-flex}.hce-table-btn-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);margin-bottom:6px;padding:4px 8px;font-size:11px;line-height:1.2;white-space:nowrap;color:#fff;background:#1e293b;border-radius:6px;pointer-events:none;opacity:0;transition:opacity .15s;z-index:60}.hce-table-btn-wrap:hover .hce-table-btn-tooltip{opacity:1}.hce-card-frame,.tiptap [data-type=card]{position:relative}.hce-card-frame{box-sizing:border-box;width:100%;margin:12px 0;padding:26px 30px;display:flex;flex-direction:column;justify-content:center;gap:12px;overflow:hidden;border:1px solid hsla(0,0%,100%,.52);border-radius:8px;box-shadow:inset 0 0 0 1px hsla(0,0%,100%,.28),0 8px 24px rgba(15,23,42,.08);overflow-wrap:anywhere;isolation:isolate}.hce-card-frame:before{content:"";position:absolute;inset:0;z-index:0;background:linear-gradient(90deg,hsla(0,0%,100%,.14),hsla(0,0%,100%,.04) 48%,rgba(15,23,42,.08));pointer-events:none}.hce-card-content{position:relative;z-index:1;box-sizing:border-box;width:100%;max-height:100%;min-height:0;padding:18px 20px;display:flex;flex-direction:column;gap:10px;overflow:hidden;border:1px solid hsla(0,0%,100%,.56);border-radius:8px;background:hsla(0,0%,100%,.82);color:#111827;box-shadow:0 14px 42px rgba(15,23,42,.12),inset 0 1px 0 hsla(0,0%,100%,.52);backdrop-filter:blur(18px) saturate(1.16);-webkit-backdrop-filter:blur(18px) saturate(1.16)}@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.hce-card-content{background:hsla(0,0%,100%,.9)}}.hce-card-header{flex:0 0 auto}.hce-card-title{margin:0;font-size:1.65rem;font-weight:700;line-height:1.18;color:#111827}.hce-card-title[hidden]{display:none}.hce-card-title-input{box-sizing:border-box;width:100%;padding:8px 12px;border:1px solid rgba(17,24,39,.12);border-radius:6px;background:hsla(0,0%,100%,.7);color:#111827;font:inherit;font-size:1.15rem;font-weight:700;outline:none}.hce-card-title-input:focus{border-color:var(--primary,#3382f2)}.hce-card-body{flex:1 1 auto;min-height:0;overflow:auto;font-size:1rem;line-height:1.55;color:#111827}.hce-card-body>p:first-child{margin-top:0}.hce-card-body>p:last-child{margin-bottom:0}.hce-card-bg-button{position:absolute;top:8px;right:8px;z-index:3;padding:4px 10px;border:1px solid rgba(17,24,39,.12);border-radius:6px;background:hsla(0,0%,100%,.86);color:#111827;font-size:.8125rem;line-height:1.4;cursor:pointer}.hce-card-bg-button:hover{background:#fff}@media (max-width:640px){.hce-card-frame{padding:18px}.hce-card-content{padding:14px 16px}.hce-card-title{font-size:1.35rem}}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:bg-muted:hover{background-color:var(--muted)}.hover\:bg-white\/10:hover{background-color:hsla(0,0%,100%,.1)}.hover\:text-destructive:hover{color:var(--destructive)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-primary:focus{border-color:var(--primary)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.disabled\:opacity-30:disabled{opacity:.3}
package/dist/types.d.ts CHANGED
@@ -12,7 +12,7 @@ export type PromptHandler = (defaultValue: string) => Promise<string | null>;
12
12
  /** 툴바 모드 */
13
13
  export type ToolbarMode = 'minimal' | 'standard' | 'full';
14
14
  /** 개별 툴바 기능 ID */
15
- export type ToolbarFeature = 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'superscript' | 'subscript' | 'code' | 'text-color' | 'align-left' | 'align-center' | 'align-right' | 'paragraph' | 'h1' | 'h2' | 'h3' | 'bullet-list' | 'ordered-list' | 'checklist' | 'blockquote' | 'horizontal-rule' | 'toggle' | 'link' | 'image' | 'pdf' | 'file' | 'mbus' | 'columns-2' | 'columns-3' | 'table' | 'code-block' | 'undo' | 'redo' | 'fixed-toolbar' | 'bubble-toolbar' | 'slash-menu' | 'table-menu' | 'character-count' | 'upload-overlay';
15
+ export type ToolbarFeature = 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'superscript' | 'subscript' | 'code' | 'text-color' | 'align-left' | 'align-center' | 'align-right' | 'paragraph' | 'h1' | 'h2' | 'h3' | 'bullet-list' | 'ordered-list' | 'checklist' | 'blockquote' | 'horizontal-rule' | 'toggle' | 'link' | 'image' | 'pdf' | 'file' | 'mbus' | 'card' | 'columns-2' | 'columns-3' | 'table' | 'code-block' | 'undo' | 'redo' | 'fixed-toolbar' | 'bubble-toolbar' | 'slash-menu' | 'table-menu' | 'character-count' | 'upload-overlay';
16
16
  /** 모드별 기본 feature 프리셋 */
17
17
  export declare const TOOLBAR_PRESETS: Record<ToolbarMode, ToolbarFeature[]>;
18
18
  /** features 배열 → Set 변환. features가 있으면 그걸 쓰고 없으면 toolbar 모드 프리셋 */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -7,7 +7,7 @@ export const TOOLBAR_PRESETS = {
7
7
  'paragraph', 'h1', 'h2', 'h3',
8
8
  'bullet-list', 'ordered-list', 'checklist',
9
9
  'blockquote', 'horizontal-rule', 'toggle',
10
- 'link', 'image', 'pdf', 'file', 'mbus',
10
+ 'link', 'image', 'pdf', 'file', 'mbus', 'card',
11
11
  'columns-2', 'columns-3', 'table', 'code-block',
12
12
  'undo', 'redo',
13
13
  'fixed-toolbar', 'slash-menu',
@@ -19,7 +19,7 @@ export const TOOLBAR_PRESETS = {
19
19
  'paragraph', 'h1', 'h2', 'h3',
20
20
  'bullet-list', 'ordered-list', 'checklist',
21
21
  'blockquote', 'horizontal-rule', 'toggle',
22
- 'link', 'image', 'pdf', 'file', 'mbus',
22
+ 'link', 'image', 'pdf', 'file', 'mbus', 'card',
23
23
  'columns-2', 'columns-3', 'table', 'code-block',
24
24
  'fixed-toolbar', 'slash-menu', 'table-menu'
25
25
  ],
@@ -0,0 +1,41 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
3
+ /**
4
+ * 노드뷰에 드래그 리사이즈 손잡이를 단다.
5
+ *
6
+ * 원래 `PdfBlock` 과 `MbusVideo` 가 **같은 코드를 복붙**해서 각자 들고 있었다(변수명·매직넘버·
7
+ * cssText 문자열까지 동일). 세 번째 소비자(카드)가 생기면서 한 곳으로 모은다.
8
+ *
9
+ * 합치면서 세 가지가 달라졌다:
10
+ *
11
+ * 1. **세로 축이 생겼다.** 기존 둘은 가로(폭)만 있었다. 카드·접기처럼 내용이 흐르는 블록은
12
+ * 폭보다 높이를 잡고 싶다.
13
+ * 2. **`node` 를 getter 로 받는다.** 기존 코드는 `addNodeView` 클로저가 잡은 `node` 를 그대로
14
+ * `setNodeMarkup(pos, undefined, {...node.attrs, width})` 에 썼다. 그 사이 다른 속성이
15
+ * 바뀌었으면 **리사이즈가 그 변경을 되돌려 버린다.** 항상 최신 노드를 읽도록 바꿨다.
16
+ * 3. **정리 함수를 돌려준다.** 기존엔 `destroy()` 에서 손잡이 리스너를 떼지 않았다.
17
+ *
18
+ * ⚠️ 손잡이는 `position:absolute` 로 노드뷰 밖 12px 을 침범한다. 부모가 `overflow:hidden` 이면
19
+ * 잘려서 안 보인다(`editor.css` 가 `padding-right:4px` 로 땜빵 중인 이유).
20
+ */
21
+ export type ResizeAxis = "x" | "y";
22
+ export interface AttachResizeOptions {
23
+ /** 크기를 실제로 먹일 요소. 보통 노드뷰 루트(`dom`). */
24
+ dom: HTMLElement;
25
+ editor: Editor;
26
+ getPos: () => number | undefined;
27
+ /** ⚠️ 값이 아니라 **getter**. 클로저가 낡은 노드를 잡는 것을 막는다. */
28
+ getNode: () => ProseMirrorNode;
29
+ /** 기본 `'x'`(폭). `'y'` 면 높이를 조절한다. */
30
+ axis?: ResizeAxis;
31
+ /** 크기를 저장할 노드 속성 이름. 기본은 축에 따라 `width` / `height`. */
32
+ attr?: string;
33
+ min?: number;
34
+ max?: number;
35
+ /** 스크린리더용 이름. 예: `'PDF 너비 조절'` */
36
+ label?: string;
37
+ /** 저장 형식. 기본은 `'240px'` 같은 px 문자열. */
38
+ format?: (value: number) => string;
39
+ }
40
+ export declare function attachResize(options: AttachResizeOptions): () => void;
41
+ //# sourceMappingURL=resize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/utils/resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG,GAAG,CAAC;AAEnC,MAAM,WAAW,mBAAmB;IACnC,uCAAuC;IACvC,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACjC,mDAAmD;IACnD,OAAO,EAAE,MAAM,eAAe,CAAC;IAC/B,qCAAqC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AAUD,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,IAAI,CA+GrE"}
@@ -0,0 +1,103 @@
1
+ const DEFAULTS = {
2
+ x: { min: 240, max: 1600, label: "너비 조절", cursor: "ew-resize" },
3
+ y: { min: 120, max: 900, label: "높이 조절", cursor: "ns-resize" }
4
+ };
5
+ const IDLE = "var(--border, #d1d5db)";
6
+ const ACTIVE = "var(--primary, #3382f2)";
7
+ export function attachResize(options) {
8
+ const { dom, editor, getPos, getNode, axis = "x", attr = axis === "x" ? "width" : "height", format = (v) => `${Math.round(v)}px` } = options;
9
+ const preset = DEFAULTS[axis];
10
+ const min = options.min ?? preset.min;
11
+ const max = options.max ?? preset.max;
12
+ const handle = document.createElement("button");
13
+ handle.type = "button";
14
+ handle.contentEditable = "false";
15
+ handle.className = "hce-resize-handle";
16
+ handle.setAttribute("aria-label", options.label ?? preset.label);
17
+ handle.dataset.resizeAxis = axis;
18
+ handle.style.cssText =
19
+ axis === "x"
20
+ ? "position:absolute;right:-12px;top:0;bottom:0;width:14px;padding:0;margin:0;border:0;background:transparent;cursor:ew-resize;z-index:2;display:flex;align-items:center;justify-content:center;"
21
+ : "position:absolute;left:0;right:0;bottom:-12px;height:14px;padding:0;margin:0;border:0;background:transparent;cursor:ns-resize;z-index:2;display:flex;align-items:center;justify-content:center;";
22
+ const bar = document.createElement("span");
23
+ bar.style.cssText =
24
+ axis === "x"
25
+ ? `display:block;width:3px;height:48px;background:${IDLE};border-radius:2px;transition:background 0.15s;pointer-events:none;`
26
+ : `display:block;width:48px;height:3px;background:${IDLE};border-radius:2px;transition:background 0.15s;pointer-events:none;`;
27
+ handle.appendChild(bar);
28
+ let resizing = false;
29
+ let start = 0;
30
+ let startSize = 0;
31
+ const paint = (active) => {
32
+ bar.style.background = active ? ACTIVE : IDLE;
33
+ };
34
+ const onEnter = () => paint(true);
35
+ const onLeave = () => {
36
+ if (!resizing)
37
+ paint(false);
38
+ };
39
+ const onPointerDown = (e) => {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ resizing = true;
43
+ const rect = dom.getBoundingClientRect();
44
+ start = axis === "x" ? e.clientX : e.clientY;
45
+ startSize = axis === "x" ? rect.width : rect.height;
46
+ paint(true);
47
+ handle.setPointerCapture(e.pointerId);
48
+ };
49
+ const onPointerMove = (e) => {
50
+ if (!resizing)
51
+ return;
52
+ const delta = (axis === "x" ? e.clientX : e.clientY) - start;
53
+ const next = Math.max(min, Math.min(max, startSize + delta));
54
+ if (axis === "x")
55
+ dom.style.width = `${next}px`;
56
+ else
57
+ dom.style.height = `${next}px`;
58
+ };
59
+ const endResize = (e) => {
60
+ if (!resizing)
61
+ return;
62
+ resizing = false;
63
+ paint(false);
64
+ try {
65
+ handle.releasePointerCapture(e.pointerId);
66
+ }
67
+ catch {
68
+ // 이미 캡처를 놓쳤을 수 있다 — 무시해도 되는 실패다.
69
+ }
70
+ const raw = axis === "x" ? dom.style.width : dom.style.height;
71
+ if (!raw)
72
+ return;
73
+ const pos = getPos();
74
+ if (pos == null)
75
+ return;
76
+ const parsed = Number.parseFloat(raw);
77
+ if (!Number.isFinite(parsed))
78
+ return;
79
+ // ⚠️ `getNode()` — 클로저가 잡아 둔 낡은 노드를 쓰면 리사이즈가 그 사이의 다른
80
+ // 속성 변경을 되돌린다(복붙 시절의 버그).
81
+ const node = getNode();
82
+ editor.view.dispatch(editor.view.state.tr.setNodeMarkup(pos, undefined, {
83
+ ...node.attrs,
84
+ [attr]: format(parsed)
85
+ }));
86
+ };
87
+ handle.addEventListener("mouseenter", onEnter);
88
+ handle.addEventListener("mouseleave", onLeave);
89
+ handle.addEventListener("pointerdown", onPointerDown);
90
+ handle.addEventListener("pointermove", onPointerMove);
91
+ handle.addEventListener("pointerup", endResize);
92
+ handle.addEventListener("pointercancel", endResize);
93
+ dom.appendChild(handle);
94
+ return () => {
95
+ handle.removeEventListener("mouseenter", onEnter);
96
+ handle.removeEventListener("mouseleave", onLeave);
97
+ handle.removeEventListener("pointerdown", onPointerDown);
98
+ handle.removeEventListener("pointermove", onPointerMove);
99
+ handle.removeEventListener("pointerup", endResize);
100
+ handle.removeEventListener("pointercancel", endResize);
101
+ handle.remove();
102
+ };
103
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AA4BA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcjD;AA0BD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsDxD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAM,GAAG,MAAM,CAWrE"}
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAgDA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcjD;AA0BD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8ExD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAM,GAAG,MAAM,CAWrE"}
@@ -15,7 +15,27 @@ const ALLOWED_ATTRS = {
15
15
  col: new Set(["style", "width"]),
16
16
  td: new Set(["colspan", "rowspan", "colwidth", "style"]),
17
17
  th: new Set(["colspan", "rowspan", "colwidth", "style"]),
18
- div: new Set(["data-pdf-src", "data-pdf-name"]),
18
+ // ⚠️ 에디터가 실제로 뱉는 `div` 속성을 전부 적는다. 여기 빠진 것은 조용히 지워져
19
+ // 블록이 평범한 빈 `div` 로 내려앉는다 — 파일·영상·단·카드가 그렇게 사라진다.
20
+ // (`data-pdf-*` 만 있던 시절엔 나머지가 전부 누락돼 있었다. `sanitizeHtml` 이
21
+ // 아직 아무 데서도 호출되지 않아 드러나지 않았을 뿐이다.)
22
+ div: new Set([
23
+ "data-type",
24
+ "data-pdf-src",
25
+ "data-pdf-name",
26
+ "data-pdf-id",
27
+ "data-pdf-width",
28
+ "data-file-id",
29
+ "data-file-src",
30
+ "data-file-name",
31
+ "data-file-size",
32
+ "data-mbus-src",
33
+ "data-mbus-width",
34
+ "data-card-title",
35
+ "data-card-background",
36
+ "data-card-height",
37
+ "style",
38
+ ]),
19
39
  pre: new Set(["class"]),
20
40
  code: new Set(["class"]),
21
41
  "tiptap-midibus": new Set(["id", "start", "uuid", "width", "height"]),
@@ -70,6 +90,27 @@ export function transformLegacyHtml(html) {
70
90
  // <tiptap-collapsable title="X">content</tiptap-collapsable>
71
91
  // → <details><summary>X</summary><div>content</div></details>
72
92
  .replace(/<tiptap-collapsable\s+title="([^"]*)">([\s\S]*?)<\/tiptap-collapsable>/gi, '<details><summary>$1</summary><div>$2</div></details>')
93
+ // <tiptap-card title="T" background="B" height="H">content</tiptap-card>
94
+ // → <div data-type="card" data-card-*>content</div>
95
+ //
96
+ // 정올 전용이던 카드 블록. 속성이 셋 다 선택적이고 순서도 보장되지 않아,
97
+ // 여는 태그를 통째로 잡은 뒤 그 안에서 개별 속성을 뽑는다 —
98
+ // 위 embed 처럼 순서별 규칙을 여러 개 두면 3! 가지가 되어 감당이 안 된다.
99
+ .replace(/<tiptap-card\b([^>]*)>([\s\S]*?)<\/tiptap-card>/gi, (_match, rawAttrs, inner) => {
100
+ const pick = (name) => {
101
+ const m = rawAttrs.match(new RegExp(`\\b${name}="([^"]*)"`, "i"));
102
+ return m ? m[1] : "";
103
+ };
104
+ const attrs = [
105
+ `data-type="card"`,
106
+ pick("title") && `data-card-title="${pick("title")}"`,
107
+ pick("background") && `data-card-background="${pick("background")}"`,
108
+ `data-card-height="${pick("height") || "190"}"`,
109
+ ]
110
+ .filter(Boolean)
111
+ .join(" ");
112
+ return `<div ${attrs}>${inner}</div>`;
113
+ })
73
114
  // <embed src="X" type="application/pdf" ...>
74
115
  // → <div data-pdf-src="X" data-pdf-name="filename">
75
116
  .replace(/<embed\s+[^>]*src="([^"]*)"[^>]*type="application\/pdf"[^>]*\/?>/gi, (_match, src) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.7.12",
3
+ "version": "0.8.0",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",