Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: steven-tey/novel
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 8d168f58ca8e6cc9a859f232a570a2ded6532367
Choose a base ref
...
head repository: steven-tey/novel
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.2.3
Choose a head ref
  • 4 commits
  • 10 files changed
  • 1 contributor

Commits on Feb 13, 2024

  1. chore: bump version

    andrewdoro committed Feb 13, 2024
    Copy the full SHA
    cfc4cce View commit details
  2. chore: cleanup packages

    andrewdoro committed Feb 13, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    6098232 View commit details
  3. Copy the full SHA
    957e5dc View commit details
  4. chore: update lock

    andrewdoro committed Feb 13, 2024
    Copy the full SHA
    1c1e149 View commit details
25 changes: 9 additions & 16 deletions packages/headless/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"name": "novel",
"version": "0.2.1",
"version": "0.2.2",
"description": "Notion-style WYSIWYG editor with AI-powered autocompletions",
"license": "Apache-2.0",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -27,13 +30,14 @@
"scripts": {
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"build": "tsup --clean"
"build": "tsup"
},
"sideEffects": false,
"peerDependencies": {
"react": "^18.2.0"
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"dependencies": {
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-slot": "^1.0.2",
"@tiptap/core": "^2.1.7",
"@tiptap/extension-color": "^2.1.7",
@@ -51,30 +55,19 @@
"@tiptap/starter-kit": "^2.1.7",
"@tiptap/suggestion": "^2.1.7",
"@types/node": "18.15.3",
"@upstash/ratelimit": "^0.4.4",
"clsx": "^1.2.1",
"cmdk": "^0.2.1",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"eventsource-parser": "^0.1.0",
"jotai": "^2.6.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"react-moveable": "^0.56.0",
"sonner": "^0.7.0",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2",
"tunnel-rat": "^0.1.2"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "18.2.19",
"eslint": "^7.32.0",
"postcss": "^8.4.29",
"tsconfig": "workspace:*",
"tsup": "^7.2.0",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
},
"author": "Steven Tey <stevensteel97@gmail.com>",
103 changes: 53 additions & 50 deletions packages/headless/src/components/editor-bubble.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,62 @@
import { BubbleMenu, type BubbleMenuProps, isNodeSelection, useCurrentEditor } from "@tiptap/react";
import React, { type ReactNode, useMemo, useState, useRef, useEffect } from "react";
import { type ReactNode, useMemo, useRef, useEffect, forwardRef } from "react";
import type { Instance, Props } from "tippy.js";

export interface EditorBubbleProps extends Omit<BubbleMenuProps, "editor"> {
children: ReactNode;
}

export const EditorBubble = ({ children, tippyOptions, ...rest }: EditorBubbleProps) => {
const { editor } = useCurrentEditor();
const instanceRef = useRef<Instance<Props> | null>(null);

useEffect(() => {
if (!instanceRef.current || !tippyOptions?.placement) return;

instanceRef.current.setProps({ placement: tippyOptions.placement });
instanceRef.current.popperInstance?.update();
}, [tippyOptions?.placement]);

const bubbleMenuProps: Omit<BubbleMenuProps, "children"> = useMemo(() => {
const shouldShow: BubbleMenuProps["shouldShow"] = ({ editor, state }) => {
const { selection } = state;
const { empty } = selection;

// don't show bubble menu if:
// - the selected node is an image
// - the selection is empty
// - the selection is a node selection (for drag handles)
if (editor.isActive("image") || empty || isNodeSelection(selection)) {
return false;
}
return true;
};

return {
shouldShow,
tippyOptions: {
onCreate: (val) => {
instanceRef.current = val;
export const EditorBubble = forwardRef<HTMLDivElement, EditorBubbleProps>(
({ children, tippyOptions, ...rest }, ref) => {
const { editor } = useCurrentEditor();
const instanceRef = useRef<Instance<Props> | null>(null);

useEffect(() => {
if (!instanceRef.current || !tippyOptions?.placement) return;

instanceRef.current.setProps({ placement: tippyOptions.placement });
instanceRef.current.popperInstance?.update();
}, [tippyOptions?.placement]);

const bubbleMenuProps: Omit<BubbleMenuProps, "children"> = useMemo(() => {
const shouldShow: BubbleMenuProps["shouldShow"] = ({ editor, state }) => {
const { selection } = state;
const { empty } = selection;

// don't show bubble menu if:
// - the selected node is an image
// - the selection is empty
// - the selection is a node selection (for drag handles)
if (editor.isActive("image") || empty || isNodeSelection(selection)) {
return false;
}
return true;
};

return {
shouldShow,
tippyOptions: {
onCreate: (val) => {
instanceRef.current = val;
},
moveTransition: "transform 0.15s ease-out",
...tippyOptions,
},
moveTransition: "transform 0.15s ease-out",
...tippyOptions,
},
...rest,
};
}, [rest, tippyOptions]);

if (!editor) return null;

return (
//We need to add this because of https://github.com/ueberdosis/tiptap/issues/2658
<div>
<BubbleMenu editor={editor} {...bubbleMenuProps}>
{children}
</BubbleMenu>
</div>
);
};
...rest,
};
}, [rest, tippyOptions]);

if (!editor) return null;

return (
//We need to add this because of https://github.com/ueberdosis/tiptap/issues/2658
<div ref={ref}>
<BubbleMenu editor={editor} {...bubbleMenuProps}>
{children}
</BubbleMenu>
</div>
);
}
);

export default EditorBubble;
53 changes: 29 additions & 24 deletions packages/headless/src/components/editor-command.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { atom, useAtom, useSetAtom } from "jotai";
import { useEffect, useRef, type ComponentPropsWithoutRef } from "react";
import { useEffect, useRef, type ComponentPropsWithoutRef, forwardRef } from "react";
import tunnel from "tunnel-rat";
import { novelStore } from "./editor";
import { Command } from "cmdk";
@@ -10,7 +10,13 @@ const t = tunnel();
export const queryAtom = atom("");
export const rangeAtom = atom<Range | null>(null);

export const EditorCommandOut = ({ query, range }: { query: string; range: Range }) => {
export const EditorCommandOut = ({
query,
range,
}: {
query: string;
range: Range;
}): JSX.Element => {
const setQuery = useSetAtom(queryAtom, { store: novelStore });
const setRange = useSetAtom(rangeAtom, { store: novelStore });

@@ -46,26 +52,25 @@ export const EditorCommandOut = ({ query, range }: { query: string; range: Range
return <t.Out />;
};

export const EditorCommand = ({
children,
className,
...rest
}: ComponentPropsWithoutRef<typeof Command>) => {
const commandListRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useAtom(queryAtom);
export const EditorCommand = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof Command>>(
({ children, className, ...rest }, ref) => {
const commandListRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useAtom(queryAtom);

return (
<t.In>
<Command
onKeyDown={(e) => {
e.stopPropagation();
}}
id='slash-command'
className={className}
{...rest}>
<Command.Input value={query} onValueChange={setQuery} style={{ display: "none" }} />
<Command.List ref={commandListRef}>{children}</Command.List>
</Command>
</t.In>
);
};
return (
<t.In>
<Command
ref={ref}
onKeyDown={(e) => {
e.stopPropagation();
}}
id='slash-command'
className={className}
{...rest}>
<Command.Input value={query} onValueChange={setQuery} style={{ display: "none" }} />
<Command.List ref={commandListRef}>{children}</Command.List>
</Command>
</t.In>
);
}
);
39 changes: 18 additions & 21 deletions packages/headless/src/components/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,41 @@
import React, { useMemo, type ReactNode, useState, useEffect, useRef } from "react";
import { useMemo, type ReactNode, useState, useEffect, useRef, forwardRef } from "react";
import { EditorProvider, type EditorProviderProps, type JSONContent } from "@tiptap/react";
import { Provider, createStore } from "jotai";
import { simpleExtensions } from "../extensions";
import { startImageUpload } from "../plugins/upload-images";
import { Editor } from "@tiptap/core";
export interface EditorProps {
children: React.ReactNode;
children: ReactNode;
className?: string;
}

export const novelStore = createStore();

export const EditorRoot = ({ children }: { children: ReactNode }) => {
export const EditorRoot = ({ children }: { children: ReactNode }): JSX.Element => {
return <Provider store={novelStore}>{children}</Provider>;
};

export type EditorContentProps = {
children: React.ReactNode;
children: ReactNode;
className?: string;
initialContent?: JSONContent;
} & Omit<EditorProviderProps, "content">;

export const EditorContent = ({
className,
children,
initialContent,
...rest
}: EditorContentProps) => {
const extensions = useMemo(() => {
return [...simpleExtensions, ...(rest.extensions ?? [])];
}, [rest.extensions]);
export const EditorContent = forwardRef<HTMLDivElement, EditorContentProps>(
({ className, children, initialContent, ...rest }) => {
const extensions = useMemo(() => {
return [...simpleExtensions, ...(rest.extensions ?? [])];
}, [rest.extensions]);

return (
<div className={className}>
<EditorProvider {...rest} content={initialContent} extensions={extensions}>
{children}
</EditorProvider>
</div>
);
};
return (
<div className={className}>
<EditorProvider {...rest} content={initialContent} extensions={extensions}>
{children}
</EditorProvider>
</div>
);
}
);

export const defaultEditorProps: EditorProviderProps["editorProps"] = {
handleDOMEvents: {
2 changes: 1 addition & 1 deletion packages/headless/src/extensions/image-resizer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Moveable from "react-moveable";
import { useEditor } from "../components";

export const ImageResizer = () => {
export const ImageResizer = (): JSX.Element | null => {
const { editor } = useEditor();

if (!editor?.isActive("image")) return null;
83 changes: 41 additions & 42 deletions packages/headless/src/plugins/upload-images.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//@ts-nocheck
//TODO: remove ts-nocheck from here some day
import { toast } from "sonner";
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";

@@ -52,12 +51,13 @@ function findPlaceholder(state: EditorState, id: {}) {
export function startImageUpload(file: File, view: EditorView, pos: number) {
// check if the file is an image
if (!file.type.includes("image/")) {
toast.error("File type not supported.");
//TODO add toast back
// toast.error("File type not supported.");
return;

// check if the file size is less than 20MB
} else if (file.size / 1024 / 1024 > 20) {
toast.error("File size too big (max 20MB).");
// toast.error("File size too big (max 20MB).");
return;
}

@@ -105,43 +105,42 @@ export function startImageUpload(file: File, view: EditorView, pos: number) {
}

export const handleImageUpload = (file: File) => {
// upload to Vercel Blob
return new Promise((resolve) => {
toast.promise(
fetch("/api/upload", {
method: "POST",
headers: {
"content-type": file?.type || "application/octet-stream",
"x-vercel-filename": file?.name || "image.png",
},
body: file,
}).then(async (res) => {
// Successfully uploaded image
if (res.status === 200) {
const { url } = (await res.json()) as any;
// preload the image
let image = new Image();
image.src = url;
image.onload = () => {
resolve(url);
};
// No blob store configured
} else if (res.status === 401) {
resolve(file);

throw new Error(
"`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead."
);
// Unknown error
} else {
throw new Error(`Error uploading image. Please try again.`);
}
}),
{
loading: "Uploading image...",
success: "Image uploaded successfully.",
error: (e) => e.message,
}
);
});
// upload to Vercel Blob, TODO: fix toat
// return new Promise((resolve) => {
// toast.promise(
// fetch("/api/upload", {
// method: "POST",
// headers: {
// "content-type": file?.type || "application/octet-stream",
// "x-vercel-filename": file?.name || "image.png",
// },
// body: file,
// }).then(async (res) => {
// // Successfully uploaded image
// if (res.status === 200) {
// const { url } = (await res.json()) as any;
// // preload the image
// let image = new Image();
// image.src = url;
// image.onload = () => {
// resolve(url);
// };
// // No blob store configured
// } else if (res.status === 401) {
// resolve(file);
// throw new Error(
// "`BLOB_READ_WRITE_TOKEN` environment variable not found, reading image locally instead."
// );
// // Unknown error
// } else {
// throw new Error(`Error uploading image. Please try again.`);
// }
// }),
// {
// loading: "Uploading image...",
// success: "Image uploaded successfully.",
// error: (e) => e.message,
// }
// );
// });
};
6 changes: 0 additions & 6 deletions packages/headless/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { Editor } from "@tiptap/core";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export function isValidUrl(url: string) {
try {
new URL(url);
6 changes: 4 additions & 2 deletions packages/headless/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -9,10 +9,12 @@ export default defineConfig((options: Options) => ({
banner: {
js: "'use client'",
},
minify: true,

format: ["cjs", "esm"],
dts: true,
clean: true,
external: ["react"],
injectStyle: true,

external: ["react", "react-dom"],
...options,
}));
2 changes: 1 addition & 1 deletion packages/tsconfig/react.json
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
"extends": "./base.json",
"compilerOptions": {
"lib": ["DOM"],
"target": "ES6",
"target": "ESNext",
"jsx": "react-jsx"
}
}
2,783 changes: 1,496 additions & 1,287 deletions pnpm-lock.yaml

Large diffs are not rendered by default.