@yonailab/levit-react 0.0.0-stage → 0.4.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.
package/README.md CHANGED
@@ -1,3 +1,48 @@
1
- # Temporary Holding Version
1
+ # @yonailab/levit-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Levit](https://github.com/azusawa/levit) 에디터의 React 컴포넌트입니다. React 18, 19를 지원합니다.
4
+
5
+ ```bash
6
+ npm install @yonailab/levit @yonailab/levit-react
7
+ ```
8
+
9
+ ## 컴포넌트
10
+
11
+ ```tsx
12
+ import { useState } from 'react';
13
+ import { LevitEditor } from '@yonailab/levit-react';
14
+ import { ko, type DocumentData } from '@yonailab/levit';
15
+ import '@yonailab/levit/levit.css';
16
+
17
+ export function Note() {
18
+ const [doc, setDoc] = useState<DocumentData>();
19
+ return <LevitEditor value={doc} onChange={setDoc} placeholder="메모를 입력하세요" messages={ko} />;
20
+ }
21
+ ```
22
+
23
+ - `value`와 `onChange`를 함께 쓰면 제어 컴포넌트, `defaultValue`만 쓰면 비제어 컴포넌트입니다.
24
+ - `onChange`가 돌려준 값을 다시 `value`로 넘겨도 커서는 그대로입니다.
25
+ - 에디터 옵션(`plugins`, `messages`, `uploadImage`, `highlightCode`, `onError` 등)을 그대로 props로 넘깁니다.
26
+ - `bubbleMenu={false}`, `slashMenu={false}`로 메뉴를 끌 수 있습니다.
27
+ - `ref`나 `onReady`로 에디터 인스턴스를 받습니다.
28
+ - `'use client'`가 들어 있어 Next.js App Router에서 바로 쓸 수 있습니다.
29
+
30
+ ## 훅
31
+
32
+ 화면 구성을 직접 하려면 `useLevitEditor`를 씁니다.
33
+
34
+ ```tsx
35
+ import { useLevitEditor } from '@yonailab/levit-react';
36
+
37
+ function Editor() {
38
+ const { ref, editor } = useLevitEditor({ placeholder: '입력하세요' });
39
+ return (
40
+ <>
41
+ <button onClick={() => editor?.chain().toggleBold().run()}>굵게</button>
42
+ <div ref={ref} />
43
+ </>
44
+ );
45
+ }
46
+ ```
47
+
48
+ `editor`는 마운트 전에는 `null`입니다. 옵션은 에디터를 만들 때 한 번 읽고, 콜백은 항상 최신 것을 부릅니다.
@@ -0,0 +1,39 @@
1
+ import { type CSSProperties } from 'react';
2
+ import { type DocumentData, type EditorOptions, type LevitEditor as Editor } from '@yonailab/levit';
3
+ export type { Editor };
4
+ /** Editor options, plus whether to add the bubble and slash menus. */
5
+ export interface UseLevitEditorOptions extends Omit<EditorOptions, 'element'> {
6
+ /** Show the formatting toolbar on selected text. Default: true. */
7
+ bubbleMenu?: boolean;
8
+ /** Open the block menu with "/". Default: true. */
9
+ slashMenu?: boolean;
10
+ /** Called once the editor exists. */
11
+ onReady?: (editor: Editor) => void;
12
+ }
13
+ /**
14
+ * Creates a Levit editor on the element you attach `ref` to. The editor is
15
+ * made after mount (so this is safe in server rendering) and destroyed on
16
+ * unmount. Options are read when the editor is made; callbacks always call
17
+ * the latest version you passed.
18
+ */
19
+ export declare function useLevitEditor(options?: UseLevitEditorOptions): {
20
+ ref: (element: HTMLElement | null) => void;
21
+ editor: Editor | null;
22
+ };
23
+ export interface LevitEditorProps extends Omit<UseLevitEditorOptions, 'initialContent' | 'onChange'> {
24
+ /** The document, for a controlled editor. Pass back what onChange gives you. */
25
+ value?: DocumentData;
26
+ /** The starting document, for an uncontrolled editor. */
27
+ defaultValue?: DocumentData;
28
+ /** Called with the document JSON after every change. */
29
+ onChange?: (value: DocumentData, editor: Editor) => void;
30
+ className?: string;
31
+ style?: CSSProperties;
32
+ id?: string;
33
+ }
34
+ /**
35
+ * A Levit editor with its menus. Use `defaultValue` for an uncontrolled
36
+ * editor, or `value` with `onChange` for a controlled one. The ref gives the
37
+ * editor instance.
38
+ */
39
+ export declare const LevitEditor: import("react").ForwardRefExoticComponent<LevitEditorProps & import("react").RefAttributes<Editor | null>>;
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react';
4
+ import { createBubbleMenu, createLevitEditor, createSlashMenu, sameContent, } from '@yonailab/levit';
5
+ /**
6
+ * Creates a Levit editor on the element you attach `ref` to. The editor is
7
+ * made after mount (so this is safe in server rendering) and destroyed on
8
+ * unmount. Options are read when the editor is made; callbacks always call
9
+ * the latest version you passed.
10
+ */
11
+ export function useLevitEditor(options = {}) {
12
+ const [element, setElement] = useState(null);
13
+ const [editor, setEditor] = useState(null);
14
+ const latest = useRef(options);
15
+ latest.current = options;
16
+ useEffect(() => {
17
+ if (!element)
18
+ return;
19
+ const { bubbleMenu = true, slashMenu = true, ...editorOptions } = latest.current;
20
+ const instance = createLevitEditor(element, {
21
+ ...editorOptions,
22
+ onChange: (ed) => latest.current.onChange?.(ed),
23
+ onSelectionChange: (selection) => latest.current.onSelectionChange?.(selection),
24
+ onError: (error) => {
25
+ if (latest.current.onError)
26
+ latest.current.onError(error);
27
+ else
28
+ console.error(error);
29
+ },
30
+ });
31
+ const menus = [bubbleMenu && createBubbleMenu(instance), slashMenu && createSlashMenu(instance)];
32
+ setEditor(instance);
33
+ latest.current.onReady?.(instance);
34
+ return () => {
35
+ for (const menu of menus)
36
+ if (menu)
37
+ menu.destroy();
38
+ instance.destroy();
39
+ setEditor(null);
40
+ };
41
+ }, [element]);
42
+ return { ref: setElement, editor };
43
+ }
44
+ /**
45
+ * A Levit editor with its menus. Use `defaultValue` for an uncontrolled
46
+ * editor, or `value` with `onChange` for a controlled one. The ref gives the
47
+ * editor instance.
48
+ */
49
+ export const LevitEditor = forwardRef(function LevitEditor({ value, defaultValue, onChange, className, style, id, ...options }, ref) {
50
+ const handleChange = useCallback((ed) => onChange?.(ed.getJSON(), ed), [onChange]);
51
+ const { ref: elementRef, editor } = useLevitEditor({
52
+ ...options,
53
+ initialContent: value ?? defaultValue,
54
+ onChange: handleChange,
55
+ });
56
+ useImperativeHandle(ref, () => editor, [editor]);
57
+ // A controlled value replaces the content only when it differs, so the
58
+ // value that comes back from onChange does not reset the caret.
59
+ useEffect(() => {
60
+ if (!editor || value === undefined)
61
+ return;
62
+ if (!sameContent(value, editor.getJSON()))
63
+ editor.setJSON(value);
64
+ }, [editor, value]);
65
+ return _jsx("div", { ref: elementRef, className: className, style: style, id: id });
66
+ });
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@yonailab/levit-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "React components for the Levit editor.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit",
23
+ "check:package": "node ../levit/scripts/check-package.mjs --wrapper",
24
+ "prepublishOnly": "npm run typecheck && npm test && npm run build && npm run check:package"
25
+ },
26
+ "peerDependencies": {
27
+ "@yonailab/levit": "^0.4.0",
28
+ "react": ">=18"
29
+ },
30
+ "keywords": [
31
+ "editor",
32
+ "wysiwyg",
33
+ "rich-text",
34
+ "levit",
35
+ "react"
36
+ ],
37
+ "author": "yonailab",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/azusawa/levit.git",
41
+ "directory": "packages/react"
42
+ },
43
+ "homepage": "https://github.com/azusawa/levit/tree/main/packages/react#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/azusawa/levit/issues"
46
+ },
47
+ "license": "MIT",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "devDependencies": {
52
+ "@types/react": "^19.3.0",
53
+ "@types/react-dom": "^19.3.0",
54
+ "react": "^19.3.1",
55
+ "react-dom": "^19.3.1"
56
+ }
57
+ }