npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

shadow-tiptap

v0.1.12

Published

Shadow DOM 기반 Tiptap React 라이브러리

Readme

shadow-tiptap

Shadow DOM 기반 Tiptap React 라이브러리입니다. 현재 런타임 공개 export 는 ShadowTiptapBase, ShadowTiptapEditor, ShadowTiptapViewer 3개입니다.

ShadowTiptapBase는 패키지 내부 Shadow DOM host로 스타일을 격리하며, 필요한 경우에만 Emotion cache를 사용할 수 있습니다. 패키지 내장 Editor/Viewer는 기본적으로 Emotion cache를 생성하지 않지만, Emotion 기반 custom block/UI를 Shadow DOM 안에서 소비해야 하면 emotionCache로 켤 수 있습니다.

설치

bun add shadow-tiptap

AI 프롬프트용 사용법

아래 블록을 LLM 프롬프트나 에이전트 컨텍스트에 그대로 붙여넣어 shadow-tiptap 사용법을 전달할 수 있습니다.

shadow-tiptap — Shadow DOM 기반 Tiptap React 라이브러리

## 설치
bun add shadow-tiptap

## Peer Dependencies
react, react-dom (`^18.3.1 || ^19.0.0`)

Tiptap 런타임과 Shadow DOM host 관련 패키지는 shadow-tiptap dependency로 함께 설치됩니다.

## 공개 API
import {
  ShadowTiptapBase,
  ShadowTiptapEditor,
  ShadowTiptapViewer,
} from 'shadow-tiptap';

### ShadowTiptapBase
Shadow DOM 경계 + 스타일 격리 래퍼.
<ShadowTiptapBase scopeId="my-scope" targetSelector="#my-scope">
  <div>커스텀 콘텐츠</div>
</ShadowTiptapBase>
// Emotion 기반 UI를 직접 감쌀 때만 기본값(true)을 사용하고, 순수 CSS 기반이면 emotionCache={false}로 cache 생성을 생략할 수 있습니다.

### ShadowTiptapEditor
Props:
- content: string | object — 초기 콘텐츠
- contentType?: 'auto' | 'json' | 'markdown' | 'html'
- outputFormat?: 'json' | 'html' | 'markdown'
- onChange?: (value) => void
- placeholder?: string
- maxLength?: number
- disabled?: boolean
- imageUpload?: (file: File) => Promise<string>
- textColorToolbar?: { colors?: { label: string; value: string }[] }
- extensions?: Extensions | ((defaultExtensions) => Extensions) — 커스텀 Tiptap extension 구성. 배열은 기본 extensions를 대체하고, 함수는 기본 extensions를 받아 확장합니다.
- editorProps?: EditorOptions['editorProps'] — handlePaste, attributes 등 Tiptap editorProps 전달
- initialMode?: 'editor' | 'markdown' | 'split'
- showModeToggle?: boolean
- showToolbar?: boolean
- toolbar?: React.ReactNode | ((context) => React.ReactNode)
- markdownMinHeight?: number (px)
- markdownMaxHeight?: number (px)
- emotionCache?: boolean — 기본값 false. Emotion 기반 UI를 Editor 내부에서 렌더링해야 할 때 true

예시:
<ShadowTiptapEditor
  content="# 제목"
  contentType="markdown"
  outputFormat="markdown"
  initialMode="split"
  showModeToggle
  onChange={(v) => console.log(v)}
/>

### ShadowTiptapViewer
Props:
- content: string | object — 표시할 콘텐츠
- contentType?: 'auto' | 'json' | 'markdown' | 'html'
- customBlocks?: Record<string, { fields?: Record<string, 'string' | 'number' | 'boolean' | 'json'>; render: (node, ctx) => React.ReactNode }>
- styleInjection?: { scopeId, targetSelector, disableDefaultStyles?, position?: 'top'|'bottom', styles?: { id: string; cssText: string }[] }
- adaptiveStyleInjection?: { adoptedStyleSheets?: CSSStyleSheet[] }
- emotionCache?: boolean — 기본값 false. customBlocks가 Emotion 기반 컴포넌트를 렌더링해야 할 때 true

예시:
<ShadowTiptapViewer
  content="**읽기 전용**"
  contentType="markdown"
  styleInjection={{ scopeId: 'v', targetSelector: '#v', styles: [{ id: 'c', cssText: '.shadow-tiptap-viewer { color: #111; }' }] }}
  emotionCache={true} // Emotion 기반 customBlocks를 Shadow DOM 내부에서 스타일링해야 할 때만 사용
/>

## 주의사항
- SSR 환경에서는 편집기 컴포넌트를 'use client' 경계 안에 두세요.
- split 모드는 markdown textarea + viewer preview 조합입니다 (WYSIWYG + preview 아님).

Storybook으로 확인하기

라이브러리의 ShadowTiptapBase, ShadowTiptapEditor, ShadowTiptapViewer를 시각적으로 확인하려면 Storybook을 실행하세요.

bun install
bun run storybook
  • 기본 포트: 6006
  • Storybook 내부 stories 는 모두 import { ... } from 'shadow-tiptap' 형식을 유지하고, 로컬 개발에서는 Storybook Vite alias가 루트 src/index.ts를 바라봅니다.
  • 정적 산출물 확인이 필요하면 bun run build-storybook 을 실행합니다.

로컬 검증

패키지 변경이나 릴리즈 전에는 아래 명령을 순서대로 확인하는 것을 권장합니다.

bun install
bun x vitest run
bun run build
bun run build-storybook
bun pm pack --dry-run
  • bun x vitest run — 공개 API, 문서 계약, Storybook 계약, 릴리즈 준비성 테스트를 한 번에 확인합니다.
  • bun run build — 스타일 생성(build:styles)과 타입/번들 산출물을 갱신합니다.
  • bun pm pack --dry-run — 실제 배포에 포함될 파일 목록과 dist 산출물을 점검합니다.

Peer Dependencies

아래 패키지는 앱에서 직접 설치해야 합니다.

  • react, react-dom (^18.3.1 || ^19.0.0)

Tiptap 런타임과 Shadow DOM host 관련 패키지(@tiptap/*, shadow-react-host, @emotion/*, react-shadow)는 shadow-tiptap의 dependency로 함께 설치됩니다.

내장 ShadowTiptapEditor/ShadowTiptapViewer는 Emotion cache가 필요 없는 소비처를 위해 기본 emotionCache 값을 false로 둡니다. Emotion 기반 custom block 또는 UI를 Shadow DOM 안에서 스타일링해야 하는 소비처는 해당 컴포넌트에 emotionCache={true}를 명시해 내부 ShadowDomHost cache를 사용할 수 있습니다. ShadowTiptapBase를 직접 사용할 때는 기존 호환을 위해 기본값이 true이며, 순수 CSS 또는 adoptedStyleSheets 기반이면 emotionCache={false}를 명시할 수 있습니다.

기본 import

import {
  ShadowTiptapBase,
  ShadowTiptapEditor,
  ShadowTiptapViewer,
} from 'shadow-tiptap';

Base 예시

import { ShadowTiptapBase } from 'shadow-tiptap';

export function BaseExample() {
  return (
    <ShadowTiptapBase scopeId="post-editor" targetSelector="#post-editor" emotionCache={false}>
      <div>커스텀 콘텐츠</div>
    </ShadowTiptapBase>
  );
}

Editor 예시

import { ShadowTiptapEditor } from 'shadow-tiptap';

export function EditorExample() {
  return (
    <ShadowTiptapEditor
      content="# 제목"
      contentType="markdown"
      outputFormat="markdown"
      initialMode="split"
      showModeToggle
      showToolbar
      toolbar={({ editor, mode, setMode }) => (
        <button type="button" onClick={() => setMode(mode === 'split' ? 'editor' : 'split')}>
          {editor ? '커스텀 툴바' : '준비 중'}
        </button>
      )}
      onChange={(value) => {
        console.log(value);
      }}
    />
  );
}

Editor 입력/출력 계약

  • content: 초기 콘텐츠 (string | object)
  • contentType: 'auto' | 'json' | 'markdown' | 'html'
  • outputFormat: 'json' | 'html' | 'markdown'
  • initialMode: 'editor' | 'markdown' | 'split'
  • showModeToggle: 모드 토글 버튼 노출 여부
  • showToolbar: 기본값 true. editor 상단 toolbar 영역 노출 여부
  • toolbar: 커스텀 toolbar. ReactNode 또는 { editor, mode, setMode, disabled } context를 받는 render prop을 전달할 수 있습니다.
  • showModeToggle: 기본 toolbar 안의 모드 토글 노출 여부. toolbar를 직접 전달하면 커스텀 toolbar가 기본 모드 토글을 대체합니다.
  • markdownMinHeight: markdown 모드 textarea 최소 높이(px)
  • markdownMaxHeight: markdown 모드 textarea 최대 높이(px)
  • emotionCache: 기본값 false. Emotion 기반 UI를 Editor 내부에서 렌더링해야 하면 true로 켭니다.
  • extensions: 커스텀 Tiptap extension 구성입니다. 배열을 넘기면 패키지 기본 extensions를 대체하고, 함수를 넘기면 기본 extensions를 받아 추가/교체할 수 있습니다.
  • editorProps: Tiptap useEditoreditorProps로 전달됩니다. handlePaste, handleDrop, attributes.class, aria-label 같은 기존 editor 설정을 이관할 때 사용합니다.

markdown 모드 작성 UX

  • initialMode="markdown" 이면 읽기 전용 <pre> 대신 수정 가능한 textarea 가 렌더됩니다.
  • textarea 높이는 입력량에 따라 자동 조절되며, markdownMinHeight / markdownMaxHeight 로 범위를 제어할 수 있습니다.
  • 동일 내용은 내부 editor JSON state 와 동기화되므로 split / editor 모드로 전환해도 수정 내용이 유지됩니다.

기존 Tiptap editor toolbar 마이그레이션

기존 앱에서 직접 useEditorEditorContent를 구성했다면, toolbar UI만 toolbar로 옮기는 것만으로는 부족할 수 있습니다. toolbar 버튼이 호출하는 명령을 사용하려면 기존 extension과 editorProps도 함께 전달합니다.

import { Mark, mergeAttributes } from '@tiptap/core';
import Highlight from '@tiptap/extension-highlight';
import Image from '@tiptap/extension-image';
import Link from '@tiptap/extension-link';
import Underline from '@tiptap/extension-underline';
import StarterKit from '@tiptap/starter-kit';
import { ShadowTiptapEditor } from 'shadow-tiptap';

const SemanticTextColorExtension = Mark.create({
  name: 'semanticTextColor',
  renderHTML({ HTMLAttributes }) {
    return ['span', mergeAttributes(HTMLAttributes), 0];
  },
});

<ShadowTiptapEditor
  content={value}
  contentType="json"
  outputFormat="json"
  extensions={[
    StarterKit,
    Image,
    Link.configure({ openOnClick: false }),
    Highlight,
    Underline,
    SemanticTextColorExtension,
  ]}
  editorProps={{
    handlePaste: (_view, event) => {
      // 기존 image paste 처리 로직을 여기에 이관합니다.
      return false;
    },
    attributes: {
      'aria-label': '내용',
      class: 'min-h-[260px] rounded-clay-medium border border-clay-border-default bg-white',
    },
  }}
  toolbar={({ editor, disabled }) => (
    <button
      type="button"
      disabled={!editor || disabled}
      onClick={() => editor?.chain().focus().toggleBold().run()}
    >
      굵게
    </button>
  )}
/>

split 모드 동작

  • initialMode="split" 이면 좌측에는 markdown textarea, 우측에는 ShadowTiptapViewer 기반 preview 가 렌더됩니다.
  • 즉, split 모드는 WYSIWYG editor + preview 조합이 아니라 text editor + viewer preview 조합입니다.

Viewer 예시

import { ShadowTiptapViewer } from 'shadow-tiptap';

export function ViewerExample() {
  return (
    <ShadowTiptapViewer
      content={'# 제목\n\n본문은 패키지 기본 typography preset으로 바로 렌더됩니다.'}
      contentType="markdown"
    />
  );
}

viewer typography 기본값과 styleInjection

ShadowTiptapViewer.shadow-tiptap-viewer 셀렉터 기준 기본 typography preset을 패키지 기본값으로 함께 주입합니다.

  • 기본 동작: heading, list, blockquote, code spacing 을 별도 CSS 없이 렌더합니다.
  • headless/opt-out: styleInjection.disableDefaultStylestrue로 설정합니다.
  • custom override: styleInjection.stylesposition: 'bottom'을 함께 사용해 consumer CSS가 기본 preset 뒤에서 덮어쓰게 합니다.
  • adaptive style injection: constructable stylesheet는 adaptiveStyleInjection.adoptedStyleSheets로 별도 전달할 수 있습니다.
<ShadowTiptapViewer
  content="## Override example"
  contentType="markdown"
  styleInjection={{
    scopeId: 'viewer-scope',
    targetSelector: '#viewer-scope',
    position: 'bottom',
    styles: [
      {
        id: 'viewer-override',
        cssText:
          '#viewer-scope .shadow-tiptap-viewer { color: #111; } #viewer-scope .shadow-tiptap-viewer h2 { color: #7c3aed; }',
      },
    ],
  }}
  adaptiveStyleInjection={{
    adoptedStyleSheets: [viewerConstructableStyleSheet],
  }}
/>

custom block registry 사용

customBlocks로 노드 타입별 렌더러를 등록할 수 있습니다.

import { ShadowTiptapViewer } from 'shadow-tiptap';

const customBlocks = {
  callout: {
    fields: { tone: 'string' },
    render: (node, context) => (
      <aside data-tone={String(node.attrs?.tone ?? 'info')}>
        {context.renderChildren(node.content)}
      </aside>
    ),
  },
};

export function CustomBlockExample() {
  return <ShadowTiptapViewer content={{ type: 'doc', content: [] }} customBlocks={customBlocks} />;
}

export function EmotionCustomBlockExample() {
  return <ShadowTiptapViewer content={{ type: 'doc', content: [] }} customBlocks={customBlocks} emotionCache />;
}

customBlocks가 Emotion 기반 컴포넌트를 반환하면 emotionCache를 켜고, 순수 CSS/React 노드만 반환하면 기본값 false를 유지합니다.

SSR / client boundary 주의사항

  • import { ... } from 'shadow-tiptap' 자체는 Node SSR 환경에서도 안전합니다.
  • 실제 편집 상호작용은 브라우저에서 동작하므로, Next.js 같은 환경에서는 편집기 사용 컴포넌트를 클라이언트 경계('use client') 안에 두세요.
  • 서버 렌더 시에는 wrapper 마크업이 렌더되며, 클라이언트 하이드레이션 이후 Shadow DOM/에디터 동작이 활성화됩니다.

Vite dev server / optimized dependency 설정

Magnet Shadow DOM Web Component, lazy route, side sheet처럼 shadow-tiptap이 별도 lazy boundary에서 처음 import되는 앱은 Vite dev server의 optimizeDeps 결과에서 React/Emotion이 shadow-tiptap.js 안으로 함께 pre-bundle될 수 있습니다. 이 경우 소비 앱 React dispatcher와 분리되어 Invalid hook call이 발생할 수 있으므로 Vite 설정에서 React/Emotion singleton을 명시적으로 고정하세요.

import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    dedupe: ['react', 'react-dom', '@emotion/react', '@emotion/cache'],
  },
  optimizeDeps: {
    include: [
      'react',
      'react-dom',
      'react-dom/server',
      'react-dom/client',
      'react/jsx-runtime',
      '@emotion/react',
      '@emotion/cache',
      'react-shadow',
      'react-shadow/emotion',
      'shadow-react-host',
      'shadow-tiptap',
    ],
  },
});

이 설정은 shadow-tiptap 배포 번들의 external 처리와 별개로, Vite dev pre-bundling 단계에서도 React/React DOM/Emotion을 앱의 단일 optimized entry로 분리하도록 강제합니다. 프로덕션 번들은 기존 peer dependency 해석을 따르며, 로컬 dev server에서 .vite/deps/shadow-tiptap.jsReactCurrentDispatcher 또는 You are loading @emotion/react when it is already loaded 같은 런타임 코드/경고 문자열이 직접 들어가는 회귀를 막기 위한 설정입니다.

Storybook 스토리 구성

  • Base/ShadowTiptapBase — Shadow DOM 경계와 기본 스타일 주입 확인
  • Editor/ShadowTiptapEditor — editor / markdown / split mode 와 출력 직렬화 확인
  • Viewer/ShadowTiptapViewer — markdown, custom block, empty state, styleInjection 확인

기존 앱에서 마이그레이션/대체 포인트

  • 앱 내부 editor/viewer 래퍼 대신 ShadowTiptapEditor, ShadowTiptapViewer로 교체합니다.
  • 기존 스타일 격리 로직은 ShadowTiptapBase + styleInjection(scopeId, targetSelector)으로 대체합니다.
  • 뷰어 커스텀 노드 렌더 스위치문은 customBlocks 레지스트리로 이동합니다.
  • 라이브러리 공개 API는 컴포넌트 3개와 타입 export 만 사용하고, helper 직접 import 의존은 제거합니다.