feat: add editor
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
"@tiptap/react": "^2.1.13",
|
"@tiptap/react": "^2.1.13",
|
||||||
"@vidstack/react": "^1.9.8",
|
"@vidstack/react": "^1.9.8",
|
||||||
"get-urls": "^12.1.0",
|
"get-urls": "^12.1.0",
|
||||||
|
"jotai": "^2.6.1",
|
||||||
"markdown-to-jsx": "^7.4.0",
|
"markdown-to-jsx": "^7.4.0",
|
||||||
"minidenticons": "^4.2.0",
|
"minidenticons": "^4.2.0",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export class Ark {
|
|||||||
public async getUserProfile({ pubkey }: { pubkey: string }) {
|
public async getUserProfile({ pubkey }: { pubkey: string }) {
|
||||||
try {
|
try {
|
||||||
// get clean pubkey without any special characters
|
// get clean pubkey without any special characters
|
||||||
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "");
|
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hexstring.startsWith("npub1") ||
|
hexstring.startsWith("npub1") ||
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function NoteReaction() {
|
|||||||
className="size-6"
|
className="size-6"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ReactionIcon className="size-6 group-hover:text-blue-500" />
|
<ReactionIcon className="size-5 group-hover:text-blue-500" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ReplyIcon } from "@lume/icons";
|
import { ReplyIcon } from "@lume/icons";
|
||||||
|
import { editorAtom, editorValueAtom } from "@lume/utils";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { createSearchParams, useNavigate } from "react-router-dom";
|
import { useSetAtom } from "jotai";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
export function NoteReply({
|
export function NoteReply() {
|
||||||
rootEventId,
|
|
||||||
}: {
|
|
||||||
rootEventId?: string;
|
|
||||||
}) {
|
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
const setEditorValue = useSetAtom(editorValueAtom);
|
||||||
|
const setIsEditorOpen = useSetAtom(editorAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
@@ -17,18 +17,24 @@ export function NoteReply({
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
navigate({
|
setEditorValue([
|
||||||
pathname: "/new/",
|
{
|
||||||
search: createSearchParams({
|
type: "event",
|
||||||
replyTo: event.id,
|
// @ts-expect-error, useless
|
||||||
rootReplyTo: rootEventId,
|
eventId: `nostr:${nip19.noteEncode(event.id)}`,
|
||||||
}).toString(),
|
children: [{ text: "" }],
|
||||||
})
|
},
|
||||||
}
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ text: "" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setIsEditorOpen(true);
|
||||||
|
}}
|
||||||
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<ReplyIcon className="size-6 group-hover:text-blue-500" />
|
<ReplyIcon className="size-5 group-hover:text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function NoteRepost() {
|
|||||||
>
|
>
|
||||||
<RepostIcon
|
<RepostIcon
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"size-6 group-hover:text-blue-600",
|
"size-5 group-hover:text-blue-600",
|
||||||
isRepost ? "text-blue-500" : "",
|
isRepost ? "text-blue-500" : "",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function NoteZap() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<ZapIcon className="size-6 group-hover:text-blue-500" />
|
<ZapIcon className="size-5 group-hover:text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { useEvent } from "../../../hooks/useEvent";
|
|||||||
|
|
||||||
export const MentionNote = memo(function MentionNote({
|
export const MentionNote = memo(function MentionNote({
|
||||||
eventId,
|
eventId,
|
||||||
}: { eventId: string }) {
|
openable = true,
|
||||||
|
}: { eventId: string; openable?: boolean }) {
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
const renderKind = (event: NDKEvent) => {
|
const renderKind = (event: NDKEvent) => {
|
||||||
@@ -24,7 +25,10 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900">
|
<div
|
||||||
|
contentEditable={false}
|
||||||
|
className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
Loading
|
Loading
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -32,7 +36,10 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900">
|
<div
|
||||||
|
contentEditable={false}
|
||||||
|
className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
Failed to fetch event
|
Failed to fetch event
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -46,12 +53,14 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-3 pb-3 mt-1">
|
<div className="px-3 pb-3 mt-1">
|
||||||
{renderKind(data)}
|
{renderKind(data)}
|
||||||
|
{openable ? (
|
||||||
<Link
|
<Link
|
||||||
to={`/events/${data.id}`}
|
to={`/events/${data.id}`}
|
||||||
className="mt-2 text-blue-500 hover:text-blue-600"
|
className="mt-2 text-blue-500 hover:text-blue-600"
|
||||||
>
|
>
|
||||||
Show more
|
Show more
|
||||||
</Link>
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Note.Root>
|
</Note.Root>
|
||||||
</Note.Provider>
|
</Note.Provider>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function NoteRoot({
|
|||||||
"flex h-min w-full flex-col overflow-hidden rounded-xl bg-neutral-50 dark:bg-neutral-950",
|
"flex h-min w-full flex-col overflow-hidden rounded-xl bg-neutral-50 dark:bg-neutral-950",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
contentEditable={false}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,3 +101,4 @@ export * from "./src/moveRight";
|
|||||||
export * from "./src/help";
|
export * from "./src/help";
|
||||||
export * from "./src/plusSquare";
|
export * from "./src/plusSquare";
|
||||||
export * from "./src/column";
|
export * from "./src/column";
|
||||||
|
export * from "./src/addMedia";
|
||||||
|
|||||||
20
packages/icons/src/addMedia.tsx
Normal file
20
packages/icons/src/addMedia.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M19 22v-3m0 0v-3m0 3h-3m3 0h3m0-5.648V11l-.001-1m-9.464 11H10c-.756 0-1.41 0-1.983-.01M22 10H21c-1.393 0-2.09 0-2.676.06A11.5 11.5 0 008.06 20.324c-.02.2-.034.415-.043.665M22 10c-.008-2.15-.068-3.336-.544-4.27a5 5 0 00-2.185-2.185C18.2 3 16.8 3 14 3h-4c-2.8 0-4.2 0-5.27.545A5 5 0 002.545 5.73C2 6.8 2 8.2 2 11v2c0 2.8 0 4.2.545 5.27a5 5 0 002.185 2.185c.78.398 1.738.505 3.287.534M7.5 9.5a1 1 0 110-2 1 1 0 010 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { appConfigDir, resolveResource } from "@tauri-apps/api/path";
|
|||||||
import { Platform } from "@tauri-apps/plugin-os";
|
import { Platform } from "@tauri-apps/plugin-os";
|
||||||
import { Child, Command } from "@tauri-apps/plugin-shell";
|
import { Child, Command } from "@tauri-apps/plugin-shell";
|
||||||
import Database from "@tauri-apps/plugin-sql";
|
import Database from "@tauri-apps/plugin-sql";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
export class LumeStorage {
|
export class LumeStorage {
|
||||||
#db: Database;
|
#db: Database;
|
||||||
@@ -226,9 +227,10 @@ export class LumeStorage {
|
|||||||
if (!results.length) return [];
|
if (!results.length) return [];
|
||||||
|
|
||||||
const users: NDKCacheUserProfile[] = results.map((item) => ({
|
const users: NDKCacheUserProfile[] = results.map((item) => ({
|
||||||
pubkey: item.pubkey,
|
npub: nip19.npubEncode(item.pubkey),
|
||||||
...JSON.parse(item.profile as string),
|
...JSON.parse(item.profile as string),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
|
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-alpha.5",
|
"@tauri-apps/plugin-shell": "2.0.0-alpha.5",
|
||||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.5",
|
"@tauri-apps/plugin-sql": "2.0.0-alpha.5",
|
||||||
|
"nostr-tools": "1.17",
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
2
packages/types/index.d.ts
vendored
2
packages/types/index.d.ts
vendored
@@ -115,7 +115,7 @@ export interface NDKCacheUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NDKCacheUserProfile extends NDKUserProfile {
|
export interface NDKCacheUserProfile extends NDKUserProfile {
|
||||||
pubkey: string;
|
npub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NDKCacheEvent {
|
export interface NDKCacheEvent {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@lume/ark": "workspace:^",
|
"@lume/ark": "workspace:^",
|
||||||
"@lume/icons": "workspace:^",
|
"@lume/icons": "workspace:^",
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
@@ -17,10 +18,15 @@
|
|||||||
"@tauri-apps/api": "2.0.0-alpha.13",
|
"@tauri-apps/api": "2.0.0-alpha.13",
|
||||||
"@tauri-apps/plugin-http": "2.0.0-alpha.6",
|
"@tauri-apps/plugin-http": "2.0.0-alpha.6",
|
||||||
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
|
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
|
||||||
|
"framer-motion": "^10.17.0",
|
||||||
|
"jotai": "^2.6.1",
|
||||||
"minidenticons": "^4.2.0",
|
"minidenticons": "^4.2.0",
|
||||||
"nostr-tools": "~1.17.0",
|
"nostr-tools": "~1.17.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
|
"slate": "^0.101.5",
|
||||||
|
"slate-react": "^0.101.5",
|
||||||
"sonner": "^1.3.1"
|
"sonner": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
45
packages/ui/src/editor/addMedia.tsx
Normal file
45
packages/ui/src/editor/addMedia.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { AddMediaIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useSlateStatic } from "slate-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { insertImage } from "./utils";
|
||||||
|
|
||||||
|
export function EditorAddMedia() {
|
||||||
|
const ark = useArk();
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const uploadToNostrBuild = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const image = await ark.upload({
|
||||||
|
fileExts: ["mp4", "mp3", "webm", "mkv", "avi", "mov"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
insertImage(editor, image);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(`Upload failed, error: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadToNostrBuild()}
|
||||||
|
className="inline-flex items-center justify-center text-sm font-medium rounded-lg size-9 bg-neutral-100 text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<AddMediaIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
packages/ui/src/editor/column.tsx
Normal file
33
packages/ui/src/editor/column.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { editorAtom } from "@lume/utils";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { EditorForm } from "./form";
|
||||||
|
|
||||||
|
export function Editor() {
|
||||||
|
const isEditorOpen = useAtomValue(editorAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={false} mode="wait">
|
||||||
|
{isEditorOpen ? (
|
||||||
|
<motion.div
|
||||||
|
key={isEditorOpen ? "editor-open" : "editor-close"}
|
||||||
|
layout
|
||||||
|
initial={{ scale: 0.9, opacity: 0, translateX: -20 }}
|
||||||
|
animate={{
|
||||||
|
scale: [0.95, 1],
|
||||||
|
opacity: [0.5, 1],
|
||||||
|
translateX: [-10, 0],
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
scale: [0.95, 0.9],
|
||||||
|
opacity: [0.5, 0],
|
||||||
|
translateX: [-10, -20],
|
||||||
|
}}
|
||||||
|
className="h-full w-[350px] px-1 pb-1 shrink-0"
|
||||||
|
>
|
||||||
|
<EditorForm />
|
||||||
|
</motion.div>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
packages/ui/src/editor/form.tsx
Normal file
299
packages/ui/src/editor/form.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { MentionNote, useStorage } from "@lume/ark";
|
||||||
|
import { TrashIcon } from "@lume/icons";
|
||||||
|
import { NDKCacheUserProfile } from "@lume/types";
|
||||||
|
import { cn, editorValueAtom } from "@lume/utils";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Editor, Range, Transforms, createEditor } from "slate";
|
||||||
|
import {
|
||||||
|
Editable,
|
||||||
|
ReactEditor,
|
||||||
|
Slate,
|
||||||
|
useFocused,
|
||||||
|
useSelected,
|
||||||
|
useSlateStatic,
|
||||||
|
withReact,
|
||||||
|
} from "slate-react";
|
||||||
|
import { User } from "../user";
|
||||||
|
import { EditorAddMedia } from "./addMedia";
|
||||||
|
import {
|
||||||
|
Portal,
|
||||||
|
insertImage,
|
||||||
|
insertMention,
|
||||||
|
insertNostrEvent,
|
||||||
|
isImageUrl,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
const withNostrEvent = (editor: ReactEditor) => {
|
||||||
|
const { insertData, isVoid } = editor;
|
||||||
|
|
||||||
|
editor.isVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "event" ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.insertData = (data) => {
|
||||||
|
const text = data.getData("text/plain");
|
||||||
|
|
||||||
|
if (text.startsWith("nevent1") || text.startsWith("note1")) {
|
||||||
|
insertNostrEvent(editor, text);
|
||||||
|
} else {
|
||||||
|
insertData(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withMentions = (editor: ReactEditor) => {
|
||||||
|
const { isInline, isVoid, markableVoid } = editor;
|
||||||
|
|
||||||
|
editor.isInline = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "mention" ? true : isInline(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.isVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "mention" ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.markableVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "mention" || markableVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withImages = (editor: ReactEditor) => {
|
||||||
|
const { insertData, isVoid } = editor;
|
||||||
|
|
||||||
|
editor.isVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "image" ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.insertData = (data) => {
|
||||||
|
const text = data.getData("text/plain");
|
||||||
|
|
||||||
|
if (isImageUrl(text)) {
|
||||||
|
insertImage(editor, text);
|
||||||
|
} else {
|
||||||
|
insertData(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Image = ({ attributes, children, element }) => {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
|
const selected = useSelected();
|
||||||
|
const focused = useFocused();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...attributes}>
|
||||||
|
{children}
|
||||||
|
<div contentEditable={false} className="relative">
|
||||||
|
<img
|
||||||
|
src={element.url}
|
||||||
|
alt={element.url}
|
||||||
|
className={cn(
|
||||||
|
"object-cover w-full h-auto border rounded-lg border-neutral-100 dark:border-neutral-900 ring-2",
|
||||||
|
selected && focused ? "ring-blue-500" : "ring-transparent",
|
||||||
|
)}
|
||||||
|
contentEditable={false}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
|
className="absolute inline-flex items-center justify-center text-white bg-red-500 rounded-lg top-2 right-2 size-8 hover:bg-red-600"
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mention = ({ attributes, element }) => {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...attributes}
|
||||||
|
type="button"
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
|
className="inline-block text-blue-500 align-baseline hover:text-blue-600"
|
||||||
|
>{`@${element.name}`}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Event = ({ attributes, element, children }) => {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...attributes}>
|
||||||
|
{children}
|
||||||
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
||||||
|
<div
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
|
className="relative user-select-none"
|
||||||
|
>
|
||||||
|
<MentionNote
|
||||||
|
eventId={element.eventId.replace("nostr:", "")}
|
||||||
|
openable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Element = (props) => {
|
||||||
|
const { attributes, children, element } = props;
|
||||||
|
|
||||||
|
switch (element.type) {
|
||||||
|
case "image":
|
||||||
|
return <Image {...props} />;
|
||||||
|
case "mention":
|
||||||
|
return <Mention {...props} />;
|
||||||
|
case "event":
|
||||||
|
return <Event {...props} />;
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<p {...attributes} className="text-lg">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditorForm() {
|
||||||
|
const storage = useStorage();
|
||||||
|
const ref = useRef<HTMLDivElement | null>();
|
||||||
|
const initialValue = useAtomValue(editorValueAtom);
|
||||||
|
|
||||||
|
const [contacts, setContacts] = useState<NDKCacheUserProfile[]>([]);
|
||||||
|
const [target, setTarget] = useState<Range | undefined>();
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [editor] = useState(() =>
|
||||||
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filters = contacts
|
||||||
|
?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase()))
|
||||||
|
?.slice(0, 10);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadContacts() {
|
||||||
|
const res = await storage.getAllCacheUsers();
|
||||||
|
if (res) setContacts(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadContacts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (target && filters.length > 0) {
|
||||||
|
const el = ref.current;
|
||||||
|
const domRange = ReactEditor.toDOMRange(editor, target);
|
||||||
|
const rect = domRange.getBoundingClientRect();
|
||||||
|
el.style.top = `${rect.top + window.pageYOffset + 24}px`;
|
||||||
|
el.style.left = `${rect.left + window.pageXOffset}px`;
|
||||||
|
}
|
||||||
|
}, [filters.length, editor, index, search, target]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
||||||
|
<Slate
|
||||||
|
editor={editor}
|
||||||
|
initialValue={initialValue}
|
||||||
|
onChange={() => {
|
||||||
|
const { selection } = editor;
|
||||||
|
|
||||||
|
if (selection && Range.isCollapsed(selection)) {
|
||||||
|
const [start] = Range.edges(selection);
|
||||||
|
const wordBefore = Editor.before(editor, start, { unit: "word" });
|
||||||
|
const before = wordBefore && Editor.before(editor, wordBefore);
|
||||||
|
const beforeRange = before && Editor.range(editor, before, start);
|
||||||
|
const beforeText =
|
||||||
|
beforeRange && Editor.string(editor, beforeRange);
|
||||||
|
const beforeMatch = beforeText?.match(/^@(\w+)$/);
|
||||||
|
const after = Editor.after(editor, start);
|
||||||
|
const afterRange = Editor.range(editor, start, after);
|
||||||
|
const afterText = Editor.string(editor, afterRange);
|
||||||
|
const afterMatch = afterText.match(/^(\s|$)/);
|
||||||
|
|
||||||
|
if (beforeMatch && afterMatch) {
|
||||||
|
setTarget(beforeRange);
|
||||||
|
setSearch(beforeMatch[1]);
|
||||||
|
setIndex(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTarget(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="py-6 overflow-y-auto px-7">
|
||||||
|
<Editable
|
||||||
|
autoFocus={false}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
spellCheck={false}
|
||||||
|
renderElement={(props) => <Element {...props} />}
|
||||||
|
placeholder="What are you up to?"
|
||||||
|
className="focus:outline-none"
|
||||||
|
/>
|
||||||
|
{target && filters.length > 0 && (
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="top-[-9999px] left-[-9999px] absolute z-10 w-[250px] p-1 bg-white border border-neutral-50 dark:border-neutral-900 dark:bg-neutral-950 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
{filters.map((contact, i) => (
|
||||||
|
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||||
|
<div
|
||||||
|
key={contact.npub}
|
||||||
|
onClick={() => {
|
||||||
|
Transforms.select(editor, target);
|
||||||
|
insertMention(editor, contact);
|
||||||
|
setTarget(null);
|
||||||
|
}}
|
||||||
|
className="px-2 py-2 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<User pubkey={contact.npub} variant="simple" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between h-16 px-3 border-t border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
|
||||||
|
<div />
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<EditorAddMedia />
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-6 mx-3 bg-neutral-200 dark:bg-neutral-800" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center w-20 pb-[2px] font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Post
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Slate>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
packages/ui/src/editor/utils.ts
Normal file
73
packages/ui/src/editor/utils.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { NDKCacheUserProfile } from "@lume/types";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { BaseEditor, Transforms } from "slate";
|
||||||
|
import { type ReactEditor } from "slate-react";
|
||||||
|
|
||||||
|
export const Portal = ({ children }: { children?: ReactNode }) => {
|
||||||
|
return typeof document === "object"
|
||||||
|
? ReactDOM.createPortal(children, document.body)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isImageUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
if (!url) return false;
|
||||||
|
const ext = new URL(url).pathname.split(".").pop();
|
||||||
|
return ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"].includes(ext);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertImage = (editor: ReactEditor | BaseEditor, url: string) => {
|
||||||
|
const text = { text: "" };
|
||||||
|
const image = [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
url,
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Transforms.insertNodes(editor, image);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertMention = (
|
||||||
|
editor: ReactEditor | BaseEditor,
|
||||||
|
contact: NDKCacheUserProfile,
|
||||||
|
) => {
|
||||||
|
const mention = {
|
||||||
|
type: "mention",
|
||||||
|
npub: `nostr:${contact.npub}`,
|
||||||
|
name: contact.name || contact.displayName || "anon",
|
||||||
|
children: [{ text: "" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
Transforms.insertNodes(editor, mention);
|
||||||
|
Transforms.move(editor);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertNostrEvent = (
|
||||||
|
editor: ReactEditor | BaseEditor,
|
||||||
|
eventId: string,
|
||||||
|
) => {
|
||||||
|
const text = { text: "" };
|
||||||
|
const event = [
|
||||||
|
{
|
||||||
|
type: "event",
|
||||||
|
eventId: `nostr:${eventId}`,
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Transforms.insertNodes(editor, event);
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type Platform } from "@tauri-apps/plugin-os";
|
import { type Platform } from "@tauri-apps/plugin-os";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { Editor } from "../editor/column";
|
||||||
import { Navigation } from "../navigation";
|
import { Navigation } from "../navigation";
|
||||||
import { WindowTitleBar } from "../titlebar";
|
import { WindowTitleBar } from "../titlebar";
|
||||||
|
|
||||||
@@ -17,9 +18,10 @@ export function AppLayout({ platform }: { platform: Platform }) {
|
|||||||
) : (
|
) : (
|
||||||
<div data-tauri-drag-region className="h-9 shrink-0" />
|
<div data-tauri-drag-region className="h-9 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex w-full h-full min-h-0">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div className="h-full flex-1 px-1 pb-1">
|
<Editor />
|
||||||
|
<div className="flex-1 h-full px-1 pb-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import {
|
|||||||
HomeIcon,
|
HomeIcon,
|
||||||
NwcFilledIcon,
|
NwcFilledIcon,
|
||||||
NwcIcon,
|
NwcIcon,
|
||||||
|
PlusIcon,
|
||||||
RelayFilledIcon,
|
RelayFilledIcon,
|
||||||
RelayIcon,
|
RelayIcon,
|
||||||
} from "@lume/icons";
|
} from "@lume/icons";
|
||||||
import { cn } from "@lume/utils";
|
import { cn, editorAtom } from "@lume/utils";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import { ActiveAccount } from "./account/active";
|
import { ActiveAccount } from "./account/active";
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
|
const setIsEditorOpen = useSetAtom(editorAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-between w-20 h-full px-4 py-3 shrink-0">
|
<div className="flex flex-col justify-between w-20 h-full px-4 py-3 shrink-0">
|
||||||
<div className="flex flex-col flex-1 gap-5">
|
<div className="flex flex-col flex-1 gap-5">
|
||||||
@@ -154,6 +158,13 @@ export function Navigation() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 p-1 shrink-0">
|
<div className="flex flex-col gap-3 p-1 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditorOpen((prev) => !prev)}
|
||||||
|
className="flex items-center justify-center w-full h-auto text-black aspect-square rounded-xl bg-black/10 hover:bg-blue-500 hover:text-white dark:bg-white/10 dark:text-white dark:hover:bg-blue-500"
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
<ActiveAccount />
|
<ActiveAccount />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export * from "./src/hooks/useNetworkStatus";
|
|||||||
export * from "./src/hooks/useOpenGraph";
|
export * from "./src/hooks/useOpenGraph";
|
||||||
export * from "./src/cn";
|
export * from "./src/cn";
|
||||||
export * from "./src/image";
|
export * from "./src/image";
|
||||||
|
export * from "./src/state";
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@tauri-apps/plugin-notification": "2.0.0-alpha.5",
|
"@tauri-apps/plugin-notification": "2.0.0-alpha.5",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"jotai": "^2.6.1",
|
||||||
"nostr-tools": "1.17.0",
|
"nostr-tools": "1.17.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tailwind-merge": "^1.14.0"
|
"tailwind-merge": "^1.14.0"
|
||||||
|
|||||||
9
packages/utils/src/state.ts
Normal file
9
packages/utils/src/state.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const editorAtom = atom(false);
|
||||||
|
export const editorValueAtom = atom([
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ text: "" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
159
pnpm-lock.yaml
generated
159
pnpm-lock.yaml
generated
@@ -315,6 +315,9 @@ importers:
|
|||||||
get-urls:
|
get-urls:
|
||||||
specifier: ^12.1.0
|
specifier: ^12.1.0
|
||||||
version: 12.1.0
|
version: 12.1.0
|
||||||
|
jotai:
|
||||||
|
specifier: ^2.6.1
|
||||||
|
version: 2.6.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
markdown-to-jsx:
|
markdown-to-jsx:
|
||||||
specifier: ^7.4.0
|
specifier: ^7.4.0
|
||||||
version: 7.4.0(react@18.2.0)
|
version: 7.4.0(react@18.2.0)
|
||||||
@@ -805,6 +808,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-sql':
|
'@tauri-apps/plugin-sql':
|
||||||
specifier: 2.0.0-alpha.5
|
specifier: 2.0.0-alpha.5
|
||||||
version: 2.0.0-alpha.5
|
version: 2.0.0-alpha.5
|
||||||
|
nostr-tools:
|
||||||
|
specifier: '1.17'
|
||||||
|
version: 1.17.0(typescript@5.3.3)
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
@@ -855,6 +861,9 @@ importers:
|
|||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@dnd-kit/core':
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@lume/ark':
|
'@lume/ark':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../ark
|
version: link:../ark
|
||||||
@@ -894,6 +903,12 @@ importers:
|
|||||||
'@tauri-apps/plugin-os':
|
'@tauri-apps/plugin-os':
|
||||||
specifier: 2.0.0-alpha.6
|
specifier: 2.0.0-alpha.6
|
||||||
version: 2.0.0-alpha.6
|
version: 2.0.0-alpha.6
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^10.17.0
|
||||||
|
version: 10.17.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
jotai:
|
||||||
|
specifier: ^2.6.1
|
||||||
|
version: 2.6.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
minidenticons:
|
minidenticons:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@@ -903,9 +918,18 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
react-dom:
|
||||||
|
specifier: ^18.2.0
|
||||||
|
version: 18.2.0(react@18.2.0)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.21.1
|
specifier: ^6.21.1
|
||||||
version: 6.21.1(react-dom@18.2.0)(react@18.2.0)
|
version: 6.21.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
slate:
|
||||||
|
specifier: ^0.101.5
|
||||||
|
version: 0.101.5
|
||||||
|
slate-react:
|
||||||
|
specifier: ^0.101.5
|
||||||
|
version: 0.101.5(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.1(react-dom@18.2.0)(react@18.2.0)
|
version: 1.3.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -949,6 +973,9 @@ importers:
|
|||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.10
|
specifier: ^1.11.10
|
||||||
version: 1.11.10
|
version: 1.11.10
|
||||||
|
jotai:
|
||||||
|
specifier: ^2.6.1
|
||||||
|
version: 2.6.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: 1.17.0
|
specifier: 1.17.0
|
||||||
version: 1.17.0(typescript@5.3.3)
|
version: 1.17.0(typescript@5.3.3)
|
||||||
@@ -1072,6 +1099,37 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@dnd-kit/accessibility@3.1.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
'@dnd-kit/accessibility': 3.1.0(react@18.2.0)
|
||||||
|
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@dnd-kit/utilities@3.2.2(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@emotion/is-prop-valid@0.8.8:
|
/@emotion/is-prop-valid@0.8.8:
|
||||||
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@@ -1377,6 +1435,10 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@juggle/resize-observer@3.4.0:
|
||||||
|
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@noble/ciphers@0.2.0:
|
/@noble/ciphers@0.2.0:
|
||||||
resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
|
resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -2398,8 +2460,8 @@ packages:
|
|||||||
/@scure/bip39@1.2.1:
|
/@scure/bip39@1.2.1:
|
||||||
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.3.1
|
'@noble/hashes': 1.3.3
|
||||||
'@scure/base': 1.1.1
|
'@scure/base': 1.1.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@swc/core-darwin-arm64@1.3.102:
|
/@swc/core-darwin-arm64@1.3.102:
|
||||||
@@ -2834,6 +2896,14 @@ packages:
|
|||||||
'@tiptap/pm': 2.1.13
|
'@tiptap/pm': 2.1.13
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/is-hotkey@0.1.10:
|
||||||
|
resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/lodash@4.14.202:
|
||||||
|
resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/node@20.10.6:
|
/@types/node@20.10.6:
|
||||||
resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==}
|
resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3267,6 +3337,10 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/compute-scroll-into-view@3.1.0:
|
||||||
|
resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/content-disposition@0.5.2:
|
/content-disposition@0.5.2:
|
||||||
resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
|
resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3436,6 +3510,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/direction@1.0.4:
|
||||||
|
resolution: {integrity: sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dlv@1.1.3:
|
/dlv@1.1.3:
|
||||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4009,6 +4088,10 @@ packages:
|
|||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/immer@10.0.3:
|
||||||
|
resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/inherits@2.0.3:
|
/inherits@2.0.3:
|
||||||
resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
|
resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4113,6 +4196,10 @@ packages:
|
|||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-hotkey@0.2.0:
|
||||||
|
resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-negative-zero@2.0.2:
|
/is-negative-zero@2.0.2:
|
||||||
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4142,6 +4229,11 @@ packages:
|
|||||||
isobject: 3.0.1
|
isobject: 3.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/is-plain-object@5.0.0:
|
||||||
|
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-regex@1.1.4:
|
/is-regex@1.1.4:
|
||||||
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4228,6 +4320,22 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jotai@2.6.1(@types/react@18.2.46)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-GLQtAnA9iEKRMXnyCjf1azIxfQi5JausX2EI5qSlb59j4i73ZEyV/EXPDEAQj4uQNZYEefi3degv/Pw3+L/Dtg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=17.0.0'
|
||||||
|
react: '>=17.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.46
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/js-tokens@4.0.0:
|
/js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -4346,6 +4454,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==}
|
resolution: {integrity: sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash@4.17.5:
|
/lodash@4.17.5:
|
||||||
resolution: {integrity: sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==}
|
resolution: {integrity: sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5258,6 +5370,12 @@ packages:
|
|||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/scroll-into-view-if-needed@3.1.0:
|
||||||
|
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
|
||||||
|
dependencies:
|
||||||
|
compute-scroll-into-view: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/semver@5.7.2:
|
/semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -5348,6 +5466,35 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/slate-react@0.101.5(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5):
|
||||||
|
resolution: {integrity: sha512-KfnC1Je7dIZo1Uv4g5d1+No8hKkgXKcSEGGOH7zzZEX9iYGckSg6aBgO0hFmoilidowSiSU45/baL5aeYma9Vg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18.2.0'
|
||||||
|
react-dom: '>=18.2.0'
|
||||||
|
slate: '>=0.99.0'
|
||||||
|
dependencies:
|
||||||
|
'@juggle/resize-observer': 3.4.0
|
||||||
|
'@types/is-hotkey': 0.1.10
|
||||||
|
'@types/lodash': 4.14.202
|
||||||
|
direction: 1.0.4
|
||||||
|
is-hotkey: 0.2.0
|
||||||
|
is-plain-object: 5.0.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
scroll-into-view-if-needed: 3.1.0
|
||||||
|
slate: 0.101.5
|
||||||
|
tiny-invariant: 1.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/slate@0.101.5:
|
||||||
|
resolution: {integrity: sha512-ZZt1ia8ayRqxtpILRMi2a4MfdvwdTu64CorxTVq9vNSd0GQ/t3YDkze6wKjdeUtENmBlq5wNIDInZbx38Hfu5Q==}
|
||||||
|
dependencies:
|
||||||
|
immer: 10.0.3
|
||||||
|
is-plain-object: 5.0.0
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/smol-toml@1.1.3:
|
/smol-toml@1.1.3:
|
||||||
resolution: {integrity: sha512-qTyy6Owjho1ISBmxj4HdrFWB2kMQ5RczU6J04OqslSfdSH656OIHuomHS4ZDvhwm37nig/uXyiTMJxlC9zIVfw==}
|
resolution: {integrity: sha512-qTyy6Owjho1ISBmxj4HdrFWB2kMQ5RczU6J04OqslSfdSH656OIHuomHS4ZDvhwm37nig/uXyiTMJxlC9zIVfw==}
|
||||||
engines: {node: '>= 18', pnpm: '>= 8'}
|
engines: {node: '>= 18', pnpm: '>= 8'}
|
||||||
@@ -5613,6 +5760,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg==}
|
resolution: {integrity: sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tiny-invariant@1.3.1:
|
||||||
|
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/tiny-warning@1.0.3:
|
||||||
|
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tippy.js@6.3.7:
|
/tippy.js@6.3.7:
|
||||||
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user