@teriusu/rich-editor 0.11.2 → 0.11.4

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.
@@ -24,6 +24,7 @@
24
24
  ListChecks,
25
25
  Quote,
26
26
  ChevronDown,
27
+ Check,
27
28
  Sigma,
28
29
  } from "lucide-svelte";
29
30
  import { cn } from "../utils/cn";
@@ -102,16 +103,6 @@
102
103
  return t("paragraph");
103
104
  }
104
105
 
105
- function isParagraphActive(): boolean {
106
- return (
107
- !isActive("heading") &&
108
- !isActive("bulletList") &&
109
- !isActive("orderedList") &&
110
- !isActive("taskList") &&
111
- !isActive("blockquote")
112
- );
113
- }
114
-
115
106
  async function addLink() {
116
107
  const selection = editor.state.selection.getBookmark();
117
108
  const documentBeforePrompt = editor.state.doc;
@@ -175,7 +166,9 @@
175
166
  const themeTokens = [
176
167
  '--border', '--background', '--foreground', '--popover', '--muted',
177
168
  '--muted-foreground', '--primary', '--primary-foreground', '--accent',
178
- '--accent-foreground', '--ring', '--hce-menu-z', '--hce-menu-surface'
169
+ '--accent-foreground', '--ring', '--hce-menu-z', '--hce-menu-surface',
170
+ '--wh-100', '--wh-500', '--gray-500', '--gray-700', '--gray-900',
171
+ '--primary-500', '--color-line', '--radius-8', '--radius-2xl', '--shadow-popup'
179
172
  ];
180
173
  const syncTheme = () => {
181
174
  const hostStyle = getComputedStyle(editor.view.dom);
@@ -192,7 +185,7 @@
192
185
  const left = viewport?.offsetLeft ?? 0;
193
186
  const top = viewport?.offsetTop ?? 0;
194
187
  anchor.style.left = `${left + width / 2}px`;
195
- anchor.style.top = `${top + height}px`;
188
+ anchor.style.top = `calc(${top + height}px - env(safe-area-inset-bottom, 0px))`;
196
189
  element.style.maxWidth = mobile ? `${Math.max(0, width - 16)}px` : '';
197
190
  element.style.setProperty('--bubble-popup-height', `${Math.max(44, height - element.getBoundingClientRect().height - 24)}px`);
198
191
  };
@@ -329,125 +322,55 @@
329
322
  );
330
323
  </script>
331
324
 
325
+ {#snippet blockItem(labelKey: EditorMessageKey, Icon: typeof Type, select: () => void)}
326
+ {@const selected = getCurrentBlockLabel() === t(labelKey)}
327
+ <button
328
+ type="button"
329
+ class="block-menu-item"
330
+ role="menuitemradio"
331
+ aria-checked={selected}
332
+ onclick={() => { select(); showBlockMenu = false; }}
333
+ >
334
+ <span class="block-menu-icon"><Icon size={16} /></span>
335
+ <span class="block-menu-label">{t(labelKey)}</span>
336
+ {#if selected}<span class="block-menu-check"><Check size={16} /></span>{/if}
337
+ </button>
338
+ {/snippet}
339
+
332
340
  {#snippet blockPopover()}
333
- {#if showBlockMenu}
334
- <div
335
- bind:this={blockPopupEl}
336
- class="bubble-popover absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
337
- style="min-width: 140px"
338
- onmousedown={(e) => e.preventDefault()}
339
- role="menu"
340
- tabindex="-1"
341
- >
342
- <button
343
- type="button"
344
- class={cn(
345
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
346
- isParagraphActive()
347
- ? "hce-active"
348
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
349
- )}
350
- onclick={() => {
351
- editor.chain().focus().setParagraph().run();
352
- showBlockMenu = false;
353
- }}
354
- >
355
- <Type size={12} /> {t('paragraph')}
356
- </button>
357
- {#each [1, 2, 3] as level (level)}
358
- {#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
359
- {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
360
- <button
361
- type="button"
362
- class={cn(
363
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
364
- isActive("heading", { level })
365
- ? "hce-active"
366
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
367
- )}
368
- onclick={() => {
369
- editor
370
- .chain()
371
- .focus()
372
- .toggleHeading({ level: level as 1 | 2 | 3 })
373
- .run();
374
- showBlockMenu = false;
375
- }}
376
- >
377
- <Icon size={12} /> {t(level === 1 ? 'heading1' : level === 2 ? 'heading2' : 'heading3')}
378
- </button>
379
- {/if}
380
- {/each}
381
- {#if has('bullet-list')}
382
- <button
383
- type="button"
384
- class={cn(
385
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
386
- isActive("bulletList")
387
- ? "hce-active"
388
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
389
- )}
390
- onclick={() => {
391
- editor.chain().focus().toggleBulletList().run();
392
- showBlockMenu = false;
393
- }}
394
- >
395
- <List size={12} /> {t('bulletList')}
396
- </button>
397
- {/if}
398
- {#if has('ordered-list')}
399
- <button
400
- type="button"
401
- class={cn(
402
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
403
- isActive("orderedList")
404
- ? "hce-active"
405
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
406
- )}
407
- onclick={() => {
408
- editor.chain().focus().toggleOrderedList().run();
409
- showBlockMenu = false;
410
- }}
411
- >
412
- <ListOrdered size={12} /> {t('orderedList')}
413
- </button>
414
- {/if}
415
- {#if has('checklist')}
416
- <button
417
- type="button"
418
- class={cn(
419
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
420
- isActive("taskList")
421
- ? "hce-active"
422
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
423
- )}
424
- onclick={() => {
425
- editor.chain().focus().toggleTaskList().run();
426
- showBlockMenu = false;
427
- }}
428
- >
429
- <ListChecks size={12} /> {t('taskList')}
430
- </button>
431
- {/if}
432
- {#if has('blockquote')}
433
- <button
434
- type="button"
435
- class={cn(
436
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
437
- isActive("blockquote")
438
- ? "hce-active"
439
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
440
- )}
441
- onclick={() => {
442
- editor.chain().focus().toggleBlockquote().run();
443
- showBlockMenu = false;
444
- }}
445
- >
446
- <Quote size={12} /> {t('blockquote')}
447
- </button>
448
- {/if}
449
- </div>
341
+ {#if showBlockMenu}
342
+ <div
343
+ bind:this={blockPopupEl}
344
+ class="bubble-popover bubble-block-menu absolute bottom-full left-0 mb-1"
345
+ onmousedown={(e) => e.preventDefault()}
346
+ role="menu"
347
+ aria-orientation="vertical"
348
+ tabindex="-1"
349
+ >
350
+ {@render blockItem('paragraph', Type, () => { editor.chain().focus().setParagraph().run(); })}
351
+ {#each [1, 2, 3] as level (level)}
352
+ {#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
353
+ {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
354
+ {@render blockItem(
355
+ level === 1 ? 'heading1' : level === 2 ? 'heading2' : 'heading3', Icon,
356
+ () => { editor.chain().focus().toggleHeading({ level: level as 1 | 2 | 3 }).run(); }
357
+ )}
450
358
  {/if}
359
+ {/each}
360
+ {#if has('bullet-list')}
361
+ {@render blockItem('bulletList', List, () => { editor.chain().focus().toggleBulletList().run(); })}
362
+ {/if}
363
+ {#if has('ordered-list')}
364
+ {@render blockItem('orderedList', ListOrdered, () => { editor.chain().focus().toggleOrderedList().run(); })}
365
+ {/if}
366
+ {#if has('checklist')}
367
+ {@render blockItem('taskList', ListChecks, () => { editor.chain().focus().toggleTaskList().run(); })}
368
+ {/if}
369
+ {#if has('blockquote')}
370
+ {@render blockItem('blockquote', Quote, () => { editor.chain().focus().toggleBlockquote().run(); })}
371
+ {/if}
372
+ </div>
373
+ {/if}
451
374
  {/snippet}
452
375
 
453
376
  {#snippet colorPopover()}
@@ -509,12 +432,13 @@
509
432
  type="button"
510
433
  onclick={() => (showBlockMenu = !showBlockMenu)}
511
434
  aria-haspopup="menu"
435
+ aria-label={getCurrentBlockLabel()}
512
436
  aria-expanded={showBlockMenu}
513
- class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
437
+ class="bubble-block-trigger flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
514
438
  >
515
- <Type size={12} />
516
- {getCurrentBlockLabel()}
517
- <ChevronDown size={12} />
439
+ <Type size={mobile ? 14 : 12} />
440
+ {#if !mobile}{getCurrentBlockLabel()}{/if}
441
+ <ChevronDown size={mobile ? 8 : 12} />
518
442
  </button>
519
443
  {#if !mobile}{@render blockPopover()}{/if}
520
444
  </div>
@@ -688,28 +612,30 @@
688
612
 
689
613
  <style>
690
614
  .mobile .hce-floating-panel {
691
- border-radius: 12px;
692
- padding: 4px;
693
- padding-bottom: max(4px, env(safe-area-inset-bottom));
615
+ border-radius: 8px;
616
+ padding: 1px 3px;
694
617
  }
695
618
  .mobile .bubble-toolbar-rail {
696
619
  display: flex;
697
620
  flex-wrap: nowrap;
698
621
  max-width: 100%;
699
- overflow-x: auto;
622
+ gap: 1px;
700
623
  padding: 0;
701
- scrollbar-width: none;
702
- touch-action: pan-x;
703
624
  }
704
- .mobile .bubble-toolbar-rail::-webkit-scrollbar { display: none; }
705
625
  .mobile .bubble-toolbar-rail > * { flex-shrink: 0; }
706
- .mobile button {
707
- min-width: 44px;
708
- min-height: 44px;
626
+ .mobile .bubble-toolbar-rail button {
627
+ width: 28px;
628
+ height: 28px;
629
+ padding: 0;
709
630
  display: inline-flex;
710
631
  align-items: center;
711
632
  justify-content: center;
712
633
  }
634
+ .mobile .bubble-toolbar-rail .bubble-block-trigger {
635
+ width: 32px;
636
+ gap: 1px;
637
+ }
638
+ .mobile .bubble-popover:not(.bubble-block-menu) button { min-width: 44px; min-height: 44px; }
713
639
  .mobile input[type="color"] { min-width: 44px; min-height: 44px; }
714
640
  .mobile .bubble-divider { display: none; }
715
641
  .mobile .bubble-popover-anchor { position: static; }
@@ -719,4 +645,39 @@
719
645
  max-height: min(var(--bubble-popup-height, 50dvh), 300px);
720
646
  overflow-y: auto;
721
647
  }
648
+ .bubble-block-menu {
649
+ width: 200px;
650
+ box-sizing: border-box;
651
+ padding: 4px;
652
+ background: var(--wh-100, var(--popover, var(--background, #fff)));
653
+ border: 1px solid var(--color-line, var(--border));
654
+ border-radius: var(--radius-2xl, 12px);
655
+ box-shadow: var(--shadow-popup, 0 8px 24px rgb(0 0 0 / 12%));
656
+ }
657
+ .mobile .bubble-block-menu { right: auto; max-width: 100%; }
658
+ .block-menu-item {
659
+ display: flex;
660
+ align-items: center;
661
+ width: 100%;
662
+ min-height: 32px;
663
+ gap: 8px;
664
+ padding: 6px 8px;
665
+ border-radius: var(--radius-8, 8px);
666
+ color: var(--gray-700, var(--foreground));
667
+ font: inherit;
668
+ font-size: 13px;
669
+ line-height: 20px;
670
+ text-align: left;
671
+ white-space: nowrap;
672
+ }
673
+ .block-menu-item:hover, .block-menu-item:focus-visible {
674
+ background: var(--wh-500, var(--muted));
675
+ }
676
+ .block-menu-item[aria-checked='true'] {
677
+ color: var(--gray-900, var(--foreground));
678
+ font-weight: 600;
679
+ }
680
+ .block-menu-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
681
+ .block-menu-icon { display: inline-flex; flex: none; color: var(--gray-500, var(--muted-foreground)); }
682
+ .block-menu-check { display: inline-flex; flex: none; color: var(--primary-500, var(--primary)); }
722
683
  </style>
@@ -1 +1 @@
1
- {"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AA+iBJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA2B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AAmfJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAUrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BAmLpB,CAAC"}
1
+ {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAWrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BA0NpB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
2
  import { attachResize } from "../utils/resize";
3
+ import { applyCrossOriginEmbedAttrs, canEmbedCrossOrigin } from "../utils/embed";
3
4
  import { getEditorTranslator } from "../i18n";
4
5
  import { mediaRatioCss, normalizeMediaAlign, normalizeMediaHeight, normalizeMediaRatio } from "../utils/media-size";
5
6
  export const MbusVideo = Node.create({
@@ -99,16 +100,53 @@ export const MbusVideo = Node.create({
99
100
  dom.style.marginRight = align === "center" ? "auto" : "";
100
101
  };
101
102
  applyLayout(node.attrs);
102
- if (node.attrs.src) {
103
+ /*
104
+ * ⚠️ **`credentialless` 를 반드시 붙인다** — `VideoEmbed` 와 같은 이유·같은
105
+ * 레시피다(`utils/embed.ts`). 정올은 전 문서가 `COEP: require-corp` 라, 이
106
+ * 속성이 없으면 mbus iframe 이 **크롬에서도 통째로 막히고** 그 자리에 크롬의
107
+ * 오류 화면(`play.mbus.tv 에서 연결을 거부했습니다`)이 뜬다.
108
+ *
109
+ * 이 노드만 그 처리가 빠져 있었다. 저장한 뒤 읽는 화면은 호스트의 정적 렌더
110
+ * (`trinity …/tiptap/static-enhance.ts` 의 `buildVideoBox`)가 같은 상자를 다시
111
+ * 지으면서 속성을 붙이므로 **에디터에서만** 거부로 보였다 — "영상을 올리면
112
+ * 연결이 거부된다"는 신고의 실체가 이 비대칭이다.
113
+ * (브라우저 실측, 격리된 문서: 속성 없는 mbus iframe = COEP 위반 보고 1건,
114
+ * 속성 있는 쪽 = 0건.)
115
+ */
116
+ if (node.attrs.src && canEmbedCrossOrigin()) {
103
117
  const iframe = document.createElement("iframe");
104
118
  iframe.src = node.attrs.src;
105
119
  iframe.allow = "autoplay; fullscreen; encrypted-media; picture-in-picture";
106
120
  iframe.setAttribute("allowfullscreen", "");
107
121
  iframe.setAttribute("loading", "lazy");
122
+ applyCrossOriginEmbedAttrs(iframe);
108
123
  iframe.style.cssText =
109
124
  "position:absolute;inset:0;width:100%;height:100%;border:0;display:block;";
110
125
  aspect.appendChild(iframe);
111
126
  }
127
+ else if (node.attrs.src) {
128
+ /*
129
+ * 실을 수 없는 브라우저(파폭·사파리 + 격리) — **빈 박스로 두지 않는다.**
130
+ * 그러면 영상이 깨진 줄 알지 이유를 모른다. `VideoEmbed`·호스트 정적 렌더와
131
+ * **같은 문구·같은 모양**의 포스터를 세운다.
132
+ */
133
+ const poster = document.createElement("div");
134
+ poster.style.cssText =
135
+ "position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:16px;text-align:center;color:#c7cbd4;";
136
+ const label = document.createElement("span");
137
+ label.textContent = t("videoCannotEmbed");
138
+ label.style.cssText = "font-size:13px;line-height:1.5;";
139
+ poster.appendChild(label);
140
+ const open = document.createElement("a");
141
+ open.href = node.attrs.src;
142
+ open.target = "_blank";
143
+ open.rel = "noopener noreferrer";
144
+ open.textContent = t("openInNewTab");
145
+ open.style.cssText =
146
+ "font-size:13px;font-weight:600;color:#fff;background:rgba(255,255,255,0.16);border-radius:6px;padding:6px 14px;text-decoration:none;";
147
+ poster.appendChild(open);
148
+ aspect.appendChild(poster);
149
+ }
112
150
  if (editor.isEditable) {
113
151
  const del = document.createElement("button");
114
152
  del.type = "button";
@@ -1 +1 @@
1
- {"version":3,"file":"VideoEmbed.d.ts","sourceRoot":"","sources":["../../src/extensions/VideoEmbed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAWrD,MAAM,WAAW,iBAAiB;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,UAAU,EAAE;YACX,aAAa,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACtE,CAAC;KACF;CACD;AAyBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2B9C;AAmBD,eAAO,MAAM,UAAU,8BAmSrB,CAAC"}
1
+ {"version":3,"file":"VideoEmbed.d.ts","sourceRoot":"","sources":["../../src/extensions/VideoEmbed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAYrD,MAAM,WAAW,iBAAiB;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,UAAU,EAAE;YACX,aAAa,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACtE,CAAC;KACF;CACD;AAyBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2B9C;AAOD,eAAO,MAAM,UAAU,8BA+RrB,CAAC"}
@@ -9,6 +9,7 @@
9
9
  import { Node, mergeAttributes } from "@tiptap/core";
10
10
  import { Plugin, PluginKey } from "@tiptap/pm/state";
11
11
  import { attachResize } from "../utils/resize";
12
+ import { applyCrossOriginEmbedAttrs, canEmbedCrossOrigin } from "../utils/embed";
12
13
  import { getEditorTranslator } from "../i18n";
13
14
  import { mediaRatioCss, normalizeMediaAlign, normalizeMediaHeight, normalizeMediaRatio } from "../utils/media-size";
14
15
  /** `1m30s` · `90` 처럼 적히는 유튜브 타임스탬프를 초로 바꾼다. */
@@ -126,24 +127,10 @@ export function toEmbedUrl(raw) {
126
127
  }
127
128
  return trimmed;
128
129
  }
129
- /**
130
- * 이 브라우저에서 **COEP 없는 남의 iframe** 을 실을 수 있는지.
131
- *
132
- * ⚠️ 정올은 전 브라우저에 `COEP: require-corp`(교차 출처 격리)를 건다. 격리된 문서에
133
- * 자기 COEP 를 안 보내는 제3자 iframe(유튜브·Vimeo·mbus 전부 해당)을 넣는 방법은
134
- * `credentialless` 속성뿐인데 **크로미움 전용**이다(파이어폭스 미구현·사파리 거부).
135
- * 그래서 정올 파폭·사파리에서는 mbus 영상이 지금 **빈 회색 박스**로 남아 있다.
136
- *
137
- * 격리되지 않은 문서(코드패스의 비-크로미움 등)에서는 평범한 크로스오리진 iframe 이라
138
- * 아무 문제가 없다 — 그래서 격리 여부와 지원 여부를 **함께** 본다.
130
+ /*
131
+ * 판정은 `utils/embed.ts` 로 옮겼다 — `MbusVideo` 가 같은 규칙을 써야 하는데 여기
132
+ * 모듈 안에만 있어서 그쪽이 통째로 빠뜨리고 있었다(그 노드 주석 참조).
139
133
  */
140
- function canEmbedCrossOrigin() {
141
- if (typeof window === "undefined")
142
- return true;
143
- if (!window.crossOriginIsolated)
144
- return true;
145
- return "credentialless" in HTMLIFrameElement.prototype;
146
- }
147
134
  export const VideoEmbed = Node.create({
148
135
  name: "videoEmbed",
149
136
  group: "block",
@@ -261,11 +248,7 @@ export const VideoEmbed = Node.create({
261
248
  iframe.allow = "autoplay; fullscreen; encrypted-media; picture-in-picture";
262
249
  iframe.setAttribute("allowfullscreen", "");
263
250
  iframe.setAttribute("loading", "lazy");
264
- /*
265
- * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이
266
- * 속성 자체가 무시되므로 조건 없이 붙여도 해가 없다.
267
- */
268
- iframe.setAttribute("credentialless", "");
251
+ applyCrossOriginEmbedAttrs(iframe);
269
252
  iframe.style.cssText =
270
253
  "position:absolute;inset:0;width:100%;height:100%;border:0;display:block;";
271
254
  aspect.appendChild(iframe);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 「이 문서에서 **COEP 를 안 보내는 남의 iframe** 을 실을 수 있는가」.
3
+ *
4
+ * ⚠️ 정올은 전 문서에 `Cross-Origin-Embedder-Policy: require-corp`(교차 출처 격리)를
5
+ * 건다. 격리된 문서에 자기 COEP 를 안 보내는 제3자 iframe(유튜브·Vimeo·mbus 전부
6
+ * 해당)을 넣는 방법은 **`credentialless` 속성뿐**인데 크로미움 전용이다
7
+ * (파이어폭스 미구현·사파리 거부).
8
+ *
9
+ * 격리되지 않은 문서(코드패스의 비-크로미움 등)에서는 평범한 크로스오리진 iframe 이라
10
+ * 아무 문제가 없다 — 그래서 격리 여부와 지원 여부를 **함께** 본다.
11
+ *
12
+ * ⚠️ 이 판정과 `applyCrossOriginEmbedAttrs` 는 **항상 짝으로** 쓴다. 한쪽만 쓰면
13
+ * 격리된 문서에서 브라우저가 iframe 을 막고 크롬이 제 오류 화면
14
+ * (`…에서 연결을 거부했습니다`)을 그 자리에 그린다 — `MbusVideo` 가 실제로 그랬다.
15
+ */
16
+ export declare function canEmbedCrossOrigin(): boolean;
17
+ /**
18
+ * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이 속성 자체가
19
+ * 무시되므로 조건 없이 붙여도 해가 없다.
20
+ */
21
+ export declare function applyCrossOriginEmbedAttrs(iframe: HTMLIFrameElement): void;
22
+ //# sourceMappingURL=embed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/utils/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAI7C;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1E"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 「이 문서에서 **COEP 를 안 보내는 남의 iframe** 을 실을 수 있는가」.
3
+ *
4
+ * ⚠️ 정올은 전 문서에 `Cross-Origin-Embedder-Policy: require-corp`(교차 출처 격리)를
5
+ * 건다. 격리된 문서에 자기 COEP 를 안 보내는 제3자 iframe(유튜브·Vimeo·mbus 전부
6
+ * 해당)을 넣는 방법은 **`credentialless` 속성뿐**인데 크로미움 전용이다
7
+ * (파이어폭스 미구현·사파리 거부).
8
+ *
9
+ * 격리되지 않은 문서(코드패스의 비-크로미움 등)에서는 평범한 크로스오리진 iframe 이라
10
+ * 아무 문제가 없다 — 그래서 격리 여부와 지원 여부를 **함께** 본다.
11
+ *
12
+ * ⚠️ 이 판정과 `applyCrossOriginEmbedAttrs` 는 **항상 짝으로** 쓴다. 한쪽만 쓰면
13
+ * 격리된 문서에서 브라우저가 iframe 을 막고 크롬이 제 오류 화면
14
+ * (`…에서 연결을 거부했습니다`)을 그 자리에 그린다 — `MbusVideo` 가 실제로 그랬다.
15
+ */
16
+ export function canEmbedCrossOrigin() {
17
+ if (typeof window === "undefined")
18
+ return true;
19
+ if (!window.crossOriginIsolated)
20
+ return true;
21
+ return "credentialless" in HTMLIFrameElement.prototype;
22
+ }
23
+ /**
24
+ * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이 속성 자체가
25
+ * 무시되므로 조건 없이 붙여도 해가 없다.
26
+ */
27
+ export function applyCrossOriginEmbedAttrs(iframe) {
28
+ iframe.setAttribute("credentialless", "");
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",