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

@lumir-company/editor

v0.5.7

Published

LumirEditor — vanilla (HTML/JS/CSS) rich text editor, BlockNote-JSON round-trip compatible.

Readme

@lumir-company/editor

의존성 0(vanilla HTML/JS/CSS) 리치 텍스트 에디터 — BlockNote-JSON 라운드트립 호환.

npm version License: MIT

구 BlockNote 기반 @lumir-company/[email protected]드롭인 대체입니다. 동일한 API 표면(LumirEditor 컴포넌트·props·타입·s3Upload 계약)을 유지하되, 코어에서 React / BlockNote / ProseMirror 런타임을 완전히 제거했습니다. 코어는 react-free라 서버·명령형 환경에서도 사용할 수 있고, React 컴포넌트는 선택적 래퍼로 제공됩니다.


목차


특징

| 특징 | 설명 | | --- | --- | | 의존성 0 | 코어에 React/BlockNote/ProseMirror 런타임 없음(vanilla HTML/JS/CSS). React는 선택적 peer | | 드롭인 호환 | 구 @lumir-company/[email protected]와 동일한 컴포넌트·props·타입·저장 JSON | | react-free 코어 | mountLumirEditor() 명령형 API로 서버 컴포넌트·비-React 환경에서도 사용 가능 | | 이미지 업로드 | 드래그앤드롭·붙여넣기·슬래시 메뉴, S3 presigned URL 내장, 파일명 커스터마이징, 로딩 표시 | | 동영상/오디오/파일 | allowVideoUpload·allowAudioUpload·allowFileUpload opt-in | | 테이블 | Notion 스타일 grip 핸들, 셀 배경/글자색·정렬, 병합/분할, 행 높이·열 너비 리사이즈, 표 전체 종횡비 스케일, 에디터 폭 자동 맞춤, Excel/Word 붙여넣기 | | 2·3단 컬럼 | 노션식 다단 컬럼 레이아웃(MIT 자체 구현), 슬래시로 2·3단 삽입, 블록 DnD로 2단 생성, 블록별/전역 구분선, 마지막 블록에서 Enter 두 번으로 컬럼 탈출 | | 글자 크기 | 인라인 글자 크기(프리셋 + 1px 스테퍼, 8~96px), 구버전 안전 직렬화 | | 여러 줄 선택·편집 | 블록 경계를 넘는 드래그로 여러 줄 선택 → 서식 일괄 적용, 선택 영역 삭제·타이핑·붙여넣기·부분 복사(무손실), Shift+클릭·Ctrl/Cmd+A 전체선택, 단일 Undo | | 링크 | URL 붙여넣기 → 인라인 링크, Notion식 링크 툴바(hover 툴팁 → 편집 popup) | | 직렬화 | 블록 JSON ↔ HTML ↔ Markdown 상호 변환 유틸 공개 export | | TypeScript | 전 표면 .d.ts 제공 | | 테마 | 라이트/다크 + 커스텀 테마 객체 |

지원 파일 형식

| 종류 | 형식 | 기본 용량 한도 | | --- | --- | --- | | 이미지 | PNG · JPEG/JPG · GIF · WebP · BMP (SVG는 XSS 방지로 제외) | 10MB | | 동영상 | MP4 · WebM · OGG · MOV | 100MB | | 오디오/파일 | allowAudioUpload·allowFileUpload 활성화 시 | maxAudioFileSize 등으로 설정 |


설치

npm i @lumir-company/editor
# 또는
yarn add @lumir-company/editor
# 또는
pnpm add @lumir-company/editor

Peer dependencies (선택):

  • react ≥ 18.0.0
  • react-dom ≥ 18.0.0

React/react-domReact 컴포넌트(LumirEditor)나 드롭인 루트를 사용할 때만 필요합니다. ./core 서브패스(vanilla)만 사용한다면 React 없이 동작합니다.


빠른 시작

중요: @lumir-company/editor/style.css를 반드시 임포트하세요. 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.

1. React 드롭인

import LumirEditor from "@lumir-company/editor";        // default export
// 또는: import { LumirEditor } from "@lumir-company/editor";
import "@lumir-company/editor/style.css";

export default function App() {
  return (
    <div style={{ height: 500 }}>
      <LumirEditor
        initialContent={blocks}
        editable
        s3Upload={{ apiEndpoint: "/api/s3/presigned", env: "production", path: "cms/wiki", appendUUID: true }}
        onContentChange={(blocks) => save(JSON.stringify(blocks))}
        onImageDelete={(url) => deleteFromS3(url)}
      />
    </div>
  );
}

컨테이너에 높이를 지정해야 에디터가 보입니다.

2. Next.js

브라우저 전용 API를 사용하므로 SSR을 비활성화합니다.

"use client";

import dynamic from "next/dynamic";
import "@lumir-company/editor/style.css";

const LumirEditor = dynamic(
  () => import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
  { ssr: false },
);

export default function EditorPage() {
  return (
    <div style={{ height: 500 }}>
      <LumirEditor />
    </div>
  );
}

3. react-free 코어 (vanilla)

React 없이 임의의 DOM에 마운트하는 명령형 API입니다. 서버 컴포넌트·순수 JS·다른 프레임워크에서 사용할 수 있습니다.

import { mountLumirEditor } from "@lumir-company/editor/core";
import "@lumir-company/editor/style.css";

const editor = mountLumirEditor(document.getElementById("host"), {
  initialContent: blocks,
  s3Upload: { apiEndpoint: "/api/s3/presigned", env: "production", path: "docs" },
  onContentChange: (blocks) => save(blocks),
});

editor.getDocument();   // 현재 블록 JSON
editor.getHTML();       // HTML 문자열
editor.getMarkdown();   // Markdown 문자열
editor.destroy();       // 정리

자세한 인스턴스 메서드는 react-free 코어 API를 참고하세요.


Exports (서브패스)

| 서브패스 | 내용 | React 필요 | | --- | --- | --- | | . | React LumirEditor(default + named) + 코어/유틸 전체 ("use client") | ✅ | | ./react | .과 동일 표면(하위호환 별칭) | ✅ | | ./core | react-free 코어(mountLumirEditor·직렬화·유틸) — 서버 컴포넌트 안전 | ❌ | | ./style.css | 단일 번들 CSS | — |

각 서브패스는 types(.d.ts) / import(ESM) / require(CJS) 조건을 모두 제공합니다.

참고: 구버전에 있던 @lumir-company/editor/api/link-preview 서브패스는 제거되었습니다. 마이그레이션을 참고하세요.


이미지 업로드

이미지는 붙여넣기 · 드래그 앤 드롭 · 슬래시 메뉴(/ → Image) · 사이드/플로팅 메뉴로 삽입됩니다. 업로드 방식은 아래 우선순위로 결정됩니다.

  1. uploadFile prop이 있으면 → 해당 함수로 업로드
  2. 없고 s3Upload가 있으면 → S3 presigned URL 업로드
  3. 둘 다 없으면 → 파일 삽입 시 업로드 실패

S3 업로드 (권장)

<LumirEditor
  s3Upload={{
    apiEndpoint: "/api/s3/presigned",
    env: "production",
    path: "blog/images",
  }}
/>

S3 저장 경로: {env}/{path}/{filename} — 예: production/blog/images/my-photo.png

API 엔드포인트 계약

클라이언트는 GET {apiEndpoint}?key={파일키}&contentType={MIME} 형태로 요청하고, 서버는 다음 JSON을 반환해야 합니다.

{
  "presignedUrl": "https://s3.amazonaws.com/bucket/upload-url",
  "publicUrl": "https://cdn.example.com/production/blog/images/my-photo.png"
}

Presigned URL API 예시 (Next.js App Router)

app/api/s3/presigned/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
  // ⚠️ 필수: AWS SDK v3(≥3.729)는 기본 체크섬(WHEN_SUPPORTED)으로 presigned PUT 서명에
  // x-amz-checksum-crc32 를 넣는데, 브라우저 PUT은 Content-Type만 전송 → 불일치로 S3 403.
  // 아래처럼 체크섬을 "요구될 때만" 계산하도록 낮춰야 브라우저 직접 업로드가 통과됩니다.
  requestChecksumCalculation: "WHEN_REQUIRED",
  responseChecksumValidation: "WHEN_REQUIRED",
});

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const key = searchParams.get("key");                 // 업로드할 파일 키 ({env}/{path}/{filename})
  const contentType = searchParams.get("contentType"); // MIME (선택, 없으면 application/octet-stream)
  if (!key) return NextResponse.json({ error: "key is required" }, { status: 400 });

  const command = new PutObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET!,
    Key: key,
    ContentType: contentType || "application/octet-stream",
  });

  const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 60 }); // 유효 60초
  const publicUrl = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
  return NextResponse.json({ presignedUrl, publicUrl, key });
}

필요 환경 변수: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET.

주의: requestChecksumCalculation / responseChecksumValidation"WHEN_REQUIRED"로 두지 않으면 최신 AWS SDK v3에서 브라우저 업로드가 403으로 실패합니다. 이 레포의 동작 예시는 lumir-editor-test/src/app/api/s3/presigned/route.ts에 있습니다.

Express / Remix / SvelteKit 등도 동일하게 key·contentType을 받아 { presignedUrl, publicUrl }을 반환하는 GET 라우트를 만들면 됩니다. apiEndpoint만 해당 서버 주소로 맞추세요.

파일명 커스터마이징

여러 파일을 동시에 올릴 때 이름 충돌을 방지합니다. 기본적으로 확장자는 자동으로 붙습니다(preserveExtension: false로 끌 수 있음).

<LumirEditor
  s3Upload={{
    apiEndpoint: "/api/s3/presigned",
    env: "production",
    path: "uploads",
    // 확장자를 제거한 이름(nameWithoutExt)이 전달됩니다
    fileNameTransform: (nameWithoutExt, file) => `user123_${nameWithoutExt}`,
    appendUUID: true,          // 변환 후 UUID를 확장자 앞에 추가
    // preserveExtension: false // 확장자를 붙이지 않음(서버에서 WebP 변환 등)
  }}
/>

결과: photo.pnguser123_photo_550e8400-…-446655440000.png

커스텀 업로더

<LumirEditor
  uploadFile={async (file) => {
    const fd = new FormData();
    fd.append("file", file);
    const res = await fetch("/api/upload", { method: "POST", body: fd });
    const { url } = await res.json();
    return url; // 공개 URL 문자열 반환
  }}
/>

헬퍼: createS3Uploader

import { createS3Uploader } from "@lumir-company/editor";

const s3Uploader = createS3Uploader({
  apiEndpoint: "/api/s3/presigned",
  env: "production",
  path: "images",
  appendUUID: true,
});

<LumirEditor uploadFile={s3Uploader} />;
// 또는 독립 사용: const url = await s3Uploader(file);

동영상·오디오·파일 업로드

동영상은 allowVideoUpload={true}일 때만 활성화됩니다. 업로드 설정(s3Upload/uploadFile)은 이미지와 동일하게 공유됩니다.

<LumirEditor
  allowVideoUpload
  maxVideoFileSize={200 * 1024 * 1024} // 200MB (기본 100MB)
  s3Upload={{ apiEndpoint: "/api/s3/presigned", env: "production", path: "videos", appendUUID: true }}
/>
  • 삽입 경로: 붙여넣기, 드래그 앤 드롭, 슬래시 메뉴("Video"), 플로팅/사이드 메뉴
  • 지원 URL: 비디오 블록의 url직접 재생 가능한 파일 URL(예: .mp4, .webm, .ogg)만 지원합니다. YouTube/Vimeo 등 스트리밍 페이지 URL은 <video>로 재생되지 않습니다.
  • 오디오/일반 파일: allowAudioUpload / allowFileUpload로 opt-in. 용량은 maxAudioFileSize 등으로 조절합니다.

이미지·동영상 경로 분리

fileNameTransform의 두 번째 인자 file로 종류를 구분해 prefix를 나눌 수 있습니다.

fileNameTransform: (nameWithoutExt, file) => {
  const isVideo = file.type.startsWith("video/");
  return `${isVideo ? "videos" : "images"}/${nameWithoutExt}`;
}
// 결과: production/uploads/videos/clip_xxx.mp4, production/uploads/images/photo_xxx.png

저장 데이터 구조

{ "type": "image", "props": { "url": "https://cdn/…/photo.png", "caption": "", "previewWidth": 512 }, "content": [], "children": [] }
{ "type": "video", "props": { "url": "https://cdn/…/clip.mp4" }, "content": [], "children": [] }

업로드 진행률

S3 업로드 시 s3Upload.onProgress 콜백으로 0~100% 진행률을 받을 수 있습니다. 에디터는 이 값을 툴바에 n%로 자동 표시합니다. 대용량 파일에서 브라우저가 progress 이벤트를 드물게 보내는 경우를 대비해 중간 진행률을 보간하여 부드럽게 갱신합니다.

<LumirEditor
  allowVideoUpload
  s3Upload={{
    apiEndpoint: "/api/s3/presigned",
    env: "production",
    path: "videos",
    uploadTimeoutMs: 180000, // PUT 타임아웃(ms). 기본 120000
    maxRetries: 2,           // PUT 실패 재시도. 기본 2(최대 3회 시도)
    onProgress: (percent) => console.log(`${percent}%`),
  }}
/>
  • onProgressS3 PUT 요청 시에만 호출됩니다(presigned URL 요청 단계 제외).
  • 업로드 시작 직후 onProgress(0), 완료 시 onProgress(100)이 보장됩니다.

이미지·비디오 삭제

에디터에서 이미지 또는 비디오 블록이 삭제되면 onImageDelete(url)이 호출됩니다(둘 다 동일 콜백).

<LumirEditor
  s3Upload={{ /* … */ }}
  onImageDelete={(url) => {
    // S3 등 외부 스토리지 삭제
  }}
/>

권장: 지연 삭제 (Undo/Redo 대응)

Undo로 복원 가능하도록 실제 삭제를 지연시키는 패턴을 권장합니다.

"use client";
import { useRef, useCallback } from "react";

function Editor() {
  const pending = useRef(new Map<string, ReturnType<typeof setTimeout>>());

  const handleImageDelete = useCallback((url: string) => {
    if (pending.current.has(url)) return;
    const t = setTimeout(async () => {
      pending.current.delete(url);
      await fetch(`/api/s3/delete?url=${encodeURIComponent(url)}`, { method: "DELETE" });
    }, 5 * 60 * 1000); // 5분 후 삭제
    pending.current.set(url, t);
  }, []);

  return <LumirEditor s3Upload={{ /* … */ }} onImageDelete={handleImageDelete} />;
}

| 항목 | 권장 | | --- | --- | | Undo/Redo | 지연 삭제(5~10분)로 복원 가능하게 구현 | | 권한 검증 | 프로덕션에서는 인증/인가 필수 | | 참조 카운트 | 같은 URL을 여러 문서에서 쓰는지 확인 |


테이블

슬래시 메뉴(/ → Table)나 Excel/Word 셀 붙여넣기로 표를 만듭니다.

Notion 스타일 grip 핸들

셀에 포커스하면 주변에 grip 핸들이 표시됩니다.

| 위치 | 동작 | | --- | --- | | 상단 grip | 클릭 → 열 메뉴(삭제 / 좌·우 열 추가 / 색) · 드래그 → 열 이동 | | 좌측 grip | 클릭 → 행 메뉴(삭제 / 위·아래 행 추가 / 색) · 드래그 → 행 이동 | | 우측 grip | hover → 셀 메뉴(셀 배경색 등) |

  • 행/열 메뉴가 열리면 해당 행/열 전체가 하이라이트됩니다.
  • 표 우측/하단 가장자리 hover 시 행/열 추가 버튼이 나타납니다.

셀 색·정렬

  • 단일 셀: 우측 grip 또는 행/열 메뉴의 "색"에서 배경색/글자색 적용
  • 다중 셀: 드래그로 범위 선택 후 플로팅 툴바 색상/정렬 버튼으로 일괄 적용
  • 표 전체 선택: 셀 포커스 상태에서 Ctrl/Cmd + A → 표의 모든 셀 선택(다시 누르면 문서 전체로 확장)

리사이즈 · 스케일 · 정렬

  • 열 너비: 열 경계 드래그
  • 행 높이: 행 경계 드래그(셀 rowHeight attr로 저장·라운드트립)
  • 표 전체 스케일: 표 우하단 모서리 hover → 대각 드래그로 종횡비를 고정한 채 표 전체를 균일 배율로 확대/축소
  • 표 정렬: 포매팅 툴바/드래그핸들 메뉴에서 표 전체를 좌/가운데/우 정렬
  • 에디터 폭 자동 맞춤: 에디터보다 넓은 표를 붙여넣으면 열 비율을 유지한 채 폭에 맞춰 축소(가로 스크롤 미제공)

Excel/Word 붙여넣기

Excel·Word 등에서 복사한 셀 범위를 붙여넣으면(Ctrl+V) 이미지가 아닌 편집 가능한 테이블로 삽입됩니다. 셀 배경색·글자색·정렬·글자 크기·세로 정렬·굵게/기울임/밑줄이 함께 변환됩니다(글꼴·정확한 hex 색은 10색 팔레트로 근사).

테이블 기능 설정

<LumirEditor
  tables={{
    splitCells: true,          // 셀 병합/분할 (기본 true)
    cellBackgroundColor: true, // 셀 배경색 (기본 true)
    cellTextColor: true,       // 셀 글자색 (기본 true)
    headers: true,             // 헤더 행/열 (기본 true)
  }}
  tableHandles={true}          // 핸들/코너 스케일 전체 게이트 (기본 true, false면 모두 끔)
/>

다단 컬럼 (2·3단)

노션식 다단 컬럼 레이아웃입니다. 공식 @blocknote/xl-multi-column(AGPL) 대신 MIT 자체 구현을 사용합니다. 2단·3단을 지원합니다.

  • 삽입: 슬래시 메뉴 /2단 컬럼·/2단 컬럼 (구분선)·/3단 컬럼·/3단 컬럼 (구분선).
  • 블록 DnD 생성: 블록을 다른 블록의 좌/우 가장자리로 끌어다 놓으면 2단 컬럼이 생성됩니다(세로 드롭 인디케이터). (3단은 슬래시 메뉴로 생성)
  • 컬럼 간 이동: 블록 그립(⠿)을 다른 컬럼 안으로 드래그. 컬럼의 마지막 블록을 옮겨도 원래 컬럼은 빈 문단으로 유지되어(사라지지 않음) 다시 블록을 넣을 수 있습니다.
  • 컬럼 탈출: 컬럼 맨 마지막 블록에서 Enter 두 번 → 컬럼 아래에 일반 문단 생성·포커스(어느 컬럼에서든 동일).
  • 너비 리사이즈: 컬럼 사이 경계를 드래그해 인접 두 컬럼의 비율 조절(합 유지).
  • 구분선:
    • 블록별: 생성 시 "구분선" 항목을 고르면 그 블록에 고정되어 저장·라운드트립(showDivider).
    • 전역: columnDivider prop(기본 false)으로 모든 컬럼 사이에 세로 구분선 표시.
<LumirEditor columnDivider />

구분선 색은 CSS 변수 --lumir-column-divider-color로 조절합니다(미지정 시 라이트 #d1d5db / 다크 #4a4a4a). 구분선 양옆에는 블록 그립이 뜰 여백(약 18–20px)이 예약됩니다.

하위호환: 4단 이상(레거시/외부 데이터)으로 저장된 columnList는 로드 시 초과 컬럼의 블록이 마지막(3번째) 컬럼으로 병합되어 3단으로 정규화됩니다. 1단은 펼쳐서 일반 블록으로 승격됩니다.


글자 크기

텍스트를 선택한 뒤 포매팅 툴바 또는 상단 고정 툴바(FloatingMenu) 의 글자 크기 컨트롤로 인라인 크기를 변경합니다.

  • 프리셋: 기본(14px, 스타일 제거) / 10 · 12 · 14 · 16 · 18 · 20 · 24 · 28 (px)
  • 1px 스테퍼: 드롭다운 상단의 /+ 버튼과 직접 입력(/ 키)으로 8~96px 범위 내 임의 값 지정. 명시 크기가 없으면 14px 기준으로 증감
  • 테이블 셀 텍스트에도 동일하게 적용됩니다.
  • 외부 HTML(웹페이지·Excel 등)을 붙여넣을 때의 글자 크기는 가져오지 않습니다.

하위호환 직렬화 포맷 (중요)

글자 크기는 저장 JSON에서 styles 맵이 아니라 styled-text의 형제(sibling) 키 fontSize 로 직렬화됩니다.

{
  "type": "paragraph",
  "content": [
    { "type": "text", "text": "큰 글씨", "styles": { "bold": true }, "fontSize": "18px" }
  ]
}

이유: BlockNote는 styles 맵에 스키마에 없는 키가 있으면 예외를 던집니다. styles.fontSize로 저장하면 fontSize 스펙이 없는 구버전 SDK(≤0.4.15) 가 그 JSON을 로드할 때 크래시합니다. 형제 키 방식은 구버전에서 조용히 무시되어(글자 크기만 미표시) 안전하게 로드됩니다.

  • 에디터 로드/저장 시 변환은 자동입니다(initialContentonContentChange).
  • 외부 렌더러에서 저장 JSON을 직접 다룬다면, 공개 export된 liftFontSize(blocks)로 형제 키를 styles.fontSize로 복원한 뒤 사용하세요. 반대로 외부로 내보낼 때는 반드시 lowerFontSize(blocks)를 거쳐야 합니다(styles.fontSize가 유출되면 구버전 소비 앱이 크래시).
  • 직렬화 형태 타입은 SerializedStyledText로 export됩니다.

링크

URL을 텍스트에 붙여넣으면 자동으로 인라인 링크로 변환됩니다(구버전의 자동 OG 카드 생성은 제거됨).

  • 링크 툴바(Notion식): 링크에 hover하면 URL 툴팁이 뜨고, 클릭하면 URL·텍스트를 편집하는 popup이 열립니다.
  • 보안: javascript:·data:·vbscript:·file: 등 위험 프로토콜은 차단됩니다.
  • linkToolbar prop(기본 true)으로 링크 툴바 표시를 제어합니다.

구버전의 linkPreview prop과 /api/link-preview 서브패스는 제거되었습니다.


Placeholder

에디터가 비어있을 때 안내 텍스트를 표시합니다.

<LumirEditor placeholder="내용을 입력하세요..." />
  • 빈 블록에 연한 색으로 표시되고, 입력을 시작하면 사라집니다.
  • 모든 빈 블록(첫 블록 포함)에 동일하게 적용됩니다.

테마

<LumirEditor theme="dark" />
<LumirEditor theme="light" />
<LumirEditor theme={{ /* 커스텀 테마 객체 */ }} />

theme"light" | "dark" | 커스텀 테마 객체를 받습니다(기본 "light").


Props API

자주 쓰는 Props

| Prop | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | initialContent | DefaultPartialBlock[] \| string | undefined | 초기 콘텐츠(블록 배열 또는 JSON 문자열) | | onContentChange | (blocks: DefaultPartialBlock[]) => void | undefined | 콘텐츠 변경 콜백 | | s3Upload | S3UploaderConfig | undefined | S3 업로드 설정 | | uploadFile | (file: File) => Promise<string> | undefined | 커스텀 업로드 함수 | | onImageDelete | (url: string) => void | undefined | 이미지·비디오 삭제 콜백 | | onError | (error: LumirEditorError) => void | undefined | 에러 콜백 | | editable | boolean | true | 편집 가능 여부 | | placeholder | string | undefined | 빈 블록 안내 텍스트 | | theme | "light" \| "dark" \| object | "light" | 테마 | | allowVideoUpload | boolean | false | 동영상 업로드 허용 | | tables | TableConfig | 모두 true | 테이블 기능 | | maxImageFileSize | number | 10MB | 이미지 최대 용량(바이트) | | maxVideoFileSize | number | 100MB | 동영상 최대 용량(바이트) | | className | string | "" | 컨테이너 CSS 클래스 |

전체 Props

interface LumirEditorProps {
  // 콘텐츠
  initialContent?: DefaultPartialBlock[] | string;
  initialEmptyBlocks?: number;          // 초기 빈 블록 개수 (기본 3)
  placeholder?: string;

  // 업로드
  uploadFile?: (file: File) => Promise<string>;
  s3Upload?: S3UploaderConfig;
  allowVideoUpload?: boolean;           // 기본 false
  allowAudioUpload?: boolean;           // 기본 false
  allowFileUpload?: boolean;            // 기본 false
  maxImageFileSize?: number;            // 미설정 시 10MB
  maxVideoFileSize?: number;            // 미설정 시 100MB
  maxAudioFileSize?: number;

  // 기능
  tables?: { splitCells?: boolean; cellBackgroundColor?: boolean; cellTextColor?: boolean; headers?: boolean };
  heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] };
  defaultStyles?: boolean;              // 기본 true
  disableExtensions?: string[];
  tabBehavior?: "prefer-navigate-ui" | "prefer-indent"; // 기본 "prefer-navigate-ui"
  trailingBlock?: boolean;              // 기본 true
  trailingBlockType?: string;

  // UI
  editable?: boolean;                   // 기본 true
  theme?: "light" | "dark" | Record<string, unknown>; // 기본 "light"
  formattingToolbar?: boolean;          // 기본 true
  linkToolbar?: boolean;                // 기본 true
  sideMenu?: boolean;                   // 기본 true
  sideMenuAddButton?: boolean;          // 기본 false
  slashMenu?: boolean;                  // 기본 true
  emojiPicker?: boolean;                // 기본 true
  filePanel?: boolean;                  // 기본 true
  tableHandles?: boolean;               // 기본 true
  columnDivider?: boolean;              // 2단 컬럼 세로 구분선 (기본 false)
  floatingMenu?: boolean;               // 상단 고정 툴바 (기본 false)
  floatingMenuPosition?: "sticky" | "fixed"; // 기본 "sticky"
  className?: string;

  // 로케일/기타
  locale?: string | LumirLocale;        // "ko" | "en" 등
  resolveFileUrl?: (url: string) => string | Promise<string>;
  fetchLinkPreview?: (url: string) => Promise<LinkPreview> | LinkPreview; // 링크 카드 메타 조회(서버 프록시 권장)
  showErrorToast?: boolean;

  // 콜백
  onContentChange?: (blocks: DefaultPartialBlock[]) => void;
  onError?: (error: LumirEditorError) => void;
  onSelectionChange?: () => void;
  onImageDelete?: (url: string) => void;
  onUploadStart?: (file: File) => void;
  onUploadEnd?: (file: File) => void;
}

S3UploaderConfig

interface S3UploaderConfig {
  apiEndpoint: string;                  // Presigned URL API 엔드포인트
  env: "development" | "production";
  path: string;                         // S3 저장 경로

  fileNameTransform?: (nameWithoutExt: string, file: File) => string;
  appendUUID?: boolean;                 // 파일명 뒤(확장자 앞)에 UUID 추가
  preserveExtension?: boolean;          // 기본 true. false면 확장자 미부착

  onProgress?: (percent: number) => void; // 0~100, S3 PUT 시만 호출(보간)
  uploadTimeoutMs?: number;             // PUT 타임아웃. 기본 120000
  maxRetries?: number;                  // PUT 재시도. 기본 2
}

React ref (imperative API)

import { useRef } from "react";
import { LumirEditor, type LumirEditorReactRef } from "@lumir-company/editor";

const ref = useRef<LumirEditorReactRef>(null);

<LumirEditor ref={ref} />;

ref.current?.getDocument();      // DefaultPartialBlock[] | null
ref.current?.undo();  ref.current?.redo();
ref.current?.canUndo();  ref.current?.canRedo();
ref.current?.insertFile(file);
ref.current?.setTheme("dark");
ref.current?.setEditable(false);
ref.current?.editor;             // 내부 VanillaEditor 인스턴스

react-free 코어 API

mountLumirEditor(host, options)./core(또는 루트)에서 import하며 VanillaEditor 인스턴스를 반환합니다.

import { mountLumirEditor, type VanillaEditor } from "@lumir-company/editor/core";

const editor: VanillaEditor = mountLumirEditor(hostEl, options /* LumirEditorProps */);

VanillaEditor 인스턴스 메서드

| 메서드 | 반환 | 설명 | | --- | --- | --- | | getDocument() | DefaultPartialBlock[] | 현재 블록 JSON | | getHTML() | string | 콘텐츠 HTML | | getFullHTML(opts?) | string | <html> 문서 HTML({ title, style }) | | getMarkdown() | string | 콘텐츠 Markdown | | setBlocks(blocks) | void | 블록 교체 | | setMarkdown(md) | void | Markdown으로 콘텐츠 설정 | | commit() | boolean | 편집 커밋 | | undo() / redo() | boolean | 실행 취소/재실행 | | canUndo() / canRedo() | boolean | 가능 여부 | | insertFile(file) | void | 파일 삽입(업로드) | | isEditable() / setEditable(on) | — | 편집 가능 상태 | | setTheme(theme) | void | 테마 변경 | | setOnContentChange(fn) | void | 변경 콜백 교체 | | destroy() | void | 정리(리스너 해제) |

직렬화·변환 유틸

블록 JSON ↔ HTML ↔ Markdown ↔ DOM 변환 함수가 공개 export됩니다.

import {
  blocksToHtml, blocksToFullHtml, parseHtmlToBlocks,
  blocksToMarkdown, markdownToBlocks,
  blocksToDom, domToBlocks, blockToElement, elementToBlock,
  inlineContentToHtml, htmlToInlineContent, blocksFromText,
} from "@lumir-company/editor/core";

const html = blocksToHtml(blocks);
const blocks2 = parseHtmlToBlocks(html);
const md = blocksToMarkdown(blocks);

색상·표 모델(TableModel, newTableModel, modelToTableContent 등) 저수준 유틸도 함께 export됩니다. 전체 목록은 타입 선언(dist/core.d.ts)을 참고하세요.


유틸리티 API

ContentUtils

import { ContentUtils } from "@lumir-company/editor";

ContentUtils.isValidJSONString('[{"type":"paragraph"}]'); // boolean
ContentUtils.parseJSONContent(jsonString);                // DefaultPartialBlock[] | null
ContentUtils.createDefaultBlock();                        // DefaultPartialBlock
ContentUtils.validateContent(content, emptyBlockCount);   // DefaultPartialBlock[]
ContentUtils.createEmptyBlocks(3);                        // DefaultPartialBlock[]

글자 크기 유틸

import {
  liftFontSize, lowerFontSize,
  FONT_SIZE_PRESETS, FONT_SIZE_MIN, FONT_SIZE_MAX, FONT_SIZE_DEFAULT_PX, FONT_SIZE_STEP,
  parseFontSizePx, clampFontSizePx, toFontSizeValue,
} from "@lumir-company/editor";

색상 상수

import { TEXT_COLORS, BACKGROUND_COLORS, getHexFromColorValue } from "@lumir-company/editor";

에러

import { LumirEditorError, LUMIR_ERROR_CODES } from "@lumir-company/editor";
// code: "UPLOAD_FAILED" | "INVALID_FILE_TYPE" | "S3_CONFIG_ERROR" | "NETWORK_ERROR" | "CONTENT_PARSE_ERROR" | "UNKNOWN_ERROR"

로케일

import { KO, EN, LOCALES, resolveLocale } from "@lumir-company/editor";
<LumirEditor locale="ko" />; // 또는 locale={EN}

기타

createS3Uploader, generateUUID, cn(className 결합) 등도 export됩니다.


스타일링

@lumir-company/editor/style.css가 모든 스타일을 단일 번들로 포함합니다(별도 CSS import 불필요).

Tailwind CSS

import { LumirEditor, cn } from "@lumir-company/editor";

<LumirEditor
  className={cn(
    "min-h-[400px] rounded-xl border border-gray-200 shadow-lg",
    "focus-within:ring-2 focus-within:ring-blue-500",
  )}
/>;

커스텀 CSS

에디터 본문은 .bn-editor, 블록은 [data-content-type="..."] 셀렉터로 접근합니다.

.my-editor .bn-editor {
  padding: 20px 30px;
  font-size: 16px;
  line-height: 1.6;
}
.my-editor [data-content-type="heading"] {
  font-weight: 700;
}
<LumirEditor className="my-editor" />

CSS 변수

| 변수 | 기본값 | 용도 | | --- | --- | --- | | --lumir-column-divider-color | #e5e7eb | 2단 컬럼 세로 구분선 색 | | --lumir-column-grip-space | 28px | 컬럼 구분선 양쪽 여백 | | --lumir-inactive-selection | — | blur 상태 선택 하이라이트 색 |


트러블슈팅

필수 체크리스트

  • [ ] CSS 임포트: import "@lumir-company/editor/style.css";
  • [ ] 컨테이너에 높이 지정(부모 요소)
  • [ ] Next.js: dynamic(..., { ssr: false }) 사용
  • [ ] React 사용 시 버전 ≥ 18

에디터가 보이지 않음 → CSS 임포트 누락 또는 컨테이너 높이 미지정.

Next.js hydration 오류dynamic으로 ssr: false 처리(위 Next.js 예시).

이미지 업로드 실패uploadFile 또는 s3Upload 중 하나는 반드시 설정.

여러 이미지 업로드 시 파일명 중복s3Upload.appendUUID: true.


0.4.x → 0.5.0 마이그레이션

대부분 코드 변경 없이 버전만 올리면 됩니다. import 경로·컴포넌트·props·타입(LumirEditorProps/DefaultPartialBlocks3Upload 계약이 동일합니다. package.json에서 버전을 ^0.5.0으로 올리세요. (엔진 교체가 파괴적이지 않도록 마이너 버전으로 옵트인)

제거된 기능 — 사용처 정리 필요:

| 제거 항목 | 조치 | | --- | --- | | linkPreview prop | 제거. URL 붙여넣기는 이제 항상 인라인 링크로 처리됩니다 | | @lumir-company/editor/api/link-preview 서브패스 | 제거. 해당 API 라우트/import 삭제 | | htmlPreview 블록 · HtmlPreviewBlock export | 제거. 저장 JSON에 남아 있어도 로드는 무에러(알 수 없는 블록은 안전하게 폴백) | | 독립 FloatingMenu 컴포넌트 | <LumirEditor floatingMenu floatingMenuPosition="sticky|fixed" /> prop으로 대체(기존 export는 null 렌더 스텁으로 유지) | | 독립 FontSizeButton 컴포넌트 | 포매팅 툴바에 내장(기존 export는 null 렌더 스텁) |

위 제거 항목을 참조하지 않는 프로젝트라면 별도 조치 없이 그대로 동작합니다.


변경 이력

v0.5.7 (2026-07-15)

  • 3단 컬럼: 슬래시 메뉴에 /3단 컬럼·/3단 컬럼 (구분선) 추가(기존 2단과 함께). 로드·라운드트립에서 최대 3단 보존, 4단 이상은 마지막 컬럼으로 병합해 3단 정규화. 슬래시 메뉴 컬럼 아이콘을 단 수(2·3)·구분선 유무에 따라 구분.
  • 컬럼 탈출(Enter 두 번): 컬럼 맨 마지막 블록에서 Enter를 두 번 누르면 컬럼 아래에 일반 문단을 만들고 포커스(1·2·3단 어느 컬럼에서든). 코드블록·토글 탈출과 동일한 UX.
  • 빈 컬럼 방지(수정): 컬럼의 마지막 블록을 다른 컬럼으로 드래그해 옮겨도 원래 컬럼이 사라지거나 드롭 불가가 되지 않도록 빈 문단 플레이스홀더 유지. 빈 플레이스홀더만 있는 컬럼에 드롭하면 불필요한 빈 줄 없이 교체.
  • 컬럼 구분선·그립 정렬: 구분선을 오른쪽 컬럼 콘텐츠 옆에 붙이고 블록 그립(⠿)을 콘텐츠에 밀착, 구분선 좌우 여백을 균형 있게 배치. 구분선 색은 --lumir-column-divider-color CSS 변수로 커스터마이즈.

v0.5.6 (2026-07-13)

  • 여러 줄(크로스블록) 선택 + 일괄 서식: 블록 경계를 넘는 마우스 드래그로 여러 줄 선택(첫/끝 줄 중간의 부분 선택 포함), 선택 범위 전체에 굵기·색·크기 등 인라인 서식을 한 번에 적용(전 범위 균일 토글 — Word/Notion 정합). Shift+클릭 확장, Ctrl+B/I/U 지원. 일괄 서식은 단일 Undo로 원복.
  • 선택 영역 편집: 타이핑(선택 대체) · Backspace/Delete(삭제·블록 병합, 완전 커버 시 빈 블록 정리) · Enter(삭제 후 분할) · 붙여넣기(선택 대체) · 부분 복사/잘라내기(무손실 직렬화). 한글(IME) 입력 포함 모든 동작이 단일 Undo 단위.
  • Ctrl/Cmd+A 전체선택 서식: 문서 전체 선택 상태에서도 서식 툴바 표시·일괄 적용. 서식 툴바는 화면 세로 1/4 지점에 고정하되 에디터 영역을 벗어나지 않게 클램프.
  • v1 범위: 표·2단 컬럼·중첩 목록 경계를 건드리는 선택은 대상에서 제외(기존 동작 보존).

v0.5.3 (2026-07-07)

  • 표 열 리사이즈 폭 튐 수정: 내부 열 경계 드래그 시 표 전체 폭이 변하던 문제 해결(드래그 시작 시 유효한 columnWidths를 재측정 없이 사용, 측정 경로는 병합/colspan 안전한 경계 기반으로 교체, min-width 정합).
  • 리사이즈 드래그 피드백: 드래그 중에도 리사이즈 커서·경계 하이라이트 유지(.resizing) + 하이라이트가 이동하는 경계를 실시간 추종. 행 리사이즈 후 핸들 재배치 및 최소높이에서 보더·하이라이트 동시 정지.
  • 표 폭 스케일 복원: fitWidths가 예산에 정확히 도달하도록 보정 — 표 폭을 줄였다가 다시 늘릴 때 최대폭(최초 채움 폭)으로 복원.
  • 에디터 표 폭 정합: 표가 콘텐츠 영역 좌·우변에 flush(네이티브 표와 정렬).
  • 플로팅 툴바 표시 수정: 표·서식 툴바 z-index 상향 — 호스트 앱의 position:fixed 컨테이너(예: z-index:100) 위에 정상 표시.
  • 블록 그립 노출 개선: 좌측 거터에 마우스를 올려도 해당 블록의 드래그 그립(⠿)이 뜨도록(블록 콘텐츠 밖에서도 노출).
  • (구조) 내부 리사이저 세그먼트화 + 행/열 이동 그립·드래그 재정렬 제거.

v0.5.2 (2026-07-06)

  • 링크 카드·임베드 블록(bookmark): 인라인 링크에 hover → 링크 툴바의 “카드/임베드” 버튼으로 전환. 카드는 fetchLinkPreview(url) 콜백(서버 프록시 권장)으로 제목·설명·썸네일을 채우고, 임베드는 YouTube/Vimeo 만 허용(도메인 화이트리스트 + sandbox iframe). 붙여넣기는 기존대로 인라인 링크 유지(부작용 최소).
  • 카드/임베드 폭 리사이즈: 좌/우 핸들 드래그로 previewWidth 조절(이미지/비디오와 동일 UX). 임베드는 16:9 비율 유지, 읽기전용·모바일에선 핸들 숨김.
  • 상단 고정 툴바(FloatingMenu) 모바일 개선: 모바일 폭(<440px) 진입 시 기본으로 펼쳐진 상태로 표시(접기 토글은 유지).
  • 표 전체 선택/해제: 셀에서 Ctrl/Cmd+A 전체 셀 선택, Esc 로 선택 해제.
  • 표 모바일 가로 스크롤: 모바일(≤640px)에서 표가 편집영역보다 넓으면 열 압축 대신 가로 스크롤(데스크톱은 기존 fit-to-width 유지).
  • 수정: 링크 툴바 버튼(편집/카드/임베드) 세로 줄바뀜 교정.
  • 커스텀 bookmark 블록은 HTML/Markdown 내보내기 시 링크로 degrade(라운드트립 안전).

v0.5.1 (2026-07-03)

  • 키보드 블록 내비게이션: 방향키(←→↑↓)로 블록 간 캐럿 이동, Ctrl/Cmd+A 단계 전체선택(블록 텍스트 → 전체 블록 선택 모드: 삭제/복사).
  • 코드블록 탈출: 빈 마지막 줄에서 Enter 2번 → 하단 문단으로 탈출. 후행 개행 렌더 보정.
  • 인라인 코드 탈출: →/← 및 스페이스 2번으로 코드 밖 평문 이어쓰기. Enter 분할 시 빈 <code> 잔존 제거.
  • 표 셀 방향키: 상하좌우 셀 이동, 첫 행 ↑·마지막 행 ↓로 표 밖 인접 블록 탈출, 셀 Enter 줄바꿈(<br>).
  • 디바이더: 점선 → 검정 실선. 목록 마커: 절대배치로 커서가 마커 앞에 붙던 문제 교정.

v0.5.0

  • 엔진 교체: BlockNote/ProseMirror/React 런타임 제거 → vanilla(의존성 0) 코어. 저장 JSON은 BlockNote 형식과 라운드트립 호환 유지.
  • 드롭인 대체: 구 @lumir-company/[email protected]와 동일한 컴포넌트·props·타입·s3Upload 계약. 코드 변경 없이 버전만 상향.
  • react-free 코어(./core) + mountLumirEditor() 공개: 서버 컴포넌트·비-React 환경에서 명령형 사용.
  • 직렬화 유틸 공개 export: 블록 JSON ↔ HTML ↔ Markdown ↔ DOM 변환 함수, 표 모델·색상 유틸.
  • 패키징: ESM + CJS + .d.ts + 단일 번들 CSS. . / ./react / ./core / ./style.css 서브패스.
  • 제거: linkPreview(prop·/api/link-preview 서브패스), htmlPreview 블록/HtmlPreviewBlock.
  • 링크 동작 변경: URL 붙여넣기 → 항상 인라인 링크, Notion식 링크 툴바(hover 툴팁 → 편집 popup).

v0.4.23 (2026-06-22)

  • 다중 블록 선택 시 글자 크기 스테퍼 누적 버그 수정(readSelectionFontSize + 낙관적 갱신).
  • 툴바 드롭다운 조작 시 선택 하이라이트 유지(InactiveSelectionExtension, --lumir-inactive-selection).

v0.4.22 (2026-06-22)

  • 글자 크기 1px 스테퍼 + 직접 입력(8~96px) 추가. FONT_SIZE_MIN/MAX/DEFAULT_PX/STEP, parseFontSizePx/clampFontSizePx/toFontSizeValue 공개.
  • 글자 크기 스타일을 동기 스펙으로 변경(적용 직후 툴바가 (0,0)으로 튀는 문제 수정).

v0.4.21 (2026-06-18)

  • Word/docx 표 붙여넣기 개선: 에디터 폭 자동 맞춤, 셀 서식(글자 크기·세로 정렬 포함) 보존 확대.

v0.4.20 (2026-06-18)

  • 2단 컬럼 구분선을 생성 시 블록별로 선택·고정(showDivider 라운드트립).
  • 표 셀에서 Ctrl/Cmd+A → 표 전체 선택(재입력 시 문서 전체로 확장).

v0.4.19 (2026-06-17)

  • 2단 컬럼 중앙 세로 구분선 전역 옵션(columnDivider), CSS 변수 제어.
  • 표 전체 종횡비 고정 스케일(우하단 코너 드래그), tableHandles로 게이트.

v0.4.18 (2026-06-17)

  • 표 열 삭제 버그 수정(병합 셀·첫 열 포함), 병합 셀 collapse 시 행 높이 보존.

v0.4.17 (2026-06-16)

  • 표 행 높이 리사이즈(rowHeight 라운드트립), 표 블록 정렬(좌/가운데/우), 표 하단 여백 축소.
  • 2단(다단) 컬럼 레이아웃 신규(슬래시 메뉴·블록 DnD, MIT 자체 구현).

v0.4.16 (2026-06-05)

  • 인라인 글자 크기(기본 + 10~28px 8단계), 구버전 호환 형제 키 직렬화, liftFontSize/lowerFontSize·SerializedStyledText 공개.
  • floatingMenu 사용 시 중복 팝업 툴바 억제.

v0.4.15 (2026-06-05)

  • Notion 스타일 테이블 셀 색상·정렬·포커스 핸들(LumirTableHandlesController), 다중 셀 일괄 색 적용.

v0.4.14 (2026-05-29)

  • Excel/스프레드시트 셀 붙여넣기 → 편집 가능한 테이블(클립보드 <table> 우선 파싱, 서식 매핑).

v0.4.13 (2026-04-03)

  • @tiptap/core 외부화로 중복 인스턴스 런타임 에러 수정.

v0.4.12 (2026-04-03)

  • Numbered/Bullet List 글자 크기 14px로 통일.

v0.4.10 (2026-03-18)

  • 업로드 진행률 보간 표시(onProgress 0→…→100 부드럽게 갱신).

v0.4.9 (2026-03-17)

  • maxImageFileSize/maxVideoFileSize 추가.

v0.4.5 (2026-03-06)

  • 이미지·동영상 업로드 문서화(형식/용량/삽입 경로/삭제 콜백/에러 처리).

v0.4.3 (2026-02-23)

  • 이미지 삭제 콜백(onImageDelete) 추가, placeholder prop 추가, URL 프로토콜 보안 강화.

v0.4.2 (2026-02-23)

  • LumirEditorError 커스텀 에러 클래스·onError 콜백 추가, 코드 구조 리팩토링.

v0.4.1 (2026-01-15)

  • preserveExtension 추가. Breaking: fileNameTransform이 확장자 제외 이름(nameWithoutExt)을 전달.

v0.4.0 (2026-01-15)

  • fileNameTransform·appendUUID 추가, 다중 이미지 업로드 중복 해결.

라이선스

MIT