notra-editor 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ NotraEditor: () => NotraEditor,
34
+ NotraReader: () => NotraReader
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/notra-editor.tsx
39
+ var import_react3 = require("@tiptap/react");
40
+
41
+ // src/hooks/use-markdown-editor.ts
42
+ var import_react = require("@tiptap/react");
43
+ var import_react2 = require("react");
44
+
45
+ // src/extensions/shared.ts
46
+ var import_extension_list = require("@tiptap/extension-list");
47
+ var import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
48
+ var starterKitBaseConfig = {
49
+ heading: { levels: [1, 2, 3, 4, 5, 6] },
50
+ link: {
51
+ openOnClick: false,
52
+ autolink: true
53
+ },
54
+ // Disable StarterKit's built-in list handling; use @tiptap/extension-list instead
55
+ bulletList: false,
56
+ orderedList: false,
57
+ listItem: false,
58
+ listKeymap: false
59
+ };
60
+ var sharedExtensions = [
61
+ import_starter_kit.default.configure({
62
+ ...starterKitBaseConfig,
63
+ dropcursor: false,
64
+ gapcursor: false,
65
+ undoRedo: false,
66
+ trailingNode: false
67
+ }),
68
+ import_extension_list.ListKit
69
+ ];
70
+
71
+ // src/extensions/editor.ts
72
+ var import_extension_list2 = require("@tiptap/extension-list");
73
+ var import_starter_kit2 = __toESM(require("@tiptap/starter-kit"), 1);
74
+ var import_tiptap_markdown = require("tiptap-markdown");
75
+ var editorExtensions = [
76
+ import_starter_kit2.default.configure(starterKitBaseConfig),
77
+ import_extension_list2.ListKit,
78
+ import_tiptap_markdown.Markdown.configure({
79
+ html: false,
80
+ transformPastedText: true,
81
+ transformCopiedText: true
82
+ })
83
+ ];
84
+
85
+ // src/hooks/use-markdown-editor.ts
86
+ function getMarkdown(storage) {
87
+ return storage.markdown.getMarkdown();
88
+ }
89
+ function useMarkdownEditor({
90
+ value,
91
+ onChange,
92
+ editable = true
93
+ }) {
94
+ const externalValue = (0, import_react2.useRef)(value);
95
+ const onChangeRef = (0, import_react2.useRef)(onChange);
96
+ onChangeRef.current = onChange;
97
+ const editor = (0, import_react.useEditor)({
98
+ extensions: editorExtensions,
99
+ editable,
100
+ content: value,
101
+ onUpdate({ editor: editor2 }) {
102
+ const md = getMarkdown(
103
+ editor2.storage
104
+ );
105
+ externalValue.current = md;
106
+ onChangeRef.current(md);
107
+ }
108
+ });
109
+ (0, import_react2.useEffect)(() => {
110
+ if (!editor) return;
111
+ if (value === externalValue.current) return;
112
+ externalValue.current = value;
113
+ editor.commands.setContent(value);
114
+ }, [value, editor]);
115
+ (0, import_react2.useEffect)(() => {
116
+ if (!editor) return;
117
+ editor.setEditable(editable);
118
+ }, [editable, editor]);
119
+ return { editor };
120
+ }
121
+
122
+ // src/notra-editor.tsx
123
+ var import_jsx_runtime = require("react/jsx-runtime");
124
+ function NotraEditor({
125
+ value,
126
+ onChange,
127
+ placeholder,
128
+ readOnly = false,
129
+ className
130
+ }) {
131
+ const { editor } = useMarkdownEditor({
132
+ value,
133
+ onChange,
134
+ placeholder,
135
+ editable: !readOnly
136
+ });
137
+ const classNames = ["notra", "notra-editor", className].filter(Boolean).join(" ");
138
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: classNames, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.EditorContent, { editor }) });
139
+ }
140
+
141
+ // src/notra-reader.tsx
142
+ var import_react4 = require("@tiptap/static-renderer/pm/react");
143
+
144
+ // src/utils/markdown-to-json.ts
145
+ var import_core = require("@tiptap/core");
146
+ var import_tiptap_markdown2 = require("tiptap-markdown");
147
+ var parserExtensions = [
148
+ ...sharedExtensions,
149
+ import_tiptap_markdown2.Markdown.configure({ html: false })
150
+ ];
151
+ var parserEditor = null;
152
+ function getParserEditor() {
153
+ if (!parserEditor) {
154
+ parserEditor = new import_core.Editor({
155
+ extensions: parserExtensions,
156
+ content: ""
157
+ });
158
+ }
159
+ return parserEditor;
160
+ }
161
+ function markdownToJSON(markdown) {
162
+ const editor = getParserEditor();
163
+ editor.commands.setContent(markdown);
164
+ return editor.getJSON();
165
+ }
166
+
167
+ // src/notra-reader.tsx
168
+ var import_jsx_runtime2 = require("react/jsx-runtime");
169
+ function NotraReader({ content, className }) {
170
+ const json = markdownToJSON(content);
171
+ const rendered = (0, import_react4.renderToReactElement)({
172
+ extensions: sharedExtensions,
173
+ content: json
174
+ });
175
+ const classNames = ["notra", "notra-reader", className].filter(Boolean).join(" ");
176
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: classNames, children: rendered });
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ NotraEditor,
181
+ NotraReader
182
+ });
183
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/notra-editor.tsx","../src/hooks/use-markdown-editor.ts","../src/extensions/shared.ts","../src/extensions/editor.ts","../src/notra-reader.tsx","../src/utils/markdown-to-json.ts"],"sourcesContent":["export { NotraEditor } from './notra-editor';\nexport type { NotraEditorProps } from './notra-editor';\n\nexport { NotraReader } from './notra-reader';\nexport type { NotraReaderProps } from './notra-reader';\n","import { EditorContent } from '@tiptap/react';\n\nimport { useMarkdownEditor } from './hooks/use-markdown-editor';\n\nexport interface NotraEditorProps {\n\t/** Markdown content (source of truth) */\n\tvalue: string;\n\t/** Called when content changes, receives updated Markdown */\n\tonChange: (value: string) => void;\n\t/** Placeholder text shown when editor is empty */\n\tplaceholder?: string;\n\t/** Disable editing */\n\treadOnly?: boolean;\n\t/** Additional CSS class on the wrapper element */\n\tclassName?: string;\n}\n\nexport function NotraEditor({\n\tvalue,\n\tonChange,\n\tplaceholder,\n\treadOnly = false,\n\tclassName\n}: NotraEditorProps) {\n\tconst { editor } = useMarkdownEditor({\n\t\tvalue,\n\t\tonChange,\n\t\tplaceholder,\n\t\teditable: !readOnly\n\t});\n\n\tconst classNames = ['notra', 'notra-editor', className]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\treturn (\n\t\t<div className={classNames}>\n\t\t\t<EditorContent editor={editor} />\n\t\t</div>\n\t);\n}\n","import { useEditor } from '@tiptap/react';\nimport { useEffect, useRef } from 'react';\n\nimport { editorExtensions } from '../extensions';\n\nimport type { MarkdownStorage } from 'tiptap-markdown';\n\nexport interface UseMarkdownEditorOptions {\n\tvalue: string;\n\tonChange: (value: string) => void;\n\tplaceholder?: string;\n\teditable?: boolean;\n}\n\nfunction getMarkdown(storage: Record<string, unknown>): string {\n\treturn (storage.markdown as MarkdownStorage).getMarkdown();\n}\n\nexport function useMarkdownEditor({\n\tvalue,\n\tonChange,\n\teditable = true\n}: UseMarkdownEditorOptions) {\n\tconst externalValue = useRef(value);\n\tconst onChangeRef = useRef(onChange);\n\n\tonChangeRef.current = onChange;\n\n\tconst editor = useEditor({\n\t\textensions: editorExtensions,\n\t\teditable,\n\t\tcontent: value,\n\t\tonUpdate({ editor }) {\n\t\t\tconst md = getMarkdown(\n\t\t\t\teditor.storage as unknown as Record<string, unknown>\n\t\t\t);\n\n\t\t\texternalValue.current = md;\n\t\t\tonChangeRef.current(md);\n\t\t}\n\t});\n\n\tuseEffect(() => {\n\t\tif (!editor) return;\n\n\t\tif (value === externalValue.current) return;\n\n\t\texternalValue.current = value;\n\t\teditor.commands.setContent(value);\n\t}, [value, editor]);\n\n\tuseEffect(() => {\n\t\tif (!editor) return;\n\n\t\teditor.setEditable(editable);\n\t}, [editable, editor]);\n\n\treturn { editor };\n}\n","import { ListKit } from '@tiptap/extension-list';\nimport StarterKit, { type StarterKitOptions } from '@tiptap/starter-kit';\n\n// Shared StarterKit config: content nodes/marks, no lists (use ListKit instead)\nexport const starterKitBaseConfig: Partial<StarterKitOptions> = {\n\theading: { levels: [1, 2, 3, 4, 5, 6] },\n\tlink: {\n\t\topenOnClick: false,\n\t\tautolink: true\n\t},\n\t// Disable StarterKit's built-in list handling; use @tiptap/extension-list instead\n\tbulletList: false,\n\torderedList: false,\n\tlistItem: false,\n\tlistKeymap: false\n};\n\n// Content model extensions — shared by editor and reader\n// No interactive features (dropcursor, gapcursor, undoRedo, trailingNode)\nexport const sharedExtensions = [\n\tStarterKit.configure({\n\t\t...starterKitBaseConfig,\n\t\tdropcursor: false,\n\t\tgapcursor: false,\n\t\tundoRedo: false,\n\t\ttrailingNode: false\n\t}),\n\tListKit\n];\n","import { ListKit } from '@tiptap/extension-list';\nimport StarterKit from '@tiptap/starter-kit';\nimport { Markdown } from 'tiptap-markdown';\n\nimport { starterKitBaseConfig } from './shared';\n\n// Editor extensions = shared content model + interactive features + Markdown\nexport const editorExtensions = [\n\tStarterKit.configure(starterKitBaseConfig),\n\tListKit,\n\tMarkdown.configure({\n\t\thtml: false,\n\t\ttransformPastedText: true,\n\t\ttransformCopiedText: true\n\t})\n];\n","import { renderToReactElement } from '@tiptap/static-renderer/pm/react';\n\nimport { sharedExtensions } from './extensions';\nimport { markdownToJSON } from './utils/markdown-to-json';\n\nexport interface NotraReaderProps {\n\t/** Markdown content to render */\n\tcontent: string;\n\t/** Additional CSS class on the wrapper element */\n\tclassName?: string;\n}\n\nexport function NotraReader({ content, className }: NotraReaderProps) {\n\tconst json = markdownToJSON(content);\n\n\tconst rendered = renderToReactElement({\n\t\textensions: sharedExtensions,\n\t\tcontent: json\n\t});\n\n\tconst classNames = ['notra', 'notra-reader', className]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\treturn <div className={classNames}>{rendered}</div>;\n}\n","import { Editor } from '@tiptap/core';\nimport { Markdown } from 'tiptap-markdown';\n\nimport { sharedExtensions } from '../extensions';\n\n// Parser needs shared content model + Markdown for markdown→JSON conversion\n// No clipboard features needed (transformPastedText/transformCopiedText are editor-only)\nconst parserExtensions = [\n\t...sharedExtensions,\n\tMarkdown.configure({ html: false })\n];\n\nlet parserEditor: Editor | null = null;\n\nfunction getParserEditor(): Editor {\n\tif (!parserEditor) {\n\t\tparserEditor = new Editor({\n\t\t\textensions: parserExtensions,\n\t\t\tcontent: ''\n\t\t});\n\t}\n\n\treturn parserEditor;\n}\n\n/**\n * Convert a Markdown string to Tiptap-compatible JSON (ProseMirror document).\n * Uses a singleton headless Tiptap editor for parsing.\n */\nexport function markdownToJSON(markdown: string): Record<string, unknown> {\n\tconst editor = getParserEditor();\n\n\teditor.commands.setContent(markdown);\n\n\treturn editor.getJSON() as Record<string, unknown>;\n}\n"],"mappings":"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","names":["import_react","import_react","StarterKit","import_extension_list","import_starter_kit","StarterKit","editor","import_react","import_tiptap_markdown","import_jsx_runtime"]}
@@ -0,0 +1,25 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface NotraEditorProps {
4
+ /** Markdown content (source of truth) */
5
+ value: string;
6
+ /** Called when content changes, receives updated Markdown */
7
+ onChange: (value: string) => void;
8
+ /** Placeholder text shown when editor is empty */
9
+ placeholder?: string;
10
+ /** Disable editing */
11
+ readOnly?: boolean;
12
+ /** Additional CSS class on the wrapper element */
13
+ className?: string;
14
+ }
15
+ declare function NotraEditor({ value, onChange, placeholder, readOnly, className }: NotraEditorProps): react_jsx_runtime.JSX.Element;
16
+
17
+ interface NotraReaderProps {
18
+ /** Markdown content to render */
19
+ content: string;
20
+ /** Additional CSS class on the wrapper element */
21
+ className?: string;
22
+ }
23
+ declare function NotraReader({ content, className }: NotraReaderProps): react_jsx_runtime.JSX.Element;
24
+
25
+ export { NotraEditor, type NotraEditorProps, NotraReader, type NotraReaderProps };
@@ -0,0 +1,25 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface NotraEditorProps {
4
+ /** Markdown content (source of truth) */
5
+ value: string;
6
+ /** Called when content changes, receives updated Markdown */
7
+ onChange: (value: string) => void;
8
+ /** Placeholder text shown when editor is empty */
9
+ placeholder?: string;
10
+ /** Disable editing */
11
+ readOnly?: boolean;
12
+ /** Additional CSS class on the wrapper element */
13
+ className?: string;
14
+ }
15
+ declare function NotraEditor({ value, onChange, placeholder, readOnly, className }: NotraEditorProps): react_jsx_runtime.JSX.Element;
16
+
17
+ interface NotraReaderProps {
18
+ /** Markdown content to render */
19
+ content: string;
20
+ /** Additional CSS class on the wrapper element */
21
+ className?: string;
22
+ }
23
+ declare function NotraReader({ content, className }: NotraReaderProps): react_jsx_runtime.JSX.Element;
24
+
25
+ export { NotraEditor, type NotraEditorProps, NotraReader, type NotraReaderProps };
package/dist/index.mjs ADDED
@@ -0,0 +1,145 @@
1
+ // src/notra-editor.tsx
2
+ import { EditorContent } from "@tiptap/react";
3
+
4
+ // src/hooks/use-markdown-editor.ts
5
+ import { useEditor } from "@tiptap/react";
6
+ import { useEffect, useRef } from "react";
7
+
8
+ // src/extensions/shared.ts
9
+ import { ListKit } from "@tiptap/extension-list";
10
+ import StarterKit from "@tiptap/starter-kit";
11
+ var starterKitBaseConfig = {
12
+ heading: { levels: [1, 2, 3, 4, 5, 6] },
13
+ link: {
14
+ openOnClick: false,
15
+ autolink: true
16
+ },
17
+ // Disable StarterKit's built-in list handling; use @tiptap/extension-list instead
18
+ bulletList: false,
19
+ orderedList: false,
20
+ listItem: false,
21
+ listKeymap: false
22
+ };
23
+ var sharedExtensions = [
24
+ StarterKit.configure({
25
+ ...starterKitBaseConfig,
26
+ dropcursor: false,
27
+ gapcursor: false,
28
+ undoRedo: false,
29
+ trailingNode: false
30
+ }),
31
+ ListKit
32
+ ];
33
+
34
+ // src/extensions/editor.ts
35
+ import { ListKit as ListKit2 } from "@tiptap/extension-list";
36
+ import StarterKit2 from "@tiptap/starter-kit";
37
+ import { Markdown } from "tiptap-markdown";
38
+ var editorExtensions = [
39
+ StarterKit2.configure(starterKitBaseConfig),
40
+ ListKit2,
41
+ Markdown.configure({
42
+ html: false,
43
+ transformPastedText: true,
44
+ transformCopiedText: true
45
+ })
46
+ ];
47
+
48
+ // src/hooks/use-markdown-editor.ts
49
+ function getMarkdown(storage) {
50
+ return storage.markdown.getMarkdown();
51
+ }
52
+ function useMarkdownEditor({
53
+ value,
54
+ onChange,
55
+ editable = true
56
+ }) {
57
+ const externalValue = useRef(value);
58
+ const onChangeRef = useRef(onChange);
59
+ onChangeRef.current = onChange;
60
+ const editor = useEditor({
61
+ extensions: editorExtensions,
62
+ editable,
63
+ content: value,
64
+ onUpdate({ editor: editor2 }) {
65
+ const md = getMarkdown(
66
+ editor2.storage
67
+ );
68
+ externalValue.current = md;
69
+ onChangeRef.current(md);
70
+ }
71
+ });
72
+ useEffect(() => {
73
+ if (!editor) return;
74
+ if (value === externalValue.current) return;
75
+ externalValue.current = value;
76
+ editor.commands.setContent(value);
77
+ }, [value, editor]);
78
+ useEffect(() => {
79
+ if (!editor) return;
80
+ editor.setEditable(editable);
81
+ }, [editable, editor]);
82
+ return { editor };
83
+ }
84
+
85
+ // src/notra-editor.tsx
86
+ import { jsx } from "react/jsx-runtime";
87
+ function NotraEditor({
88
+ value,
89
+ onChange,
90
+ placeholder,
91
+ readOnly = false,
92
+ className
93
+ }) {
94
+ const { editor } = useMarkdownEditor({
95
+ value,
96
+ onChange,
97
+ placeholder,
98
+ editable: !readOnly
99
+ });
100
+ const classNames = ["notra", "notra-editor", className].filter(Boolean).join(" ");
101
+ return /* @__PURE__ */ jsx("div", { className: classNames, children: /* @__PURE__ */ jsx(EditorContent, { editor }) });
102
+ }
103
+
104
+ // src/notra-reader.tsx
105
+ import { renderToReactElement } from "@tiptap/static-renderer/pm/react";
106
+
107
+ // src/utils/markdown-to-json.ts
108
+ import { Editor } from "@tiptap/core";
109
+ import { Markdown as Markdown2 } from "tiptap-markdown";
110
+ var parserExtensions = [
111
+ ...sharedExtensions,
112
+ Markdown2.configure({ html: false })
113
+ ];
114
+ var parserEditor = null;
115
+ function getParserEditor() {
116
+ if (!parserEditor) {
117
+ parserEditor = new Editor({
118
+ extensions: parserExtensions,
119
+ content: ""
120
+ });
121
+ }
122
+ return parserEditor;
123
+ }
124
+ function markdownToJSON(markdown) {
125
+ const editor = getParserEditor();
126
+ editor.commands.setContent(markdown);
127
+ return editor.getJSON();
128
+ }
129
+
130
+ // src/notra-reader.tsx
131
+ import { jsx as jsx2 } from "react/jsx-runtime";
132
+ function NotraReader({ content, className }) {
133
+ const json = markdownToJSON(content);
134
+ const rendered = renderToReactElement({
135
+ extensions: sharedExtensions,
136
+ content: json
137
+ });
138
+ const classNames = ["notra", "notra-reader", className].filter(Boolean).join(" ");
139
+ return /* @__PURE__ */ jsx2("div", { className: classNames, children: rendered });
140
+ }
141
+ export {
142
+ NotraEditor,
143
+ NotraReader
144
+ };
145
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notra-editor.tsx","../src/hooks/use-markdown-editor.ts","../src/extensions/shared.ts","../src/extensions/editor.ts","../src/notra-reader.tsx","../src/utils/markdown-to-json.ts"],"sourcesContent":["import { EditorContent } from '@tiptap/react';\n\nimport { useMarkdownEditor } from './hooks/use-markdown-editor';\n\nexport interface NotraEditorProps {\n\t/** Markdown content (source of truth) */\n\tvalue: string;\n\t/** Called when content changes, receives updated Markdown */\n\tonChange: (value: string) => void;\n\t/** Placeholder text shown when editor is empty */\n\tplaceholder?: string;\n\t/** Disable editing */\n\treadOnly?: boolean;\n\t/** Additional CSS class on the wrapper element */\n\tclassName?: string;\n}\n\nexport function NotraEditor({\n\tvalue,\n\tonChange,\n\tplaceholder,\n\treadOnly = false,\n\tclassName\n}: NotraEditorProps) {\n\tconst { editor } = useMarkdownEditor({\n\t\tvalue,\n\t\tonChange,\n\t\tplaceholder,\n\t\teditable: !readOnly\n\t});\n\n\tconst classNames = ['notra', 'notra-editor', className]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\treturn (\n\t\t<div className={classNames}>\n\t\t\t<EditorContent editor={editor} />\n\t\t</div>\n\t);\n}\n","import { useEditor } from '@tiptap/react';\nimport { useEffect, useRef } from 'react';\n\nimport { editorExtensions } from '../extensions';\n\nimport type { MarkdownStorage } from 'tiptap-markdown';\n\nexport interface UseMarkdownEditorOptions {\n\tvalue: string;\n\tonChange: (value: string) => void;\n\tplaceholder?: string;\n\teditable?: boolean;\n}\n\nfunction getMarkdown(storage: Record<string, unknown>): string {\n\treturn (storage.markdown as MarkdownStorage).getMarkdown();\n}\n\nexport function useMarkdownEditor({\n\tvalue,\n\tonChange,\n\teditable = true\n}: UseMarkdownEditorOptions) {\n\tconst externalValue = useRef(value);\n\tconst onChangeRef = useRef(onChange);\n\n\tonChangeRef.current = onChange;\n\n\tconst editor = useEditor({\n\t\textensions: editorExtensions,\n\t\teditable,\n\t\tcontent: value,\n\t\tonUpdate({ editor }) {\n\t\t\tconst md = getMarkdown(\n\t\t\t\teditor.storage as unknown as Record<string, unknown>\n\t\t\t);\n\n\t\t\texternalValue.current = md;\n\t\t\tonChangeRef.current(md);\n\t\t}\n\t});\n\n\tuseEffect(() => {\n\t\tif (!editor) return;\n\n\t\tif (value === externalValue.current) return;\n\n\t\texternalValue.current = value;\n\t\teditor.commands.setContent(value);\n\t}, [value, editor]);\n\n\tuseEffect(() => {\n\t\tif (!editor) return;\n\n\t\teditor.setEditable(editable);\n\t}, [editable, editor]);\n\n\treturn { editor };\n}\n","import { ListKit } from '@tiptap/extension-list';\nimport StarterKit, { type StarterKitOptions } from '@tiptap/starter-kit';\n\n// Shared StarterKit config: content nodes/marks, no lists (use ListKit instead)\nexport const starterKitBaseConfig: Partial<StarterKitOptions> = {\n\theading: { levels: [1, 2, 3, 4, 5, 6] },\n\tlink: {\n\t\topenOnClick: false,\n\t\tautolink: true\n\t},\n\t// Disable StarterKit's built-in list handling; use @tiptap/extension-list instead\n\tbulletList: false,\n\torderedList: false,\n\tlistItem: false,\n\tlistKeymap: false\n};\n\n// Content model extensions — shared by editor and reader\n// No interactive features (dropcursor, gapcursor, undoRedo, trailingNode)\nexport const sharedExtensions = [\n\tStarterKit.configure({\n\t\t...starterKitBaseConfig,\n\t\tdropcursor: false,\n\t\tgapcursor: false,\n\t\tundoRedo: false,\n\t\ttrailingNode: false\n\t}),\n\tListKit\n];\n","import { ListKit } from '@tiptap/extension-list';\nimport StarterKit from '@tiptap/starter-kit';\nimport { Markdown } from 'tiptap-markdown';\n\nimport { starterKitBaseConfig } from './shared';\n\n// Editor extensions = shared content model + interactive features + Markdown\nexport const editorExtensions = [\n\tStarterKit.configure(starterKitBaseConfig),\n\tListKit,\n\tMarkdown.configure({\n\t\thtml: false,\n\t\ttransformPastedText: true,\n\t\ttransformCopiedText: true\n\t})\n];\n","import { renderToReactElement } from '@tiptap/static-renderer/pm/react';\n\nimport { sharedExtensions } from './extensions';\nimport { markdownToJSON } from './utils/markdown-to-json';\n\nexport interface NotraReaderProps {\n\t/** Markdown content to render */\n\tcontent: string;\n\t/** Additional CSS class on the wrapper element */\n\tclassName?: string;\n}\n\nexport function NotraReader({ content, className }: NotraReaderProps) {\n\tconst json = markdownToJSON(content);\n\n\tconst rendered = renderToReactElement({\n\t\textensions: sharedExtensions,\n\t\tcontent: json\n\t});\n\n\tconst classNames = ['notra', 'notra-reader', className]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\treturn <div className={classNames}>{rendered}</div>;\n}\n","import { Editor } from '@tiptap/core';\nimport { Markdown } from 'tiptap-markdown';\n\nimport { sharedExtensions } from '../extensions';\n\n// Parser needs shared content model + Markdown for markdown→JSON conversion\n// No clipboard features needed (transformPastedText/transformCopiedText are editor-only)\nconst parserExtensions = [\n\t...sharedExtensions,\n\tMarkdown.configure({ html: false })\n];\n\nlet parserEditor: Editor | null = null;\n\nfunction getParserEditor(): Editor {\n\tif (!parserEditor) {\n\t\tparserEditor = new Editor({\n\t\t\textensions: parserExtensions,\n\t\t\tcontent: ''\n\t\t});\n\t}\n\n\treturn parserEditor;\n}\n\n/**\n * Convert a Markdown string to Tiptap-compatible JSON (ProseMirror document).\n * Uses a singleton headless Tiptap editor for parsing.\n */\nexport function markdownToJSON(markdown: string): Record<string, unknown> {\n\tconst editor = getParserEditor();\n\n\teditor.commands.setContent(markdown);\n\n\treturn editor.getJSON() as Record<string, unknown>;\n}\n"],"mappings":";AAAA,SAAS,qBAAqB;;;ACA9B,SAAS,iBAAiB;AAC1B,SAAS,WAAW,cAAc;;;ACDlC,SAAS,eAAe;AACxB,OAAO,gBAA4C;AAG5C,IAAM,uBAAmD;AAAA,EAC/D,SAAS,EAAE,QAAQ,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE;AAAA,EACtC,MAAM;AAAA,IACL,aAAa;AAAA,IACb,UAAU;AAAA,EACX;AAAA;AAAA,EAEA,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,EACV,YAAY;AACb;AAIO,IAAM,mBAAmB;AAAA,EAC/B,WAAW,UAAU;AAAA,IACpB,GAAG;AAAA,IACH,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,cAAc;AAAA,EACf,CAAC;AAAA,EACD;AACD;;;AC5BA,SAAS,WAAAA,gBAAe;AACxB,OAAOC,iBAAgB;AACvB,SAAS,gBAAgB;AAKlB,IAAM,mBAAmB;AAAA,EAC/BC,YAAW,UAAU,oBAAoB;AAAA,EACzCC;AAAA,EACA,SAAS,UAAU;AAAA,IAClB,MAAM;AAAA,IACN,qBAAqB;AAAA,IACrB,qBAAqB;AAAA,EACtB,CAAC;AACF;;;AFDA,SAAS,YAAY,SAA0C;AAC9D,SAAQ,QAAQ,SAA6B,YAAY;AAC1D;AAEO,SAAS,kBAAkB;AAAA,EACjC;AAAA,EACA;AAAA,EACA,WAAW;AACZ,GAA6B;AAC5B,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,cAAc,OAAO,QAAQ;AAEnC,cAAY,UAAU;AAEtB,QAAM,SAAS,UAAU;AAAA,IACxB,YAAY;AAAA,IACZ;AAAA,IACA,SAAS;AAAA,IACT,SAAS,EAAE,QAAAC,QAAO,GAAG;AACpB,YAAM,KAAK;AAAA,QACVA,QAAO;AAAA,MACR;AAEA,oBAAc,UAAU;AACxB,kBAAY,QAAQ,EAAE;AAAA,IACvB;AAAA,EACD,CAAC;AAED,YAAU,MAAM;AACf,QAAI,CAAC,OAAQ;AAEb,QAAI,UAAU,cAAc,QAAS;AAErC,kBAAc,UAAU;AACxB,WAAO,SAAS,WAAW,KAAK;AAAA,EACjC,GAAG,CAAC,OAAO,MAAM,CAAC;AAElB,YAAU,MAAM;AACf,QAAI,CAAC,OAAQ;AAEb,WAAO,YAAY,QAAQ;AAAA,EAC5B,GAAG,CAAC,UAAU,MAAM,CAAC;AAErB,SAAO,EAAE,OAAO;AACjB;;;ADrBG;AApBI,SAAS,YAAY;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACD,GAAqB;AACpB,QAAM,EAAE,OAAO,IAAI,kBAAkB;AAAA,IACpC;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,CAAC;AAAA,EACZ,CAAC;AAED,QAAM,aAAa,CAAC,SAAS,gBAAgB,SAAS,EACpD,OAAO,OAAO,EACd,KAAK,GAAG;AAEV,SACC,oBAAC,SAAI,WAAW,YACf,8BAAC,iBAAc,QAAgB,GAChC;AAEF;;;AIxCA,SAAS,4BAA4B;;;ACArC,SAAS,cAAc;AACvB,SAAS,YAAAC,iBAAgB;AAMzB,IAAM,mBAAmB;AAAA,EACxB,GAAG;AAAA,EACHC,UAAS,UAAU,EAAE,MAAM,MAAM,CAAC;AACnC;AAEA,IAAI,eAA8B;AAElC,SAAS,kBAA0B;AAClC,MAAI,CAAC,cAAc;AAClB,mBAAe,IAAI,OAAO;AAAA,MACzB,YAAY;AAAA,MACZ,SAAS;AAAA,IACV,CAAC;AAAA,EACF;AAEA,SAAO;AACR;AAMO,SAAS,eAAe,UAA2C;AACzE,QAAM,SAAS,gBAAgB;AAE/B,SAAO,SAAS,WAAW,QAAQ;AAEnC,SAAO,OAAO,QAAQ;AACvB;;;ADXQ,gBAAAC,YAAA;AAZD,SAAS,YAAY,EAAE,SAAS,UAAU,GAAqB;AACrE,QAAM,OAAO,eAAe,OAAO;AAEnC,QAAM,WAAW,qBAAqB;AAAA,IACrC,YAAY;AAAA,IACZ,SAAS;AAAA,EACV,CAAC;AAED,QAAM,aAAa,CAAC,SAAS,gBAAgB,SAAS,EACpD,OAAO,OAAO,EACd,KAAK,GAAG;AAEV,SAAO,gBAAAA,KAAC,SAAI,WAAW,YAAa,oBAAS;AAC9C;","names":["ListKit","StarterKit","StarterKit","ListKit","editor","Markdown","Markdown","jsx"]}
@@ -0,0 +1,76 @@
1
+ /* =====================
2
+ Editor-specific Variables
3
+ ===================== */
4
+ .notra-editor {
5
+ --notra-cursor-color: rgba(98, 41, 255, 1);
6
+ --notra-selection-color: rgba(157, 138, 255, 0.2);
7
+ --notra-placeholder-color: rgba(40, 44, 51, 0.42);
8
+ }
9
+
10
+ /* =====================
11
+ Editor Container
12
+ ===================== */
13
+ .notra-editor .tiptap.ProseMirror {
14
+ white-space: pre-wrap;
15
+ outline: none;
16
+ caret-color: var(--notra-cursor-color);
17
+ }
18
+
19
+ /* =====================
20
+ Selection
21
+ ===================== */
22
+ .notra-editor .tiptap.ProseMirror ::selection {
23
+ background-color: var(--notra-selection-color);
24
+ }
25
+
26
+ .notra-editor .tiptap.ProseMirror .selection {
27
+ display: inline;
28
+ background-color: var(--notra-selection-color);
29
+ }
30
+
31
+ .notra-editor .tiptap.ProseMirror .selection::selection {
32
+ background: transparent;
33
+ }
34
+
35
+ .notra-editor
36
+ .tiptap.ProseMirror
37
+ .ProseMirror-selectednode:not(img):not(pre):not(.react-renderer) {
38
+ border-radius: var(--notra-radius);
39
+ background-color: var(--notra-selection-color);
40
+ }
41
+
42
+ /* =====================
43
+ Placeholder
44
+ ===================== */
45
+ .notra-editor .tiptap.ProseMirror > * {
46
+ position: relative;
47
+ }
48
+
49
+ .notra-editor
50
+ .tiptap.ProseMirror
51
+ .is-empty[data-placeholder]:has(
52
+ > .ProseMirror-trailingBreak:only-child
53
+ )::before {
54
+ content: attr(data-placeholder);
55
+ pointer-events: none;
56
+ height: 0;
57
+ position: absolute;
58
+ width: 100%;
59
+ text-align: inherit;
60
+ left: 0;
61
+ right: 0;
62
+ color: var(--notra-placeholder-color);
63
+ }
64
+
65
+ /* =====================
66
+ Drop Cursor
67
+ ===================== */
68
+ .notra-editor .prosemirror-dropcursor-block,
69
+ .notra-editor .prosemirror-dropcursor-inline {
70
+ background: var(--notra-cursor-color) !important;
71
+ border-radius: 0.25rem;
72
+ margin-left: -1px;
73
+ margin-right: -1px;
74
+ width: 100%;
75
+ height: 0.188rem;
76
+ }
@@ -0,0 +1,14 @@
1
+ /* =====================
2
+ Reader-specific Styles
3
+ ===================== */
4
+ .notra-reader {
5
+ font-family: var(--notra-font-body);
6
+ font-size: var(--notra-font-size);
7
+ line-height: var(--notra-line-height);
8
+ color: var(--notra-color-text);
9
+ }
10
+
11
+ /* Ensure native selection in reader mode */
12
+ .notra-reader *::selection {
13
+ background-color: highlight;
14
+ }
@@ -0,0 +1,431 @@
1
+ /* =====================
2
+ CSS Custom Properties
3
+ ===================== */
4
+ .notra {
5
+ --notra-font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
6
+ --notra-font-mono: 'JetBrains Mono NL', ui-monospace, 'SF Mono', monospace;
7
+ --notra-font-size: 1rem;
8
+ --notra-line-height: 1.6;
9
+
10
+ /* Light mode colors */
11
+ --notra-color-text: rgba(29, 30, 32, 0.98);
12
+ --notra-color-bg: #ffffff;
13
+ --notra-color-border: rgba(37, 39, 45, 0.1);
14
+ --notra-color-link: rgba(98, 41, 255, 1);
15
+
16
+ /* Code */
17
+ --notra-code-bg: rgba(15, 22, 36, 0.05);
18
+ --notra-code-text: rgba(35, 37, 42, 0.87);
19
+ --notra-code-border: rgba(37, 39, 45, 0.1);
20
+ --notra-codeblock-bg: rgba(56, 56, 56, 0.04);
21
+ --notra-codeblock-text: rgba(30, 32, 36, 0.95);
22
+ --notra-codeblock-border: rgba(37, 39, 45, 0.1);
23
+
24
+ /* Blockquote */
25
+ --notra-blockquote-bar: rgba(29, 30, 32, 0.98);
26
+
27
+ /* Horizontal rule */
28
+ --notra-hr-color: rgba(37, 39, 45, 0.1);
29
+
30
+ /* Task list */
31
+ --notra-checklist-bg: rgba(15, 22, 36, 0.05);
32
+ --notra-checklist-bg-active: rgba(29, 30, 32, 0.98);
33
+ --notra-checklist-border: rgba(37, 39, 45, 0.1);
34
+ --notra-checklist-border-active: rgba(29, 30, 32, 0.98);
35
+ --notra-checklist-check-color: #ffffff;
36
+ --notra-checklist-text-active: rgba(52, 55, 60, 0.64);
37
+
38
+ --notra-radius: 6px;
39
+ }
40
+
41
+ /* =====================
42
+ Base Typography
43
+ ===================== */
44
+ .notra .tiptap,
45
+ .notra-reader {
46
+ font-family: var(--notra-font-body);
47
+ font-size: var(--notra-font-size);
48
+ line-height: var(--notra-line-height);
49
+ color: var(--notra-color-text);
50
+ }
51
+
52
+ /* =====================
53
+ Paragraphs
54
+ ===================== */
55
+ .notra .tiptap p:not(:first-child):not(td p):not(th p),
56
+ .notra-reader p:not(:first-child) {
57
+ font-size: 1rem;
58
+ line-height: 1.6;
59
+ font-weight: normal;
60
+ margin-top: 20px;
61
+ }
62
+
63
+ /* =====================
64
+ Headings
65
+ ===================== */
66
+ .notra .tiptap h1,
67
+ .notra .tiptap h2,
68
+ .notra .tiptap h3,
69
+ .notra .tiptap h4,
70
+ .notra .tiptap h5,
71
+ .notra .tiptap h6,
72
+ .notra-reader h1,
73
+ .notra-reader h2,
74
+ .notra-reader h3,
75
+ .notra-reader h4,
76
+ .notra-reader h5,
77
+ .notra-reader h6 {
78
+ position: relative;
79
+ color: inherit;
80
+ font-style: inherit;
81
+ }
82
+
83
+ .notra .tiptap > h1:first-child,
84
+ .notra .tiptap > h2:first-child,
85
+ .notra .tiptap > h3:first-child,
86
+ .notra .tiptap > h4:first-child,
87
+ .notra-reader > h1:first-child,
88
+ .notra-reader > h2:first-child,
89
+ .notra-reader > h3:first-child,
90
+ .notra-reader > h4:first-child {
91
+ margin-top: 0;
92
+ }
93
+
94
+ .notra .tiptap h1,
95
+ .notra-reader h1 {
96
+ font-size: 1.5em;
97
+ font-weight: 700;
98
+ margin-top: 3em;
99
+ }
100
+
101
+ .notra .tiptap h2,
102
+ .notra-reader h2 {
103
+ font-size: 1.25em;
104
+ font-weight: 700;
105
+ margin-top: 2.5em;
106
+ }
107
+
108
+ .notra .tiptap h3,
109
+ .notra-reader h3 {
110
+ font-size: 1.125em;
111
+ font-weight: 600;
112
+ margin-top: 2em;
113
+ }
114
+
115
+ .notra .tiptap h4,
116
+ .notra-reader h4 {
117
+ font-size: 1em;
118
+ font-weight: 600;
119
+ margin-top: 2em;
120
+ }
121
+
122
+ .notra .tiptap h5,
123
+ .notra-reader h5 {
124
+ font-size: 0.875em;
125
+ font-weight: 600;
126
+ margin-top: 1.5em;
127
+ }
128
+
129
+ .notra .tiptap h6,
130
+ .notra-reader h6 {
131
+ font-size: 0.75em;
132
+ font-weight: 600;
133
+ margin-top: 1.5em;
134
+ }
135
+
136
+ /* =====================
137
+ Lists — Common
138
+ ===================== */
139
+ .notra .tiptap ol,
140
+ .notra .tiptap ul,
141
+ .notra-reader ol,
142
+ .notra-reader ul {
143
+ margin-top: 1.5em;
144
+ margin-bottom: 1.5em;
145
+ padding-left: 1.5em;
146
+ }
147
+
148
+ .notra .tiptap ol:first-child,
149
+ .notra .tiptap ul:first-child,
150
+ .notra-reader ol:first-child,
151
+ .notra-reader ul:first-child {
152
+ margin-top: 0;
153
+ }
154
+
155
+ .notra .tiptap ol:last-child,
156
+ .notra .tiptap ul:last-child,
157
+ .notra-reader ol:last-child,
158
+ .notra-reader ul:last-child {
159
+ margin-bottom: 0;
160
+ }
161
+
162
+ .notra .tiptap ol ol,
163
+ .notra .tiptap ol ul,
164
+ .notra .tiptap ul ol,
165
+ .notra .tiptap ul ul,
166
+ .notra-reader ol ol,
167
+ .notra-reader ol ul,
168
+ .notra-reader ul ol,
169
+ .notra-reader ul ul {
170
+ margin-top: 0;
171
+ margin-bottom: 0;
172
+ }
173
+
174
+ .notra .tiptap li p,
175
+ .notra-reader li p {
176
+ margin-top: 0;
177
+ line-height: 1.6;
178
+ }
179
+
180
+ /* =====================
181
+ Ordered Lists — Nested styles
182
+ ===================== */
183
+ .notra .tiptap ol,
184
+ .notra-reader ol {
185
+ list-style: decimal;
186
+ }
187
+
188
+ .notra .tiptap ol ol,
189
+ .notra-reader ol ol {
190
+ list-style: lower-alpha;
191
+ }
192
+
193
+ .notra .tiptap ol ol ol,
194
+ .notra-reader ol ol ol {
195
+ list-style: lower-roman;
196
+ }
197
+
198
+ /* =====================
199
+ Unordered Lists — Nested styles
200
+ ===================== */
201
+ .notra .tiptap ul:not([data-type='taskList']),
202
+ .notra-reader ul:not([data-type='taskList']) {
203
+ list-style: disc;
204
+ }
205
+
206
+ .notra .tiptap ul:not([data-type='taskList']) ul,
207
+ .notra-reader ul:not([data-type='taskList']) ul {
208
+ list-style: circle;
209
+ }
210
+
211
+ .notra .tiptap ul:not([data-type='taskList']) ul ul,
212
+ .notra-reader ul:not([data-type='taskList']) ul ul {
213
+ list-style: square;
214
+ }
215
+
216
+ /* =====================
217
+ Task Lists
218
+ ===================== */
219
+ .notra .tiptap ul[data-type='taskList'],
220
+ .notra-reader ul[data-type='taskList'] {
221
+ padding-left: 0.25em;
222
+ list-style: none;
223
+ }
224
+
225
+ .notra .tiptap ul[data-type='taskList'] li,
226
+ .notra-reader ul[data-type='taskList'] li {
227
+ display: flex;
228
+ flex-direction: row;
229
+ align-items: flex-start;
230
+ }
231
+
232
+ .notra .tiptap ul[data-type='taskList'] li[data-checked='true'] > div > p,
233
+ .notra-reader ul[data-type='taskList'] li[data-checked='true'] > div > p {
234
+ opacity: 0.5;
235
+ text-decoration: line-through;
236
+ }
237
+
238
+ .notra .tiptap ul[data-type='taskList'] li label,
239
+ .notra-reader ul[data-type='taskList'] li label {
240
+ position: relative;
241
+ padding-top: 0.375rem;
242
+ padding-right: 0.5rem;
243
+ }
244
+
245
+ .notra .tiptap ul[data-type='taskList'] li label input[type='checkbox'],
246
+ .notra-reader ul[data-type='taskList'] li label input[type='checkbox'] {
247
+ position: absolute;
248
+ opacity: 0;
249
+ width: 0;
250
+ height: 0;
251
+ }
252
+
253
+ .notra .tiptap ul[data-type='taskList'] li label span,
254
+ .notra-reader ul[data-type='taskList'] li label span {
255
+ display: block;
256
+ width: 1em;
257
+ height: 1em;
258
+ border: 1px solid var(--notra-checklist-border);
259
+ border-radius: 0.25rem;
260
+ position: relative;
261
+ cursor: pointer;
262
+ background-color: var(--notra-checklist-bg);
263
+ transition:
264
+ background-color 80ms ease-out,
265
+ border-color 80ms ease-out;
266
+ }
267
+
268
+ .notra .tiptap ul[data-type='taskList'] li label span::before,
269
+ .notra-reader ul[data-type='taskList'] li label span::before {
270
+ content: '';
271
+ position: absolute;
272
+ left: 50%;
273
+ top: 50%;
274
+ transform: translate(-50%, -50%);
275
+ width: 0.75em;
276
+ height: 0.75em;
277
+ background-color: var(--notra-checklist-check-color);
278
+ opacity: 0;
279
+ -webkit-mask: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M21.4142 4.58579C22.1953 5.36683 22.1953 6.63317 21.4142 7.41421L10.4142 18.4142C9.63317 19.1953 8.36684 19.1953 7.58579 18.4142L2.58579 13.4142C1.80474 12.6332 1.80474 11.3668 2.58579 10.5858C3.36683 9.80474 4.63317 9.80474 5.41421 10.5858L9 14.1716L18.5858 4.58579C19.3668 3.80474 20.6332 3.80474 21.4142 4.58579Z' fill='currentColor'/%3E%3C/svg%3E")
280
+ center / contain no-repeat;
281
+ mask: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M21.4142 4.58579C22.1953 5.36683 22.1953 6.63317 21.4142 7.41421L10.4142 18.4142C9.63317 19.1953 8.36684 19.1953 7.58579 18.4142L2.58579 13.4142C1.80474 12.6332 1.80474 11.3668 2.58579 10.5858C3.36683 9.80474 4.63317 9.80474 5.41421 10.5858L9 14.1716L18.5858 4.58579C19.3668 3.80474 20.6332 3.80474 21.4142 4.58579Z' fill='currentColor'/%3E%3C/svg%3E")
282
+ center / contain no-repeat;
283
+ }
284
+
285
+ .notra
286
+ .tiptap
287
+ ul[data-type='taskList']
288
+ li
289
+ label
290
+ input[type='checkbox']:checked
291
+ + span,
292
+ .notra-reader
293
+ ul[data-type='taskList']
294
+ li
295
+ label
296
+ input[type='checkbox']:checked
297
+ + span {
298
+ background: var(--notra-checklist-bg-active);
299
+ border-color: var(--notra-checklist-border-active);
300
+ }
301
+
302
+ .notra
303
+ .tiptap
304
+ ul[data-type='taskList']
305
+ li
306
+ label
307
+ input[type='checkbox']:checked
308
+ + span::before,
309
+ .notra-reader
310
+ ul[data-type='taskList']
311
+ li
312
+ label
313
+ input[type='checkbox']:checked
314
+ + span::before {
315
+ opacity: 1;
316
+ }
317
+
318
+ .notra .tiptap ul[data-type='taskList'] li div,
319
+ .notra-reader ul[data-type='taskList'] li div {
320
+ flex: 1 1 0%;
321
+ min-width: 0;
322
+ }
323
+
324
+ /* =====================
325
+ Code — Inline
326
+ ===================== */
327
+ .notra .tiptap code,
328
+ .notra-reader code {
329
+ background-color: var(--notra-code-bg);
330
+ color: var(--notra-code-text);
331
+ border: 1px solid var(--notra-code-border);
332
+ font-family: var(--notra-font-mono);
333
+ font-size: 0.875em;
334
+ line-height: 1.4;
335
+ border-radius: var(--notra-radius);
336
+ padding: 0.1em 0.2em;
337
+ }
338
+
339
+ /* =====================
340
+ Code — Block
341
+ ===================== */
342
+ .notra .tiptap pre,
343
+ .notra-reader pre {
344
+ background-color: var(--notra-codeblock-bg);
345
+ color: var(--notra-codeblock-text);
346
+ border: 1px solid var(--notra-codeblock-border);
347
+ margin-top: 1.5em;
348
+ margin-bottom: 1.5em;
349
+ padding: 1em;
350
+ font-size: 1rem;
351
+ border-radius: var(--notra-radius);
352
+ }
353
+
354
+ .notra .tiptap pre code,
355
+ .notra-reader pre code {
356
+ background-color: transparent;
357
+ border: none;
358
+ border-radius: 0;
359
+ color: inherit;
360
+ padding: 0;
361
+ }
362
+
363
+ /* =====================
364
+ Blockquote
365
+ ===================== */
366
+ .notra .tiptap blockquote,
367
+ .notra-reader blockquote {
368
+ position: relative;
369
+ padding-left: 1em;
370
+ padding-top: 0.375em;
371
+ padding-bottom: 0.375em;
372
+ margin: 1.5rem 0;
373
+ }
374
+
375
+ .notra .tiptap blockquote p,
376
+ .notra-reader blockquote p {
377
+ margin-top: 0;
378
+ }
379
+
380
+ .notra .tiptap blockquote::before,
381
+ .notra-reader blockquote::before {
382
+ position: absolute;
383
+ bottom: 0;
384
+ left: 0;
385
+ top: 0;
386
+ height: 100%;
387
+ width: 0.25em;
388
+ background-color: var(--notra-blockquote-bar);
389
+ content: '';
390
+ border-radius: 0;
391
+ }
392
+
393
+ /* =====================
394
+ Horizontal Rule
395
+ ===================== */
396
+ .notra .tiptap hr,
397
+ .notra-reader hr {
398
+ border: none;
399
+ height: 1px;
400
+ background-color: var(--notra-hr-color);
401
+ }
402
+
403
+ .notra .tiptap [data-type='horizontalRule'],
404
+ .notra-reader [data-type='horizontalRule'] {
405
+ margin-top: 2.25em;
406
+ margin-bottom: 2.25em;
407
+ padding-top: 0.75rem;
408
+ padding-bottom: 0.75rem;
409
+ }
410
+
411
+ /* =====================
412
+ Links
413
+ ===================== */
414
+ .notra .tiptap a,
415
+ .notra-reader a {
416
+ color: var(--notra-color-link);
417
+ text-decoration: underline;
418
+ }
419
+
420
+ /* =====================
421
+ Inline Text Decoration
422
+ ===================== */
423
+ .notra .tiptap a span,
424
+ .notra-reader a span {
425
+ text-decoration: underline;
426
+ }
427
+
428
+ .notra .tiptap s span,
429
+ .notra-reader s span {
430
+ text-decoration: line-through;
431
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "notra-editor",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "A Markdown-first rich text editor for React",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "editor",
9
+ "markdown",
10
+ "rich-text",
11
+ "tiptap",
12
+ "react",
13
+ "notion"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.mjs"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "./themes/default/shared.css": "./dist/themes/default/shared.css",
27
+ "./themes/default/editor.css": "./dist/themes/default/editor.css",
28
+ "./themes/default/reader.css": "./dist/themes/default/reader.css"
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.mjs",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": "^18.0.0 || ^19.0.0",
38
+ "react-dom": "^18.0.0 || ^19.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@tiptap/core": "^3.22.4",
42
+ "@tiptap/extension-link": "^3.22.4",
43
+ "@tiptap/extension-list": "^3.22.4",
44
+ "@tiptap/pm": "^3.22.4",
45
+ "@tiptap/react": "^3.22.4",
46
+ "@tiptap/starter-kit": "^3.22.4",
47
+ "@tiptap/static-renderer": "^3.22.4",
48
+ "tiptap-markdown": "^0.8.10"
49
+ },
50
+ "devDependencies": {
51
+ "@testing-library/jest-dom": "^6.6.3",
52
+ "@testing-library/react": "^16.3.0",
53
+ "@types/react": "19.2.2",
54
+ "@types/react-dom": "19.2.2",
55
+ "jsdom": "^26.1.0",
56
+ "react": "^19.2.0",
57
+ "react-dom": "^19.2.0",
58
+ "tsup": "^8.4.0",
59
+ "typescript": "^5.8.0",
60
+ "vitest": "^4.0.18"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "dev": "tsup --watch",
65
+ "test": "vitest run --passWithNoTests",
66
+ "test:watch": "vitest",
67
+ "pub:beta": "pnpm build && pnpm publish --tag beta --access public",
68
+ "pub:release": "pnpm build && pnpm publish --access public"
69
+ }
70
+ }