@teriusu/rich-editor 0.10.8 → 0.11.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.
Files changed (147) hide show
  1. package/dist/components/BubbleToolbar.svelte +40 -36
  2. package/dist/components/BubbleToolbar.svelte.d.ts +3 -0
  3. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  4. package/dist/components/EmojiSuggestionMenu.svelte +291 -0
  5. package/dist/components/EmojiSuggestionMenu.svelte.d.ts +9 -0
  6. package/dist/components/EmojiSuggestionMenu.svelte.d.ts.map +1 -0
  7. package/dist/components/FixedToolbar.svelte +53 -47
  8. package/dist/components/FixedToolbar.svelte.d.ts +3 -0
  9. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  10. package/dist/components/FloatingHelper.svelte +219 -0
  11. package/dist/components/FloatingHelper.svelte.d.ts +14 -0
  12. package/dist/components/FloatingHelper.svelte.d.ts.map +1 -0
  13. package/dist/components/InputModal.svelte +7 -3
  14. package/dist/components/InputModal.svelte.d.ts +3 -0
  15. package/dist/components/InputModal.svelte.d.ts.map +1 -1
  16. package/dist/components/MathModal.svelte +33 -29
  17. package/dist/components/MathModal.svelte.d.ts +3 -0
  18. package/dist/components/MathModal.svelte.d.ts.map +1 -1
  19. package/dist/components/MediaPickerModal.svelte +7 -3
  20. package/dist/components/MediaPickerModal.svelte.d.ts +3 -0
  21. package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -1
  22. package/dist/components/SlashCommandMenu.svelte +88 -72
  23. package/dist/components/SlashCommandMenu.svelte.d.ts +3 -0
  24. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  25. package/dist/components/TableBubbleMenu.svelte +41 -33
  26. package/dist/components/TableBubbleMenu.svelte.d.ts +3 -0
  27. package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
  28. package/dist/components/TipTapEditor.svelte +239 -127
  29. package/dist/components/TipTapEditor.svelte.d.ts +7 -0
  30. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  31. package/dist/extensions/CardBlock.d.ts.map +1 -1
  32. package/dist/extensions/CardBlock.js +6 -4
  33. package/dist/extensions/FileAttachment.d.ts.map +1 -1
  34. package/dist/extensions/FileAttachment.js +2 -1
  35. package/dist/extensions/FixedDetails.d.ts.map +1 -1
  36. package/dist/extensions/FixedDetails.js +5 -3
  37. package/dist/extensions/LegacyBlock.d.ts.map +1 -1
  38. package/dist/extensions/LegacyBlock.js +8 -6
  39. package/dist/extensions/Math.d.ts.map +1 -1
  40. package/dist/extensions/Math.js +5 -2
  41. package/dist/extensions/MbusVideo.d.ts.map +1 -1
  42. package/dist/extensions/MbusVideo.js +73 -9
  43. package/dist/extensions/MediaToolbar.d.ts +41 -0
  44. package/dist/extensions/MediaToolbar.d.ts.map +1 -0
  45. package/dist/extensions/MediaToolbar.js +196 -0
  46. package/dist/extensions/MediaToolbar.test.d.ts +2 -0
  47. package/dist/extensions/MediaToolbar.test.d.ts.map +1 -0
  48. package/dist/extensions/MediaToolbar.test.js +231 -0
  49. package/dist/extensions/OrderedListMarker.d.ts +2 -0
  50. package/dist/extensions/OrderedListMarker.d.ts.map +1 -0
  51. package/dist/extensions/OrderedListMarker.js +114 -0
  52. package/dist/extensions/OrderedListMarker.test.d.ts +2 -0
  53. package/dist/extensions/OrderedListMarker.test.d.ts.map +1 -0
  54. package/dist/extensions/OrderedListMarker.test.js +176 -0
  55. package/dist/extensions/PdfBlock.d.ts.map +1 -1
  56. package/dist/extensions/PdfBlock.js +25 -23
  57. package/dist/extensions/ResizableImage.d.ts.map +1 -1
  58. package/dist/extensions/ResizableImage.js +20 -1
  59. package/dist/extensions/TableKeymap.d.ts +16 -0
  60. package/dist/extensions/TableKeymap.d.ts.map +1 -0
  61. package/dist/extensions/TableKeymap.js +97 -0
  62. package/dist/extensions/TableKeymap.test.d.ts +2 -0
  63. package/dist/extensions/TableKeymap.test.d.ts.map +1 -0
  64. package/dist/extensions/TableKeymap.test.js +147 -0
  65. package/dist/extensions/TabsBlock.d.ts.map +1 -1
  66. package/dist/extensions/TabsBlock.js +12 -9
  67. package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
  68. package/dist/extensions/TiptapMidibus.js +60 -6
  69. package/dist/extensions/UploadSkeleton.d.ts +61 -0
  70. package/dist/extensions/UploadSkeleton.d.ts.map +1 -0
  71. package/dist/extensions/UploadSkeleton.js +233 -0
  72. package/dist/extensions/UploadSkeleton.test.d.ts +2 -0
  73. package/dist/extensions/UploadSkeleton.test.d.ts.map +1 -0
  74. package/dist/extensions/UploadSkeleton.test.js +128 -0
  75. package/dist/extensions/VideoEmbed.d.ts +8 -0
  76. package/dist/extensions/VideoEmbed.d.ts.map +1 -1
  77. package/dist/extensions/VideoEmbed.js +226 -45
  78. package/dist/extensions/VideoEmbed.test.js +131 -2
  79. package/dist/i18n/en.d.ts +3 -0
  80. package/dist/i18n/en.d.ts.map +1 -0
  81. package/dist/i18n/en.js +191 -0
  82. package/dist/i18n/es.d.ts +3 -0
  83. package/dist/i18n/es.d.ts.map +1 -0
  84. package/dist/i18n/es.js +191 -0
  85. package/dist/i18n/index.d.ts +43 -0
  86. package/dist/i18n/index.d.ts.map +1 -0
  87. package/dist/i18n/index.js +109 -0
  88. package/dist/i18n/index.test.d.ts +2 -0
  89. package/dist/i18n/index.test.d.ts.map +1 -0
  90. package/dist/i18n/index.test.js +173 -0
  91. package/dist/i18n/ja.d.ts +3 -0
  92. package/dist/i18n/ja.d.ts.map +1 -0
  93. package/dist/i18n/ja.js +191 -0
  94. package/dist/i18n/ko.d.ts +198 -0
  95. package/dist/i18n/ko.d.ts.map +1 -0
  96. package/dist/i18n/ko.js +215 -0
  97. package/dist/i18n/zh-hans.d.ts +3 -0
  98. package/dist/i18n/zh-hans.d.ts.map +1 -0
  99. package/dist/i18n/zh-hans.js +191 -0
  100. package/dist/i18n/zh-hant.d.ts +3 -0
  101. package/dist/i18n/zh-hant.d.ts.map +1 -0
  102. package/dist/i18n/zh-hant.js +192 -0
  103. package/dist/index.d.ts +7 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +13 -1
  106. package/dist/static/highlight.d.ts +13 -0
  107. package/dist/static/highlight.d.ts.map +1 -0
  108. package/dist/static/highlight.js +99 -0
  109. package/dist/static/index.d.ts +33 -0
  110. package/dist/static/index.d.ts.map +1 -0
  111. package/dist/static/index.js +367 -0
  112. package/dist/static/index.test.d.ts +2 -0
  113. package/dist/static/index.test.d.ts.map +1 -0
  114. package/dist/static/index.test.js +194 -0
  115. package/dist/static/tokenizer.d.ts +52 -0
  116. package/dist/static/tokenizer.d.ts.map +1 -0
  117. package/dist/static/tokenizer.js +289 -0
  118. package/dist/styles/editor.css +1 -1
  119. package/dist/utils/emoji-data.d.ts +15 -0
  120. package/dist/utils/emoji-suggestion.d.ts +48 -0
  121. package/dist/utils/emoji-suggestion.d.ts.map +1 -0
  122. package/dist/utils/emoji-suggestion.js +51 -0
  123. package/dist/utils/emoji-suggestion.test.d.ts +2 -0
  124. package/dist/utils/emoji-suggestion.test.d.ts.map +1 -0
  125. package/dist/utils/emoji-suggestion.test.js +117 -0
  126. package/dist/utils/floating-helper.d.ts +60 -0
  127. package/dist/utils/floating-helper.d.ts.map +1 -0
  128. package/dist/utils/floating-helper.js +63 -0
  129. package/dist/utils/floating-helper.test.d.ts +2 -0
  130. package/dist/utils/floating-helper.test.d.ts.map +1 -0
  131. package/dist/utils/floating-helper.test.js +141 -0
  132. package/dist/utils/guard-vscode-paste.d.ts +19 -0
  133. package/dist/utils/guard-vscode-paste.d.ts.map +1 -0
  134. package/dist/utils/guard-vscode-paste.js +46 -0
  135. package/dist/utils/guard-vscode-paste.test.d.ts +2 -0
  136. package/dist/utils/guard-vscode-paste.test.d.ts.map +1 -0
  137. package/dist/utils/guard-vscode-paste.test.js +86 -0
  138. package/dist/utils/media-size.d.ts +29 -0
  139. package/dist/utils/media-size.d.ts.map +1 -0
  140. package/dist/utils/media-size.js +90 -0
  141. package/dist/utils/resize.d.ts +12 -0
  142. package/dist/utils/resize.d.ts.map +1 -1
  143. package/dist/utils/resize.js +5 -5
  144. package/dist/utils/sanitize.d.ts +2 -0
  145. package/dist/utils/sanitize.d.ts.map +1 -1
  146. package/dist/utils/sanitize.js +28 -3
  147. package/package.json +8 -1
@@ -4,6 +4,8 @@
4
4
  import { TableCell } from "@tiptap/extension-table-cell";
5
5
  import { Extension } from "@tiptap/core";
6
6
  import { TextSelection } from "@tiptap/pm/state";
7
+ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
8
+ import { guardVSCodePaste } from "../utils/guard-vscode-paste";
7
9
  import { lowlight } from "../utils/lowlight";
8
10
 
9
11
  /*
@@ -89,8 +91,20 @@
89
91
  },
90
92
  });
91
93
 
94
+ /*
95
+ * ⚠️ 업스트림 `codeBlockVSCodeHandler` 는 클립보드의 `vscode-editor-data` 를 검증 없이
96
+ * `JSON.parse` 한다 — VS Code 계열이 아닌 앱이 같은 타입에 깨진 값을 실으면 붙여넣기
97
+ * 한 번에 handlePaste 가 던져 에디터가 멈춘다. 부모 플러그인을 그대로 두고
98
+ * 메타데이터 검증만 앞에 끼운다(`utils/guard-vscode-paste.ts`).
99
+ */
100
+ const GuardedCodeBlockLowlight = CodeBlockLowlight.extend({
101
+ addProseMirrorPlugins() {
102
+ return (this.parent?.() ?? []).map((plugin) => guardVSCodePaste(plugin));
103
+ },
104
+ });
105
+
92
106
  /**
93
- * 편집 크롬(툴바 2 · 메뉴 2 · 모달 3)은 **`editable` 일 때만 그려지는데**
107
+ * 편집 크롬(툴바 2 · 메뉴 3 · 헬퍼 1 · 모달 3)은 **`editable` 일 때만 그려지는데**
94
108
  * 정적 import 면 읽기 전용 페이지도 통째로 받는다.
95
109
  *
96
110
  * 실측(정올 prod 빌드, `apps/jungol/.svelte-kit/output/client`): 이 일곱만 쓰는
@@ -122,6 +136,8 @@
122
136
  BubbleToolbar: typeof import("./BubbleToolbar.svelte").default;
123
137
  TableBubbleMenu: typeof import("./TableBubbleMenu.svelte").default;
124
138
  SlashCommandMenu: typeof import("./SlashCommandMenu.svelte").default;
139
+ EmojiSuggestionMenu: typeof import("./EmojiSuggestionMenu.svelte").default;
140
+ FloatingHelper: typeof import("./FloatingHelper.svelte").default;
125
141
  MediaPickerModal: typeof import("./MediaPickerModal.svelte").default;
126
142
  InputModal: typeof import("./InputModal.svelte").default;
127
143
  MathModal: typeof import("./MathModal.svelte").default;
@@ -136,15 +152,19 @@
136
152
  import("./BubbleToolbar.svelte"),
137
153
  import("./TableBubbleMenu.svelte"),
138
154
  import("./SlashCommandMenu.svelte"),
155
+ import("./EmojiSuggestionMenu.svelte"),
156
+ import("./FloatingHelper.svelte"),
139
157
  import("./MediaPickerModal.svelte"),
140
158
  import("./InputModal.svelte"),
141
159
  import("./MathModal.svelte"),
142
- ]).then(([fixed, bubble, table, slash, media, input, math]) => {
160
+ ]).then(([fixed, bubble, table, slash, emoji, floating, media, input, math]) => {
143
161
  sharedChrome = {
144
162
  FixedToolbar: fixed.default,
145
163
  BubbleToolbar: bubble.default,
146
164
  TableBubbleMenu: table.default,
147
165
  SlashCommandMenu: slash.default,
166
+ EmojiSuggestionMenu: emoji.default,
167
+ FloatingHelper: floating.default,
148
168
  MediaPickerModal: media.default,
149
169
  InputModal: input.default,
150
170
  MathModal: math.default,
@@ -158,7 +178,6 @@
158
178
  import { onMount, onDestroy } from "svelte";
159
179
  import { Editor } from "@tiptap/core";
160
180
  import StarterKit from "@tiptap/starter-kit";
161
- import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
162
181
  import Placeholder from "@tiptap/extension-placeholder";
163
182
  import Link from "@tiptap/extension-link";
164
183
  import Underline from "@tiptap/extension-underline";
@@ -180,14 +199,22 @@
180
199
  LeveledDetailsSummary,
181
200
  NotionToggleInputRule,
182
201
  } from "../extensions/NotionInputRules";
202
+ import { OrderedListMarker } from "../extensions/OrderedListMarker";
183
203
  import FileHandler from "@tiptap/extension-file-handler";
184
- import type { AnyExtension } from "@tiptap/core";
204
+ import type { AnyExtension, JSONContent } from "@tiptap/core";
185
205
  import { PdfBlock } from "../extensions/PdfBlock";
186
206
  import { Columns } from "../extensions/Columns";
187
207
  import { Column } from "../extensions/Column";
188
208
  import { TabsBlock, Tab } from "../extensions/TabsBlock";
189
- import { transformLegacyHtml } from "../utils/sanitize";
209
+ import { transformLegacyHtml, stripUploadSkeletonHtml } from "../utils/sanitize";
210
+ import {
211
+ UploadSkeleton,
212
+ insertUploadSkeleton,
213
+ type UploadSkeletonKind,
214
+ } from "../extensions/UploadSkeleton";
215
+ import { MediaResizeToolbar } from "../extensions/MediaToolbar";
190
216
  import { Indent } from "../extensions/Indent";
217
+ import { TableKeymap } from "../extensions/TableKeymap";
191
218
  import { FileAttachment } from "../extensions/FileAttachment";
192
219
  import { MbusVideo } from "../extensions/MbusVideo";
193
220
  import { ResizableImage } from "../extensions/ResizableImage";
@@ -198,12 +225,14 @@
198
225
  import { MathInline, MathDisplay, type MathPrompt } from "../extensions/Math";
199
226
  import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
200
227
  import { resolveFeatures } from "../types";
228
+ import { createTranslator, EditorI18n, type EditorLocaleInput } from "../i18n";
201
229
  import type { FileResolver } from "../extensions/FileAttachment";
202
230
 
203
231
  let {
204
232
  content = "",
205
233
  onChange,
206
- placeholder = "'/'를 눌러 명령어를 입력하세요...",
234
+ placeholder,
235
+ locale,
207
236
  onUploadFile,
208
237
  onResolveFile,
209
238
  fileDownloadBaseUrl,
@@ -223,7 +252,13 @@
223
252
  }: {
224
253
  content: string;
225
254
  onChange: (html: string) => void;
255
+ /** 본문 안내문. 미지정 시 로케일의 기본 안내문(ko: "'/'를 눌러 명령어를 입력하세요..."). */
226
256
  placeholder?: string;
257
+ /**
258
+ * 에디터 UI 로케일 — 코드('ko'·'en'·'ja'·'es'·'zh-hans'·'zh-hant', 지역 변형 허용) 또는
259
+ * 부분 메시지 오버라이드 객체. **미주입 시 ko** — 기존 호스트와 픽셀 단위 동일(하위호환).
260
+ */
261
+ locale?: EditorLocaleInput;
227
262
  onUploadFile?: UploadHandler;
228
263
  onResolveFile?: FileResolver;
229
264
  fileDownloadBaseUrl?: string;
@@ -259,6 +294,12 @@
259
294
 
260
295
  const features = $derived(resolveFeatures(toolbar, featuresOverride));
261
296
 
297
+ /*
298
+ * 에디터 UI 번역기. 크롬 컴포넌트에는 prop 으로, vanilla NodeView 에는 `EditorI18n`
299
+ * 확장(→ `editor.storage.editorI18n.t`)으로 내려간다. 미주입 시 ko — 하위호환 계약.
300
+ */
301
+ const t = $derived(createTranslator(locale));
302
+
262
303
  let editorElement: HTMLDivElement | undefined = $state();
263
304
  let editor: Editor | undefined = $state();
264
305
 
@@ -277,6 +318,8 @@
277
318
  const BubbleToolbar = $derived(sharedChrome?.BubbleToolbar);
278
319
  const TableBubbleMenu = $derived(sharedChrome?.TableBubbleMenu);
279
320
  const SlashCommandMenu = $derived(sharedChrome?.SlashCommandMenu);
321
+ const EmojiSuggestionMenu = $derived(sharedChrome?.EmojiSuggestionMenu);
322
+ const FloatingHelper = $derived(sharedChrome?.FloatingHelper);
280
323
  const MediaPickerModal = $derived(sharedChrome?.MediaPickerModal);
281
324
  const InputModal = $derived(sharedChrome?.InputModal);
282
325
  const MathModal = $derived(sharedChrome?.MathModal);
@@ -355,9 +398,9 @@
355
398
  try {
356
399
  const path = new URL(raw, "http://x").pathname;
357
400
  const last = decodeURIComponent(path.split("/").filter(Boolean).pop() || "");
358
- return last || "파일";
401
+ return last || t("file");
359
402
  } catch {
360
- return "파일";
403
+ return t("file");
361
404
  }
362
405
  }
363
406
 
@@ -391,22 +434,51 @@
391
434
  * ⚠️ 업로드는 **실패해도 모달을 닫는다.** 열어 둔 채로 두면 `업로드 중...` 오버레이가
392
435
  * 모달 뒤에 깔려 무슨 일이 일어나는지 안 보인다. 실패는 아래 `catch` 가 알린다.
393
436
  */
394
- function insertImageFile(file: File) {
395
- mediaPicker = null;
437
+ /*
438
+ * ── 업로드 스켈레톤 흐름 ──────────────────────────────────────────────────
439
+ * 업로드가 시작되면 **삽입 지점에** 스켈레톤을 세우고, 끝나면 그 자리를 실제 노드로
440
+ * 바꾼다(실패하면 제거). 전체 오버레이와 달리 문서를 막지 않아서, 업로드가 도는
441
+ * 동안에도 커서를 옮기고 다른 곳을 계속 편집할 수 있다. 그 사이 위쪽 편집으로 자리가
442
+ * 밀려도 핸들이 id 로 다시 찾고, 사용자가 스켈레톤을 지워 버렸으면 결과를 현재 커서
443
+ * 자리에 넣는다 — 업로드까지 끝난 파일을 버리지 않는다. 스켈레톤을 못 세우는 드문
444
+ * 경우(스키마 미등록)에만 예전 전체 오버레이(`uploading`)로 물러난다.
445
+ */
446
+ function uploadWithSkeleton(opts: {
447
+ file: File;
448
+ kind: UploadSkeletonKind;
449
+ at?: number;
450
+ makeContent: (result: string) => JSONContent;
451
+ errorMessage: string;
452
+ }) {
396
453
  if (!editor || !onUploadFile) return;
397
- uploading = true;
398
- onUploadFile(file)
399
- .then((url) => {
400
- editor!.chain().focus().setImage({ src: url }).run();
454
+ const handle = insertUploadSkeleton(editor, { kind: opts.kind, at: opts.at });
455
+ if (!handle) uploading = true;
456
+ onUploadFile(opts.file)
457
+ .then((result) => {
458
+ const content = opts.makeContent(result);
459
+ if (!handle || !handle.replaceWith(content)) {
460
+ editor?.chain().focus().insertContent(content).run();
461
+ }
401
462
  })
402
463
  .catch(() => {
403
- alert("이미지 업로드에 실패했습니다.");
464
+ handle?.remove();
465
+ alert(opts.errorMessage);
404
466
  })
405
467
  .finally(() => {
406
- uploading = false;
468
+ if (!handle) uploading = false;
407
469
  });
408
470
  }
409
471
 
472
+ function insertImageFile(file: File) {
473
+ mediaPicker = null;
474
+ uploadWithSkeleton({
475
+ file,
476
+ kind: "image",
477
+ makeContent: (url) => ({ type: "image", attrs: { src: url } }),
478
+ errorMessage: t("imageUploadFailed"),
479
+ });
480
+ }
481
+
410
482
  function insertImageUrl(url: string) {
411
483
  mediaPicker = null;
412
484
  editor?.chain().focus().setImage({ src: url }).run();
@@ -435,6 +507,13 @@
435
507
  let slashStartPos: number | null = null;
436
508
  const MENU_HEIGHT = 320;
437
509
 
510
+ /*
511
+ * 이모지 메뉴 열림 여부. 그 메뉴는 상태를 스스로 들고 있어(해당 파일 주석) 여기서는
512
+ * 알림만 받는다 — 빈 줄 플로팅 헬퍼가 슬래시·이모지 팝업과 **동시에 뜨지 않게** 끄는
513
+ * 데에만 쓴다(좌표계가 달라 자동 회피가 안 된다. `FloatingHelper.svelte` 주석 참고).
514
+ */
515
+ let emojiMenuOpen = $state(false);
516
+
438
517
  function updateSlashMenuPosition() {
439
518
  if (!editor || slashStartPos === null) return;
440
519
  const { from } = editor.state.selection;
@@ -491,54 +570,36 @@
491
570
  }
492
571
 
493
572
  function uploadPdf(file: File) {
494
- if (!editor || !onUploadFile) return;
495
- uploading = true;
496
- onUploadFile(file)
497
- .then((url) => {
498
- editor!
499
- .chain()
500
- .focus()
501
- .insertContent({
502
- type: "pdfBlock",
503
- attrs: { src: url, name: file.name },
504
- })
505
- .run();
506
- })
507
- .catch(() => {
508
- alert("PDF 업로드에 실패했습니다.");
509
- })
510
- .finally(() => {
511
- uploading = false;
512
- });
573
+ uploadWithSkeleton({
574
+ file,
575
+ kind: "pdf",
576
+ makeContent: (url) => ({
577
+ type: "pdfBlock",
578
+ attrs: { src: url, name: file.name },
579
+ }),
580
+ errorMessage: t("pdfUploadFailed"),
581
+ });
513
582
  }
514
583
 
515
584
  function uploadFile(file: File) {
516
- if (!editor || !onUploadFile) return;
517
- uploading = true;
518
585
  const size = file.size;
519
- onUploadFile(file)
520
- .then((result) => {
586
+ uploadWithSkeleton({
587
+ file,
588
+ kind: "file",
589
+ makeContent: (result) => {
521
590
  const isFileId = result && !result.includes("/") && !result.includes(":");
522
- editor!
523
- .chain()
524
- .focus()
525
- .insertContent({
526
- type: "fileAttachment",
527
- attrs: {
528
- src: isFileId ? null : result,
529
- fileId: isFileId ? result : null,
530
- name: file.name,
531
- size,
532
- },
533
- })
534
- .run();
535
- })
536
- .catch(() => {
537
- alert("파일 업로드에 실패했습니다.");
538
- })
539
- .finally(() => {
540
- uploading = false;
541
- });
591
+ return {
592
+ type: "fileAttachment",
593
+ attrs: {
594
+ src: isFileId ? null : result,
595
+ fileId: isFileId ? result : null,
596
+ name: file.name,
597
+ size,
598
+ },
599
+ };
600
+ },
601
+ errorMessage: t("fileUploadFailed"),
602
+ });
542
603
  }
543
604
 
544
605
  // Slash command handlers (component-level for cleanup access)
@@ -600,6 +661,13 @@
600
661
  editor = new Editor({
601
662
  element: editorElement,
602
663
  extensions: [
664
+ /*
665
+ * UI 번역기를 storage 로 나르는 확장 — vanilla NodeView 가 `getEditorTranslator` 로
666
+ * 읽는다. 호스트가 직접 넘겼으면 비켜선다(`math` 와 같은 꼴).
667
+ */
668
+ ...(extraExtensions.some((ext) => (ext as any).name === "editorI18n")
669
+ ? []
670
+ : [EditorI18n.configure({ locale })]),
603
671
  /*
604
672
  * `link`·`underline` 을 끄는 것은 **v3 이전의 뒷정리**다.
605
673
  *
@@ -630,11 +698,18 @@
630
698
  blockquote: false,
631
699
  link: false,
632
700
  underline: false,
701
+ /*
702
+ * `codeBlock: false` 와 같은 꼴 — **끄고 자기 것을 단다.** `a.`·`가.` 같은
703
+ * 마커 입력 규칙은 `configure` 로 못 넣어서 확장을 갈아 끼우는 수밖에 없다
704
+ * (`OrderedListMarker` 주석 참고).
705
+ */
706
+ orderedList: false,
633
707
  }),
708
+ OrderedListMarker,
634
709
  NotionBlockquote,
635
710
  ...(extraExtensions.some((ext) => (ext as any).name === 'codeBlock')
636
711
  ? []
637
- : [CodeBlockLowlight.configure({
712
+ : [GuardedCodeBlockLowlight.configure({
638
713
  lowlight,
639
714
  defaultLanguage: "cpp",
640
715
  })]),
@@ -652,11 +727,11 @@
652
727
  if (node.type.name === "codeBlock") return "";
653
728
  if (node.type.name === "heading") {
654
729
  const level = node.attrs.level;
655
- if (level === 1) return "제목 1";
656
- if (level === 2) return "제목 2";
657
- if (level === 3) return "제목 3";
730
+ if (level === 1) return t("heading1");
731
+ if (level === 2) return t("heading2");
732
+ if (level === 3) return t("heading3");
658
733
  }
659
- return placeholder;
734
+ return placeholder ?? t("placeholder");
660
735
  },
661
736
  showOnlyWhenEditable: true,
662
737
  showOnlyCurrent: true,
@@ -696,6 +771,19 @@
696
771
  TableRow,
697
772
  CustomTableHeader,
698
773
  CustomTableCell,
774
+ /*
775
+ * Tab 셀 이동·행 추가·빈 표 삭제. 업스트림 `Table` 에도 Tab 단축키가 있지만
776
+ * 아래 `Indent` 의 Tab(NBSP 삽입)이 키맵 순서상 먼저 먹어 죽는다 —
777
+ * `TableKeymap`(priority 110)이 표 안에서만 그걸 이긴다. 경위는 그 파일 주석.
778
+ */
779
+ TableKeymap,
780
+ /*
781
+ * 업로드 자리표시자. 스키마에 항상 실어 둔다 — 저장에 안 남으므로 읽기 전용
782
+ * 문서에 나타날 일은 없지만, 편집·읽기의 스키마가 갈리면 다른 데서 탈이 난다.
783
+ */
784
+ UploadSkeleton,
785
+ /* 미디어(이미지·영상) 선택 시 비율·정렬·폭 프리셋 칩. 읽기 전용엔 싣지 않는다. */
786
+ ...(editable ? [MediaResizeToolbar] : []),
699
787
  PdfBlock,
700
788
  FileAttachment.configure({
701
789
  resolver: onResolveFile ?? null,
@@ -755,60 +843,48 @@
755
843
  "application/pdf",
756
844
  ],
757
845
  onDrop: (_currentEditor, files, pos) => {
846
+ /* 파일마다 스켈레톤(1) + 뒤에 심는 빈 문단(2)만큼 다음 자리를 민다. */
847
+ let at = pos;
758
848
  for (const file of files) {
759
849
  if (file.type.startsWith("image/")) {
760
- uploading = true;
761
- onUploadFile!(file)
762
- .then((url) => {
763
- _currentEditor
764
- .chain()
765
- .focus()
766
- .insertContentAt(pos, {
767
- type: "image",
768
- attrs: { src: url },
769
- })
770
- .run();
771
- })
772
- .catch(() =>
773
- alert("이미지 업로드에 실패했습니다."),
774
- )
775
- .finally(() => (uploading = false));
850
+ uploadWithSkeleton({
851
+ file,
852
+ kind: "image",
853
+ at,
854
+ makeContent: (url) => ({
855
+ type: "image",
856
+ attrs: { src: url },
857
+ }),
858
+ errorMessage: t("imageUploadFailed"),
859
+ });
860
+ at += 3;
776
861
  } else if (file.type === "application/pdf") {
777
- uploading = true;
778
- onUploadFile!(file)
779
- .then((url) => {
780
- _currentEditor
781
- .chain()
782
- .focus()
783
- .insertContentAt(pos, {
784
- type: "pdfBlock",
785
- attrs: { src: url, name: file.name },
786
- })
787
- .run();
788
- })
789
- .catch(() =>
790
- alert("PDF 업로드에 실패했습니다."),
791
- )
792
- .finally(() => (uploading = false));
862
+ uploadWithSkeleton({
863
+ file,
864
+ kind: "pdf",
865
+ at,
866
+ makeContent: (url) => ({
867
+ type: "pdfBlock",
868
+ attrs: { src: url, name: file.name },
869
+ }),
870
+ errorMessage: t("pdfUploadFailed"),
871
+ });
872
+ at += 3;
793
873
  }
794
874
  }
795
875
  },
796
876
  onPaste: (_currentEditor, files) => {
797
877
  for (const file of files) {
798
878
  if (file.type.startsWith("image/")) {
799
- uploading = true;
800
- onUploadFile!(file)
801
- .then((url) => {
802
- _currentEditor
803
- .chain()
804
- .focus()
805
- .setImage({ src: url })
806
- .run();
807
- })
808
- .catch(() =>
809
- alert("이미지 업로드에 실패했습니다."),
810
- )
811
- .finally(() => (uploading = false));
879
+ uploadWithSkeleton({
880
+ file,
881
+ kind: "image",
882
+ makeContent: (url) => ({
883
+ type: "image",
884
+ attrs: { src: url },
885
+ }),
886
+ errorMessage: t("imageUploadFailed"),
887
+ });
812
888
  }
813
889
  }
814
890
  },
@@ -823,10 +899,10 @@
823
899
  editable,
824
900
  onUpdate: ({ editor: e }) => {
825
901
  // 빈 paragraph는 <p></p>로 저장 (ProseMirror가 편집기 DOM에 넣는 trailing <br>는 출력에서 제거).
826
- const html = e
827
- .getHTML()
828
- .replace(/<p><br\s*\/?><\/p>/g, "<p></p>")
829
- .replace(/(<p><\/p>\s*)+$/, "");
902
+ // 업로드 스켈레톤은 화면 상태지 내용이 아니다 — 저장 HTML 에서 걷어낸다.
903
+ const html = stripUploadSkeletonHtml(
904
+ e.getHTML().replace(/<p><br\s*\/?><\/p>/g, "<p></p>"),
905
+ ).replace(/(<p><\/p>\s*)+$/, "");
830
906
  lastEmittedHtml = html;
831
907
  onChange(html);
832
908
  },
@@ -879,6 +955,18 @@
879
955
  });
880
956
  });
881
957
 
958
+ /*
959
+ * 로케일이 마운트 뒤에 바뀌면 storage 의 번역기를 갈아 끼운다. 이미 그려진 NodeView 는
960
+ * 예전 문자열을 유지한다(다시 그려질 때 반영) — 크롬 컴포넌트는 prop 이라 즉시 따라온다.
961
+ */
962
+ $effect(() => {
963
+ const translator = t;
964
+ const storage = editor?.storage as
965
+ | { editorI18n?: { t: typeof translator } }
966
+ | undefined;
967
+ if (storage?.editorI18n) storage.editorI18n.t = translator;
968
+ });
969
+
882
970
  // Scroll handler for slash menu position
883
971
  $effect(() => {
884
972
  if (!slashMenuOpen) return;
@@ -921,6 +1009,7 @@
921
1009
  <FixedToolbar
922
1010
  {editor}
923
1011
  {features}
1012
+ {t}
924
1013
  {onPromptLink}
925
1014
  {onPromptMbus}
926
1015
  {onPromptVideo}
@@ -940,11 +1029,11 @@
940
1029
 
941
1030
  {#if editor && editable}
942
1031
  {#if BubbleToolbar && features.has('bubble-toolbar')}
943
- <BubbleToolbar {editor} {features} {onPromptLink} />
1032
+ <BubbleToolbar {editor} {features} {onPromptLink} {t} />
944
1033
  {/if}
945
1034
 
946
1035
  {#if TableBubbleMenu && features.has('table-menu')}
947
- <TableBubbleMenu {editor} />
1036
+ <TableBubbleMenu {editor} {t} />
948
1037
  {/if}
949
1038
 
950
1039
  {#if features.has('upload-overlay') && uploading}
@@ -952,7 +1041,7 @@
952
1041
  class="absolute inset-0 flex items-center justify-center bg-background/60 rounded-xl"
953
1042
  >
954
1043
  <p class="text-sm text-muted-foreground animate-pulse">
955
- 업로드 중...
1044
+ {t('uploading')}
956
1045
  </p>
957
1046
  </div>
958
1047
  {/if}
@@ -965,6 +1054,7 @@
965
1054
  <SlashCommandMenu
966
1055
  {editor}
967
1056
  {features}
1057
+ {t}
968
1058
  {onPromptLink}
969
1059
  {onPromptMbus}
970
1060
  {onPromptVideo}
@@ -981,6 +1071,24 @@
981
1071
  </div>
982
1072
  {/if}
983
1073
 
1074
+ <!--
1075
+ 이모지 `:` 자동완성. 슬래시 메뉴와 한 벌이라 같은 feature 로 켠다.
1076
+ 열림·위치·검색어를 컴포넌트가 스스로 판정하므로(에디터 이벤트 직접 구독)
1077
+ 여기는 마운트만 한다. 열림 여부만 받아 둔다 — 아래 플로팅 헬퍼 상호배제용.
1078
+ -->
1079
+ {#if EmojiSuggestionMenu && features.has('slash-menu')}
1080
+ <EmojiSuggestionMenu {editor} onOpenChange={(open) => (emojiMenuOpen = open)} />
1081
+ {/if}
1082
+
1083
+ <!--
1084
+ 빈 줄 플로팅 헬퍼. "/" 안내가 본체라 슬래시 메뉴와 같은 feature 로 켠다.
1085
+ 표시 판정·위치는 컴포넌트가 스스로 하고(에디터 이벤트 직접 구독), 여기는
1086
+ 슬래시·이모지 팝업이 열려 있는 동안만 꺼 달라는 신호를 넘긴다.
1087
+ -->
1088
+ {#if FloatingHelper && features.has('slash-menu')}
1089
+ <FloatingHelper {editor} {features} {t} suppressed={slashMenuOpen || emojiMenuOpen} />
1090
+ {/if}
1091
+
984
1092
  <!--
985
1093
  수식 프롬프트. `onPromptMath` 를 준 호스트에겐 열리지 않는다(그쪽이 직접 띄운다).
986
1094
  `features` 로 막지 않는다 — `$…$` 입력 규칙과 붙여넣기 변환은 feature 와 무관하게
@@ -992,12 +1100,13 @@
992
1100
  -->
993
1101
  {#if MediaPickerModal && mediaPicker === 'image'}
994
1102
  <MediaPickerModal
995
- title="이미지 추가"
1103
+ title={t('addImage')}
996
1104
  accept="image/*"
997
- uploadLabel="파일 업로드"
998
- linkPlaceholder="이미지 링크 붙여넣기"
999
- linkConfirmLabel="이미지 임베드"
1000
- linkHint="웹에 있는 모든 이미지와 호환됨"
1105
+ uploadLabel={t('uploadFile')}
1106
+ linkPlaceholder={t('imageLinkPlaceholder')}
1107
+ linkConfirmLabel={t('imageEmbed')}
1108
+ linkHint={t('imageLinkHint')}
1109
+ {t}
1001
1110
  onUpload={insertImageFile}
1002
1111
  onLink={insertImageUrl}
1003
1112
  onCancel={() => (mediaPicker = null)}
@@ -1005,10 +1114,11 @@
1005
1114
  {/if}
1006
1115
  {#if MediaPickerModal && mediaPicker === 'file'}
1007
1116
  <MediaPickerModal
1008
- title="파일 추가"
1009
- uploadLabel="파일을 선택하세요"
1010
- linkPlaceholder="파일 링크 붙여넣기"
1011
- linkConfirmLabel="파일 임베드"
1117
+ title={t('addFile')}
1118
+ uploadLabel={t('chooseFile')}
1119
+ linkPlaceholder={t('fileLinkPlaceholder')}
1120
+ linkConfirmLabel={t('fileEmbed')}
1121
+ {t}
1012
1122
  onUpload={insertFileUpload}
1013
1123
  onLink={insertFileUrl}
1014
1124
  onCancel={() => (mediaPicker = null)}
@@ -1018,7 +1128,8 @@
1018
1128
  <!-- 업로드를 못 하는 호스트용 폴백(URL 만 받는다). 위 `pickImage` 참고. -->
1019
1129
  {#if InputModal && imageUrlPrompt}
1020
1130
  <InputModal
1021
- title="이미지 URL 입력"
1131
+ title={t('imageUrlTitle')}
1132
+ {t}
1022
1133
  placeholder="https://example.com/image.png"
1023
1134
  onConfirm={(url) => {
1024
1135
  imageUrlPrompt = false;
@@ -1031,6 +1142,7 @@
1031
1142
  {#if MathModal && mathPrompt}
1032
1143
  <MathModal
1033
1144
  latex={mathPrompt.latex}
1145
+ {t}
1034
1146
  displayMode={mathPrompt.displayMode}
1035
1147
  onConfirm={(value) => closeMathPrompt(value)}
1036
1148
  onCancel={() => closeMathPrompt(null)}
@@ -1041,7 +1153,7 @@
1041
1153
  <div
1042
1154
  class="flex justify-end px-4 py-2 text-xs text-muted-foreground border-t border-border"
1043
1155
  >
1044
- {counts.chars} 자 · {counts.words} 단어
1156
+ {t('characterCount', { chars: counts.chars, words: counts.words })}
1045
1157
  </div>
1046
1158
  {/if}
1047
1159
 
@@ -4,11 +4,18 @@ import { type MidibusRenderer } from "../extensions/TiptapMidibus";
4
4
  import { type LegacyBlockRenderer } from "../extensions/LegacyBlock";
5
5
  import { type MathPrompt } from "../extensions/Math";
6
6
  import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
7
+ import { type EditorLocaleInput } from "../i18n";
7
8
  import type { FileResolver } from "../extensions/FileAttachment";
8
9
  type $$ComponentProps = {
9
10
  content: string;
10
11
  onChange: (html: string) => void;
12
+ /** 본문 안내문. 미지정 시 로케일의 기본 안내문(ko: "'/'를 눌러 명령어를 입력하세요..."). */
11
13
  placeholder?: string;
14
+ /**
15
+ * 에디터 UI 로케일 — 코드('ko'·'en'·'ja'·'es'·'zh-hans'·'zh-hant', 지역 변형 허용) 또는
16
+ * 부분 메시지 오버라이드 객체. **미주입 시 ko** — 기존 호스트와 픽셀 단위 동일(하위호환).
17
+ */
18
+ locale?: EditorLocaleInput;
12
19
  onUploadFile?: UploadHandler;
13
20
  onResolveFile?: FileResolver;
14
21
  fileDownloadBaseUrl?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA6JA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA2BtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAWjD,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAElF,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC9E,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,uDAAuD;IACvD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,mDAAmD;IACnD,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,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;AAoyBJ,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":"AAiLA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA2BtC,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,cAAc,CAAC;AAkB9D,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAElF,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,EAAgC,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC/E,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,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,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,uDAAuD;IACvD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,mDAAmD;IACnD,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,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;AAw2BJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1 +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"}
1
+ {"version":3,"file":"CardBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/CardBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAkCrD,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,6BAyNpB,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"}