@teriusu/rich-editor 0.9.12 → 0.9.14

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.
@@ -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;AAKnE,eAAO,MAAM,QAAQ,sBAwnBnB,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;AAYnE,eAAO,MAAM,QAAQ,sBAs1BnB,CAAC"}
@@ -1,6 +1,12 @@
1
1
  import { Node as TiptapNode, mergeAttributes } from "@tiptap/core";
2
+ import { NodeSelection } from "@tiptap/pm/state";
2
3
  import { getPdfJs } from "../utils/pdf";
3
4
  import { attachResize } from "../utils/resize";
5
+ /**
6
+ * 방향키 → 쪽 넘김. 노드뷰가 쪽 상태를 쥐고 있으므로 **이벤트로 건넨다** —
7
+ * 확장이 노드뷰 내부를 직접 들여다보지 않게 하는 게 목적이다.
8
+ */
9
+ const PAGE_EVENT = "hce-pdf-page";
4
10
  export const PdfBlock = TiptapNode.create({
5
11
  name: "pdfBlock",
6
12
  group: "block",
@@ -101,10 +107,23 @@ export const PdfBlock = TiptapNode.create({
101
107
  dom.style.position = "relative";
102
108
  dom.style.boxSizing = "border-box";
103
109
  dom.style.maxWidth = "100%";
104
- if (node.attrs.width)
105
- dom.style.width = node.attrs.width;
110
+ // 초기 폭은 `applyWidth` 를 선언한 뒤에 먹인다(그 전에 부르면 TDZ 다).
106
111
  const frame = document.createElement("div");
107
112
  frame.className = "pdf-frame";
113
+ /*
114
+ * ── 방향키로 쪽을 넘기려면 **판이 포커스를 받아야 한다** ──────────────
115
+ *
116
+ * ⚠️ 처음엔 ProseMirror 의 노드 선택에 얹었는데, 그건 편집 모드 개념이다.
117
+ * 정작 방향키가 필요한 곳은 **읽는 화면**이고 거기엔 선택도, 선택 표시도 없다
118
+ * (사용자 지적). 게다가 방향키는 Gapcursor 같은 다른 확장도 잡으므로 키맵으로는
119
+ * 누가 먼저 가져갈지 확실하지 않다.
120
+ *
121
+ * DOM 포커스는 두 모드에서 똑같이 동작하고, 이벤트가 판 안에서 시작하므로 순서
122
+ * 다툼도 없다. 포커스 링이 곧 "지금 이게 키를 듣는다"는 표시가 된다.
123
+ */
124
+ frame.tabIndex = 0;
125
+ frame.setAttribute("role", "group");
126
+ frame.setAttribute("aria-label", "PDF 문서 — 좌우 방향키로 쪽 넘김");
108
127
  dom.appendChild(frame);
109
128
  // 리사이즈 핸들 (편집 가능 모드에서만)
110
129
  if (editor.isEditable) {
@@ -174,17 +193,78 @@ export const PdfBlock = TiptapNode.create({
174
193
  * 세로형에는 595px, 가로형에는 842px 을 준다 — 글자 크기가 같으니 맞는 결과다.
175
194
  * (폭 기준이던 시절 세로형이 부당하게 커 보이던 것이 이래서였다.)
176
195
  *
177
- * ⚠️ `폭맞춤`(칼럼 채우기)은 **일부러 뺐다.** 1378px 칼럼에서 세로형에 걸면 배율
178
- * 231%, 높이 1950px 로 화면 밖으로 나간다 — 저자 의도가 아니라 우연히 나오는 값이다.
196
+ * 그 옆에 **맞춤 두 가지**가 따로 있다. 배율은 "글자를 이만큼 크게"이고 맞춤은
197
+ * "이 상자에 딱 들어가게"라 축이 다르다 — 그래서 고정 px 이 아니라 **지시자**로
198
+ * 저장하고, 보는 쪽에서 그때그때 푼다:
199
+ *
200
+ * 너비 맞춤 `100%` — CSS 가 알아서 칼럼을 채운다. 별도 계산이 없다.
201
+ * 화면 맞춤 `fit` — 한 장이 창 높이에 들어가는 폭. 창마다 다르므로 JS 로 푼다.
202
+ *
203
+ * ⚠️ 이 둘을 **클릭 시점의 px 로 굳히면 안 된다.** 그러면 저자 화면에서만 맞고
204
+ * 좁은 화면으로 보는 사람에겐 아무 의미가 없다. 맞춤은 본디 보는 쪽 사정이다.
205
+ * (한때 칼럼 채우기를 아예 뺐던 건 그게 **기본 의미**여서였다 — 저자가 고르지도
206
+ * 않았는데 세로형이 배율 231%·높이 1950px 이 됐다. 이름 붙은 선택지로는 정당하다.)
179
207
  */
180
208
  const PRESET_IDLE = "pdf-preset";
181
209
  const PRESET_ACTIVE = "pdf-preset is-active";
182
210
  const ZOOMS = [0.5, 0.75, 1, 1.5];
211
+ /*
212
+ * 숫자만으로는 **무엇에 대한 %인지** 알 수 없다(사용자 지적). 칼럼 대비로 읽히기
213
+ * 쉬운데 실제로는 페이지 원본 대비다 — 호버 설명에서 그걸 먼저 말한다.
214
+ */
215
+ const ZOOM_TIPS = {
216
+ 0.5: "원본 크기의 50% — 썸네일. 글자는 읽기 어렵습니다",
217
+ 0.75: "원본 크기의 75% — 그림·도표 확인용",
218
+ 1: "원본 크기 그대로 (100%)",
219
+ 1.5: "원본 크기의 150% — 읽기 편한 크기",
220
+ };
221
+ /** 저장값이 이것이면 창 높이에 맞춰 푼다. */
222
+ const FIT_SCREEN = "fit";
223
+ /** 저장값이 이것이면 CSS 가 칼럼을 채운다. */
224
+ const FIT_WIDTH = "100%";
183
225
  const presetButtons = [];
184
- /** 페이지 원본 폭(CSS px). PDF 를 읽기 전에는 모른다. */
226
+ /** 페이지 원본 크기(CSS px). PDF 를 읽기 전에는 모른다. */
185
227
  let naturalWidth = 0;
228
+ let naturalHeight = 0;
186
229
  /** 배율 → 저장할 px. 원본 폭을 아직 모르면 `null`. */
187
230
  const zoomToWidth = (zoom) => naturalWidth > 0 ? `${Math.round(naturalWidth * zoom)}px` : null;
231
+ /**
232
+ * 저장값을 실제 `width` 로 푼다.
233
+ *
234
+ * `fit` 만 계산이 필요하다 — 한 장이 창에 들어가려면 **높이**가 기준이므로
235
+ * `원본폭 × (쓸 수 있는 높이 / 원본높이)` 다. 82vh 는 위쪽 고정 크롬(정올 실측
236
+ * 113px)과 아래 여백을 뺀 어림값이고, 칼럼보다 넓어질 일은 `max-width: 100%` 가 막는다.
237
+ */
238
+ const applyWidth = (raw) => {
239
+ if (raw !== FIT_SCREEN) {
240
+ dom.style.width = raw || "";
241
+ return;
242
+ }
243
+ if (naturalWidth <= 0 || naturalHeight <= 0) {
244
+ dom.style.width = "";
245
+ return;
246
+ }
247
+ const usable = Math.max(240, window.innerHeight * 0.82);
248
+ dom.style.width = `${Math.round(naturalWidth * (usable / naturalHeight))}px`;
249
+ };
250
+ // `fit` 는 원본 크기를 읽은 뒤에야 풀린다 — 로드 후 다시 부른다.
251
+ applyWidth(node.attrs.width ?? null);
252
+ /*
253
+ * 창 높이가 바뀌면 `화면에 맞춤` 만 다시 푼다. 다른 값은 창과 무관하고,
254
+ * 폭 변화는 `ResizeObserver` 가 이미 잡는다.
255
+ */
256
+ const onViewportResize = () => {
257
+ if (currentNode.attrs.width !== FIT_SCREEN)
258
+ return;
259
+ clearTimeout(resizeTimeout);
260
+ resizeTimeout = setTimeout(() => {
261
+ if (destroyed)
262
+ return;
263
+ applyWidth(FIT_SCREEN);
264
+ renderPage();
265
+ }, 100);
266
+ };
267
+ window.addEventListener("resize", onViewportResize);
188
268
  if (editor.isEditable) {
189
269
  /*
190
270
  * ⚠️ 프리셋은 **문서에 저장되는 크기**이지 보기 상태가 아니다. 옆의 다운로드·새 탭과
@@ -195,7 +275,8 @@ export const PdfBlock = TiptapNode.create({
195
275
  const btn = document.createElement("button");
196
276
  btn.type = "button";
197
277
  btn.className = PRESET_IDLE;
198
- btn.title = `페이지 원본 크기의 ${label}`;
278
+ btn.dataset.tip = ZOOM_TIPS[zoom] ?? `원본 크기의 ${label}`;
279
+ btn.setAttribute("aria-label", btn.dataset.tip);
199
280
  btn.textContent = label;
200
281
  // 원본 폭을 알기 전(로딩 중)에는 누를 수 없다 — 계산할 근거가 없다.
201
282
  btn.disabled = true;
@@ -219,6 +300,42 @@ export const PdfBlock = TiptapNode.create({
219
300
  endCluster.appendChild(btn);
220
301
  presetButtons.push({ zoom, el: btn });
221
302
  }
303
+ /*
304
+ * 맞춤 둘은 **아이콘**이다. 배율 칩 넷에 글자 칩 둘을 더하면 가장 좁은 50%
305
+ * (세로형 298px)에서 줄이 넘친다 — 아이콘이면 칩 하나 값이면 된다.
306
+ */
307
+ const FITS = [
308
+ {
309
+ value: FIT_WIDTH,
310
+ tip: "너비에 맞춤 — 본문 너비를 가득 채웁니다",
311
+ paths: '<polyline points="18 8 22 12 18 16"/><polyline points="6 8 2 12 6 16"/><line x1="2" x2="22" y1="12" y2="12"/>'
312
+ },
313
+ {
314
+ value: FIT_SCREEN,
315
+ tip: "화면에 맞춤 — 한 쪽이 화면에 들어오는 크기",
316
+ paths: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>'
317
+ }
318
+ ];
319
+ for (const fit of FITS) {
320
+ const btn = document.createElement("button");
321
+ btn.type = "button";
322
+ btn.className = "pdf-ctl pdf-fit";
323
+ btn.dataset.tip = fit.tip;
324
+ btn.setAttribute("aria-label", fit.tip);
325
+ btn.innerHTML = icon(fit.paths);
326
+ btn.disabled = true;
327
+ btn.addEventListener("mousedown", (e) => {
328
+ e.preventDefault();
329
+ e.stopPropagation();
330
+ });
331
+ btn.addEventListener("click", (e) => {
332
+ e.preventDefault();
333
+ e.stopPropagation();
334
+ setAttrs({ width: fit.value });
335
+ });
336
+ endCluster.appendChild(btn);
337
+ presetButtons.push({ value: fit.value, el: btn });
338
+ }
222
339
  const divider = document.createElement("div");
223
340
  divider.className = "pdf-divider";
224
341
  endCluster.appendChild(divider);
@@ -226,17 +343,27 @@ export const PdfBlock = TiptapNode.create({
226
343
  /**
227
344
  * 현재 `width` 와 같은 프리셋을 눌린 상태로 만든다.
228
345
  *
229
- * ⚠️ 문자열 비교로는 안 된다. 저장값이 `"893px"` 처럼 계산된 px 라 반올림 오차가
230
- * 있고, 손잡이로 끌면 아무 px 이나 들어온다. 1px 여유로 견준다.
346
+ * ⚠️ 배율 칩은 문자열 비교로는 안 된다. 저장값이 `"893px"` 처럼 계산된 px 라 반올림
347
+ * 오차가 있고, 손잡이로 끌면 아무 px 이나 들어온다. 1px 여유로 견준다.
348
+ * 맞춤 둘은 지시자라 그냥 같은 값인지만 본다.
349
+ *
231
350
  * 옛 문서의 `"50%"` 같은 값은 어느 칩과도 안 맞는다 — **그대로 두는 게 맞다.**
232
- * 저자가 다시 고를 때까지 예전 크기로 렌더되고, 스키마는 건드리지 않는다.
351
+ * (`"100%"` 만은 이제 `너비에 맞춤` 과 같은 값이라 자연스럽게 눌린 것으로 잡힌다.)
233
352
  */
234
353
  const syncPresets = (width) => {
235
354
  const px = width?.endsWith("px") ? Number.parseFloat(width) : NaN;
236
- for (const { zoom, el } of presetButtons) {
237
- const target = naturalWidth > 0 ? naturalWidth * zoom : NaN;
238
- const on = Number.isFinite(px) && Math.abs(px - target) <= 1;
239
- el.className = on ? PRESET_ACTIVE : PRESET_IDLE;
355
+ for (const { zoom, value, el } of presetButtons) {
356
+ const on = value !== undefined
357
+ ? width === value
358
+ : Number.isFinite(px) &&
359
+ naturalWidth > 0 &&
360
+ Math.abs(px - naturalWidth * zoom) <= 1;
361
+ const base = value !== undefined ? "pdf-ctl pdf-fit" : PRESET_IDLE;
362
+ el.className = on
363
+ ? value !== undefined
364
+ ? `${base} is-active`
365
+ : PRESET_ACTIVE
366
+ : base;
240
367
  el.setAttribute("aria-pressed", on ? "true" : "false");
241
368
  }
242
369
  };
@@ -245,7 +372,8 @@ export const PdfBlock = TiptapNode.create({
245
372
  downloadLink.rel = "noopener noreferrer";
246
373
  downloadLink.setAttribute("download", cleanName(node.attrs.name));
247
374
  downloadLink.className = "pdf-ctl";
248
- downloadLink.title = "\uB2E4\uC6B4\uB85C\uB4DC";
375
+ downloadLink.dataset.tip = "\uB2E4\uC6B4\uB85C\uB4DC";
376
+ downloadLink.setAttribute("aria-label", "\uB2E4\uC6B4\uB85C\uB4DC");
249
377
  downloadLink.innerHTML = icon('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>');
250
378
  downloadLink.addEventListener("click", async (e) => {
251
379
  if (!downloadLink.href)
@@ -279,14 +407,16 @@ export const PdfBlock = TiptapNode.create({
279
407
  openLink.target = "_blank";
280
408
  openLink.rel = "noopener noreferrer";
281
409
  openLink.className = "pdf-ctl";
282
- openLink.title = "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30";
410
+ openLink.dataset.tip = "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30 \u2014 \uBE0C\uB77C\uC6B0\uC800 \uBDF0\uC5B4\uB85C \uD06C\uAC8C \uBD05\uB2C8\uB2E4";
411
+ openLink.setAttribute("aria-label", "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30");
283
412
  openLink.innerHTML = icon('<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>');
284
413
  endCluster.appendChild(openLink);
285
414
  if (editor.isEditable) {
286
415
  const deleteBtn = document.createElement("button");
287
416
  deleteBtn.type = "button";
288
417
  deleteBtn.className = "pdf-ctl pdf-ctl-danger";
289
- deleteBtn.title = "\uC0AD\uC81C";
418
+ deleteBtn.dataset.tip = "\uC0AD\uC81C";
419
+ deleteBtn.setAttribute("aria-label", "\uC0AD\uC81C");
290
420
  deleteBtn.innerHTML = icon('<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>');
291
421
  deleteBtn.addEventListener("click", () => {
292
422
  const pos = getPos();
@@ -410,6 +540,69 @@ export const PdfBlock = TiptapNode.create({
410
540
  e.stopPropagation();
411
541
  goToPage(currentPage + (e.key === "ArrowLeft" ? -1 : 1));
412
542
  });
543
+ /*
544
+ * 블록을 클릭해 선택한 뒤의 ← → (확장의 `addKeyboardShortcuts` 가 보낸다).
545
+ * 편집 모드에서 포커스가 ProseMirror 에 있을 때의 경로다.
546
+ * ⚠️ **한 장짜리면 손대지 않는다** — `preventDefault` 를 안 하면 확장이 `false` 를
547
+ * 돌려주고, 화살표는 원래대로 블록 밖으로 빠져나가는 데 쓰인다.
548
+ */
549
+ dom.addEventListener(PAGE_EVENT, (e) => {
550
+ if (totalPages <= 1)
551
+ return;
552
+ const delta = e.detail?.delta ?? 0;
553
+ if (!delta)
554
+ return;
555
+ e.preventDefault();
556
+ goToPage(currentPage + delta);
557
+ });
558
+ /* 판에 포커스가 있을 때의 ← → — 읽기 화면을 포함해 두 모드 모두 여기로 온다. */
559
+ frame.addEventListener("keydown", (e) => {
560
+ // 조작부의 입력칸(쪽 번호)에서는 캐럿 이동이 우선이다.
561
+ if (e.target instanceof HTMLInputElement)
562
+ return;
563
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
564
+ return;
565
+ if (totalPages <= 1)
566
+ return;
567
+ e.preventDefault();
568
+ e.stopPropagation();
569
+ goToPage(currentPage + (e.key === "ArrowLeft" ? -1 : 1));
570
+ });
571
+ /*
572
+ * **클릭이 곧 포커스**다 — 사용자가 기대하는 "한 번 누르고 방향키". 두 모드 모두
573
+ * 같은 방식이라야 동작을 설명할 수 있다.
574
+ *
575
+ * ⚠️ 조작부 버튼을 누를 때는 하지 않는다. 그 버튼들이 스스로 포커스를 가져가고,
576
+ * 여기서 뺏으면 연달아 누르기가 어긋난다.
577
+ */
578
+ frame.addEventListener("pointerdown", (e) => {
579
+ if (e.target instanceof Element && e.target.closest("[data-pdf-control]")) {
580
+ return;
581
+ }
582
+ frame.focus({ preventScroll: true });
583
+ });
584
+ /*
585
+ * 편집 모드에서 판이 포커스를 쥐면 ProseMirror 는 포커스를 잃는다. 그러면
586
+ * **Backspace 로 블록을 지우는 길이 막히므로** 여기서 받아 준다.
587
+ * (읽기 모드에서는 지울 것이 없으니 손대지 않는다.)
588
+ */
589
+ if (editor.isEditable) {
590
+ frame.addEventListener("keydown", (e) => {
591
+ if (e.target instanceof HTMLInputElement)
592
+ return;
593
+ if (e.key !== "Backspace" && e.key !== "Delete")
594
+ return;
595
+ e.preventDefault();
596
+ const pos = getPos();
597
+ if (pos == null)
598
+ return;
599
+ editor
600
+ .chain()
601
+ .focus()
602
+ .deleteRange({ from: pos, to: pos + currentNode.nodeSize })
603
+ .run();
604
+ });
605
+ }
413
606
  /*
414
607
  * ⚠️ **그리는 중에 들어온 요청을 버리면 안 된다.** 예전엔 `if (rendering) return` 으로
415
608
  * 그냥 흘려보냈는데, 폭 프리셋이나 창 크기 변경이 **직전 렌더가 끝나기 전에** 오면
@@ -484,9 +677,13 @@ export const PdfBlock = TiptapNode.create({
484
677
  const firstPage = await pdfDoc.getPage(1);
485
678
  if (destroyed)
486
679
  return;
487
- naturalWidth = firstPage.getViewport({ scale: 1 }).width;
680
+ const vp1 = firstPage.getViewport({ scale: 1 });
681
+ naturalWidth = vp1.width;
682
+ naturalHeight = vp1.height;
488
683
  for (const { el } of presetButtons)
489
684
  el.disabled = false;
685
+ // `fit` 는 여기서 처음으로 풀린다.
686
+ applyWidth(currentNode.attrs.width ?? null);
490
687
  syncPresets(currentNode.attrs.width ?? null);
491
688
  statusDiv.style.display = "none";
492
689
  canvas.style.display = "";
@@ -558,7 +755,7 @@ export const PdfBlock = TiptapNode.create({
558
755
  const newWidth = updatedNode.attrs.width;
559
756
  const changed = newWidth !== currentNode.attrs.width;
560
757
  if (changed)
561
- dom.style.width = newWidth || "";
758
+ applyWidth(newWidth);
562
759
  currentNode = updatedNode;
563
760
  syncPresets(newWidth ?? null);
564
761
  /*
@@ -594,11 +791,39 @@ export const PdfBlock = TiptapNode.create({
594
791
  destroyed = true;
595
792
  clearTimeout(resizeTimeout);
596
793
  resizeObserver?.disconnect();
794
+ window.removeEventListener("resize", onViewportResize);
597
795
  detachResize?.();
598
796
  },
599
797
  };
600
798
  };
601
799
  },
800
+ /*
801
+ * ← → 로 쪽을 넘긴다. **한 번 클릭해 블록이 선택된 상태**에서만 듣는다 — 그래야
802
+ * 본문을 쓰다가 우연히 넘어가지 않고, 선택 테두리가 곧 "지금 이게 듣는다"는 표시가 된다.
803
+ *
804
+ * ⚠️ 처리하지 못했으면 반드시 `false` 를 돌려준다. atom 노드에서 좌우 화살표는
805
+ * ProseMirror 가 **블록 밖으로 빠져나가는 길**로 쓰는데, 무조건 삼키면 한 장짜리
806
+ * PDF 에 갇힌다. 노드뷰가 실제로 넘겼을 때만(`preventDefault`) 삼킨다.
807
+ */
808
+ addKeyboardShortcuts() {
809
+ const step = (delta) => () => {
810
+ const { state, view } = this.editor;
811
+ const sel = state.selection;
812
+ if (!(sel instanceof NodeSelection) || sel.node.type.name !== this.name) {
813
+ return false;
814
+ }
815
+ const dom = view.nodeDOM(sel.from);
816
+ if (!(dom instanceof HTMLElement))
817
+ return false;
818
+ const event = new CustomEvent(PAGE_EVENT, {
819
+ detail: { delta },
820
+ cancelable: true,
821
+ });
822
+ dom.dispatchEvent(event);
823
+ return event.defaultPrevented;
824
+ };
825
+ return { ArrowLeft: step(-1), ArrowRight: step(1) };
826
+ },
602
827
  addStorage() {
603
828
  return {};
604
829
  },
@@ -10,13 +10,17 @@ import { PdfBlock } from './PdfBlock';
10
10
  * 파일 맨 위로 끌어올려져서 보통 `const` 는 아직 초기화 전이다).
11
11
  */
12
12
  const PAGE = vi.hoisted(() => ({ w: 595.28, h: 841.89 }));
13
+ // 쪽수도 테스트마다 바꿀 수 있어야 한다(화살표가 마지막 장에서 어떻게 되는지 보려면).
14
+ const PAGES = vi.hoisted(() => ({ count: 3 }));
13
15
  const A4_WIDTH = 595.28;
14
16
  const A4_LANDSCAPE_WIDTH = 841.89;
15
17
  vi.mock('../utils/pdf', () => ({
16
18
  getPdfJs: async () => ({
17
19
  getDocument: () => ({
18
20
  promise: Promise.resolve({
19
- numPages: 3,
21
+ get numPages() {
22
+ return PAGES.count;
23
+ },
20
24
  getPage: async () => ({
21
25
  getViewport: ({ scale }) => ({
22
26
  width: PAGE.w * scale,
@@ -285,6 +289,100 @@ describe('PdfBlock extension', () => {
285
289
  const legacy = css.match(/\.ProseMirror-selectednode:not\(img\)[^{]*\{/)?.[0] ?? '';
286
290
  expect(legacy).toContain(":not([data-type='pdfBlock'])");
287
291
  });
292
+ /*
293
+ * ⚠️ 맞춤 둘은 **고정 px 로 굳히면 안 된다.** 그러면 저자 화면에서만 맞고 좁은
294
+ * 화면으로 보는 사람에겐 의미가 없다. 지시자로 저장하고 보는 쪽에서 푼다.
295
+ */
296
+ it('stores the fit modes as directives, not a frozen pixel size', async () => {
297
+ const { overlay } = await mountLoaded();
298
+ const width = () => editor
299
+ .getJSON()
300
+ .content?.find((n) => n.type === 'pdfBlock')
301
+ ?.attrs?.width;
302
+ const fit = (name) => [...overlay.querySelectorAll('.pdf-fit')].find((b) => b.dataset.tip?.startsWith(name)).click();
303
+ fit('너비에 맞춤');
304
+ expect(width()).toBe('100%');
305
+ fit('화면에 맞춤');
306
+ expect(width()).toBe('fit');
307
+ });
308
+ /*
309
+ * 숫자·아이콘만으로는 무엇을 하는지 알 수 없다 — 호버 설명이 붙어 있어야 한다.
310
+ * (툴바 툴팁은 예전에 걷어냈으므로, 규칙이 `.pdf-overlay` 안에만 있는지도 함께 본다.)
311
+ */
312
+ it('explains the size controls on hover', async () => {
313
+ const { overlay } = await mountLoaded();
314
+ const tips = [...overlay.querySelectorAll('[data-tip]')].map((b) => b.dataset.tip ?? '');
315
+ expect(tips.some((t) => t.includes('원본 크기의 50%'))).toBe(true);
316
+ expect(tips.some((t) => t.startsWith('화면에 맞춤 —'))).toBe(true);
317
+ // 네이티브 툴팁과 겹치면 두 개가 뜬다.
318
+ expect(overlay.querySelector('.pdf-preset[title]')).toBeNull();
319
+ expect(overlay.querySelector('.pdf-fit[title]')).toBeNull();
320
+ const css = readFileSync('src/styles/editor.css', 'utf-8');
321
+ expect(css).toContain('.pdf-overlay [data-tip]::after');
322
+ expect(css).not.toContain('\n[data-tip]::after');
323
+ });
324
+ /* `fit` 은 CSS 로 못 쓰는 값이라 실제 폭으로 풀려 있어야 한다. */
325
+ it('resolves fit-to-screen into a real width', async () => {
326
+ await mountLoaded('<div data-pdf-src="https://example.com/doc.pdf" data-pdf-width="fit"></div>');
327
+ const block = editor.view.dom.querySelector('[data-type="pdfBlock"]');
328
+ expect(block.style.width).not.toBe('fit');
329
+ expect(block.style.width).toMatch(/^\d+px$/);
330
+ // 한 장이 창에 들어가야 하므로 높이 기준으로 나온 폭이다.
331
+ const usable = Math.max(240, window.innerHeight * 0.82);
332
+ expect(Number.parseFloat(block.style.width)).toBeCloseTo(A4_WIDTH * (usable / 841.89), 0);
333
+ });
334
+ /*
335
+ * 블록을 골라 두면 ← → 로 쪽을 넘긴다. 본문을 쓰다 우연히 넘어가면 안 되므로
336
+ * **선택돼 있을 때만** 듣는다.
337
+ */
338
+ it('turns pages with the arrow keys once the block is selected', async () => {
339
+ const { overlay } = await mountLoaded();
340
+ const counter = () => overlay.querySelector('.pdf-page')?.textContent;
341
+ expect(counter()).toBe('1 / 3');
342
+ editor.commands.setNodeSelection(0);
343
+ editor.view.someProp('handleKeyDown', (f) => f(editor.view, new KeyboardEvent('keydown', { key: 'ArrowRight' })));
344
+ expect(counter()).toBe('2 / 3');
345
+ editor.view.someProp('handleKeyDown', (f) => f(editor.view, new KeyboardEvent('keydown', { key: 'ArrowLeft' })));
346
+ expect(counter()).toBe('1 / 3');
347
+ });
348
+ /*
349
+ * ⚠️ 처리하지 못한 화살표는 **삼키면 안 된다.** atom 노드에서 좌우 화살표는
350
+ * 블록 밖으로 빠져나가는 길이라, 한 장짜리 PDF 에서 삼키면 갇힌다.
351
+ */
352
+ it('lets the arrow key through when there is nothing to turn to', async () => {
353
+ PAGES.count = 1;
354
+ await mountLoaded();
355
+ editor.commands.setNodeSelection(0);
356
+ const handled = editor.view.someProp('handleKeyDown', (f) => f(editor.view, new KeyboardEvent('keydown', { key: 'ArrowRight' })));
357
+ expect(handled).toBeFalsy();
358
+ PAGES.count = 3;
359
+ });
360
+ /*
361
+ * ⚠️ 정작 방향키가 필요한 곳은 **읽는 화면**이다. 거기엔 노드 선택도, 선택 테두리도
362
+ * 없으므로 키맵에 얹으면 성립하지 않는다 — 판이 포커스를 받아 직접 듣는다.
363
+ */
364
+ it('turns pages from the frame in read mode, where there is no selection', async () => {
365
+ const { frame, overlay } = await mountLoaded(PDF_HTML, false);
366
+ const counter = () => overlay.querySelector('.pdf-page')?.textContent;
367
+ expect(counter()).toBe('1 / 3');
368
+ // 읽기 화면에서는 클릭이 곧 포커스다.
369
+ frame.dispatchEvent(new Event('pointerdown', { bubbles: true }));
370
+ expect(document.activeElement).toBe(frame);
371
+ const arrow = (key) => frame.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
372
+ arrow('ArrowRight');
373
+ expect(counter()).toBe('2 / 3');
374
+ arrow('ArrowLeft');
375
+ expect(counter()).toBe('1 / 3');
376
+ });
377
+ /* 포커스를 받으려면 애초에 포커스 대상이어야 한다. */
378
+ it('makes the frame reachable by keyboard', async () => {
379
+ const { frame } = await mountLoaded(PDF_HTML, false);
380
+ expect(frame.tabIndex).toBe(0);
381
+ expect(frame.getAttribute('aria-label')).toContain('방향키');
382
+ const css = readFileSync('src/styles/editor.css', 'utf-8');
383
+ const rule = css.match(/\.pdf-frame:focus\s*\{[^}]*\}/)?.[0] ?? '';
384
+ expect(rule).toContain('var(--primary)');
385
+ });
288
386
  it('marks the preset matching the stored width', async () => {
289
387
  const { overlay } = await mountLoaded(`<div data-pdf-src="https://example.com/doc.pdf" data-pdf-width="${Math.round(A4_WIDTH * 0.75)}px"></div>`);
290
388
  const active = overlay.querySelector('.pdf-preset.is-active');
@@ -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-2{margin-top:.5rem;margin-bottom:.5rem}.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-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.\!table{display:table!important}.table{display:table}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-px{height:1px}.min-h-\[52px\]{min-height:52px}.w-10{width:2.5rem}.w-\[360px\]{width:360px}.w-\[420px\]{width:420px}.w-\[440px\]{width:440px}.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))}.\!transform{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))!important}.transform{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}.resize-y{resize:vertical}.resize{resize:both}.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}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.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)}.bg-background{background-color:var(--background)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-border{background-color:var(--border)}.bg-popover{background-color:var(--popover)}.bg-transparent{background-color:transparent}.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}.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-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[11px\]{font-size:11px}.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}.text-destructive{color:var(--destructive)}.text-muted-foreground{color:var(--muted-foreground)}.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-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.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)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.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-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 var(--border)}.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}.bubble-toolbar-container .hce-btn-primary,.hce-editor-wrapper .hce-btn-primary,.slash-menu .hce-btn-primary{background-color:var(--primary);color:var(--primary-foreground,#fff)}.tiptap{font-size:1.0625rem;line-height:1.8;color:var(--foreground);padding:1.5rem}.tiptap>*+*{margin-top:.25em}.tiptap [data-type=column]>:first-child,.tiptap [data-type=detailsContent]>:first-child,.tiptap blockquote>:first-child,.tiptap li>:first-child,.tiptap td>:first-child,.tiptap th>:first-child,.tiptap>:first-child{margin-top:0}.tiptap [data-type=column]>:last-child,.tiptap [data-type=detailsContent]>:last-child,.tiptap blockquote>:last-child,.tiptap li>:last-child,.tiptap td>:last-child,.tiptap th>:last-child,.tiptap>:last-child{margin-bottom:0}.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;border-radius:8px;margin:.75em 0;cursor:pointer;transition:box-shadow .15s}.pdf-frame{position:relative;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:8px;background:var(--muted)}.pdf-frame canvas{display:block;max-width:100%;border-radius:7px}.pdf-overlay{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .15s ease;-moz-user-select:none;user-select:none;-webkit-user-select:none}.pdf-overlay .pdf-page-input{-moz-user-select:text;user-select:text;-webkit-user-select:text}.pdf-frame:hover .pdf-overlay,.pdf-overlay:focus-within,.tiptap[contenteditable=true] [data-type=pdfBlock].ProseMirror-selectednode .pdf-overlay{opacity:1}@media (hover:none){.pdf-overlay{opacity:1}}@media (prefers-reduced-motion:reduce){.pdf-overlay{transition:none}}.pdf-cluster{position:absolute;display:flex;align-items:center;gap:4px;color:#fff;pointer-events:auto;max-width:calc(100% - 12px)}.pdf-cluster-end{top:6px;right:8px}.pdf-cluster-page{bottom:6px;left:50%;transform:translateX(-50%)}.pdf-nav-rail{position:sticky;top:0;height:100vh;max-height:100%;pointer-events:none}.pdf-nav{position:absolute;top:50%;transform:translateY(-50%);pointer-events:auto}.pdf-nav-prev{left:12px}.pdf-nav-next{right:12px}.pdf-overlay .pdf-ctl,.pdf-overlay .pdf-page,.pdf-overlay .pdf-preset{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:0;background:transparent;padding:0;color:#fff;font-family:inherit;cursor:pointer;opacity:.82;transition:opacity .12s}.pdf-overlay .pdf-ctl{width:22px;height:22px;flex:none;filter:drop-shadow(0 0 1px #000) drop-shadow(0 0 1px #000) drop-shadow(0 0 1px rgb(0 0 0/.85)) drop-shadow(0 1px 2px rgb(0 0 0/.55))}.pdf-overlay .pdf-ctl svg{width:15px;height:15px;stroke-width:2.25}.pdf-overlay .pdf-page,.pdf-overlay .pdf-preset{font-weight:600;text-shadow:0 0 2px #000,0 0 2px #000,0 0 3px rgba(0,0,0,.85),0 1px 2px rgba(0,0,0,.6)}.pdf-overlay .pdf-ctl:hover,.pdf-overlay .pdf-page:hover,.pdf-overlay .pdf-preset:hover{opacity:1}.pdf-overlay .pdf-ctl:disabled{opacity:.4;cursor:default}.pdf-overlay .pdf-ctl-danger:hover{opacity:1;color:#ff8f8f}.pdf-overlay .pdf-ctl:focus-visible,.pdf-overlay .pdf-page-input:focus-visible,.pdf-overlay .pdf-page:focus-visible,.pdf-overlay .pdf-preset:focus-visible{outline:2px solid hsla(0,0%,100%,.85);outline-offset:2px;border-radius:4px;opacity:1}.pdf-overlay .pdf-preset{height:18px;font-size:11px;line-height:1;font-variant-numeric:tabular-nums;flex:none}.pdf-overlay .pdf-preset.is-active,.pdf-overlay .pdf-preset.is-active:hover{opacity:1;font-weight:700;box-shadow:inset 0 -2px 0 -1px currentColor}.pdf-overlay .pdf-page{height:18px;font-size:12px;line-height:1;font-variant-numeric:tabular-nums}.pdf-overlay .pdf-nav{width:28px;height:28px}.pdf-overlay .pdf-nav svg{width:22px;height:22px;stroke-width:2.5}.pdf-overlay .pdf-nav:disabled{opacity:.4}.pdf-divider{flex:none;width:1px;height:12px;margin:0 2px;background:hsla(0,0%,100%,.55);box-shadow:0 0 2px rgba(0,0,0,.9)}.pdf-overlay .pdf-page-input{box-sizing:border-box;height:20px;width:46px;padding:0 5px;font-size:12px;text-align:center;font-variant-numeric:tabular-nums;background:rgba(24,24,27,.85);border:1px solid hsla(0,0%,100%,.5);color:#fff;border-radius:4px;font-family:inherit;outline:none}.pdf-status{display:flex;align-items:center;justify-content:center;gap:8px;padding:2.5rem 1rem;font-size:.875rem;color:var(--muted-foreground);text-align:center}.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:30px 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:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted-foreground);border-radius:4px;transition:transform .2s,color .15s,background-color .15s;flex-shrink:0;margin-top:14px;margin-left:4px}.tiptap [data-type=details]>button:hover{background:color-mix(in srgb,var(--foreground) 8%,transparent)}.tiptap [data-type=details]>button:after{content:"";display:block;width:0;height:0;border-left:7px solid;border-top:5.5px solid transparent;border-bottom:5.5px 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=details] summary[data-level="1"],.tiptap details summary[data-level="1"]{font-size:2rem;font-weight:700;line-height:1.25}.tiptap [data-type=details] summary[data-level="2"],.tiptap details summary[data-level="2"]{font-size:1.5rem;font-weight:600;line-height:1.3}.tiptap [data-type=details] summary[data-level="3"],.tiptap details summary[data-level="3"]{font-size:1.25rem;font-weight:600;line-height:1.35}.tiptap [data-type=details]:has(>div>summary[data-level="1"])>button{margin-top:19px}.tiptap [data-type=details]:has(>div>summary[data-level="2"])>button{margin-top:15px}.tiptap [data-type=details]:has(>div>summary[data-level="3"])>button{margin-top:13px}.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:7px solid var(--muted-foreground);border-top:5.5px solid transparent;border-bottom:5.5px 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:-1px;bottom:-1px;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]):not([data-type=mbusVideo]):not([data-type=videoEmbed]):before,.ProseMirror-selectednoderange:not([data-type=pdfBlock]):not([data-type=mbusVideo]):not([data-type=videoEmbed]):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}.tiptap[contenteditable=true] [data-type=mbusVideo].ProseMirror-selectednode,.tiptap[contenteditable=true] [data-type=pdfBlock].ProseMirror-selectednode,.tiptap[contenteditable=true] [data-type=videoEmbed].ProseMirror-selectednode,.tiptap[contenteditable=true] img.ProseMirror-selectednode{outline:1px solid var(--primary);outline-offset:2px;border-radius:8px}.hce-table-bubble{z-index:var(--hce-menu-z,100)}.bubble-toolbar-container{position:absolute;top:0;left:0;width:-moz-max-content;width:max-content;z-index:calc(var(--hce-menu-z, 100) + 1)}.hce-toolbar:has(.absolute)~.bubble-toolbar-container,.hce-toolbar:has(.absolute)~.hce-table-bubble{display:none}.hce-menu-surface{background-color:var(--hce-menu-surface,#333)}.hce-floating-panel{background-color:color-mix(in srgb,var(--foreground) 8%,var(--popover,var(--background,#fff)));border:1px solid color-mix(in srgb,var(--foreground) 18%,var(--border));box-shadow:0 8px 24px color-mix(in srgb,var(--foreground) 16%,transparent),0 2px 6px color-mix(in srgb,var(--foreground) 10%,transparent)}.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-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-table-btn-wrap.hce-tooltip-off:hover .hce-table-btn-tooltip{opacity:0}.tiptap math-inline,math-inline.math-node{display:inline-block;vertical-align:baseline}.tiptap math-display,math-display.math-node{display:block;margin:.9em 0;text-align:center}.math-node,.tiptap .math-node{font-size:.95em}.tiptap math-inline.math-node .katex,math-inline.math-node .katex{font-size:1.0285em}.tiptap[contenteditable=true] .math-node{cursor:pointer;border-radius:4px;transition:background-color .12s}.tiptap[contenteditable=true] .math-node:hover{background-color:color-mix(in srgb,var(--primary) 10%,transparent)}.tiptap .math-node.ProseMirror-selectednode{background-color:color-mix(in srgb,var(--primary) 18%,transparent);outline:1px solid color-mix(in srgb,var(--primary) 45%,transparent)}.tiptap .math-node.empty-math .math-placeholder{font-size:.85em;padding:0 .35em;color:var(--muted-foreground);border:1px dashed var(--border);border-radius:4px}.math-node .math-error,.tiptap .math-error{color:#dc2626;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.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}}.hce-active{background-color:color-mix(in srgb,var(--primary) 12%,transparent);color:var(--primary)}.hce-active:hover{background-color:color-mix(in srgb,var(--primary) 18%,transparent)}.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\: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\: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)}
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-2{margin-top:.5rem;margin-bottom:.5rem}.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-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.\!table{display:table!important}.table{display:table}.grid{display:grid}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-px{height:1px}.min-h-\[52px\]{min-height:52px}.w-10{width:2.5rem}.w-\[360px\]{width:360px}.w-\[420px\]{width:420px}.w-\[440px\]{width:440px}.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))}.\!transform{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))!important}.transform{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}.resize-y{resize:vertical}.resize{resize:both}.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}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.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)}.bg-background{background-color:var(--background)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-border{background-color:var(--border)}.bg-popover{background-color:var(--popover)}.bg-transparent{background-color:transparent}.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}.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-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[11px\]{font-size:11px}.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}.text-destructive{color:var(--destructive)}.text-muted-foreground{color:var(--muted-foreground)}.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-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.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)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.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-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 var(--border)}.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}.bubble-toolbar-container .hce-btn-primary,.hce-editor-wrapper .hce-btn-primary,.slash-menu .hce-btn-primary{background-color:var(--primary);color:var(--primary-foreground,#fff)}.tiptap{font-size:1.0625rem;line-height:1.8;color:var(--foreground);padding:1.5rem}.tiptap>*+*{margin-top:.25em}.tiptap [data-type=column]>:first-child,.tiptap [data-type=detailsContent]>:first-child,.tiptap blockquote>:first-child,.tiptap li>:first-child,.tiptap td>:first-child,.tiptap th>:first-child,.tiptap>:first-child{margin-top:0}.tiptap [data-type=column]>:last-child,.tiptap [data-type=detailsContent]>:last-child,.tiptap blockquote>:last-child,.tiptap li>:last-child,.tiptap td>:last-child,.tiptap th>:last-child,.tiptap>:last-child{margin-bottom:0}.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;border-radius:8px;margin:.75em 0;cursor:pointer;transition:box-shadow .15s}.pdf-frame{position:relative;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:8px;background:var(--muted)}.pdf-frame canvas{display:block;max-width:100%;border-radius:7px}.pdf-frame:focus{outline:1px solid var(--primary);outline-offset:2px}.pdf-overlay{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .15s ease;-moz-user-select:none;user-select:none;-webkit-user-select:none}.pdf-overlay .pdf-page-input{-moz-user-select:text;user-select:text;-webkit-user-select:text}.pdf-frame:hover .pdf-overlay,.pdf-overlay:focus-within,.tiptap[contenteditable=true] [data-type=pdfBlock].ProseMirror-selectednode .pdf-overlay{opacity:1}@media (hover:none){.pdf-overlay{opacity:1}}@media (prefers-reduced-motion:reduce){.pdf-overlay{transition:none}}.pdf-cluster{position:absolute;display:flex;align-items:center;gap:2px;padding:3px 5px;border-radius:999px;background:rgba(24,24,27,.42);backdrop-filter:blur(10px) saturate(1.1);color:#fff;pointer-events:auto;max-width:calc(100% - 12px);transition:background-color .15s ease}.pdf-cluster:hover{background:rgba(24,24,27,.62)}.pdf-cluster-end{top:6px;right:8px}.pdf-cluster-page{bottom:6px;left:50%;transform:translateX(-50%)}.pdf-nav-rail{position:sticky;top:0;height:100vh;max-height:100%;pointer-events:none}.pdf-nav{position:absolute;top:50%;transform:translateY(-50%);pointer-events:auto}.pdf-nav-prev{left:12px}.pdf-nav-next{right:12px}.pdf-overlay .pdf-ctl,.pdf-overlay .pdf-page,.pdf-overlay .pdf-preset{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:0;background:transparent;padding:0;color:#e4e4e7;font-family:inherit;cursor:pointer;opacity:.92;transition:opacity .12s,color .12s}.pdf-overlay .pdf-ctl{width:20px;height:20px;flex:none}.pdf-overlay .pdf-ctl svg{width:14px;height:14px}.pdf-overlay .pdf-page,.pdf-overlay .pdf-preset{font-weight:500}.pdf-overlay .pdf-ctl:hover,.pdf-overlay .pdf-page:hover,.pdf-overlay .pdf-preset:hover{opacity:1;color:#fff}.pdf-overlay .pdf-ctl:disabled{opacity:.4;cursor:default}.pdf-overlay .pdf-ctl-danger:hover{opacity:1;color:#ff8f8f}.pdf-overlay .pdf-ctl:focus-visible,.pdf-overlay .pdf-page-input:focus-visible,.pdf-overlay .pdf-page:focus-visible,.pdf-overlay .pdf-preset:focus-visible{outline:2px solid hsla(0,0%,100%,.85);outline-offset:2px;border-radius:4px;opacity:1}.pdf-overlay .pdf-preset{height:18px;font-size:11px;line-height:1;font-variant-numeric:tabular-nums;flex:none}.pdf-overlay .pdf-preset.is-active,.pdf-overlay .pdf-preset.is-active:hover{opacity:1;color:#fff;font-weight:600;box-shadow:inset 0 -1px 0 0 currentColor}.pdf-overlay .pdf-fit{width:19px;height:18px;border-radius:3px}.pdf-overlay .pdf-fit svg{width:13px;height:13px}.pdf-overlay .pdf-fit.is-active,.pdf-overlay .pdf-fit.is-active:hover{opacity:1;color:#fff;box-shadow:inset 0 -1px 0 0 currentColor}.pdf-overlay [data-tip]{position:relative}.pdf-overlay [data-tip]:after{content:attr(data-tip);position:absolute;top:calc(100% + 7px);right:0;z-index:1;white-space:nowrap;font-size:11px;line-height:1.4;font-weight:400;padding:4px 8px;border-radius:5px;background:rgba(24,24,27,.94);backdrop-filter:blur(6px);color:#fff;box-shadow:0 4px 12px rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .12s ease .35s}.pdf-overlay [data-tip]:focus-visible:after,.pdf-overlay [data-tip]:hover:after{opacity:1}.pdf-cluster-page [data-tip]:after{top:auto;bottom:calc(100% + 7px)}.pdf-nav-prev[data-tip]:after{right:auto;left:0}.pdf-overlay .pdf-page{height:16px;font-size:10px;line-height:1;font-variant-numeric:tabular-nums}.pdf-cluster-page{padding:2px 6px}.pdf-overlay .pdf-nav{width:30px;height:30px;padding:0;background:rgba(24,24,27,.42)}.pdf-overlay .pdf-nav:hover{background:rgba(24,24,27,.62)}.pdf-overlay .pdf-nav svg{width:18px;height:18px}.pdf-overlay .pdf-nav:disabled{opacity:.4}.pdf-divider{flex:none;width:1px;height:11px;margin:0 3px;background:hsla(0,0%,100%,.3)}.pdf-overlay .pdf-page-input{box-sizing:border-box;height:20px;width:46px;padding:0 5px;font-size:12px;text-align:center;font-variant-numeric:tabular-nums;background:rgba(24,24,27,.85);border:1px solid hsla(0,0%,100%,.5);color:#fff;border-radius:4px;font-family:inherit;outline:none}.pdf-status{display:flex;align-items:center;justify-content:center;gap:8px;padding:2.5rem 1rem;font-size:.875rem;color:var(--muted-foreground);text-align:center}.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:30px 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:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted-foreground);border-radius:4px;transition:transform .2s,color .15s,background-color .15s;flex-shrink:0;margin-top:14px;margin-left:4px}.tiptap [data-type=details]>button:hover{background:color-mix(in srgb,var(--foreground) 8%,transparent)}.tiptap [data-type=details]>button:after{content:"";display:block;width:0;height:0;border-left:7px solid;border-top:5.5px solid transparent;border-bottom:5.5px 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=details] summary[data-level="1"],.tiptap details summary[data-level="1"]{font-size:2rem;font-weight:700;line-height:1.25}.tiptap [data-type=details] summary[data-level="2"],.tiptap details summary[data-level="2"]{font-size:1.5rem;font-weight:600;line-height:1.3}.tiptap [data-type=details] summary[data-level="3"],.tiptap details summary[data-level="3"]{font-size:1.25rem;font-weight:600;line-height:1.35}.tiptap [data-type=details]:has(>div>summary[data-level="1"])>button{margin-top:19px}.tiptap [data-type=details]:has(>div>summary[data-level="2"])>button{margin-top:15px}.tiptap [data-type=details]:has(>div>summary[data-level="3"])>button{margin-top:13px}.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:7px solid var(--muted-foreground);border-top:5.5px solid transparent;border-bottom:5.5px 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:-1px;bottom:-1px;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]):not([data-type=mbusVideo]):not([data-type=videoEmbed]):before,.ProseMirror-selectednoderange:not([data-type=pdfBlock]):not([data-type=mbusVideo]):not([data-type=videoEmbed]):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}.tiptap[contenteditable=true] [data-type=mbusVideo].ProseMirror-selectednode,.tiptap[contenteditable=true] [data-type=pdfBlock].ProseMirror-selectednode,.tiptap[contenteditable=true] [data-type=videoEmbed].ProseMirror-selectednode,.tiptap[contenteditable=true] img.ProseMirror-selectednode{outline:1px solid var(--primary);outline-offset:2px;border-radius:8px}.hce-table-bubble{z-index:var(--hce-menu-z,100)}.bubble-toolbar-container{position:absolute;top:0;left:0;width:-moz-max-content;width:max-content;z-index:calc(var(--hce-menu-z, 100) + 1)}.hce-toolbar:has(.absolute)~.bubble-toolbar-container,.hce-toolbar:has(.absolute)~.hce-table-bubble{display:none}.hce-menu-surface{background-color:var(--hce-menu-surface,#333)}.hce-floating-panel{background-color:color-mix(in srgb,var(--foreground) 8%,var(--popover,var(--background,#fff)));border:1px solid color-mix(in srgb,var(--foreground) 18%,var(--border));box-shadow:0 8px 24px color-mix(in srgb,var(--foreground) 16%,transparent),0 2px 6px color-mix(in srgb,var(--foreground) 10%,transparent)}.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-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-table-btn-wrap.hce-tooltip-off:hover .hce-table-btn-tooltip{opacity:0}.tiptap math-inline,math-inline.math-node{display:inline-block;vertical-align:baseline}.tiptap math-display,math-display.math-node{display:block;margin:.9em 0;text-align:center}.math-node,.tiptap .math-node{font-size:.95em}.tiptap math-inline.math-node .katex,math-inline.math-node .katex{font-size:1.0285em}.tiptap[contenteditable=true] .math-node{cursor:pointer;border-radius:4px;transition:background-color .12s}.tiptap[contenteditable=true] .math-node:hover{background-color:color-mix(in srgb,var(--primary) 10%,transparent)}.tiptap .math-node.ProseMirror-selectednode{background-color:color-mix(in srgb,var(--primary) 18%,transparent);outline:1px solid color-mix(in srgb,var(--primary) 45%,transparent)}.tiptap .math-node.empty-math .math-placeholder{font-size:.85em;padding:0 .35em;color:var(--muted-foreground);border:1px dashed var(--border);border-radius:4px}.math-node .math-error,.tiptap .math-error{color:#dc2626;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.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}}.hce-active{background-color:color-mix(in srgb,var(--primary) 12%,transparent);color:var(--primary)}.hce-active:hover{background-color:color-mix(in srgb,var(--primary) 18%,transparent)}.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\: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\: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)}
@@ -1,5 +1,5 @@
1
1
  /** 등록하는 언어 이름. 테스트가 이 목록으로 실제 등록분을 검사한다. */
2
- export declare const CODE_LANGUAGES: readonly ["cpp", "java", "python", "javascript", "typescript", "kotlin", "go", "csharp", "rust", "sql", "pgsql", "graphql", "xml", "css", "json", "yaml", "bash", "shell", "makefile", "ini", "diff"];
2
+ export declare const CODE_LANGUAGES: readonly ["cpp", "java", "python", "javascript", "typescript", "kotlin", "go", "csharp", "rust", "sql", "pgsql", "graphql", "xml", "css", "json", "yaml", "bash", "shell", "makefile", "ini", "diff", "plaintext"];
3
3
  export declare const lowlight: {
4
4
  highlight: (language: string, value: string, options?: Readonly<import("lowlight").Options> | null | undefined) => import("hast").Root;
5
5
  highlightAuto: (value: string, options?: Readonly<import("lowlight").AutoOptions> | null | undefined) => import("hast").Root;
@@ -1 +1 @@
1
- {"version":3,"file":"lowlight.d.ts","sourceRoot":"","sources":["../../src/utils/lowlight.ts"],"names":[],"mappings":"AAuDA,4CAA4C;AAC5C,eAAO,MAAM,cAAc,uMA0BjB,CAAC;AAEX,eAAO,MAAM,QAAQ;wDA3EL,CAAA;0CAGM,CAAC;;;;;;;;;;;CAwEiB,CAAC"}
1
+ {"version":3,"file":"lowlight.d.ts","sourceRoot":"","sources":["../../src/utils/lowlight.ts"],"names":[],"mappings":"AA+DA,4CAA4C;AAC5C,eAAO,MAAM,cAAc,oNA4BjB,CAAC;AAEX,eAAO,MAAM,QAAQ;wDArFL,CAAA;0CAGM,CAAC;;;;;;;;;;;CAkFiB,CAAC"}
@@ -17,10 +17,17 @@
17
17
  * · DB·질의 (3) hljs 에 있는 DB 계열은 이 셋이 전부다(`plsql`·`mongodb` 는 없다)
18
18
  * · 웹·데이터 (4) `xml` 이 HTML 을 겸한다
19
19
  * · 셸·설정 (5) 코드 예시에 딸려 오는 명령·설정 파일
20
+ * · 서식 없음 (1) `plaintext` — 아래 참조
20
21
  *
21
- * 스물하나를 다 넣어도 **40KB 대(gzip)** 다 — 묶음 export 는 398KB 였다. 크기 여유가
22
+ * 스물둘을 다 넣어도 **40KB 대(gzip)** 다 — 묶음 export 는 398KB 였다. 크기 여유가
22
23
  * 크므로 "혹시 쓸까" 싶은 것은 빼기보다 넣는 쪽이 낫다.
23
24
  *
25
+ * ⚠️ **`plaintext` 는 "색을 빼는" 언어다 — 없으면 오히려 칠해진다.** 등록되지 않은
26
+ * 이름은 자동 감지로 떨어지므로, 색을 빼려고 ```text 로 적은 블록에 엉뚱한 문법
27
+ * 색이 붙었다(예시 입출력·ASCII 그림). `plaintext` 는 `text`·`txt` 를 별칭으로
28
+ * 갖고 `disableAutodetect: true` 라, 등록해도 다른 언어의 감지 후보를 늘리지
29
+ * 않는다 — `c` 를 뺀 이유와 충돌하지 않는다.
30
+ *
24
31
  * ⚠️ **`c` 는 일부러 없다.** C++ 문법이 C 를 사실상 포함해 색이 거의 같은데, 자동 감지
25
32
  * 후보에 넣으면 relevance 를 과하게 먹어 **Python 코드까지 `c` 로 판정**했다(실측).
26
33
  * `language-c` 로 지정된 블록은 자동 감지로 떨어져 `cpp` 로 칠해진다 — 눈으로는 같다.
@@ -52,6 +59,7 @@ import shell from "highlight.js/lib/languages/shell";
52
59
  import makefile from "highlight.js/lib/languages/makefile";
53
60
  import ini from "highlight.js/lib/languages/ini";
54
61
  import diff from "highlight.js/lib/languages/diff";
62
+ import plaintext from "highlight.js/lib/languages/plaintext";
55
63
  /** 등록하는 언어 이름. 테스트가 이 목록으로 실제 등록분을 검사한다. */
56
64
  export const CODE_LANGUAGES = [
57
65
  // 채점·알고리즘
@@ -79,6 +87,8 @@ export const CODE_LANGUAGES = [
79
87
  "makefile",
80
88
  "ini",
81
89
  "diff",
90
+ // 서식 없음 (별칭 `text`·`txt`)
91
+ "plaintext",
82
92
  ];
83
93
  export const lowlight = createLowlight();
84
94
  lowlight.register({
@@ -103,4 +113,5 @@ lowlight.register({
103
113
  makefile,
104
114
  ini,
105
115
  diff,
116
+ plaintext,
106
117
  });
@@ -48,6 +48,20 @@ describe("코드 하이라이터 언어 목록", () => {
48
48
  expect(lowlight.registered("ts")).toBe(true);
49
49
  expect(lowlight.registered("c++")).toBe(true);
50
50
  });
51
+ /*
52
+ * ⚠️ 이게 빠지면 증상이 "색이 없다"가 아니라 **"엉뚱한 색이 붙는다"** 라 눈에 안 띈다.
53
+ * 등록되지 않은 이름은 자동 감지로 떨어지므로, 색을 빼려고 ```text 로 적은 예시
54
+ * 입출력에 문법 색이 칠해졌다. `disableAutodetect` 라 다른 언어의 감지는 안 건드린다.
55
+ */
56
+ it("plaintext 가 text·txt 별칭으로 등록돼 있다", () => {
57
+ for (const lang of ["plaintext", "text", "txt"]) {
58
+ expect(lowlight.registered(lang)).toBe(true);
59
+ }
60
+ });
61
+ it("plaintext 는 색을 입히지 않는다", () => {
62
+ const tree = lowlight.highlight("text", "3 1\n1 2 3");
63
+ expect(tree.children.every((node) => node.type === "text")).toBe(true);
64
+ });
51
65
  it("등록하지 않은 언어는 색이 없다(깨지지는 않는다)", () => {
52
66
  expect(lowlight.registered("ruby")).toBe(false);
53
67
  // 색을 못 입혀도 예외를 던지지 않고 평범한 텍스트로 남는지
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",