diff --git a/packages/ark/package.json b/packages/ark/package.json index 58e87b92..5de44972 100644 --- a/packages/ark/package.json +++ b/packages/ark/package.json @@ -33,6 +33,7 @@ "@tiptap/react": "^2.1.13", "@vidstack/react": "^1.9.8", "get-urls": "^12.1.0", + "jotai": "^2.6.1", "markdown-to-jsx": "^7.4.0", "minidenticons": "^4.2.0", "nanoid": "^5.0.4", diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index d3c3313a..73ac9a47 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -111,7 +111,7 @@ export class Ark { public async getUserProfile({ pubkey }: { pubkey: string }) { try { // 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 ( hexstring.startsWith("npub1") || diff --git a/packages/ark/src/components/note/buttons/reaction.tsx b/packages/ark/src/components/note/buttons/reaction.tsx index e0c95adc..d29f2b8c 100644 --- a/packages/ark/src/components/note/buttons/reaction.tsx +++ b/packages/ark/src/components/note/buttons/reaction.tsx @@ -66,7 +66,7 @@ export function NoteReaction() { className="size-6" /> ) : ( - + )} diff --git a/packages/ark/src/components/note/buttons/reply.tsx b/packages/ark/src/components/note/buttons/reply.tsx index d9766145..a4dc4ee1 100644 --- a/packages/ark/src/components/note/buttons/reply.tsx +++ b/packages/ark/src/components/note/buttons/reply.tsx @@ -1,15 +1,15 @@ import { ReplyIcon } from "@lume/icons"; +import { editorAtom, editorValueAtom } from "@lume/utils"; 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"; -export function NoteReply({ - rootEventId, -}: { - rootEventId?: string; -}) { +export function NoteReply() { const event = useNoteContext(); - const navigate = useNavigate(); + + const setEditorValue = useSetAtom(editorValueAtom); + const setIsEditorOpen = useSetAtom(editorAtom); return ( @@ -17,18 +17,24 @@ export function NoteReply({ diff --git a/packages/ark/src/components/note/buttons/repost.tsx b/packages/ark/src/components/note/buttons/repost.tsx index bb9e101c..cc659368 100644 --- a/packages/ark/src/components/note/buttons/repost.tsx +++ b/packages/ark/src/components/note/buttons/repost.tsx @@ -33,7 +33,7 @@ export function NoteRepost() { > diff --git a/packages/ark/src/components/note/buttons/zap.tsx b/packages/ark/src/components/note/buttons/zap.tsx index 8ab7f12d..e2fcad93 100644 --- a/packages/ark/src/components/note/buttons/zap.tsx +++ b/packages/ark/src/components/note/buttons/zap.tsx @@ -109,7 +109,7 @@ export function NoteZap() { type="button" className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400" > - + diff --git a/packages/ark/src/components/note/mentions/note.tsx b/packages/ark/src/components/note/mentions/note.tsx index 8a1c6706..2a270da2 100644 --- a/packages/ark/src/components/note/mentions/note.tsx +++ b/packages/ark/src/components/note/mentions/note.tsx @@ -6,7 +6,8 @@ import { useEvent } from "../../../hooks/useEvent"; export const MentionNote = memo(function MentionNote({ eventId, -}: { eventId: string }) { + openable = true, +}: { eventId: string; openable?: boolean }) { const { isLoading, isError, data } = useEvent(eventId); const renderKind = (event: NDKEvent) => { @@ -24,7 +25,10 @@ export const MentionNote = memo(function MentionNote({ if (isLoading) { return ( -
+
Loading
); @@ -32,7 +36,10 @@ export const MentionNote = memo(function MentionNote({ if (isError) { return ( -
+
Failed to fetch event
); @@ -46,12 +53,14 @@ export const MentionNote = memo(function MentionNote({
{renderKind(data)} - - Show more - + {openable ? ( + + Show more + + ) : null}
diff --git a/packages/ark/src/components/note/root.tsx b/packages/ark/src/components/note/root.tsx index e1e0e796..30f0cf77 100644 --- a/packages/ark/src/components/note/root.tsx +++ b/packages/ark/src/components/note/root.tsx @@ -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", className, )} + contentEditable={false} > {children}
diff --git a/packages/icons/index.ts b/packages/icons/index.ts index acb2c03d..e2a2008e 100644 --- a/packages/icons/index.ts +++ b/packages/icons/index.ts @@ -101,3 +101,4 @@ export * from "./src/moveRight"; export * from "./src/help"; export * from "./src/plusSquare"; export * from "./src/column"; +export * from "./src/addMedia"; diff --git a/packages/icons/src/addMedia.tsx b/packages/icons/src/addMedia.tsx new file mode 100644 index 00000000..29884065 --- /dev/null +++ b/packages/icons/src/addMedia.tsx @@ -0,0 +1,20 @@ +export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) { + return ( + + + + ); +} diff --git a/packages/storage/index.ts b/packages/storage/index.ts index 661f43cb..e64648a3 100644 --- a/packages/storage/index.ts +++ b/packages/storage/index.ts @@ -11,6 +11,7 @@ import { appConfigDir, resolveResource } from "@tauri-apps/api/path"; import { Platform } from "@tauri-apps/plugin-os"; import { Child, Command } from "@tauri-apps/plugin-shell"; import Database from "@tauri-apps/plugin-sql"; +import { nip19 } from "nostr-tools"; export class LumeStorage { #db: Database; @@ -226,9 +227,10 @@ export class LumeStorage { if (!results.length) return []; const users: NDKCacheUserProfile[] = results.map((item) => ({ - pubkey: item.pubkey, + npub: nip19.npubEncode(item.pubkey), ...JSON.parse(item.profile as string), })); + return users; } diff --git a/packages/storage/package.json b/packages/storage/package.json index 92b71422..eb4839cc 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -12,6 +12,7 @@ "@tauri-apps/plugin-os": "2.0.0-alpha.6", "@tauri-apps/plugin-shell": "2.0.0-alpha.5", "@tauri-apps/plugin-sql": "2.0.0-alpha.5", + "nostr-tools": "1.17", "react": "^18.2.0" }, "devDependencies": { diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index d061216b..89c28806 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -115,7 +115,7 @@ export interface NDKCacheUser { } export interface NDKCacheUserProfile extends NDKUserProfile { - pubkey: string; + npub: string; } export interface NDKCacheEvent { diff --git a/packages/ui/package.json b/packages/ui/package.json index a3675f76..10059b43 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,6 +4,7 @@ "private": true, "main": "./src/index.ts", "dependencies": { + "@dnd-kit/core": "^6.1.0", "@lume/ark": "workspace:^", "@lume/icons": "workspace:^", "@lume/utils": "workspace:^", @@ -17,10 +18,15 @@ "@tauri-apps/api": "2.0.0-alpha.13", "@tauri-apps/plugin-http": "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", "nostr-tools": "~1.17.0", "react": "^18.2.0", + "react-dom": "^18.2.0", "react-router-dom": "^6.21.1", + "slate": "^0.101.5", + "slate-react": "^0.101.5", "sonner": "^1.3.1" }, "devDependencies": { diff --git a/packages/ui/src/editor/addMedia.tsx b/packages/ui/src/editor/addMedia.tsx new file mode 100644 index 00000000..443e696c --- /dev/null +++ b/packages/ui/src/editor/addMedia.tsx @@ -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 ( + + ); +} diff --git a/packages/ui/src/editor/column.tsx b/packages/ui/src/editor/column.tsx new file mode 100644 index 00000000..56a27a57 --- /dev/null +++ b/packages/ui/src/editor/column.tsx @@ -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 ( + + {isEditorOpen ? ( + + + + ) : null} + + ); +} diff --git a/packages/ui/src/editor/form.tsx b/packages/ui/src/editor/form.tsx new file mode 100644 index 00000000..2fc9e2df --- /dev/null +++ b/packages/ui/src/editor/form.tsx @@ -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 ( +
+ {children} +
+ {element.url} + +
+
+ ); +}; + +const Mention = ({ attributes, element }) => { + const editor = useSlateStatic(); + const path = ReactEditor.findPath(editor as ReactEditor, element); + + return ( + Transforms.removeNodes(editor, { at: path })} + className="inline-block text-blue-500 align-baseline hover:text-blue-600" + >{`@${element.name}`} + ); +}; + +const Event = ({ attributes, element, children }) => { + const editor = useSlateStatic(); + const path = ReactEditor.findPath(editor as ReactEditor, element); + + return ( +
+ {children} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
Transforms.removeNodes(editor, { at: path })} + className="relative user-select-none" + > + +
+
+ ); +}; + +const Element = (props) => { + const { attributes, children, element } = props; + + switch (element.type) { + case "image": + return ; + case "mention": + return ; + case "event": + return ; + default: + return ( +

+ {children} +

+ ); + } +}; + +export function EditorForm() { + const storage = useStorage(); + const ref = useRef(); + const initialValue = useAtomValue(editorValueAtom); + + const [contacts, setContacts] = useState([]); + const [target, setTarget] = useState(); + 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 ( +
+ { + 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); + }} + > +
+ } + placeholder="What are you up to?" + className="focus:outline-none" + /> + {target && filters.length > 0 && ( + +
+ {filters.map((contact, i) => ( + // biome-ignore lint/a11y/useKeyWithClickEvents: +
{ + 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" + > + +
+ ))} +
+
+ )} +
+
+
+
+
+ +
+
+ +
+
+ +
+ ); +} diff --git a/packages/ui/src/editor/utils.ts b/packages/ui/src/editor/utils.ts new file mode 100644 index 00000000..17c2caa7 --- /dev/null +++ b/packages/ui/src/editor/utils.ts @@ -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); +}; diff --git a/packages/ui/src/layouts/app.tsx b/packages/ui/src/layouts/app.tsx index 7249c2d1..584101d1 100644 --- a/packages/ui/src/layouts/app.tsx +++ b/packages/ui/src/layouts/app.tsx @@ -1,6 +1,7 @@ import { type Platform } from "@tauri-apps/plugin-os"; import { Outlet } from "react-router-dom"; import { twMerge } from "tailwind-merge"; +import { Editor } from "../editor/column"; import { Navigation } from "../navigation"; import { WindowTitleBar } from "../titlebar"; @@ -17,9 +18,10 @@ export function AppLayout({ platform }: { platform: Platform }) { ) : (
)} -
+
-
+ +
diff --git a/packages/ui/src/navigation.tsx b/packages/ui/src/navigation.tsx index 4a88ebee..a72f027a 100644 --- a/packages/ui/src/navigation.tsx +++ b/packages/ui/src/navigation.tsx @@ -5,14 +5,18 @@ import { HomeIcon, NwcFilledIcon, NwcIcon, + PlusIcon, RelayFilledIcon, RelayIcon, } 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 { ActiveAccount } from "./account/active"; export function Navigation() { + const setIsEditorOpen = useSetAtom(editorAtom); + return (
@@ -154,6 +158,13 @@ export function Navigation() {
+
diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 7ebd27b9..d0bffed6 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -7,3 +7,4 @@ export * from "./src/hooks/useNetworkStatus"; export * from "./src/hooks/useOpenGraph"; export * from "./src/cn"; export * from "./src/image"; +export * from "./src/state"; diff --git a/packages/utils/package.json b/packages/utils/package.json index 1dea825e..73a042bb 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -13,6 +13,7 @@ "@tauri-apps/plugin-notification": "2.0.0-alpha.5", "clsx": "^2.1.0", "dayjs": "^1.11.10", + "jotai": "^2.6.1", "nostr-tools": "1.17.0", "react": "^18.2.0", "tailwind-merge": "^1.14.0" diff --git a/packages/utils/src/state.ts b/packages/utils/src/state.ts new file mode 100644 index 00000000..4a7b2ae3 --- /dev/null +++ b/packages/utils/src/state.ts @@ -0,0 +1,9 @@ +import { atom } from "jotai"; + +export const editorAtom = atom(false); +export const editorValueAtom = atom([ + { + type: "paragraph", + children: [{ text: "" }], + }, +]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cff12615..cee5c296 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -315,6 +315,9 @@ importers: get-urls: specifier: ^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: specifier: ^7.4.0 version: 7.4.0(react@18.2.0) @@ -805,6 +808,9 @@ importers: '@tauri-apps/plugin-sql': specifier: 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: specifier: ^18.2.0 version: 18.2.0 @@ -855,6 +861,9 @@ importers: packages/ui: dependencies: + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) '@lume/ark': specifier: workspace:^ version: link:../ark @@ -894,6 +903,12 @@ importers: '@tauri-apps/plugin-os': specifier: 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: specifier: ^4.2.0 version: 4.2.0 @@ -903,9 +918,18 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) react-router-dom: specifier: ^6.21.1 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: specifier: ^1.3.1 version: 1.3.1(react-dom@18.2.0)(react@18.2.0) @@ -949,6 +973,9 @@ importers: dayjs: specifier: ^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: specifier: 1.17.0 version: 1.17.0(typescript@5.3.3) @@ -1072,6 +1099,37 @@ packages: dev: 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: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} requiresBuild: true @@ -1377,6 +1435,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@juggle/resize-observer@3.4.0: + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + dev: false + /@noble/ciphers@0.2.0: resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==} dev: false @@ -2398,8 +2460,8 @@ packages: /@scure/bip39@1.2.1: resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} dependencies: - '@noble/hashes': 1.3.1 - '@scure/base': 1.1.1 + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.5 dev: false /@swc/core-darwin-arm64@1.3.102: @@ -2834,6 +2896,14 @@ packages: '@tiptap/pm': 2.1.13 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: resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==} dependencies: @@ -3267,6 +3337,10 @@ packages: - supports-color dev: true + /compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + dev: false + /content-disposition@0.5.2: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} @@ -3436,6 +3510,11 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true + /direction@1.0.4: + resolution: {integrity: sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==} + hasBin: true + dev: false + /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dev: true @@ -4009,6 +4088,10 @@ packages: safer-buffer: 2.1.2 dev: true + /immer@10.0.3: + resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==} + dev: false + /inherits@2.0.3: resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} dev: true @@ -4113,6 +4196,10 @@ packages: is-extglob: 2.1.1 dev: true + /is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + dev: false + /is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -4142,6 +4229,11 @@ packages: isobject: 3.0.1 dev: false + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -4228,6 +4320,22 @@ packages: hasBin: 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: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false @@ -4346,6 +4454,10 @@ packages: resolution: {integrity: sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /lodash@4.17.5: resolution: {integrity: sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==} dev: true @@ -5258,6 +5370,12 @@ packages: loose-envify: 1.4.0 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: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -5348,6 +5466,35 @@ packages: engines: {node: '>=14'} 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: resolution: {integrity: sha512-qTyy6Owjho1ISBmxj4HdrFWB2kMQ5RczU6J04OqslSfdSH656OIHuomHS4ZDvhwm37nig/uXyiTMJxlC9zIVfw==} engines: {node: '>= 18', pnpm: '>= 8'} @@ -5613,6 +5760,14 @@ packages: resolution: {integrity: sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg==} 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: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} dependencies: