From 078e189ab13893664fd53a5c776db83993e70809 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Tue, 14 Oct 2025 13:36:19 +0200 Subject: [PATCH 1/8] Add support for Yjs docs as values #998 --- Cargo.lock | 101 + browser/cli/package.json | 2 +- browser/cli/src/DatatypeToTSTypeMap.ts | 1 + browser/create-template/package.json | 2 +- browser/data-browser/package.json | 37 +- browser/data-browser/src/App.tsx | 4 +- .../data-browser/src/chunks/AI/RealAIChat.tsx | 4 +- .../AIChatInput/AsyncAIChatInput.tsx | 4 +- .../AIChatInput/MentionList.tsx | 0 .../AIChatInput/resourceSuggestions.ts | 47 +- .../AIChatInput/types.ts | 0 .../AsyncMarkdownEditor.tsx | 85 +- .../{MarkdownEditor => RTE}/BubbleMenu.tsx | 2 +- .../src/chunks/RTE/CollaborativeEditor.tsx | 125 ++ .../{MarkdownEditor => RTE}/EditLinkForm.tsx | 0 .../{MarkdownEditor => RTE}/EditorEvents.tsx | 4 +- .../EditorWrapperBase.tsx | 27 + .../{MarkdownEditor => RTE}/ImagePicker.tsx | 0 .../NodeSelectMenu.tsx | 0 .../SlashMenu/CommandList.tsx | 0 .../SlashMenu/CommandsExtension.ts | 33 +- .../{MarkdownEditor => RTE}/TiptapContext.tsx | 0 .../{MarkdownEditor => RTE}/ToggleButton.tsx | 0 .../src/chunks/RTE/floatingMenu.module.css | 6 + .../src/chunks/RTE/sharedEditorStyles.ts | 59 + .../src/chunks/RTE/useAwareness.ts | 47 + .../src/components/AllPropsSimple.tsx | 4 +- .../data-browser/src/components/CodeBlock.tsx | 16 +- .../data-browser/src/components/PropVal.tsx | 2 +- .../data-browser/src/components/ValueComp.tsx | 13 +- .../data-browser/src/components/YDocValue.tsx | 45 + .../src/components/forms/InputSwitcher.tsx | 5 + .../src/components/forms/InputYDoc.tsx | 9 + .../src/components/forms/MarkdownInput.tsx | 6 +- browser/data-browser/src/locales/de.po | 156 +- browser/data-browser/src/locales/en.po | 120 +- browser/data-browser/src/locales/es.po | 130 +- browser/data-browser/src/locales/fr.po | 128 +- .../src/routes/History/useVersions.ts | 1 + .../TablePage/helpers/useTableHistory.ts | 10 +- browser/lib/package.json | 14 +- browser/lib/src/base64.ts | 42 + browser/lib/src/commit.ts | 171 +- browser/lib/src/datatypes.ts | 69 +- browser/lib/src/index.ts | 1 + browser/lib/src/ontologies/commits.ts | 9 +- browser/lib/src/ontology.ts | 4 +- browser/lib/src/parse.ts | 35 +- browser/lib/src/resource.ts | 139 +- browser/lib/src/store.ts | 84 +- browser/lib/src/value.ts | 41 +- browser/lib/src/websockets.ts | 3 + browser/lib/src/yjs.ts | 43 + browser/package.json | 4 +- browser/pnpm-lock.yaml | 1687 +++++++++-------- browser/react/package.json | 3 +- browser/react/src/hooks.ts | 31 + browser/react/src/useMarkdown.ts | 4 +- cli/Cargo.toml | 1 + cli/src/new.rs | 10 + docs/src/commits/concepts.md | 3 +- docs/src/core/json-ad.md | 1 + docs/src/js-lib/agent.md | 39 +- docs/src/js-lib/resource.md | 40 + docs/src/schema/datatypes.md | 16 + lib/Cargo.toml | 1 + lib/defaults/default_store.json | 23 +- lib/src/commit.rs | 78 +- lib/src/datatype.rs | 4 + lib/src/parse.rs | 31 +- lib/src/serialize.rs | 10 + lib/src/urls.rs | 2 + lib/src/values.rs | 10 + server/src/actor_messages.rs | 15 + server/src/appstate.rs | 6 + server/src/bin.rs | 1 + server/src/handlers/web_sockets.rs | 75 +- server/src/lib.rs | 1 + server/src/y_awareness_broadcaster.rs | 129 ++ 79 files changed, 2905 insertions(+), 1210 deletions(-) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/AIChatInput/AsyncAIChatInput.tsx (98%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/AIChatInput/MentionList.tsx (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/AIChatInput/resourceSuggestions.ts (84%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/AIChatInput/types.ts (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/AsyncMarkdownEditor.tsx (64%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/BubbleMenu.tsx (98%) create mode 100644 browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/EditLinkForm.tsx (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/EditorEvents.tsx (84%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/EditorWrapperBase.tsx (62%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/ImagePicker.tsx (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/NodeSelectMenu.tsx (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/SlashMenu/CommandList.tsx (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/SlashMenu/CommandsExtension.ts (87%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/TiptapContext.tsx (100%) rename browser/data-browser/src/chunks/{MarkdownEditor => RTE}/ToggleButton.tsx (100%) create mode 100644 browser/data-browser/src/chunks/RTE/floatingMenu.module.css create mode 100644 browser/data-browser/src/chunks/RTE/sharedEditorStyles.ts create mode 100644 browser/data-browser/src/chunks/RTE/useAwareness.ts create mode 100644 browser/data-browser/src/components/YDocValue.tsx create mode 100644 browser/data-browser/src/components/forms/InputYDoc.tsx create mode 100644 browser/lib/src/base64.ts create mode 100644 browser/lib/src/yjs.ts create mode 100644 server/src/y_awareness_broadcaster.rs diff --git a/Cargo.lock b/Cargo.lock index bd2b93578..1b765b78d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,6 +539,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -578,6 +589,7 @@ version = "0.40.0" dependencies = [ "assert_cmd", "atomic_lib", + "base64 0.21.7", "clap", "colored", "dirs", @@ -679,6 +691,7 @@ dependencies = [ "ureq", "url", "urlencoding", + "yrs", ] [[package]] @@ -1092,6 +1105,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -1326,6 +1348,20 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1593,6 +1629,27 @@ dependencies = [ "str-buf", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "exr" version = "1.73.0" @@ -1619,6 +1676,9 @@ name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +dependencies = [ + "getrandom 0.2.16", +] [[package]] name = "fd-lock" @@ -1827,8 +1887,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1928,6 +1990,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -3262,6 +3330,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4ed3a7192fa19f5f48f99871f2755047fabefd7f222f12a1df1773796a102" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.11.2" @@ -4674,6 +4748,15 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "smallstr" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b" +dependencies = [ + "smallvec", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -6219,6 +6302,24 @@ dependencies = [ "synstructure", ] +[[package]] +name = "yrs" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f904a99678a852d7cbc6958c94087f739c10cfb19642635951219c525a5fdb89" +dependencies = [ + "arc-swap", + "async-lock", + "async-trait", + "dashmap", + "fastrand", + "serde", + "serde_json", + "smallstr", + "smallvec", + "thiserror 2.0.16", +] + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/browser/cli/package.json b/browser/cli/package.json index e1e33a1c8..d00efee7c 100644 --- a/browser/cli/package.json +++ b/browser/cli/package.json @@ -13,7 +13,7 @@ "@tomic/lib": "workspace:*", "chalk": "^5.3.0", "prettier": "3.0.3", - "typescript": "^5.6.3" + "typescript": "^5.9.3" }, "description": "Generate types from Atomic Data ontologies", "license": "MIT", diff --git a/browser/cli/src/DatatypeToTSTypeMap.ts b/browser/cli/src/DatatypeToTSTypeMap.ts index 83bfd65ae..ea69df25b 100644 --- a/browser/cli/src/DatatypeToTSTypeMap.ts +++ b/browser/cli/src/DatatypeToTSTypeMap.ts @@ -13,5 +13,6 @@ export const DatatypeToTSTypeMap = { [Datatype.MARKDOWN]: 'string', [Datatype.URI]: 'string', [Datatype.JSON]: 'JSONValue', + [Datatype.YDOC]: 'never', [Datatype.UNKNOWN]: 'JSONValue', }; diff --git a/browser/create-template/package.json b/browser/create-template/package.json index ee821b97b..6771f0ba2 100644 --- a/browser/create-template/package.json +++ b/browser/create-template/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/node": "^20.17.0", - "typescript": "^5.6.3" + "typescript": "^5.9.3" }, "description": "Generate templates using Atomic Data", "license": "MIT", diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index ba251d8d2..1fa0a9fc9 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -17,6 +17,7 @@ "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.3.1", + "@floating-ui/dom": "^1.7.4", "@modelcontextprotocol/sdk": "^1.13.3", "@oddbird/css-anchor-positioning": "^0.6.1", "@openrouter/ai-sdk-provider": "^1.2.0", @@ -24,19 +25,24 @@ "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-tabs": "^1.1.1", "@tanstack/react-router": "^1.95.1", - "@tiptap/extension-file-handler": "^2.25.0", - "@tiptap/extension-image": "^2.11.7", - "@tiptap/extension-link": "^2.11.7", - "@tiptap/extension-mention": "^2.11.7", - "@tiptap/extension-placeholder": "^2.11.7", - "@tiptap/extension-typography": "^2.11.7", - "@tiptap/pm": "^2.11.7", - "@tiptap/react": "^2.11.7", - "@tiptap/starter-kit": "^2.11.7", - "@tiptap/suggestion": "^2.11.7", + "@tiptap/extension-collaboration": "^3.6.5", + "@tiptap/extension-collaboration-caret": "^3.6.5", + "@tiptap/extension-file-handler": "^3.6.5", + "@tiptap/extension-image": "^3.6.5", + "@tiptap/extension-link": "^3.6.5", + "@tiptap/extension-mention": "^3.6.5", + "@tiptap/extension-placeholder": "^3.6.5", + "@tiptap/extension-typography": "^3.6.5", + "@tiptap/pm": "^3.6.5", + "@tiptap/react": "^3.6.5", + "@tiptap/starter-kit": "^3.6.5", + "@tiptap/suggestion": "^3.6.5", + "@tiptap/y-tiptap": "^3.0.0", "@tomic/react": "workspace:*", "@uiw/codemirror-theme-github": "^4.24.1", "@uiw/react-codemirror": "^4.24.1", + "@wuchale/jsx": "^0.7.4", + "@wuchale/vite-plugin": "^0.14.6", "ai": "^5.0.29", "clsx": "^2.1.1", "downshift": "^9.0.9", @@ -45,9 +51,9 @@ "polished": "^4.3.1", "prismjs": "^1.29.0", "quick-score": "^0.2.0", - "react": "^19.0.0", + "react": "^19.2.0", "react-colorful": "^5.6.1", - "react-dom": "^19.0.0", + "react-dom": "^19.2.0", "react-dropzone": "^11.7.1", "react-hot-toast": "^2.4.1", "react-hotkeys-hook": "^3.4.7", @@ -62,11 +68,10 @@ "remark-gfm": "^4.0.0", "styled-components": "^6.1.19", "stylis": "4.3.0", - "tippy.js": "^6.3.7", "tiptap-markdown": "^0.8.10", "wuchale": "^0.16.5", - "@wuchale/jsx": "^0.7.4", - "@wuchale/vite-plugin": "^0.14.6", + "y-protocols": "^1.0.6", + "yjs": "^13.6.27", "zod": "^4.1.5" }, "devDependencies": { @@ -82,7 +87,7 @@ "gh-pages": "^5.0.0", "lint-staged": "^10.5.4", "types-wm": "^1.1.0", - "typescript": "^5.6.3", + "typescript": "^5.9.3", "vite": "^5.4.10", "vite-plugin-prismjs": "^0.0.11", "vite-plugin-pwa": "^0.20.5", diff --git a/browser/data-browser/src/App.tsx b/browser/data-browser/src/App.tsx index 27843dfba..0913939e6 100644 --- a/browser/data-browser/src/App.tsx +++ b/browser/data-browser/src/App.tsx @@ -1,4 +1,4 @@ -import { StoreContext, Store } from '@tomic/react'; +import { StoreContext, Store, enableYjs } from '@tomic/react'; import { isDev } from './config'; import { registerHandlers } from './handlers'; @@ -33,6 +33,8 @@ const store = new Store({ serverUrl, }); +await enableYjs(); + store.parseMetaTags(); declare global { diff --git a/browser/data-browser/src/chunks/AI/RealAIChat.tsx b/browser/data-browser/src/chunks/AI/RealAIChat.tsx index c7aaffa40..10bdee9d0 100644 --- a/browser/data-browser/src/chunks/AI/RealAIChat.tsx +++ b/browser/data-browser/src/chunks/AI/RealAIChat.tsx @@ -35,7 +35,7 @@ import { MessageContextItem } from './MessageContextItem'; import { useProcessMessages } from './useProcessMessages'; import { NoKeyOverlay } from './NoKeyOverlay'; import { useOpenRouterModels } from './useOpenRouterModels'; -import type { MentionItem } from '@chunks/MarkdownEditor/AIChatInput/types'; +import type { MentionItem } from '@chunks/RTE/AIChatInput/types'; import { useChat } from '@ai-sdk/react'; import { useClientOnlyTransport } from './ClientOnlyTransport'; import { useGenerativeData } from './useGenerativeData'; @@ -43,7 +43,7 @@ import { FollowUpPrompt } from './FollowUpPrompt'; import { useAISettings } from '@components/AI/AISettingsContext'; const AIChatInput = React.lazy( - () => import('@chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput'), + () => import('@chunks/RTE/AIChatInput/AsyncAIChatInput'), ); interface RealAIChatProps { diff --git a/browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx b/browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx similarity index 98% rename from browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx rename to browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx index 351c9dae9..373d2b34f 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +++ b/browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx @@ -179,7 +179,9 @@ const AsyncAIChatInput: React.FC< [serversWithResources, searchResourcesOfServer, disabled], ); - const handleChange = (value: string) => { + const handleChange = () => { + // @ts-expect-error - markdown is a valid storage + const value = editor.storage.markdown.getMarkdown(); setMarkdown(value); markdownRef.current = value; onChange(value); diff --git a/browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/MentionList.tsx b/browser/data-browser/src/chunks/RTE/AIChatInput/MentionList.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/MentionList.tsx rename to browser/data-browser/src/chunks/RTE/AIChatInput/MentionList.tsx diff --git a/browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts b/browser/data-browser/src/chunks/RTE/AIChatInput/resourceSuggestions.ts similarity index 84% rename from browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts rename to browser/data-browser/src/chunks/RTE/AIChatInput/resourceSuggestions.ts index 972ece2aa..b31abe56d 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts +++ b/browser/data-browser/src/chunks/RTE/AIChatInput/resourceSuggestions.ts @@ -1,5 +1,4 @@ import { ReactRenderer } from '@tiptap/react'; -import tippy, { type Instance } from 'tippy.js'; import { MentionList, type MentionListProps, @@ -10,6 +9,8 @@ import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'; import type { SearchResourcesOfServer } from '@components/AI/MCP/useMcpServers'; import type { MCPServer } from '@chunks/AI/types'; import type { CategorySuggestion, SearchSuggestion } from './types'; +import styles from '../floatingMenu.module.css'; +import { computePosition, flip, inline, offset, shift } from '@floating-ui/dom'; enum SuggestionState { PickingCategory, @@ -95,7 +96,25 @@ export function searchSuggestionBuilder( items, render() { let component: ReactRenderer; - let popup: Instance[]; + + const setPosition = ( + props: SuggestionProps, + ) => { + if (!props.decorationNode) { + console.error('No decoration node'); + + return; + } + + computePosition(props.decorationNode, component.element, { + placement: 'top', + middleware: [flip(), shift(), inline(), offset(10)], + }).then(({ x, y }) => { + component.element.style.setProperty('--left', `${x}px`); + component.element.style.setProperty('--top', `${y}px`); + document.body.appendChild(component.element); + }); + }; const update = ( newP: SuggestionProps, @@ -106,12 +125,9 @@ export function searchSuggestionBuilder( return; } - popup[0].setProps({ - getReferenceClientRect: newP.clientRect as () => DOMRect, - }); + setPosition(newP); }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const editPropsForMenus = ( props: SuggestionProps, ): SuggestionProps => { @@ -154,21 +170,10 @@ export function searchSuggestionBuilder( component = new ReactRenderer(MentionList, { props: newProps, editor: props.editor, + className: styles.renderer, }); - if (!props.clientRect) { - return; - } - - popup = tippy('body', { - getReferenceClientRect: props.clientRect as () => DOMRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'top-start', - }); + setPosition(props); }, onUpdate(oldProps) { @@ -178,7 +183,7 @@ export function searchSuggestionBuilder( onKeyDown(props) { if (props.event.key === 'Escape') { - popup[0].hide(); + component.destroy(); return true; } @@ -193,7 +198,7 @@ export function searchSuggestionBuilder( onExit() { state = SuggestionState.PickingCategory; - popup[0].destroy(); + // cleanup(); component.destroy(); }, }; diff --git a/browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/types.ts b/browser/data-browser/src/chunks/RTE/AIChatInput/types.ts similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/types.ts rename to browser/data-browser/src/chunks/RTE/AIChatInput/types.ts diff --git a/browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx b/browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx similarity index 64% rename from browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx rename to browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx index 05b734286..23fe37dd0 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +++ b/browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx @@ -1,21 +1,24 @@ -import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react'; +import { EditorContent, useEditor } from '@tiptap/react'; +import { FloatingMenu } from '@tiptap/react/menus'; import { StarterKit } from '@tiptap/starter-kit'; import { Link } from '@tiptap/extension-link'; import { Placeholder } from '@tiptap/extension-placeholder'; import { Typography } from '@tiptap/extension-typography'; -import { styled } from 'styled-components'; import { Markdown } from 'tiptap-markdown'; import { EditorEvents } from './EditorEvents'; import { FaCode } from 'react-icons/fa6'; import { useCallback, useState } from 'react'; import { BubbleMenu } from './BubbleMenu'; import { TiptapContextProvider } from './TiptapContext'; -import { ToggleButton } from './ToggleButton'; import { SlashCommands, buildSuggestion } from './SlashMenu/CommandsExtension'; import { ExtendedImage } from './ImagePicker'; -import { transition } from '../../helpers/transition'; import { usePopoverContainer } from '../../components/Popover'; -import { EditorWrapperBase } from './EditorWrapperBase'; +import { + StyledEditorWrapper, + RawEditor, + FloatingMenuText, + FloatingCodeButton, +} from './sharedEditorStyles'; export type AsyncMarkdownEditorProps = { placeholder?: string; @@ -27,10 +30,6 @@ export type AsyncMarkdownEditorProps = { onBlur?: () => void; }; -const MIN_EDITOR_HEIGHT = '10rem'; -// The lineheight of a textarea. -const LINE_HEIGHT = 1.15; - export default function AsyncMarkdownEditor({ placeholder, initialContent, @@ -94,10 +93,18 @@ export default function AsyncMarkdownEditor({ }, }); - const handleChange = useCallback( - (value: string) => { - setMarkdown(value); - onChange?.(value); + const handleChange = useCallback(() => { + // @ts-expect-error - markdown is a valid storage + const value = editor.storage.markdown.getMarkdown(); + + setMarkdown(value); + onChange?.(value); + }, [onChange]); + + const handleRawChange = useCallback( + (val: string) => { + setMarkdown(val); + onChange?.(val); }, [onChange], ); @@ -116,7 +123,7 @@ export default function AsyncMarkdownEditor({ {codeMode && ( handleChange(e.target.value)} + onChange={e => handleRawChange(e.target.value)} value={markdown} /> )} @@ -139,53 +146,3 @@ export default function AsyncMarkdownEditor({ ); } - -// Textareas do not automatically grow when the content exceeds the height of the textarea. -// This function calculates the height of the textarea based on the number of lines in the content. -const calcHeight = (value: string) => { - const lines = value.split('\n').length; - - return `calc(${lines * LINE_HEIGHT}em + 5px)`; -}; - -const StyledEditorWrapper = styled(EditorWrapperBase)` - min-height: ${MIN_EDITOR_HEIGHT}; - border-radius: ${p => p.theme.radius}; - box-shadow: 0 0 0 1px ${p => p.theme.colors.bg2}; - min-height: ${MIN_EDITOR_HEIGHT}; - padding: ${p => p.theme.size()}; - ${transition('box-shadow')} - - &:focus-within { - box-shadow: 0 0 0 2px ${p => p.theme.colors.main}; - } - - & .tiptap { - width: min(100%, 75ch); - min-height: ${MIN_EDITOR_HEIGHT}; - } -`; - -const RawEditor = styled.textarea.attrs(p => ({ - style: { height: calcHeight((p.value as string) ?? '') }, -}))` - border: none; - width: 100%; - min-height: ${MIN_EDITOR_HEIGHT}; - outline: none; - overflow: visible; - height: fit-content; - background-color: transparent; - color: ${p => p.theme.colors.text}; - resize: none; -`; - -const FloatingMenuText = styled.span` - color: ${p => p.theme.colors.textLight}; -`; - -const FloatingCodeButton = styled(ToggleButton)` - position: absolute; - top: 0.5rem; - right: 0.5rem; -`; diff --git a/browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx b/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx similarity index 98% rename from browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx rename to browser/data-browser/src/chunks/RTE/BubbleMenu.tsx index ac3a8c076..0ff81192b 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx @@ -1,4 +1,4 @@ -import { BubbleMenu as TipTapBubbleMenu } from '@tiptap/react'; +import { BubbleMenu as TipTapBubbleMenu } from '@tiptap/react/menus'; import { FaBold, FaCode, diff --git a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx new file mode 100644 index 000000000..06ccba788 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx @@ -0,0 +1,125 @@ +import { EditorContent, useEditor } from '@tiptap/react'; +import { FloatingMenu } from '@tiptap/react/menus'; +import { StarterKit } from '@tiptap/starter-kit'; +import { Link } from '@tiptap/extension-link'; +import { Placeholder } from '@tiptap/extension-placeholder'; +import { Typography } from '@tiptap/extension-typography'; +import Collaboration from '@tiptap/extension-collaboration'; +import CollaborationCaret from '@tiptap/extension-collaboration-caret'; +import { useState } from 'react'; +import { BubbleMenu } from './BubbleMenu'; +import { TiptapContextProvider } from './TiptapContext'; +import { SlashCommands, buildSuggestion } from './SlashMenu/CommandsExtension'; +import { ExtendedImage } from './ImagePicker'; +import { usePopoverContainer } from '../../components/Popover'; +import { StyledEditorWrapper, FloatingMenuText } from './sharedEditorStyles'; +import * as Y from 'yjs'; +import { useDebouncedSave, type Resource } from '@tomic/react'; +import { EditorEvents } from './EditorEvents'; +import { useAwareness } from './useAwareness'; +import { randomItem } from '@helpers/randomItem'; + +export type CollaborativeEditorProps = { + placeholder?: string; + doc: Y.Doc; + autoFocus?: boolean; + // onChange?: (content: string) => void; + resource: Resource; + + id?: string; + labelId?: string; + onBlur?: () => void; +}; + +const COLORS = ['#70d6ff', '#ff70a6', '#ff9770', '#ffd670', '#e9ff70']; + +export default function CollaborativeEditor({ + placeholder, + autoFocus, + doc, + id, + labelId, + resource, + onBlur, +}: CollaborativeEditorProps): React.JSX.Element { + const [save] = useDebouncedSave(resource, 500); + const containerRef = usePopoverContainer(); + + const container = containerRef.current ?? document.body; + + const awareness = useAwareness(resource, doc); + + const [extensions] = useState(() => [ + StarterKit.configure({ + undoRedo: false, + }), + Typography, + Link.configure({ + protocols: [ + 'http', + 'https', + 'mailto', + { + scheme: 'tel', + optionalSlashes: true, + }, + ], + HTMLAttributes: { + class: 'tiptap-link', + rel: 'noopener noreferrer', + target: '_blank', + }, + }), + ExtendedImage.configure({ + HTMLAttributes: { + class: 'tiptap-image', + }, + }), + Placeholder.configure({ + placeholder: placeholder ?? 'Start typing...', + }), + SlashCommands.configure({ + suggestion: buildSuggestion(container), + }), + Collaboration.configure({ + document: doc, + field: 'content', + }), + CollaborationCaret.configure({ + provider: { + awareness, + }, + user: { + name: 'Pieter Post', + color: randomItem(COLORS), + }, + }), + ]); + + const editor = useEditor({ + extensions, + // content: markdown, + onBlur, + autofocus: !!autoFocus, + editorProps: { + attributes: { + ...(id && { id }), + ...(labelId && { 'aria-labelledby': labelId }), + }, + }, + }); + + return ( + + + + + Type '/' for options + + + + + + + ); +} diff --git a/browser/data-browser/src/chunks/MarkdownEditor/EditLinkForm.tsx b/browser/data-browser/src/chunks/RTE/EditLinkForm.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/EditLinkForm.tsx rename to browser/data-browser/src/chunks/RTE/EditLinkForm.tsx diff --git a/browser/data-browser/src/chunks/MarkdownEditor/EditorEvents.tsx b/browser/data-browser/src/chunks/RTE/EditorEvents.tsx similarity index 84% rename from browser/data-browser/src/chunks/MarkdownEditor/EditorEvents.tsx rename to browser/data-browser/src/chunks/RTE/EditorEvents.tsx index 747c9bc22..bdeaf83bd 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/EditorEvents.tsx +++ b/browser/data-browser/src/chunks/RTE/EditorEvents.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useTipTapEditor } from './TiptapContext'; interface EditorEventsProps { - onChange?: (content: string) => void; + onChange?: () => void; } export function EditorEvents({ onChange }: EditorEventsProps): null { @@ -12,7 +12,7 @@ export function EditorEvents({ onChange }: EditorEventsProps): null { if (!editor) return; const callback = () => { - onChange?.(editor.storage.markdown.getMarkdown()); + onChange?.(); }; if (editor) { diff --git a/browser/data-browser/src/chunks/MarkdownEditor/EditorWrapperBase.tsx b/browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx similarity index 62% rename from browser/data-browser/src/chunks/MarkdownEditor/EditorWrapperBase.tsx rename to browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx index cee32aacb..d4bf417b3 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/EditorWrapperBase.tsx +++ b/browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx @@ -24,6 +24,33 @@ export const EditorWrapperBase = styled.div<{ hideEditor: boolean }>` height: auto; } + /* Give a remote user a caret */ + .collaboration-carets__caret { + border-left: 1px solid #0d0d0d; + border-right: 1px solid #0d0d0d; + margin-left: -1px; + margin-right: -1px; + pointer-events: none; + position: relative; + word-break: normal; + } + + /* Render the username above the caret */ + .collaboration-carets__label { + border-radius: 3px 3px 3px 0; + color: #0d0d0d; + font-size: 12px; + font-style: normal; + font-weight: 600; + left: -1px; + line-height: normal; + padding: 0.1rem 0.3rem; + position: absolute; + top: -1.4em; + user-select: none; + white-space: nowrap; + } + pre { padding: 0.75rem 1rem; background-color: ${p => p.theme.colors.bg1}; diff --git a/browser/data-browser/src/chunks/MarkdownEditor/ImagePicker.tsx b/browser/data-browser/src/chunks/RTE/ImagePicker.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/ImagePicker.tsx rename to browser/data-browser/src/chunks/RTE/ImagePicker.tsx diff --git a/browser/data-browser/src/chunks/MarkdownEditor/NodeSelectMenu.tsx b/browser/data-browser/src/chunks/RTE/NodeSelectMenu.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/NodeSelectMenu.tsx rename to browser/data-browser/src/chunks/RTE/NodeSelectMenu.tsx diff --git a/browser/data-browser/src/chunks/MarkdownEditor/SlashMenu/CommandList.tsx b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/SlashMenu/CommandList.tsx rename to browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx diff --git a/browser/data-browser/src/chunks/MarkdownEditor/SlashMenu/CommandsExtension.ts b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts similarity index 87% rename from browser/data-browser/src/chunks/MarkdownEditor/SlashMenu/CommandsExtension.ts rename to browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts index 28071de50..2505f7aa8 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/SlashMenu/CommandsExtension.ts +++ b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts @@ -1,6 +1,8 @@ import { Extension, ReactRenderer } from '@tiptap/react'; import { Suggestion, type SuggestionOptions } from '@tiptap/suggestion'; -import tippy, { type Instance } from 'tippy.js'; +import { computePosition } from '@floating-ui/dom'; +import styles from '../floatingMenu.module.css'; + import { CommandList, type CommandItem, @@ -144,45 +146,35 @@ export const buildSuggestion = ( render: () => { let component: ReactRenderer; - let popup: Instance[]; return { onStart: props => { component = new ReactRenderer(CommandList, { props, editor: props.editor, + className: styles.renderer, }); - if (!props.clientRect) { + if (!props.decorationNode) { return; } - popup = tippy('body', { - getReferenceClientRect: props.clientRect as () => DOMRect, - appendTo: () => container, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', + computePosition(props.decorationNode, component.element, { + placement: 'bottom', + }).then(({ x, y }) => { + component.element.style.setProperty('--left', `${x}px`); + component.element.style.setProperty('--top', `${y}px`); + container.appendChild(component.element); }); }, onUpdate(props) { component.updateProps(props); - - if (!props.clientRect) { - return; - } - - popup[0].setProps({ - getReferenceClientRect: props.clientRect as () => DOMRect, - }); }, onKeyDown(props) { if (props.event.key === 'Escape') { - popup[0].hide(); + component.destroy(); return true; } @@ -195,7 +187,6 @@ export const buildSuggestion = ( }, onExit() { - popup[0].destroy(); component.destroy(); }, }; diff --git a/browser/data-browser/src/chunks/MarkdownEditor/TiptapContext.tsx b/browser/data-browser/src/chunks/RTE/TiptapContext.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/TiptapContext.tsx rename to browser/data-browser/src/chunks/RTE/TiptapContext.tsx diff --git a/browser/data-browser/src/chunks/MarkdownEditor/ToggleButton.tsx b/browser/data-browser/src/chunks/RTE/ToggleButton.tsx similarity index 100% rename from browser/data-browser/src/chunks/MarkdownEditor/ToggleButton.tsx rename to browser/data-browser/src/chunks/RTE/ToggleButton.tsx diff --git a/browser/data-browser/src/chunks/RTE/floatingMenu.module.css b/browser/data-browser/src/chunks/RTE/floatingMenu.module.css new file mode 100644 index 000000000..924389b47 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/floatingMenu.module.css @@ -0,0 +1,6 @@ +.renderer { + position: absolute; + top: var(--top, 0); + left: var(--left, 0); + width: max-content; +} diff --git a/browser/data-browser/src/chunks/RTE/sharedEditorStyles.ts b/browser/data-browser/src/chunks/RTE/sharedEditorStyles.ts new file mode 100644 index 000000000..6d38bcd62 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/sharedEditorStyles.ts @@ -0,0 +1,59 @@ +// Textareas do not automatically grow when the content exceeds the height of the textarea. + +import { styled } from 'styled-components'; +import { EditorWrapperBase } from './EditorWrapperBase'; +import { ToggleButton } from './ToggleButton'; +import { transition } from '../../helpers/transition'; + +const MIN_EDITOR_HEIGHT = '10rem'; +// The lineheight of a textarea. +const LINE_HEIGHT = 1.15; + +// This function calculates the height of the textarea based on the number of lines in the content. +const calcHeight = (value: string) => { + const lines = value.split('\n').length; + + return `calc(${lines * LINE_HEIGHT}em + 5px)`; +}; + +export const StyledEditorWrapper = styled(EditorWrapperBase)` + min-height: ${MIN_EDITOR_HEIGHT}; + border-radius: ${p => p.theme.radius}; + box-shadow: 0 0 0 1px ${p => p.theme.colors.bg2}; + min-height: ${MIN_EDITOR_HEIGHT}; + padding: ${p => p.theme.size()}; + ${transition('box-shadow')} + + &:focus-within { + box-shadow: 0 0 0 2px ${p => p.theme.colors.main}; + } + + & .tiptap { + width: min(100%, 75ch); + min-height: ${MIN_EDITOR_HEIGHT}; + } +`; + +export const RawEditor = styled.textarea.attrs(p => ({ + style: { height: calcHeight((p.value as string) ?? '') }, +}))` + border: none; + width: 100%; + min-height: ${MIN_EDITOR_HEIGHT}; + outline: none; + overflow: visible; + height: fit-content; + background-color: transparent; + color: ${p => p.theme.colors.text}; + resize: none; +`; + +export const FloatingMenuText = styled.span` + color: ${p => p.theme.colors.textLight}; +`; + +export const FloatingCodeButton = styled(ToggleButton)` + position: absolute; + top: 0.5rem; + right: 0.5rem; +`; diff --git a/browser/data-browser/src/chunks/RTE/useAwareness.ts b/browser/data-browser/src/chunks/RTE/useAwareness.ts new file mode 100644 index 000000000..4e0e05fca --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/useAwareness.ts @@ -0,0 +1,47 @@ +import { useStore, type Resource } from '@tomic/react'; +import { useEffect } from 'react'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import type * as Y from 'yjs'; + +type AwarenessUpdate = { + added: number[]; + removed: number[]; + updated: number[]; +}; + +export function useAwareness( + resource: Resource, + doc: Y.Doc, +): awarenessProtocol.Awareness { + const store = useStore(); + const awareness = new awarenessProtocol.Awareness(doc); + + useEffect(() => { + // store.subscribeAwareness(resource.subject); + + awareness.on( + 'update', + ({ added, updated, removed }: AwarenessUpdate, origin: string) => { + if (origin !== 'local') { + // Only send local updates to the server. + return; + } + + const changedClients = [...updated, ...added, ...removed]; + + const encodedUpdate = awarenessProtocol.encodeAwarenessUpdate( + awareness, + changedClients, + ); + + store.notifyAwarenessUpdate(resource.subject, encodedUpdate); + }, + ); + + return store.subscribeAwareness(resource.subject, update => { + awarenessProtocol.applyAwarenessUpdate(awareness, update, 'server'); + }); + }, [awareness, resource.subject]); + + return awareness; +} diff --git a/browser/data-browser/src/components/AllPropsSimple.tsx b/browser/data-browser/src/components/AllPropsSimple.tsx index 1b8b8b514..be6d4afd9 100644 --- a/browser/data-browser/src/components/AllPropsSimple.tsx +++ b/browser/data-browser/src/components/AllPropsSimple.tsx @@ -1,6 +1,6 @@ import { datatypes, - JSONValue, + AtomicValue, properties, Resource, useResource, @@ -28,7 +28,7 @@ export function AllPropsSimple({ resource }: AllPropsSimpleProps): JSX.Element { interface RowProps { prop: string; - val: JSONValue; + val: AtomicValue; } function Row({ prop, val }: RowProps): JSX.Element { diff --git a/browser/data-browser/src/components/CodeBlock.tsx b/browser/data-browser/src/components/CodeBlock.tsx index aafe0fa56..3137acb0d 100644 --- a/browser/data-browser/src/components/CodeBlock.tsx +++ b/browser/data-browser/src/components/CodeBlock.tsx @@ -7,9 +7,14 @@ import { Button } from './Button'; interface CodeBlockProps { content?: string; loading?: boolean; + wordWrap?: boolean; } -export function CodeBlock({ content, loading }: CodeBlockProps) { +export function CodeBlock({ + content, + loading, + wordWrap = false, +}: CodeBlockProps) { const [isCopied, setIsCopied] = useState(undefined); function copyToClipboard() { @@ -19,7 +24,10 @@ export function CodeBlock({ content, loading }: CodeBlockProps) { } return ( - + {loading ? ( 'loading...' ) : ( @@ -55,4 +63,8 @@ export const CodeBlockStyled = styled.pre` font-family: monospace; width: 100%; overflow-x: auto; + + &.word-wrap { + white-space: pre-wrap; + } `; diff --git a/browser/data-browser/src/components/PropVal.tsx b/browser/data-browser/src/components/PropVal.tsx index 970a23f2c..8fbcce5f4 100644 --- a/browser/data-browser/src/components/PropVal.tsx +++ b/browser/data-browser/src/components/PropVal.tsx @@ -34,7 +34,7 @@ function PropVal({ const property = useProperty(propertyURL); const truncated = truncateUrl(propertyURL, 10, true); - if (property.loading) { + if (property.loading || resource.loading) { return ( diff --git a/browser/data-browser/src/components/ValueComp.tsx b/browser/data-browser/src/components/ValueComp.tsx index f07c999ef..95163746e 100644 --- a/browser/data-browser/src/components/ValueComp.tsx +++ b/browser/data-browser/src/components/ValueComp.tsx @@ -1,11 +1,14 @@ +import type { JSX } from 'react'; import { Datatype, valToDate, valToString, valToArray, valToResource, - JSONValue, + type AtomicValue, + type JSONValue, } from '@tomic/react'; +import * as Y from 'yjs'; import { ResourceInline } from '../views/ResourceInline'; import { DateTime } from './datatypes/DateTime'; import Markdown from './datatypes/Markdown'; @@ -13,12 +16,12 @@ import Nestedresource from './datatypes/NestedResource'; import ResourceArray from './datatypes/ResourceArray'; import { ErrMessage } from './forms/InputStyles'; -import type { JSX } from 'react'; import { JSONRenderer } from './datatypes/JSON'; import { AtomicLink } from './AtomicLink'; +import { YDocValue } from './YDocValue'; type Props = { - value: JSONValue; + value: AtomicValue; datatype: Datatype; }; @@ -43,7 +46,9 @@ function ValueComp({ value, datatype }: Props): JSX.Element { case Datatype.RESOURCEARRAY: return ; case Datatype.JSON: - return ; + return ; + case Datatype.YDOC: + return ; case Datatype.URI: return ( {value as string} diff --git a/browser/data-browser/src/components/YDocValue.tsx b/browser/data-browser/src/components/YDocValue.tsx new file mode 100644 index 000000000..5826ea974 --- /dev/null +++ b/browser/data-browser/src/components/YDocValue.tsx @@ -0,0 +1,45 @@ +import { styled } from 'styled-components'; +import * as Y from 'yjs'; +import { FaEye } from 'react-icons/fa6'; +import { Button } from './Button'; +import { useState } from 'react'; +import { CodeBlock } from './CodeBlock'; +import { Column } from './Row'; + +interface YDocValueProps { + value: Y.Doc | undefined; +} + +export const YDocValue: React.FC = ({ value }) => { + const [showState, setShowState] = useState(false); + + if (!value) { + return Empty; + } + + return ( + + setShowState(!showState)}> + + {showState ? 'Hide encoded state' : 'Show encoded state'} + + {showState && ( + + )} + + ); +}; + +const SubtleButton = styled(Button)` + color: ${p => p.theme.colors.textLight}; + display: flex; + align-items: center; + gap: 0.5rem; + &:hover, + &:focus-visible { + color: ${p => p.theme.colors.main}; + } +`; diff --git a/browser/data-browser/src/components/forms/InputSwitcher.tsx b/browser/data-browser/src/components/forms/InputSwitcher.tsx index 3a6ef363f..19ec87eca 100644 --- a/browser/data-browser/src/components/forms/InputSwitcher.tsx +++ b/browser/data-browser/src/components/forms/InputSwitcher.tsx @@ -15,6 +15,7 @@ import { FilePicker } from './FilePicker/FilePicker'; import type { JSX } from 'react'; import { InputJSON } from './InputJSON'; import InputURI from './InputURI'; +import { InputYDoc } from './InputYDoc'; /** Renders a fitting HTML input depending on the Datatype */ export default function InputSwitcher(props: InputProps): JSX.Element { @@ -71,6 +72,10 @@ export default function InputSwitcher(props: InputProps): JSX.Element { return ; } + case Datatype.YDOC: { + return ; + } + default: { return ; } diff --git a/browser/data-browser/src/components/forms/InputYDoc.tsx b/browser/data-browser/src/components/forms/InputYDoc.tsx new file mode 100644 index 000000000..35a929e97 --- /dev/null +++ b/browser/data-browser/src/components/forms/InputYDoc.tsx @@ -0,0 +1,9 @@ +import { styled } from 'styled-components'; + +export const InputYDoc = () => { + return Editing YDoc directly is not supported; +}; + +const Subtle = styled.div` + color: ${p => p.theme.colors.textLight}; +`; diff --git a/browser/data-browser/src/components/forms/MarkdownInput.tsx b/browser/data-browser/src/components/forms/MarkdownInput.tsx index 27200e23d..c49b83de3 100644 --- a/browser/data-browser/src/components/forms/MarkdownInput.tsx +++ b/browser/data-browser/src/components/forms/MarkdownInput.tsx @@ -1,10 +1,8 @@ import { lazy, Suspense } from 'react'; -import type { AsyncMarkdownEditorProps } from '../../chunks/MarkdownEditor/AsyncMarkdownEditor'; +import type { AsyncMarkdownEditorProps } from '@chunks/RTE/AsyncMarkdownEditor'; import { styled } from 'styled-components'; -const MarkdownEditor = lazy( - () => import('../../chunks/MarkdownEditor/AsyncMarkdownEditor'), -); +const MarkdownEditor = lazy(() => import('@chunks/RTE/AsyncMarkdownEditor')); export function MarkdownInput( props: AsyncMarkdownEditorProps, diff --git a/browser/data-browser/src/locales/de.po b/browser/data-browser/src/locales/de.po index 4030b92ff..5c6756687 100644 --- a/browser/data-browser/src/locales/de.po +++ b/browser/data-browser/src/locales/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-29T10:47:38.272Z\n" -"PO-Revision-Date: 2025-09-29T11:08:08.915Z\n" +"PO-Revision-Date: 2025-10-14T09:30:37.541Z\n" "Last-Translator: \n" "Language: de\n" "Language-Team: \n" @@ -27,28 +27,28 @@ msgstr "Keine Klassen" #: src/components/ComboBox.tsx #: src/views/Element.tsx -#: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/AtomicURLCell.tsx msgid "No results" msgstr "Keine Ergebnisse" #: src/components/ConfirmationDialog.tsx #: src/chunks/AI/AgentConfig.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx +#: src/routes/History/HistoryMobileView.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/History/HistoryMobileView.tsx -#: src/views/OntologyPage/NewClassButton.tsx #: src/views/OntologyPage/NewPropertyButton.tsx +#: src/views/OntologyPage/NewClassButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Abbrechen" @@ -82,14 +82,14 @@ msgid "Copy to clipboard" msgstr "In die Zwischenablage kopieren" #: src/components/HighlightedCodeBlock.tsx -#: src/views/ResourceLine.tsx -#: src/views/ResourcePage.tsx #: src/chunks/AI/AIChatPage.tsx #: src/components/Searchbar/TagSuggestionOverlay.tsx +#: src/views/ResourcePage.tsx +#: src/views/ResourceLine.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx #: src/views/Card/ResourceCard.tsx -#: src/views/File/FilePreview.tsx #: src/views/File/FilePreviewThumbnail.tsx -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/views/File/FilePreview.tsx msgid "Loading..." msgstr "Laden..." @@ -112,8 +112,8 @@ msgstr "Nutzungen beschränken (optional)" #: src/components/InviteForm.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Create" msgstr "Erstellen" @@ -140,7 +140,7 @@ msgid "Go forward" msgstr "Vorwärts" #: src/components/MetaSetter.tsx -#: src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -405,15 +405,15 @@ msgstr "<0/>{0} Tastatur-Drag & Drop in der Seitenleiste aktivieren" #. placeholder {0}: resource.title #. placeholder {0}: resource.title -#: src/routes/EditRoute.tsx #: src/routes/DataRoute.tsx +#: src/routes/EditRoute.tsx msgid "Back to {0}" msgstr "Zurück zu {0}" #: src/routes/EditRoute.tsx -#: src/views/ResourcePageDefault.tsx #: src/chunks/AI/AgentConfigItem.tsx #: src/components/ResourceContextMenu/index.tsx +#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Bearbeiten" @@ -474,8 +474,8 @@ msgid "Go home" msgstr "Zur Startseite" #: src/routes/DataRoute.tsx -#: src/views/ResourceInline/ResourceInline.tsx #: src/routes/Share/ShareRoute.tsx +#: src/views/ResourceInline/ResourceInline.tsx msgid "No subject passed" msgstr "Kein Subjekt übergeben" @@ -603,8 +603,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "Wenn Sie sich abmelden, wird Ihr Geheimnis entfernt. Wenn Sie Ihr Geheimnis nicht gespeichert haben, verlieren Sie den Zugriff auf diesen Benutzer. Möchten Sie sich wirklich abmelden?" #: src/routes/SettingsAgent.tsx -#: src/views/InvitePage.tsx #: src/components/SideBar/AppMenu.tsx +#: src/views/InvitePage.tsx msgid "User Settings" msgstr "Benutzereinstellungen" @@ -714,7 +714,7 @@ msgid "Chat input" msgstr "Chat-Eingabe" #: src/views/ChatRoomPage.tsx -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Send" msgstr "Senden" @@ -1066,9 +1066,9 @@ msgid "Temperature value" msgstr "Temperaturwert" #: src/chunks/AI/MessageContextItem.tsx +#: src/chunks/RTE/EditLinkForm.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/views/TablePage/TableHeadingMenu.tsx -#: src/chunks/MarkdownEditor/EditLinkForm.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/views/OntologyPage/Property/PropertyLineWrite.tsx msgid "Remove" @@ -1571,8 +1571,8 @@ msgid "Select a location" msgstr "Einen Speicherort auswählen" #: src/components/ParentPicker/ParentPickerDialog.tsx -#: src/views/TablePage/NewColumnButton.tsx #: src/routes/SettingsServer/DriveRow.tsx +#: src/views/TablePage/NewColumnButton.tsx msgid "Select" msgstr "Auswählen" @@ -1691,19 +1691,19 @@ msgstr "{0} um {1}" #. placeholder {0}: prop.shortname #. placeholder {0}: prop.shortname #: src/components/forms/EditFormDialog.tsx -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Edit {0}" msgstr "{0} bearbeiten" +#: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/views/Article/ArticleDescription.tsx -#: src/views/OntologyPage/NewClassButton.tsx -#: src/views/OntologyPage/NewPropertyButton.tsx -#: src/chunks/MarkdownEditor/ImagePicker.tsx -#: src/routes/Share/ShareRoute.tsx #: src/routes/SettingsServer/index.tsx +#: src/routes/Share/ShareRoute.tsx +#: src/views/OntologyPage/NewPropertyButton.tsx +#: src/views/OntologyPage/NewClassButton.tsx +#: src/views/Article/ArticleDescription.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -1730,23 +1730,23 @@ msgstr "Diese Eigenschaft löschen" msgid "Required field." msgstr "Pflichtfeld." -#: src/components/forms/InputDate.tsx #: src/components/forms/InputMarkdown.tsx +#: src/components/forms/InputSlug.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx -#: src/components/forms/InputSlug.tsx -#: src/components/forms/InputString.tsx #: src/components/forms/InputURI.tsx +#: src/components/forms/InputTimestamp.tsx +#: src/components/forms/InputDate.tsx +#: src/components/forms/InputString.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx -#: src/components/forms/InputTimestamp.tsx #: src/components/forms/FilePicker/FilePicker.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx +#: src/views/TablePage/PropertyForm/PropertyForm.tsx #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts -#: src/views/TablePage/PropertyForm/PropertyForm.tsx msgid "Required" msgstr "Erforderlich" @@ -1856,8 +1856,8 @@ msgid "Upload file(s)..." msgstr "Datei(en) hochladen..." #: src/components/forms/UploadForm.tsx -#: src/components/forms/FileDropzone/FileDropzone.tsx #: src/components/forms/FileDropzone/FileDropzoneInput.tsx +#: src/components/forms/FileDropzone/FileDropzone.tsx msgid "Uploading..." msgstr "Wird hochgeladen..." @@ -2032,8 +2032,8 @@ msgstr "<0/> Herunterladen" msgid "Sorry, your browser doesn't support embedded videos." msgstr "Entschuldigung, Ihr Browser unterstützt keine eingebetteten Videos." -#: src/views/File/FilePreview.tsx #: src/views/File/FilePreviewThumbnail.tsx +#: src/views/File/FilePreview.tsx msgid "No preview available" msgstr "Keine Vorschau verfügbar" @@ -2188,10 +2188,10 @@ msgstr "Keine Instanzen" msgid "Use <0/> in code" msgstr "<0/> im Code verwenden" -#: src/views/ResourceInline/ResourceInline.tsx #: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx -#: src/components/forms/NewForm/NewFormDialog.tsx +#: src/views/ResourceInline/ResourceInline.tsx #: src/components/forms/FilePicker/FilePickerItem.tsx +#: src/components/forms/NewForm/NewFormDialog.tsx msgid "loading" msgstr "lädt" @@ -2316,10 +2316,10 @@ msgstr "Anbieter" msgid "OpenRouter is not enabled" msgstr "OpenRouter ist nicht aktiviert" -#. placeholder {0}: modelList.length #. placeholder {0}: models.length -#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx +#. placeholder {0}: modelList.length #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx msgid "{0} Models" msgstr "{0} Modelle" @@ -2346,8 +2346,8 @@ msgstr "{0} /M Ausgabe-Token" msgid "{0} /1K web search results" msgstr "{0} /1K Web-Suchergebnisse" -#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx msgid "Select a model" msgstr "Wähle ein Modell" @@ -2375,23 +2375,23 @@ msgstr "Familie:" msgid "Parameter Size:" msgstr "Parametergröße:" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle bold" msgstr "Fett ein/aus" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle italic" msgstr "Kursiv ein/aus" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle strikethrough" msgstr "Durchgestrichen ein/aus" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle blockquote" msgstr "Blockzitat ein/aus" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle inline code" msgstr "Inline-Code ein/aus" @@ -2403,52 +2403,54 @@ msgstr "<0/> Eigenschaft hinzufügen" msgid "New Property" msgstr "Neue Eigenschaft" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Beginne zu tippen..." -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Gib '/' für Optionen ein" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx msgid "Edit raw markdown" msgstr "Rohes Markdown bearbeiten" -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx msgid "Set" msgstr "Setzen" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Paragraph" msgstr "Absatz" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Codeblock" msgstr "Codeblock" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 1" msgstr "Überschrift 1" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 2" msgstr "Überschrift 2" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 3" msgstr "Überschrift 3" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 4" msgstr "Überschrift 4" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 5" msgstr "Überschrift 5" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 6" msgstr "Überschrift 6" @@ -2477,15 +2479,15 @@ msgstr "Schreibzugriff. Umschalten, um den Zugriff zu entfernen." msgid "No write access. Toggle to give write access." msgstr "Kein Schreibzugriff. Umschalten, um Schreibzugriff zu gewähren." -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Enter a URL..." msgstr "Gib eine URL ein…" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Or" msgstr "Oder" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Alt text" msgstr "Alt-Text" @@ -2732,13 +2734,13 @@ msgstr "<0/> Speichern" msgid "Class name" msgstr "Klassenname" -#: src/views/OntologyPage/Class/ClassCardWrite.tsx #: src/views/OntologyPage/Class/ClassCardRead.tsx +#: src/views/OntologyPage/Class/ClassCardWrite.tsx msgid "Requires" msgstr "Benötigt" -#: src/views/OntologyPage/Class/ClassCardWrite.tsx #: src/views/OntologyPage/Class/ClassCardRead.tsx +#: src/views/OntologyPage/Class/ClassCardWrite.tsx msgid "Recommends" msgstr "Empfiehlt" @@ -2753,13 +2755,13 @@ msgstr "Leerer Chat" #. placeholder {0}: classType.title #. placeholder {0}: classType.title -#: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/AtomicURLCell.tsx msgid "Search {0}" msgstr "{0} suchen" -#: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/AtomicURLCell.tsx msgid "Search..." msgstr "Suchen..." @@ -2776,8 +2778,8 @@ msgstr "Einzelne Instanz" msgid "Table" msgstr "Tabelle" -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Open edit dialog" msgstr "Öffne Bearbeitungsdialog" @@ -2916,11 +2918,11 @@ msgstr "Länge" msgid "<0/> Include Time" msgstr "<0/> Zeit einschließen" -#: src/chunks/MarkdownEditor/AIChatInput/MentionList.tsx +#: src/chunks/RTE/AIChatInput/MentionList.tsx msgid "No result" msgstr "Kein Ergebnis" -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Ask me anything..." msgstr "Frag mich alles..." @@ -2962,8 +2964,8 @@ msgstr "Mein Laufwerk" msgid "New Bookmark" msgstr "Neues Lesezeichen" -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx msgid "Ok" msgstr "Ok" @@ -3038,3 +3040,23 @@ msgstr "Dezimalstellen" #: src/components/forms/ResourceForm.tsx msgid "Add another property..." msgstr "Weitere Eigenschaft hinzufügen..." + +#: src/components/YDocValue.tsx +msgid "Empty" +msgstr "Leer" + +#: src/components/YDocValue.tsx +msgid "Show encoded state" +msgstr "Kodierten Status anzeigen" + +#: src/components/YDocValue.tsx +msgid "Hide encoded state" +msgstr "Verstecke den kodierten Status" + +#: src/components/forms/InputYDoc.tsx +msgid "Editing YDoc directly is not supported" +msgstr "Das direkte Bearbeiten von YDoc wird nicht unterstützt" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Pieter Post" +msgstr "Peter Post" diff --git a/browser/data-browser/src/locales/en.po b/browser/data-browser/src/locales/en.po index 9f52b16a8..e6a2c55fd 100644 --- a/browser/data-browser/src/locales/en.po +++ b/browser/data-browser/src/locales/en.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T12:18:27.636Z\n" -"PO-Revision-Date: 2025-09-29T10:57:19.353Z\n" +"PO-Revision-Date: 2025-10-14T09:30:37.525Z\n" "Last-Translator: \n" "Language: en\n" "Language-Team: \n" @@ -48,9 +48,9 @@ msgstr "Resource is loading..." #: src/components/HighlightedCodeBlock.tsx #: src/chunks/AI/AIChatPage.tsx -#: src/views/ResourceLine.tsx -#: src/views/ResourcePage.tsx #: src/components/Searchbar/TagSuggestionOverlay.tsx +#: src/views/ResourcePage.tsx +#: src/views/ResourceLine.tsx #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx #: src/views/Card/ResourceCard.tsx #: src/views/File/FilePreviewThumbnail.tsx @@ -162,8 +162,8 @@ msgstr "Back to {0}" #: src/routes/EditRoute.tsx #: src/chunks/AI/AgentConfigItem.tsx -#: src/views/ResourcePageDefault.tsx #: src/components/ResourceContextMenu/index.tsx +#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Edit" @@ -309,14 +309,14 @@ msgstr "Drive Configuration" msgid "Current Drive" msgstr "Current Drive" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx #: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx -#: src/views/Article/ArticleDescription.tsx -#: src/views/OntologyPage/NewClassButton.tsx #: src/views/OntologyPage/NewPropertyButton.tsx +#: src/views/OntologyPage/NewClassButton.tsx +#: src/views/Article/ArticleDescription.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -364,8 +364,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "If you sign out, your secret will be removed. If you haven't saved your secret somewhere, you will lose access to this User. Are you sure you want to sign out?" #: src/routes/SettingsAgent.tsx -#: src/views/InvitePage.tsx #: src/components/SideBar/AppMenu.tsx +#: src/views/InvitePage.tsx msgid "User Settings" msgstr "User Settings" @@ -753,30 +753,30 @@ msgstr "Name" #: src/components/ConfirmationDialog.tsx #: src/chunks/AI/AgentConfig.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx +#: src/routes/History/HistoryMobileView.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/History/HistoryMobileView.tsx -#: src/views/OntologyPage/NewClassButton.tsx #: src/views/OntologyPage/NewPropertyButton.tsx +#: src/views/OntologyPage/NewClassButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Cancel" #: src/components/InviteForm.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx msgid "Create" msgstr "Create" @@ -1119,8 +1119,8 @@ msgid "Drop files or click here to upload." msgstr "Drop files or click here to upload." #: src/components/forms/UploadForm.tsx -#: src/components/forms/FileDropzone/FileDropzone.tsx #: src/components/forms/FileDropzone/FileDropzoneInput.tsx +#: src/components/forms/FileDropzone/FileDropzone.tsx msgid "Uploading..." msgstr "Uploading..." @@ -1303,7 +1303,7 @@ msgid "Chat input" msgstr "Chat input" #: src/views/ChatRoomPage.tsx -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Send" msgstr "Send" @@ -1365,7 +1365,7 @@ msgid "Edit tag" msgstr "Edit tag" #: src/components/MetaSetter.tsx -#: src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -1467,8 +1467,8 @@ msgstr "Remove drive from list" msgid "Select" msgstr "Select" -#: src/views/ResourceInline/ResourceInline.tsx #: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx +#: src/views/ResourceInline/ResourceInline.tsx #: src/components/forms/FilePicker/FilePickerItem.tsx #: src/components/forms/NewForm/NewFormDialog.tsx msgid "loading" @@ -1540,22 +1540,22 @@ msgstr "Sandbox, test components in isolation" msgid "Invalid Resource" msgstr "Invalid Resource" +#: src/components/forms/InputSlug.tsx #: src/components/forms/InputMarkdown.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputDate.tsx +#: src/components/forms/InputString.tsx +#: src/components/forms/InputTimestamp.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx -#: src/components/forms/InputSlug.tsx -#: src/components/forms/InputString.tsx #: src/components/forms/InputURI.tsx -#: src/components/forms/InputTimestamp.tsx #: src/components/forms/FilePicker/FilePicker.tsx +#: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts -#: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/views/TablePage/PropertyForm/PropertyForm.tsx msgid "Required" msgstr "Required" @@ -1565,7 +1565,7 @@ msgid "Edit resource" msgstr "Edit resource" #: src/chunks/AI/MessageContextItem.tsx -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx @@ -2099,8 +2099,8 @@ msgstr "No parent set" #. placeholder {0}: prop.shortname #. placeholder {0}: prop.shortname #: src/components/forms/EditFormDialog.tsx -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Edit {0}" msgstr "Edit {0}" @@ -2337,8 +2337,8 @@ msgstr "Datatype" msgid "Classtype" msgstr "Classtype" -#: src/views/OntologyPage/Property/EnumFormPart.tsx #: src/views/OntologyPage/Property/PropertyFormCommon.tsx +#: src/views/OntologyPage/Property/EnumFormPart.tsx msgid "Allows Only" msgstr "Allows Only" @@ -2459,8 +2459,8 @@ msgstr "Search {0}" msgid "Search..." msgstr "Search..." -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Open edit dialog" msgstr "Open edit dialog" @@ -2817,7 +2817,7 @@ msgstr "<0/> Settings" msgid "No AI provider configured." msgstr "No AI provider configured." -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Ask me anything..." msgstr "Ask me anything..." @@ -2897,16 +2897,18 @@ msgstr "Temperature" msgid "Temperature value" msgstr "Temperature value" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Start typing..." -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Type '/' for options" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx msgid "Edit raw markdown" msgstr "Edit raw markdown" @@ -2939,35 +2941,35 @@ msgstr "Set {0} as default" msgid "Provider not enabled" msgstr "Provider not enabled" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle bold" msgstr "Toggle bold" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle italic" msgstr "Toggle italic" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle strikethrough" msgstr "Toggle strikethrough" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle blockquote" msgstr "Toggle blockquote" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle inline code" msgstr "Toggle inline code" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Enter a URL..." msgstr "Enter a URL..." -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Or" msgstr "Or" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Alt text" msgstr "Alt text" @@ -2994,43 +2996,43 @@ msgstr "Thinking..." msgid "Thinking" msgstr "Thinking" -#: src/chunks/MarkdownEditor/AIChatInput/MentionList.tsx +#: src/chunks/RTE/AIChatInput/MentionList.tsx msgid "No result" msgstr "No result" -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx msgid "Set" msgstr "Set" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Paragraph" msgstr "Paragraph" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Codeblock" msgstr "Codeblock" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 1" msgstr "Heading 1" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 2" msgstr "Heading 2" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 3" msgstr "Heading 3" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 4" msgstr "Heading 4" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 5" msgstr "Heading 5" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 6" msgstr "Heading 6" @@ -3045,3 +3047,23 @@ msgstr "Row is incomplete or has invalid data" #: src/components/forms/ResourceForm.tsx msgid "Add another property..." msgstr "Add another property..." + +#: src/components/YDocValue.tsx +msgid "Empty" +msgstr "Empty" + +#: src/components/YDocValue.tsx +msgid "Show encoded state" +msgstr "Show encoded state" + +#: src/components/YDocValue.tsx +msgid "Hide encoded state" +msgstr "Hide encoded state" + +#: src/components/forms/InputYDoc.tsx +msgid "Editing YDoc directly is not supported" +msgstr "Editing YDoc directly is not supported" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Pieter Post" +msgstr "Pieter Post" diff --git a/browser/data-browser/src/locales/es.po b/browser/data-browser/src/locales/es.po index 7f52761ac..dc917e5f4 100644 --- a/browser/data-browser/src/locales/es.po +++ b/browser/data-browser/src/locales/es.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T09:59:41.856Z\n" -"PO-Revision-Date: 2025-09-29T10:57:20.018Z\n" +"PO-Revision-Date: 2025-10-14T09:30:37.529Z\n" "Last-Translator: \n" "Language: es\n" "Language-Team: \n" @@ -28,21 +28,21 @@ msgstr "No hay clases" #: src/components/ConfirmationDialog.tsx #: src/chunks/AI/AgentConfig.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx +#: src/routes/History/HistoryMobileView.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/History/HistoryMobileView.tsx #: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Cancelar" @@ -90,11 +90,11 @@ msgid "Limit Usages (optional)" msgstr "Limitar usos (opcional)" #: src/components/InviteForm.tsx +#: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx -#: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx msgid "Create" msgstr "Crear" @@ -104,9 +104,9 @@ msgstr "¡Invitación creada y copiada al portapapeles! 🚀" #: src/components/HighlightedCodeBlock.tsx #: src/chunks/AI/AIChatPage.tsx +#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/ResourcePage.tsx #: src/views/ResourceLine.tsx -#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx #: src/views/Card/ResourceCard.tsx #: src/views/File/FilePreviewThumbnail.tsx @@ -115,7 +115,7 @@ msgid "Loading..." msgstr "Cargando..." #: src/components/MetaSetter.tsx -#: src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -454,8 +454,8 @@ msgstr "Uso" #: src/routes/EditRoute.tsx #: src/chunks/AI/AgentConfigItem.tsx -#: src/views/ResourcePageDefault.tsx #: src/components/ResourceContextMenu/index.tsx +#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Editar" @@ -584,8 +584,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "Si cierras sesión, tu secreto será eliminado. Si no has guardado tu secreto en algún lugar, perderás el acceso a este Usuario. ¿Estás seguro de que quieres cerrar sesión?" #: src/routes/SettingsAgent.tsx -#: src/views/InvitePage.tsx #: src/components/SideBar/AppMenu.tsx +#: src/views/InvitePage.tsx msgid "User Settings" msgstr "Configuración de usuario" @@ -719,7 +719,7 @@ msgid "Chat input" msgstr "Entrada de chat" #: src/views/ChatRoomPage.tsx -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Send" msgstr "Enviar" @@ -1052,7 +1052,7 @@ msgid "No AI provider configured." msgstr "No hay ningún proveedor de IA configurado." #: src/chunks/AI/MessageContextItem.tsx -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx @@ -1171,39 +1171,41 @@ msgstr "Elige un emoji" msgid "Copy code" msgstr "Copiar código" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Empieza a escribir..." -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Escribe '/' para ver las opciones" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx msgid "Edit raw markdown" msgstr "Editar markdown sin procesar" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Enter a URL..." msgstr "Introduce una URL..." -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Or" msgstr "O" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Alt text" msgstr "Texto alternativo" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/Share/ShareRoute.tsx #: src/routes/SettingsServer/index.tsx -#: src/views/Article/ArticleDescription.tsx +#: src/routes/Share/ShareRoute.tsx #: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx +#: src/views/Article/ArticleDescription.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -1214,59 +1216,59 @@ msgstr "Guardar" msgid "Message in <0/>" msgstr "Mensaje en <0/>" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Paragraph" msgstr "Párrafo" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Codeblock" msgstr "Bloque de código" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 1" msgstr "Encabezado 1" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 2" msgstr "Encabezado 2" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 3" msgstr "Encabezado 3" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 4" msgstr "Encabezado 4" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 5" msgstr "Encabezado 5" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 6" msgstr "Encabezado 6" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle bold" msgstr "Activar/desactivar negrita" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle italic" msgstr "Activar/desactivar cursiva" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle strikethrough" msgstr "Activar/desactivar tachado" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle blockquote" msgstr "Activar/desactivar cita en bloque" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle inline code" msgstr "Activar/desactivar código en línea" -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx msgid "Set" msgstr "Establecer" @@ -1819,8 +1821,8 @@ msgstr "Crear nuevo recurso{0} {1}" #. placeholder {0}: prop.shortname #. placeholder {0}: prop.shortname #: src/components/forms/EditFormDialog.tsx -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Edit {0}" msgstr "Editar {0}" @@ -1857,22 +1859,22 @@ msgstr "Campo obligatorio." msgid "Invalid JSON" msgstr "JSON no válido" -#: src/components/forms/InputDate.tsx +#: src/components/forms/InputSlug.tsx +#: src/components/forms/InputMarkdown.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx -#: src/components/forms/InputMarkdown.tsx +#: src/components/forms/InputDate.tsx #: src/components/forms/InputString.tsx #: src/components/forms/InputURI.tsx +#: src/components/forms/InputTimestamp.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx -#: src/components/forms/InputSlug.tsx -#: src/components/forms/InputTimestamp.tsx #: src/components/forms/FilePicker/FilePicker.tsx +#: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts -#: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/views/TablePage/PropertyForm/PropertyForm.tsx msgid "Required" msgstr "Obligatorio" @@ -1928,8 +1930,8 @@ msgid "Upload file(s)..." msgstr "Subir archivo(s)..." #: src/components/forms/UploadForm.tsx -#: src/components/forms/FileDropzone/FileDropzoneInput.tsx #: src/components/forms/FileDropzone/FileDropzone.tsx +#: src/components/forms/FileDropzone/FileDropzoneInput.tsx msgid "Uploading..." msgstr "Subiendo..." @@ -2206,8 +2208,8 @@ msgstr "Crear nuevo recurso" msgid "New Resource" msgstr "Nuevo recurso" -#: src/views/ResourceInline/ResourceInline.tsx #: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx +#: src/views/ResourceInline/ResourceInline.tsx #: src/components/forms/FilePicker/FilePickerItem.tsx #: src/components/forms/NewForm/NewFormDialog.tsx msgid "loading" @@ -2411,10 +2413,10 @@ msgstr "Proveedor" msgid "OpenRouter is not enabled" msgstr "OpenRouter no está habilitado" -#. placeholder {0}: modelList.length #. placeholder {0}: models.length -#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx +#. placeholder {0}: modelList.length #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx msgid "{0} Models" msgstr "{0} Modelos" @@ -2441,8 +2443,8 @@ msgstr "{0} /M tokens de salida" msgid "{0} /1K web search results" msgstr "{0} /1K resultados de búsqueda web" -#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx msgid "Select a model" msgstr "Selecciona un modelo" @@ -2470,11 +2472,11 @@ msgstr "Familia:" msgid "Parameter Size:" msgstr "Tamaño del Parámetro:" -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Ask me anything..." msgstr "Pregúntame lo que quieras..." -#: src/chunks/MarkdownEditor/AIChatInput/MentionList.tsx +#: src/chunks/RTE/AIChatInput/MentionList.tsx msgid "No result" msgstr "Sin resultados" @@ -2655,8 +2657,8 @@ msgstr "URL del nuevo recurso..." msgid "The identifier of the resource. This also determines where the resource is saved, by default." msgstr "El identificador del recurso. Esto también determina dónde se guarda el recurso, por defecto." -#: src/views/OntologyPage/Class/ClassCardWrite.tsx #: src/views/OntologyPage/Class/ClassCardRead.tsx +#: src/views/OntologyPage/Class/ClassCardWrite.tsx msgid "Requires" msgstr "Requiere" @@ -2665,8 +2667,8 @@ msgstr "Requiere" msgid "none" msgstr "ninguno" -#: src/views/OntologyPage/Class/ClassCardWrite.tsx #: src/views/OntologyPage/Class/ClassCardRead.tsx +#: src/views/OntologyPage/Class/ClassCardWrite.tsx msgid "Recommends" msgstr "Recomienda" @@ -2687,8 +2689,8 @@ msgstr "Instancia única" msgid "Table" msgstr "Tabla" -#: src/views/OntologyPage/Property/EnumFormPart.tsx #: src/views/OntologyPage/Property/PropertyFormCommon.tsx +#: src/views/OntologyPage/Property/EnumFormPart.tsx msgid "Allows Only" msgstr "Permitir solo" @@ -2754,8 +2756,8 @@ msgstr "Chat vacío" msgid "Add resource" msgstr "Añadir recurso" -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Open edit dialog" msgstr "Abrir diálogo de edición" @@ -3013,3 +3015,23 @@ msgstr "La fila está incompleta o tiene datos no válidos" #: src/components/forms/ResourceForm.tsx msgid "Add another property..." msgstr "Añadir otra propiedad..." + +#: src/components/YDocValue.tsx +msgid "Empty" +msgstr "Vacío" + +#: src/components/YDocValue.tsx +msgid "Show encoded state" +msgstr "Mostrar estado codificado" + +#: src/components/YDocValue.tsx +msgid "Hide encoded state" +msgstr "Ocultar estado codificado" + +#: src/components/forms/InputYDoc.tsx +msgid "Editing YDoc directly is not supported" +msgstr "La edición directa de YDoc no es compatible" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Pieter Post" +msgstr "Pedro Cartero" diff --git a/browser/data-browser/src/locales/fr.po b/browser/data-browser/src/locales/fr.po index fd71a33a4..685cee2d3 100644 --- a/browser/data-browser/src/locales/fr.po +++ b/browser/data-browser/src/locales/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T10:06:11.465Z\n" -"PO-Revision-Date: 2025-09-29T10:57:20.624Z\n" +"PO-Revision-Date: 2025-10-14T09:30:37.538Z\n" "Last-Translator: \n" "Language: fr\n" "Language-Team: \n" @@ -28,28 +28,28 @@ msgstr "Aucune classe" #: src/components/ConfirmationDialog.tsx #: src/chunks/AI/AgentConfig.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx +#: src/routes/History/HistoryMobileView.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/History/HistoryMobileView.tsx #: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ValueForm/ValueFormEdit.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx +#: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Annuler" #: src/components/ComboBox.tsx #: src/views/Element.tsx -#: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/AtomicURLCell.tsx msgid "No results" msgstr "Aucun résultat" @@ -90,11 +90,11 @@ msgid "Limit Usages (optional)" msgstr "Limiter les utilisations (facultatif)" #: src/components/InviteForm.tsx +#: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx -#: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx msgid "Create" msgstr "Créer" @@ -104,9 +104,9 @@ msgstr "Invitation créée et copiée dans le presse-papier ! 🚀" #: src/components/HighlightedCodeBlock.tsx #: src/chunks/AI/AIChatPage.tsx +#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/ResourcePage.tsx #: src/views/ResourceLine.tsx -#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx #: src/views/Card/ResourceCard.tsx #: src/views/File/FilePreviewThumbnail.tsx @@ -115,7 +115,7 @@ msgid "Loading..." msgstr "Chargement..." #: src/components/MetaSetter.tsx -#: src/chunks/MarkdownEditor/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -420,8 +420,8 @@ msgstr "Accepter" #. placeholder {0}: resource.title #. placeholder {0}: resource.title -#: src/routes/EditRoute.tsx #: src/routes/DataRoute.tsx +#: src/routes/EditRoute.tsx msgid "Back to {0}" msgstr "Retour à {0}" @@ -472,8 +472,8 @@ msgstr "Usage" #: src/routes/EditRoute.tsx #: src/chunks/AI/AgentConfigItem.tsx -#: src/views/ResourcePageDefault.tsx #: src/components/ResourceContextMenu/index.tsx +#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Modifier" @@ -602,8 +602,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "Si vous vous déconnectez, votre secret sera supprimé. Si vous n'avez pas enregistré votre secret quelque part, vous perdrez l'accès à cet utilisateur. Êtes-vous sûr de vouloir vous déconnecter ?" #: src/routes/SettingsAgent.tsx -#: src/views/InvitePage.tsx #: src/components/SideBar/AppMenu.tsx +#: src/views/InvitePage.tsx msgid "User Settings" msgstr "Paramètres utilisateur" @@ -737,7 +737,7 @@ msgid "Chat input" msgstr "Saisie de chat" #: src/views/ChatRoomPage.tsx -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Send" msgstr "Envoyer" @@ -1074,7 +1074,7 @@ msgid "No AI provider configured." msgstr "Aucun fournisseur d'IA configuré." #: src/chunks/AI/MessageContextItem.tsx -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/views/TablePage/TableHeadingMenu.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx @@ -1193,39 +1193,41 @@ msgstr "Choisissez un emoji" msgid "Copy code" msgstr "Copier le code" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Commencez à taper..." -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Tapez « / » pour les options" -#: src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx +#: src/chunks/RTE/AsyncMarkdownEditor.tsx msgid "Edit raw markdown" msgstr "Modifier le markdown brut" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Enter a URL..." msgstr "Entrez une URL..." -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Or" msgstr "Ou" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx msgid "Alt text" msgstr "Texte alternatif" -#: src/chunks/MarkdownEditor/ImagePicker.tsx +#: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx #: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx -#: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx +#: src/views/Article/ArticleDescription.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -1236,59 +1238,59 @@ msgstr "Enregistrer" msgid "Message in <0/>" msgstr "Message dans <0/>" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Paragraph" msgstr "Paragraphe" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Codeblock" msgstr "Bloc de code" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 1" msgstr "Titre 1" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 2" msgstr "Titre 2" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 3" msgstr "Titre 3" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 4" msgstr "Titre 4" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 5" msgstr "Titre 5" -#: src/chunks/MarkdownEditor/NodeSelectMenu.tsx +#: src/chunks/RTE/NodeSelectMenu.tsx msgid "Heading 6" msgstr "Titre 6" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle bold" msgstr "Activer/désactiver le gras" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle italic" msgstr "Activer/désactiver l’italique" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle strikethrough" msgstr "Activer/désactiver le barré" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle blockquote" msgstr "Activer/désactiver la citation" -#: src/chunks/MarkdownEditor/BubbleMenu.tsx +#: src/chunks/RTE/BubbleMenu.tsx msgid "Toggle inline code" msgstr "Activer/désactiver le code en ligne" -#: src/chunks/MarkdownEditor/EditLinkForm.tsx +#: src/chunks/RTE/EditLinkForm.tsx msgid "Set" msgstr "Définir" @@ -1837,8 +1839,8 @@ msgstr "Créer une nouvelle ressource{0} {1}" #. placeholder {0}: prop.shortname #. placeholder {0}: prop.shortname #: src/components/forms/EditFormDialog.tsx -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Edit {0}" msgstr "Modifier {0}" @@ -1875,22 +1877,22 @@ msgstr "Champ obligatoire." msgid "Invalid JSON" msgstr "JSON non valide" -#: src/components/forms/InputDate.tsx +#: src/components/forms/InputSlug.tsx +#: src/components/forms/InputMarkdown.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx -#: src/components/forms/InputMarkdown.tsx +#: src/components/forms/InputURI.tsx +#: src/components/forms/InputTimestamp.tsx +#: src/components/forms/InputDate.tsx +#: src/components/forms/InputString.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx -#: src/components/forms/InputString.tsx -#: src/components/forms/InputURI.tsx -#: src/components/forms/InputTimestamp.tsx -#: src/components/forms/InputSlug.tsx #: src/components/forms/FilePicker/FilePicker.tsx +#: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts -#: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/views/TablePage/PropertyForm/PropertyForm.tsx msgid "Required" msgstr "Obligatoire" @@ -2488,11 +2490,11 @@ msgstr "Famille :" msgid "Parameter Size:" msgstr "Taille des paramètres :" -#: src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx +#: src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx msgid "Ask me anything..." msgstr "Demandez-moi n'importe quoi..." -#: src/chunks/MarkdownEditor/AIChatInput/MentionList.tsx +#: src/chunks/RTE/AIChatInput/MentionList.tsx msgid "No result" msgstr "Aucun résultat" @@ -2705,8 +2707,8 @@ msgstr "Instance unique" msgid "Table" msgstr "Tableau" -#: src/views/OntologyPage/Property/EnumFormPart.tsx #: src/views/OntologyPage/Property/PropertyFormCommon.tsx +#: src/views/OntologyPage/Property/EnumFormPart.tsx msgid "Allows Only" msgstr "Autoriser seulement" @@ -2754,13 +2756,13 @@ msgstr "Configurer {0}" #. placeholder {0}: classType.title #. placeholder {0}: classType.title -#: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/AtomicURLCell.tsx msgid "Search {0}" msgstr "Rechercher {0}" -#: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/AtomicURLCell.tsx msgid "Search..." msgstr "Rechercher..." @@ -2772,8 +2774,8 @@ msgstr "Chat vide" msgid "Add resource" msgstr "Ajouter une ressource" -#: src/views/TablePage/EditorCells/JSONCell.tsx #: src/views/TablePage/EditorCells/MarkdownCell.tsx +#: src/views/TablePage/EditorCells/JSONCell.tsx msgid "Open edit dialog" msgstr "Ouvrir la boîte de dialogue d'édition" @@ -3033,3 +3035,23 @@ msgstr "La ligne est incomplète ou contient des données invalides" #: src/components/forms/ResourceForm.tsx msgid "Add another property..." msgstr "Ajouter une autre propriété..." + +#: src/components/YDocValue.tsx +msgid "Empty" +msgstr "Vide" + +#: src/components/YDocValue.tsx +msgid "Show encoded state" +msgstr "Afficher l'état encodé" + +#: src/components/YDocValue.tsx +msgid "Hide encoded state" +msgstr "Masquer l'état encodé" + +#: src/components/forms/InputYDoc.tsx +msgid "Editing YDoc directly is not supported" +msgstr "La modification directe de YDoc n'est pas prise en charge" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Pieter Post" +msgstr "Pierre Postier" diff --git a/browser/data-browser/src/routes/History/useVersions.ts b/browser/data-browser/src/routes/History/useVersions.ts index 06a28d7b8..b8e29baa7 100644 --- a/browser/data-browser/src/routes/History/useVersions.ts +++ b/browser/data-browser/src/routes/History/useVersions.ts @@ -34,6 +34,7 @@ export function useVersions(resource: Resource): UseVersionsResult { const dedupedVersions = dedupeVersions(history); setVersions(dedupedVersions); } catch (e) { + console.error(e); setError(e); } finally { setLoading(false); diff --git a/browser/data-browser/src/views/TablePage/helpers/useTableHistory.ts b/browser/data-browser/src/views/TablePage/helpers/useTableHistory.ts index 05bee9cb9..f61dab4da 100644 --- a/browser/data-browser/src/views/TablePage/helpers/useTableHistory.ts +++ b/browser/data-browser/src/views/TablePage/helpers/useTableHistory.ts @@ -1,4 +1,10 @@ -import { JSONValue, Resource, Store, useStore } from '@tomic/react'; +import { + JSONValue, + Resource, + Store, + useStore, + type PropVals, +} from '@tomic/react'; import { useCallback, useState } from 'react'; enum HistoryItemType { @@ -22,7 +28,7 @@ interface ResourceCreatedItem { interface ResourceDeletedItem { type: HistoryItemType.ResourceDeleted; subject: string; - propVals: Map; + propVals: PropVals; } type HistoryItem = ValueChangeItem | ResourceCreatedItem | ResourceDeletedItem; diff --git a/browser/lib/package.json b/browser/lib/package.json index cf9683ded..b81b49355 100644 --- a/browser/lib/package.json +++ b/browser/lib/package.json @@ -13,9 +13,9 @@ "dependencies": { "@noble/ed25519": "1.6.0", "@noble/hashes": "^0.5.9", - "base64-arraybuffer": "^1.0.2", "fast-json-stable-stringify": "^2.1.0", - "ulidx": "^2.4.1" + "ulidx": "^2.4.1", + "yjs": "^13.6.27" }, "description": "The Atomic Data library for typescript/javascript", "devDependencies": { @@ -25,9 +25,17 @@ "@types/fast-json-stable-stringify": "^2.1.2", "tslib": "^2.8.0", "tsup": "^8.3.5", - "typescript": "^5.6.3", + "typescript": "^5.9.3", "vitest": "^2.1.3" }, + "peerDependencies": { + "yjs": "^13.6.27" + }, + "peerDependenciesMeta": { + "yjs": { + "optional": true + } + }, "files": [ "dist", "!dist/**/*.d.ts.map" diff --git a/browser/lib/src/base64.ts b/browser/lib/src/base64.ts new file mode 100644 index 000000000..309b46a16 --- /dev/null +++ b/browser/lib/src/base64.ts @@ -0,0 +1,42 @@ +export function decodeB64(base64: string): Uint8Array { + // 1. Node.js (via Buffer) + if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') { + // Buffer.from returns a Buffer, which extends Uint8Array. + return Buffer.from(base64, 'base64'); + } + + // 2. Browser (via atob) + if (typeof atob === 'function') { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + return bytes; + } + + throw new Error('Base64 decoding not supported in this environment.'); +} + +export function encodeB64(bytes: Uint8Array): string { + // 1. Node.js (via Buffer) + if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') { + return Buffer.from(bytes).toString('base64'); + } + + // 2. Browser (via btoa) + if (typeof btoa === 'function') { + // Convert Uint8Array to binary string + let binaryString = ''; + + for (let i = 0; i < bytes.length; i++) { + binaryString += String.fromCharCode(bytes[i]); + } + + return btoa(binaryString); + } + + throw new Error('Base64 encoding not supported in this environment.'); +} diff --git a/browser/lib/src/commit.ts b/browser/lib/src/commit.ts index 9ffeadfca..3b4ef2a0c 100644 --- a/browser/lib/src/commit.ts +++ b/browser/lib/src/commit.ts @@ -1,13 +1,19 @@ import { sign, getPublicKey, utils } from '@noble/ed25519'; import stringify from 'fast-json-stable-stringify'; -import { decode as decodeB64, encode as encodeB64 } from 'base64-arraybuffer'; // https://github.com/paulmillr/noble-ed25519/issues/38 import { sha512 } from '@noble/hashes/sha512'; +import { YLoader } from './yjs.js'; import { Client } from './client.js'; import { Resource } from './resource.js'; import type { Store } from './store.js'; -import type { JSONValue, JSONArray } from './value.js'; +import { + type JSONValue, + type JSONArray, + isSerializedYUpdate, + isJSONObject, +} from './value.js'; +import { decodeB64, encodeB64 } from './base64.js'; import { commits } from './ontologies/commits.js'; import { core } from './ontologies/core.js'; @@ -24,6 +30,7 @@ export interface CommitBuilderI { * be appended. https://atomicdata.dev/properties/push */ push?: Record; + yUpdate?: Record; /** The properties that need to be removed. https://atomicdata.dev/properties/remove */ remove?: string[]; /** If true, the resource must be deleted. https://atomicdata.dev/properties/destroy */ @@ -38,6 +45,7 @@ export interface CommitBuilderI { interface CommitBuilderBase { set?: Map; push?: Map>; + yUpdate?: Map; remove?: Set; destroy?: boolean; previousCommit?: string; @@ -57,6 +65,7 @@ export class CommitBuilder { private _subject: string; private _set: Map; private _push: Map>; + private _yUpdate: Map; private _remove: Set; private _destroy?: boolean; private _previousCommit?: string; @@ -66,6 +75,7 @@ export class CommitBuilder { this._subject = Client.removeQueryParamsFromURL(subject); this._set = base.set ?? new Map(); this._push = base.push ?? new Map(); + this._yUpdate = base.yUpdate ?? new Map(); this._remove = base.remove ?? new Set(); this._destroy = base.destroy; this._previousCommit = base.previousCommit; @@ -83,6 +93,10 @@ export class CommitBuilder { return this._push; } + public get yUpdate() { + return this._yUpdate; + } + public get remove() { return this._remove; } @@ -117,12 +131,28 @@ export class CommitBuilder { public addRemoveAction(property: string): CommitBuilder { this._set.delete(property); this._push.delete(property); - + this._yUpdate.delete(property); this._remove.add(property); return this; } + public addYUpdateAction(property: string, update: Uint8Array): CommitBuilder { + YLoader.loadCheck(); + const Y = YLoader.Y; + + this.removeRemoveAction(property); + const existingUpdate = this._yUpdate.get(property); + + if (existingUpdate) { + this._yUpdate.set(property, Y.mergeUpdatesV2([existingUpdate, update])); + } else { + this._yUpdate.set(property, update); + } + + return this; + } + public removeRemoveAction(property: string): CommitBuilder { this._remove.delete(property); @@ -171,7 +201,8 @@ export class CommitBuilder { this.set.size > 0 || this.push.size > 0 || this.destroy || - this.remove.size > 0 + this.remove.size > 0 || + this.yUpdate.size > 0 ); } @@ -185,6 +216,7 @@ export class CommitBuilder { const base = { set: this.set, push: this.push, + yUpdate: this.yUpdate, remove: this.remove, destroy: this.destroy, previousCommit: this.previousCommit, @@ -203,6 +235,7 @@ export class CommitBuilder { remove: Array.from(this.remove), destroy: this.destroy, previousCommit: this.previousCommit, + yUpdate: Object.fromEntries(this.yUpdate.entries()), }; } @@ -272,24 +305,48 @@ const serializeMap = { createdAt: commits.properties.createdAt, signer: commits.properties.signer, signature: commits.properties.signature, + yUpdate: commits.properties.yUpdate, id: 'id', }; /** Replaces the keys of a Commit object with their respective json-ad key */ -const commitToJsonADObject = (commit: UnsignedCommit | Commit): JSONADObject => - Object.entries(commit).reduce( - (acc, [key, value]) => { - const serializedKey = - serializeMap[key as keyof Commit | keyof UnsignedCommit]; - - acc[serializedKey] = value as JSONValue; - - return acc; - }, - { - [core.properties.isA]: [commits.classes.commit], - }, - ); +function commitToJsonADObject(commit: UnsignedCommit | Commit): JSONADObject { + const jsonAdObj: JSONADObject = { + [core.properties.isA]: [commits.classes.commit], + }; + + for (const kv of Object.entries(commit)) { + const [key, value] = kv as [keyof Commit, Commit[keyof Commit]]; + const serializedKey = serializeMap[key]; + jsonAdObj[serializedKey] = serializeCommitValue(key, value); + } + + return jsonAdObj; +} + +function serializeCommitValue( + key: K, + value: Commit[K], +): JSONValue { + // The value for yUpdate needs to be encoded to base64 before it is valid JSON-AD + if (key === 'yUpdate') { + const castValue = value as Commit['yUpdate']; + + if (castValue !== undefined) { + return Object.fromEntries( + Object.entries(castValue).map(([k, v]) => [ + k, + { type: 'ydoc', data: encodeB64(v) }, + ]), + ); + } + + return undefined; + } + + // The rest of the values can just be returned as is + return value as JSONValue; +} /** * Takes a commit and serializes it deterministically (canonicilaization). Is @@ -316,6 +373,10 @@ export function serializeDeterministically( delete commit.destroy; } + if (commit.yUpdate && Object.keys(commit.yUpdate).length === 0) { + delete commit.yUpdate; + } + const jsonadCommit = commitToJsonADObject(commit); return stringify(jsonadCommit); @@ -381,6 +442,7 @@ export function parseCommitResource(resource: Resource): Commit { subject: resource.get(commits.properties.subject), set: resource.get(commits.properties.set), push: resource.get(commits.properties.push), + yUpdate: parseYUpdateValue(resource.get(commits.properties.yUpdate)), signer: resource.get(commits.properties.signer), createdAt: resource.get(commits.properties.createdAt), remove: resource.get(commits.properties.remove), @@ -403,6 +465,7 @@ export function parseCommitJSON(str: string): Commit { const subject = jsonAdObj[commits.properties.subject]; const set = jsonAdObj[commits.properties.set]; const push = jsonAdObj[commits.properties.push]; + const yUpdate = parseYUpdateValue(jsonAdObj[commits.properties.yUpdate]); const signer = jsonAdObj[commits.properties.signer]; const createdAt = jsonAdObj[commits.properties.createdAt]; const remove: string[] | undefined = jsonAdObj[commits.properties.remove]; @@ -420,6 +483,7 @@ export function parseCommitJSON(str: string): Commit { subject, set, push, + yUpdate, signer, createdAt, remove, @@ -438,7 +502,7 @@ export function applyCommitToResource( resource: Resource, commit: Commit, ): Resource { - const { set, remove, push, destroy } = commit; + const { set, remove, push, destroy, yUpdate } = commit; if (set) { execSetCommit(set, resource); @@ -452,6 +516,10 @@ export function applyCommitToResource( execPushCommit(push, resource); } + if (yUpdate) { + execYUpdateCommit(yUpdate, resource); + } + if (destroy) { for (const [key] of resource.getPropVals()) { resource.setUnsafe(key, undefined); @@ -496,6 +564,28 @@ export function parseAndApplyCommit(jsonAdObjStr: string, store: Store) { } } +function parseYUpdateValue( + value: JSONValue, +): Record | undefined { + if (value === undefined) { + return undefined; + } + + if (!isJSONObject(value)) { + throw new Error(`YUpdate value is not an object: ${value}`); + } + + return Object.fromEntries( + Object.entries(value).map(([k, v]) => { + if (isSerializedYUpdate(v)) { + return [k, decodeB64(v.data)]; + } else { + throw new Error(`YUpdate contains invalid update: ${k}`); + } + }), + ); +} + function execSetCommit( set: Record, resource: Resource, @@ -526,3 +616,46 @@ function execPushCommit(push: Record, resource: Resource) { resource.setUnsafe(key, new_arr); } } + +function execYUpdateCommit( + yUpdate: Record, + resource: Resource, +) { + if (!YLoader.isLoaded()) { + console.warn( + 'Commit contains yUpdate but Yjs is not loaded. Skipping applying yjs updates', + ); + + return; + } + + const Y = YLoader.Y; + + for (const [key, value] of Object.entries(yUpdate)) { + const doc = resource.get(key); + + if (!doc) { + try { + const newDoc = new Y.Doc(); + Y.applyUpdateV2(newDoc, value); + resource.setUnsafe(key, newDoc); + } catch (e) { + console.error(e); + throw new Error(`Error applying yUpdate to new document: ${key}: ${e}`); + } + } else { + if (!(doc instanceof Y.Doc)) { + throw new Error(`Property ${key} is not a YDoc`); + } + + try { + Y.applyUpdateV2(doc, value); + } catch (e) { + console.error(e); + throw new Error( + `Error applying yUpdate to existing document: ${key}: ${e}`, + ); + } + } + } +} diff --git a/browser/lib/src/datatypes.ts b/browser/lib/src/datatypes.ts index 85d91f6ee..208496a7f 100644 --- a/browser/lib/src/datatypes.ts +++ b/browser/lib/src/datatypes.ts @@ -1,7 +1,7 @@ /** Each possible Atomic Datatype. See https://atomicdata.dev/collections/datatype */ -import { Client } from './index.js'; -import type { JSONValue } from './value.js'; +import { Client, YLoader } from './index.js'; +import type { AtomicValue } from './value.js'; // TODO: use strings from `./urls`, requires TS fix: https://github.com/microsoft/TypeScript/issues/40793 export enum Datatype { @@ -27,6 +27,7 @@ export enum Datatype { JSON = 'https://atomicdata.dev/datatypes/json', /** URI */ URI = 'https://atomicdata.dev/datatypes/uri', + YDOC = 'https://atomicdata.dev/datatypes/ydoc', UNKNOWN = 'unknown-datatype', } @@ -51,7 +52,7 @@ export interface ArrayError extends Error { /** Validates a JSON Value using a Datatype. Throws an error if things are wrong. */ export const validateDatatype = ( - value: JSONValue, + value: AtomicValue, datatype: Datatype, ): void => { let err: null | string = null; @@ -104,14 +105,14 @@ export const validateDatatype = ( } case Datatype.RESOURCEARRAY: { - if (!isArray(value)) { + if (!Array.isArray(value)) { err = 'Not an array'; break; } value.map((item, index) => { try { - Client.tryValidSubject(item); + Client.tryValidSubject(item as string); } catch (e) { const arrError: ArrayError = new Error(`Invalid URL`); arrError.index = index; @@ -134,6 +135,24 @@ export const validateDatatype = ( break; } + case Datatype.FLOAT: { + if (!isNumber(value)) { + err = 'Not a number'; + break; + } + + break; + } + + case Datatype.BOOLEAN: { + if (typeof value !== 'boolean') { + err = 'Not a boolean'; + break; + } + + break; + } + case Datatype.DATE: { if (!isString(value)) { err = 'Not a string'; @@ -147,6 +166,15 @@ export const validateDatatype = ( break; } + case Datatype.TIMESTAMP: { + if (!isNumber(value)) { + err = 'Not a number'; + break; + } + + break; + } + case Datatype.JSON: { try { JSON.stringify(value); @@ -166,6 +194,28 @@ export const validateDatatype = ( break; } + + case Datatype.YDOC: { + if (!YLoader.isLoaded()) { + console.warn( + 'Cannot validate YDoc because Yjs is not loaded. passing as valid', + ); + break; + } + + const Y = YLoader.Y; + + if (!(value instanceof Y.Doc)) { + err = 'Not a Yjs Doc'; + break; + } + + break; + } + + default: { + throw new Error(`Unsupported datatype: ${datatype}`); + } } if (err !== null) { @@ -173,15 +223,11 @@ export const validateDatatype = ( } }; -export function isArray(val: JSONValue): val is [] { - return Object.prototype.toString.call(val) === '[object Array]'; -} - -export function isString(val: JSONValue): val is string { +export function isString(val: AtomicValue): val is string { return typeof val === 'string'; } -export function isNumber(val: JSONValue): val is number { +export function isNumber(val: AtomicValue): val is number { return typeof val === 'number'; } @@ -198,5 +244,6 @@ export const reverseDatatypeMapping = { [Datatype.TIMESTAMP]: 'Timestamp', [Datatype.ATOMIC_URL]: 'Resource', [Datatype.RESOURCEARRAY]: 'ResourceArray', + [Datatype.YDOC]: 'YDoc', [Datatype.UNKNOWN]: 'Unknown', }; diff --git a/browser/lib/src/index.ts b/browser/lib/src/index.ts index 30eaf90e9..e22b789ae 100644 --- a/browser/lib/src/index.ts +++ b/browser/lib/src/index.ts @@ -51,3 +51,4 @@ export * from './truncate.js'; export * from './collection.js'; export * from './collectionBuilder.js'; export * from './ontology.js'; +export * from './yjs.js'; diff --git a/browser/lib/src/ontologies/commits.ts b/browser/lib/src/ontologies/commits.ts index 0e9a3313d..af12060bc 100644 --- a/browser/lib/src/ontologies/commits.ts +++ b/browser/lib/src/ontologies/commits.ts @@ -20,6 +20,7 @@ export const commits = { remove: 'https://atomicdata.dev/properties/remove', destroy: 'https://atomicdata.dev/properties/destroy', signature: 'https://atomicdata.dev/properties/signature', + yUpdate: 'https://atomicdata.dev/properties/yUpdate', }, __classDefs: { ['https://atomicdata.dev/classes/Commit']: [ @@ -30,6 +31,8 @@ export const commits = { 'https://atomicdata.dev/properties/destroy', 'https://atomicdata.dev/properties/remove', 'https://atomicdata.dev/properties/set', + 'https://atomicdata.dev/properties/push', + 'https://atomicdata.dev/properties/yUpdate', ], }, } as const satisfies OntologyBaseObject; @@ -51,7 +54,9 @@ declare module '../index.js' { recommends: | typeof commits.properties.destroy | typeof commits.properties.remove - | typeof commits.properties.set; + | typeof commits.properties.set + | typeof commits.properties.push + | typeof commits.properties.yUpdate; }; } @@ -66,6 +71,7 @@ declare module '../index.js' { [commits.properties.remove]: string[]; [commits.properties.destroy]: boolean; [commits.properties.signature]: string; + [commits.properties.yUpdate]: string; } interface PropSubjectToNameMapping { @@ -79,5 +85,6 @@ declare module '../index.js' { [commits.properties.remove]: 'remove'; [commits.properties.destroy]: 'destroy'; [commits.properties.signature]: 'signature'; + [commits.properties.yUpdate]: 'yUpdate'; } } diff --git a/browser/lib/src/ontology.ts b/browser/lib/src/ontology.ts index f3271337b..5478d28b2 100644 --- a/browser/lib/src/ontology.ts +++ b/browser/lib/src/ontology.ts @@ -1,4 +1,4 @@ -import { JSONValue } from './value.js'; +import { type AtomicValue } from './value.js'; export type OntologyBaseObject = { readonly classes: Record; @@ -49,7 +49,7 @@ export type InferTypeOfValueInTriple< ? Prop extends Requires ? PropTypeMapping[Prop] : PropTypeMapping[Prop] | undefined - : JSONValue, + : AtomicValue, > = Returns; type QuickAccessKnownPropType = { diff --git a/browser/lib/src/parse.ts b/browser/lib/src/parse.ts index e70601063..042fbceb5 100644 --- a/browser/lib/src/parse.ts +++ b/browser/lib/src/parse.ts @@ -1,8 +1,17 @@ import { AtomicError } from './error.js'; -import { Client, isArray } from './index.js'; +import { Client } from './index.js'; import { server } from './ontologies/server.js'; import { Resource, unknownSubject } from './resource.js'; -import type { JSONObject, JSONValue } from './value.js'; +import { + type JSONObject, + type JSONValue, + isJSONObject, + isSerializedYUpdate, + type SerializedYUpdate, +} from './value.js'; +import { decodeB64 } from './base64.js'; +import { YLoader } from './yjs.js'; +import type * as Y from 'yjs'; /** * Parses a JSON-AD object or array into resources. Create a new instance each time you need to parse a json-ad string. @@ -83,6 +92,12 @@ export class JSONADParser { continue; } + if (isSerializedYUpdate(value)) { + const doc = this.parseYDoc(value); + resource.setUnsafe(key, doc); + continue; + } + resource.setUnsafe(key, value); } @@ -101,7 +116,17 @@ export class JSONADParser { return resource; } -} -const isJSONObject = (value: JSONValue): value is JSONObject => - typeof value === 'object' && value !== null && !isArray(value); + private parseYDoc(value: SerializedYUpdate): Y.Doc | SerializedYUpdate { + if (!YLoader.isLoaded()) { + return value; + } + + const Y = YLoader.Y; + + const doc = new Y.Doc(); + Y.applyUpdateV2(doc, decodeB64(value.data)); + + return doc; + } +} diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index fb876987a..7a0d661e1 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -1,3 +1,5 @@ +import type * as Y from 'yjs'; +import { YLoader } from './yjs.js'; import { EventManager } from './EventManager.js'; import type { Agent } from './agent.js'; import { Client } from './client.js'; @@ -30,10 +32,12 @@ import { type JSONValue, type JSONArray, type JSONObject, + type AtomicValue, + isYDoc, } from './value.js'; /** Contains the PropertyURL / Value combinations */ -export type PropVals = Map; +export type PropVals = Map; /** * If a resource has no subject, it will have this subject. This means that the @@ -89,6 +93,8 @@ export class Resource { ResourceEventHandlers >(); + private errorRetries = 0; + public constructor(subject: string, newResource?: boolean) { if (typeof subject !== 'string') { // Check if the subject is an object with an @id property @@ -302,7 +308,38 @@ export class Resource { */ public clone(): Resource { const res = new Resource(this.subject); - res.propvals = structuredClone(this.propvals); + + // Filter out YDoc instances before cloning + if (YLoader.isLoaded()) { + const Y = YLoader.Y; + + const nonYdocPropvals = new Map(); + const ydocPropvals = new Map(); + + for (const [key, value] of this.propvals.entries()) { + if (!isYDoc(value)) { + // Property is not a YDoc so we can just clone it. + nonYdocPropvals.set(key, value); + continue; + } + + // Property is a YDoc so we need to make a new Y.Doc instance and apply the state of the existing YDoc. + const newDoc = new Y.Doc(); + Y.applyUpdateV2(newDoc, Y.encodeStateAsUpdateV2(value)); + ydocPropvals.set(key, newDoc); + } + + res.propvals = structuredClone(nonYdocPropvals); + + // Set the YDoc instances using setUnsafe to setup any event listeners. + for (const [key, value] of ydocPropvals.entries()) { + res.setUnsafe(key, value); + } + } else { + // Yjs is not loaded, so the propvals can't contain YDoc instances. + res.propvals = structuredClone(this.propvals); + } + res.loading = this.loading; res.new = this.new; res.error = structuredClone(this.error); @@ -434,6 +471,27 @@ export class Resource { .buildAndFetch(); } + /** Gets a YDoc from the resource, or creates a new one if it doesn't exist */ + public getYDoc(property: string): Y.Doc { + YLoader.loadCheck(); + const Y = YLoader.Y; + + const value = this.get(property); + + if (value instanceof Y.Doc) { + return value; + } + + if (value !== undefined) { + throw new Error(`Value of property ${property} is not a YDoc`); + } + + const doc = new Y.Doc(); + this.setUnsafe(property, doc); + + return doc; + } + /** builds all versions using the Commits */ public async getHistory( progressCallback?: (percentage: number) => void, @@ -491,6 +549,19 @@ export class Resource { } for (const [key, value] of versionPropvals.entries()) { + if (YLoader.isLoaded() && isYDoc(value)) { + // YDocs can't just be set so we need to handle them separately. + const Y = YLoader.Y; + + const undoUpdate = this.createUndoUpdateFromVersion(key, value); + const currentDoc = this.getYDoc(key); + + Y.applyUpdateV2(currentDoc, undoUpdate); + this.commitBuilder.addYUpdateAction(key, undoUpdate); + + continue; + } + await this.set(key, value); } @@ -732,12 +803,23 @@ export class Resource { // Logic for handling error if the previousCommit is wrong. // Is not stable enough, and maybe not required at the time. if (e.message.includes('previousCommit')) { + if (this.errorRetries > 3) { + this.errorRetries = 0; + throw e; + } + + this.errorRetries++; + console.warn('previousCommit missing or mismatch, retrying...'); // We try again, but first we fetch the latest version of the resource to get its `lastCommit` const resourceFetched = await this.store.fetchResourceFromServer( this.subject, ); + if (resourceFetched.error) { + throw resourceFetched.error; + } + const fixedLastCommit = resourceFetched! .get(properties.commit.lastCommit) ?.toString(); @@ -786,6 +868,13 @@ export class Resource { validate = false; } + // YDocs can not be set, sadly we can't really remove them from the value type so we have to throw an error. + if (isYDoc(value)) { + throw new Error( + 'YDoc values can not be set, you should edit the YDoc value directly.', + ); + } + if (validate) { const fullProp = await this.store.getProperty(prop); @@ -817,8 +906,12 @@ export class Resource { * Set a Property, Value combination without performing validations or adding * it to the CommitBuilder. */ - public setUnsafe(prop: string, val: JSONValue): void { + public setUnsafe(prop: string, val: AtomicValue): void { this.propvals.set(prop, val); + + if (isYDoc(val)) { + val.on('updateV2', this.buildYDocCallback(prop)); + } } /** Sets the error on the Resource. Does not Throw. */ @@ -851,6 +944,46 @@ export class Resource { return parent.new; } + + private createUndoUpdateFromVersion(key: string, oldDoc: Y.Doc): Uint8Array { + const Y = YLoader.Y; + YLoader.loadCheck(); + + const currentDoc = this.propvals.get(key) as Y.Doc | undefined; + + // If the current value does not exist anymore we just return the old state as there is nothing to undo. + if (currentDoc === undefined) { + return Y.encodeStateAsUpdateV2(oldDoc); + } + + const oldStateVector = Y.encodeStateVector(oldDoc); + + // Get an update of all changes after the old document. + const diffUpdate = Y.encodeStateAsUpdateV2(currentDoc, oldStateVector); + const undoManager = new Y.UndoManager(oldDoc); + + Y.applyUpdateV2(oldDoc, diffUpdate); + // The two docs are now in sync but the undo manager tracked the change to the old doc. + undoManager.undo(); + + // The undo manager created a new update that removes all the changes we just made effectively reverting all changes made since the old document. + return Y.encodeStateAsUpdateV2(oldDoc, Y.encodeStateVector(currentDoc)); + } + + private buildYDocCallback( + property: string, + ): ( + update: Uint8Array, + _origin: unknown, + _doc: unknown, + transaction: Y.Transaction, + ) => void { + return (update, _origin, _doc, transaction) => { + if (transaction.local) { + this.commitBuilder.addYUpdateAction(property, update); + } + }; + } } /** Type of Rights (e.g. read or write) */ diff --git a/browser/lib/src/store.ts b/browser/lib/src/store.ts index 9db5252cb..4b91e1ee9 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -22,11 +22,13 @@ import type { JSONValue } from './value.js'; import { authenticate, fetchWebSocket, startWebsocket } from './websockets.js'; import { endpoints } from './urls.js'; import { initOntologies } from './ontologies/index.js'; +import { decodeB64, encodeB64 } from './base64.js'; /** Function called when a resource is updated or removed */ type ResourceCallback = ( resource: Resource, ) => void; +type AwarenessCallback = (update: Uint8Array) => void; type SubjectCallback = (subject: string) => void; /** Callback called when the stores agent changes */ type AgentCallback = (agent: Agent | undefined) => void; @@ -115,7 +117,8 @@ const supportsWebSockets = () => typeof WebSocket !== 'undefined'; */ export class Store { /** A list of all functions that need to be called when a certain resource is updated */ - public subscribers: Map>; + public subscribers: Map; + private awarenessSubscribers: Map = new Map(); private injectedFetch: Fetch; /** * The base URL of an Atomic Server. This is where to send commits, create new @@ -815,6 +818,85 @@ export class Store { } } + /** + * Subscribe to Yjs Awareness updates for a resource. + * @param subject The subject of the resource that you want to subscribe to. + * @param callback The callback that will be called when the awareness state changes. You should apply the update to your awareness instance here. + * @returns A function that can be called to unsubscribe. + */ + public subscribeAwareness( + subject: string, + callback: (update: Uint8Array) => void, + ): () => void { + const ws = this.getWebSocketForSubject(subject); + + const unsub = () => { + const subscribers = this.awarenessSubscribers.get(subject); + + if (subscribers) { + const afterUnsub = subscribers.filter(item => item !== callback); + + if (afterUnsub.length === 0) { + this.awarenessSubscribers.delete(subject); + + if (ws?.readyState === 1) { + ws?.send(`Y_AWARENESS_UNSUBSCRIBE ${subject}`); + } + } else { + this.awarenessSubscribers.set(subject, afterUnsub); + } + } + }; + + const subscribers = this.awarenessSubscribers.get(subject); + + if (subscribers) { + subscribers.push(callback); + + return unsub; + } + + this.awarenessSubscribers.set(subject, [callback]); + + if (ws?.readyState === 1) { + ws?.send(`Y_AWARENESS_SUBSCRIBE ${subject}`); + } + + return unsub; + } + + /** + * Notify the store that your awareness state changed, the store will send the update to the server. + * @param subject The subject of the resource that your awareness state changed for. + * @param update The binary encoded update to send to the server. + */ + public notifyAwarenessUpdate(subject: string, update: Uint8Array): void { + const ws = this.getWebSocketForSubject(subject); + + const messageBody = { + subject: subject, + update: encodeB64(update), + }; + + if (ws?.readyState === 1) { + ws?.send(`Y_AWARENESS_UPDATE ${JSON.stringify(messageBody)}`); + } + } + + /** + * @Internal + */ + public __handleAwarenessUpdateMessage(message: string): void { + const messageBody = JSON.parse(message); + const update = decodeB64(messageBody.update); + + const subscribers = this.awarenessSubscribers.get(messageBody.subject); + + if (subscribers) { + subscribers.forEach(callback => callback(update)); + } + } + public unSubscribeWebSocket(subject: string): void { if (subject === unknownSubject) { return; diff --git a/browser/lib/src/value.ts b/browser/lib/src/value.ts index c37a69a9b..70d0b7520 100644 --- a/browser/lib/src/value.ts +++ b/browser/lib/src/value.ts @@ -1,16 +1,25 @@ import { JSONADParser } from './parse.js'; import type { Resource } from './resource.js'; +import type * as Y from 'yjs'; +import { YLoader } from './yjs.js'; export type JSONPrimitive = string | number | boolean; export type JSONValue = JSONPrimitive | JSONObject | JSONArray | undefined; export type JSONObject = { [key: string]: JSONValue }; export type JSONArray = Array; +export type AtomicValue = JSONValue | Y.Doc; + +export type SerializedYUpdate = { + type: 'ydoc'; + data: string; +}; + /** * Tries to convert the value as an array of resources, which can be both URLs * or Nested Resources. Throws an error when fails */ -export function valToArray(val?: JSONValue): JSONArray { +export function valToArray(val?: AtomicValue): JSONArray { if (val === undefined) { throw new Error(`Not an array: ${val}, is ${typeof val}`); } @@ -23,7 +32,7 @@ export function valToArray(val?: JSONValue): JSONArray { } /** Tries to make a boolean from this value. Throws if it is not a boolean. */ -export function valToBoolean(val?: JSONValue): boolean { +export function valToBoolean(val?: AtomicValue): boolean { if (typeof val !== 'boolean') { throw new Error(`Not a boolean: ${val}, is a ${typeof val}`); } @@ -35,7 +44,7 @@ export function valToBoolean(val?: JSONValue): boolean { * Tries to convert the value (timestamp or date) to a JS Date. Throws an error * when fails. */ -export function valToDate(val?: JSONValue): Date { +export function valToDate(val?: AtomicValue): Date { // If it's a unix epoch timestamp... if (typeof val === 'number') { const date = new Date(0); // The 0 there is the key, which sets the date to the epoch @@ -52,7 +61,7 @@ export function valToDate(val?: JSONValue): Date { } /** Returns a number of the value, or throws an error */ -export function valToNumber(val?: JSONValue): number { +export function valToNumber(val?: AtomicValue): number { if (typeof val !== 'number') { throw new Error(`Not a number: ${val}, is a ${typeof val}`); } @@ -61,13 +70,13 @@ export function valToNumber(val?: JSONValue): number { } /** Returns a default string representation of the value. */ -export function valToString(val: JSONValue): string { +export function valToString(val: AtomicValue): string { // val && val.toString(); return val?.toString() ?? 'undefined'; } /** Returns either the URL of the resource, or the NestedResource itself. */ -export function valToResource(val: JSONValue): string | Resource { +export function valToResource(val: AtomicValue): string | Resource { if (typeof val === 'string') { return val; } @@ -93,3 +102,23 @@ export function valToResource(val: JSONValue): string | Resource { throw new Error(`Not a resource: ${val}, is a ${typeof val}`); } + +export function isYDoc(val: AtomicValue): val is Y.Doc { + if (!YLoader.isLoaded()) { + return false; + } + + const Y = YLoader.Y; + + return val instanceof Y.Doc; +} + +export const isJSONObject = (value: JSONValue): value is JSONObject => + typeof value === 'object' && value !== null && !Array.isArray(value); + +export const isSerializedYUpdate = ( + value: JSONValue, +): value is SerializedYUpdate => + isJSONObject(value) && + value.type === 'ydoc' && + typeof value.data === 'string'; diff --git a/browser/lib/src/websockets.ts b/browser/lib/src/websockets.ts index 14f3ef77a..946824267 100644 --- a/browser/lib/src/websockets.ts +++ b/browser/lib/src/websockets.ts @@ -45,6 +45,9 @@ function handleMessage(ev: MessageEvent, store: Store) { } else if (ev.data.startsWith('RESOURCE ')) { const resources = parseResourceMessage(ev); store.addResources(resources); + } else if (ev.data.startsWith('Y_AWARENESS_UPDATE ')) { + const update = ev.data.slice(18); + store.__handleAwarenessUpdateMessage(update); } else { console.warn('Unknown websocket message:', ev); } diff --git a/browser/lib/src/yjs.ts b/browser/lib/src/yjs.ts new file mode 100644 index 000000000..795fde0fc --- /dev/null +++ b/browser/lib/src/yjs.ts @@ -0,0 +1,43 @@ +import type * as Y from 'yjs'; + +/** + * To prevent bloat we don't always want to include Yjs in the bundle. + * Since Yjs is an optional dependency, we need to load it lazily and it might not even be installed. + */ +export class YLoader { + private static _Y: typeof Y | undefined; + + public static get Y(): typeof Y { + if (!this._Y) { + throw new Error('Y not initialized'); + } + + return this._Y; + } + + public static async initializeY(): Promise { + if (this._Y) { + return; + } + + this._Y = await import('yjs'); + } + + public static isLoaded(): boolean { + return this._Y !== undefined; + } + + public static loadCheck(): void { + if (!this.isLoaded()) { + throw new Error('Yjs not initialized'); + } + } +} + +/** + * Enables the use of Yjs features in the library. + * Call this somewhere early on in your application and make sure the yjs package is installed. + */ +export const enableYjs = async () => { + await YLoader.initializeY(); +}; diff --git a/browser/package.json b/browser/package.json index b85d16682..0afd65d62 100644 --- a/browser/package.json +++ b/browser/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "@types/node": "^20.17.0", + "@types/node": "^24.7.0", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.1", @@ -16,7 +16,7 @@ "prettier-plugin-jsdoc": "^1.3.0", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.3.0", - "typescript": "^5.6.3", + "typescript": "^5.9.3", "vite": "^5.4.10", "vitest": "^2.1.3" }, diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 6dcabba96..6f24bc0f7 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -9,14 +9,14 @@ importers: .: devDependencies: '@types/node': - specifier: ^20.17.0 - version: 20.17.0 + specifier: ^24.7.0 + version: 24.7.0 '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^7.18.0 - version: 7.18.0(eslint@8.57.1)(typescript@5.6.3) + version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) eslint: specifier: ^8.57.1 version: 8.57.1 @@ -25,7 +25,7 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) eslint-plugin-jsx-a11y: specifier: ^6.10.1 version: 6.10.1(eslint@8.57.1) @@ -43,7 +43,7 @@ importers: version: 8.0.3 netlify-cli: specifier: 17.37.1 - version: 17.37.1(@swc/core@1.7.39)(@types/node@20.17.0)(picomatch@4.0.3) + version: 17.37.1(@swc/core@1.7.39)(@types/node@24.7.0)(picomatch@4.0.3) prettier: specifier: 3.2.5 version: 3.2.5 @@ -52,19 +52,19 @@ importers: version: 1.3.0(prettier@3.2.5) typedoc: specifier: ^0.25.13 - version: 0.25.13(typescript@5.6.3) + version: 0.25.13(typescript@5.9.3) typedoc-plugin-missing-exports: specifier: ^2.3.0 - version: 2.3.0(typedoc@0.25.13(typescript@5.6.3)) + version: 2.3.0(typedoc@0.25.13(typescript@5.9.3)) typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.3 + version: 5.9.3 vite: specifier: ^5.4.10 - version: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + version: 5.4.10(@types/node@24.7.0)(terser@5.43.1) vitest: specifier: ^2.1.3 - version: 2.1.3(@types/node@20.17.0)(terser@5.43.1) + version: 2.1.3(@types/node@24.7.0)(terser@5.43.1) cli: dependencies: @@ -78,8 +78,8 @@ importers: specifier: 3.0.3 version: 3.0.3 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.3 + version: 5.9.3 create-template: dependencies: @@ -97,14 +97,14 @@ importers: specifier: ^20.17.0 version: 20.17.0 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.3 + version: 5.9.3 data-browser: dependencies: '@ai-sdk/react': specifier: ^2.0.29 - version: 2.0.29(react@19.0.0)(zod@4.1.5) + version: 2.0.29(react@19.2.0)(zod@4.1.5) '@bugsnag/core': specifier: ^7.25.0 version: 7.25.0 @@ -125,19 +125,22 @@ importers: version: 1.1.4 '@dnd-kit/core': specifier: ^6.1.0 - version: 6.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 6.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@dnd-kit/sortable': specifier: ^8.0.0 - version: 8.0.0(@dnd-kit/core@6.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 8.0.0(@dnd-kit/core@6.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@19.0.0) + version: 3.2.2(react@19.2.0) '@emoji-mart/react': specifier: ^1.1.1 - version: 1.1.1(emoji-mart@5.6.0)(react@19.0.0) + version: 1.1.1(emoji-mart@5.6.0)(react@19.2.0) '@emotion/is-prop-valid': specifier: ^1.3.1 version: 1.3.1 + '@floating-ui/dom': + specifier: ^1.7.4 + version: 1.7.4 '@modelcontextprotocol/sdk': specifier: ^1.13.3 version: 1.17.0 @@ -149,46 +152,55 @@ importers: version: 1.2.0(ai@5.0.29(zod@4.1.5))(zod@4.1.5) '@radix-ui/react-popover': specifier: ^1.1.2 - version: 1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-scroll-area': specifier: ^1.2.0 - version: 1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-tabs': specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-router': specifier: ^1.95.1 - version: 1.95.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.95.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tiptap/extension-collaboration': + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) + '@tiptap/extension-collaboration-caret': + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) '@tiptap/extension-file-handler': - specifier: ^2.25.0 - version: 2.25.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/extension-text-style@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/extension-text-style@2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)))(@tiptap/pm@3.6.5) '@tiptap/extension-image': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) '@tiptap/extension-link': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) '@tiptap/extension-mention': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/suggestion@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) '@tiptap/extension-placeholder': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) '@tiptap/extension-typography': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) '@tiptap/pm': - specifier: ^2.11.7 - version: 2.11.7 + specifier: ^3.6.5 + version: 3.6.5 '@tiptap/react': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^3.6.5 + version: 3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tiptap/starter-kit': - specifier: ^2.11.7 - version: 2.11.7 + specifier: ^3.6.5 + version: 3.6.5 '@tiptap/suggestion': - specifier: ^2.11.7 - version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + specifier: ^3.6.5 + version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/y-tiptap': + specifier: ^3.0.0 + version: 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) '@tomic/react': specifier: workspace:* version: link:../react @@ -197,10 +209,10 @@ importers: version: 4.24.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1) '@uiw/react-codemirror': specifier: ^4.24.1 - version: 4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@wuchale/jsx': specifier: ^0.7.4 - version: 0.7.4(react@19.0.0) + version: 0.7.4(react@19.2.0) '@wuchale/vite-plugin': specifier: ^0.14.6 version: 0.14.6 @@ -212,7 +224,7 @@ importers: version: 2.1.1 downshift: specifier: ^9.0.9 - version: 9.0.10(react@19.0.0) + version: 9.0.10(react@19.2.0) emoji-mart: specifier: ^5.6.0 version: 5.6.0 @@ -229,72 +241,75 @@ importers: specifier: ^0.2.0 version: 0.2.0 react: - specifier: ^19.0.0 - version: 19.0.0 + specifier: ^19.2.0 + version: 19.2.0 react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 5.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) react-dropzone: specifier: ^11.7.1 - version: 11.7.1(react@19.0.0) + version: 11.7.1(react@19.2.0) react-hot-toast: specifier: ^2.4.1 - version: 2.4.1(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.4.1(csstype@3.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-hotkeys-hook: specifier: ^3.4.7 - version: 3.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 3.4.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-icons: specifier: ^4.12.0 - version: 4.12.0(react@19.0.0) + version: 4.12.0(react@19.2.0) react-intersection-observer: specifier: ^9.13.1 - version: 9.13.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 9.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-is: specifier: ^19.0.0 version: 19.0.0 react-markdown: specifier: ^9.0.3 - version: 9.0.3(@types/react@19.0.1)(react@19.0.0) + version: 9.0.3(@types/react@19.0.1)(react@19.2.0) react-pdf: specifier: ^9.1.1 - version: 9.1.1(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 9.1.1(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-virtualized-auto-sizer: specifier: ^1.0.24 - version: 1.0.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.0.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-window: specifier: ^1.8.10 - version: 1.8.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.8.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0) reactflow: specifier: ^11.11.4 - version: 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) remark-gfm: specifier: ^4.0.0 version: 4.0.0 styled-components: specifier: ^6.1.19 - version: 6.1.19(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0) stylis: specifier: 4.3.0 version: 4.3.0 - tippy.js: - specifier: ^6.3.7 - version: 6.3.7 tiptap-markdown: specifier: ^0.8.10 - version: 0.8.10(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + version: 0.8.10(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) wuchale: specifier: ^0.16.5 version: 0.16.5 + y-protocols: + specifier: ^1.0.6 + version: 1.0.6(yjs@13.6.27) + yjs: + specifier: ^13.6.27 + version: 13.6.27 zod: specifier: ^4.1.5 version: 4.1.5 devDependencies: '@tanstack/router-devtools': specifier: ^1.95.1 - version: 1.95.1(@tanstack/react-router@1.95.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.95.1(@tanstack/react-router@1.95.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(csstype@3.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/prismjs': specifier: ^1.26.5 version: 1.26.5 @@ -309,13 +324,13 @@ importers: version: 1.8.8 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + version: 4.3.4(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) babel-plugin-react-compiler: specifier: 19.1.0-rc.2 version: 19.1.0-rc.2 babel-plugin-styled-components: specifier: ^2.1.4 - version: 2.1.4(@babel/core@7.26.0)(styled-components@6.1.19(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) + version: 2.1.4(@babel/core@7.26.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) csstype: specifier: ^3.1.3 version: 3.1.3 @@ -329,20 +344,20 @@ importers: specifier: ^1.1.0 version: 1.1.0 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.3 + version: 5.9.3 vite: specifier: ^5.4.10 - version: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + version: 5.4.10(@types/node@24.7.0)(terser@5.43.1) vite-plugin-prismjs: specifier: ^0.0.11 version: 0.0.11(prismjs@1.29.0) vite-plugin-pwa: specifier: ^0.20.5 - version: 0.20.5(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) + version: 0.20.5(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) vite-plugin-webfont-dl: specifier: ^3.9.5 - version: 3.9.5(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + version: 3.9.5(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) e2e: devDependencies: @@ -367,22 +382,22 @@ importers: '@noble/hashes': specifier: ^0.5.9 version: 0.5.9 - base64-arraybuffer: - specifier: ^1.0.2 - version: 1.0.2 fast-json-stable-stringify: specifier: ^2.1.0 version: 2.1.0 ulidx: specifier: ^2.4.1 version: 2.4.1 + yjs: + specifier: ^13.6.27 + version: 13.6.27 devDependencies: '@arethetypeswrong/cli': specifier: ^0.17.0 version: 0.17.0 '@microsoft/api-extractor': specifier: ^7.48.0 - version: 7.48.0(@types/node@20.17.0) + version: 7.48.0(@types/node@24.7.0) '@tomic/cli': specifier: workspace:* version: link:../cli @@ -394,13 +409,13 @@ importers: version: 2.8.0 tsup: specifier: ^8.3.5 - version: 8.3.5(@microsoft/api-extractor@7.48.0(@types/node@20.17.0))(@swc/core@1.7.39)(jiti@2.3.3)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.5(@microsoft/api-extractor@7.48.0(@types/node@24.7.0))(@swc/core@1.7.39)(jiti@2.3.3)(postcss@8.4.49)(typescript@5.9.3)(yaml@2.6.0) typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.3 + version: 5.9.3 vitest: specifier: ^2.1.3 - version: 2.1.3(@types/node@20.17.0)(terser@5.43.1) + version: 2.1.3(@types/node@24.7.0)(terser@5.43.1) react: dependencies: @@ -428,10 +443,13 @@ importers: version: 5.3.3 tsup: specifier: ^8.3.5 - version: 8.3.5(@microsoft/api-extractor@7.48.0(@types/node@20.17.0))(@swc/core@1.7.39)(jiti@2.3.3)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.5(@microsoft/api-extractor@7.48.0(@types/node@24.7.0))(@swc/core@1.7.39)(jiti@2.3.3)(postcss@8.4.49)(typescript@5.9.3)(yaml@2.6.0) typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.9.3 + version: 5.9.3 + yjs: + specifier: ^13.6.27 + version: 13.6.27 svelte: dependencies: @@ -441,16 +459,16 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^3.3.0 - version: 3.3.0(@sveltejs/kit@2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))) + version: 3.3.0(@sveltejs/kit@2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))) '@sveltejs/kit': specifier: ^2.7.2 - version: 2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + version: 2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) '@sveltejs/package': specifier: ^2.3.6 version: 2.3.6(svelte@5.1.4)(typescript@5.6.3) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.0 - version: 4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + version: 4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -462,7 +480,7 @@ importers: version: 9.1.0(eslint@9.13.0(jiti@2.3.3)) eslint-plugin-svelte: specifier: ^2.46.0 - version: 2.46.0(eslint@9.13.0(jiti@2.3.3))(svelte@5.1.4)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)) + version: 2.46.0(eslint@9.13.0(jiti@2.3.3))(svelte@5.1.4)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) globals: specifier: ^15.11.0 version: 15.11.0 @@ -480,7 +498,7 @@ importers: version: 5.1.4 svelte-check: specifier: ^3.8.6 - version: 3.8.6(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4) + version: 3.8.6(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -489,10 +507,10 @@ importers: version: 8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3) vite: specifier: ^5.4.10 - version: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + version: 5.4.10(@types/node@24.7.0)(terser@5.43.1) vitest: specifier: ^2.1.3 - version: 2.1.3(@types/node@20.17.0)(terser@5.43.1) + version: 2.1.3(@types/node@24.7.0)(terser@5.43.1) packages: @@ -1828,17 +1846,11 @@ packages: '@fastify/static@7.0.4': resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} - '@floating-ui/core@1.6.8': - resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} - - '@floating-ui/core@1.7.2': - resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.6.11': - resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==} - - '@floating-ui/dom@1.7.2': - resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} '@floating-ui/react-dom@2.1.2': resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} @@ -1849,9 +1861,6 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@floating-ui/utils@0.2.8': - resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} - '@humanfs/core@0.19.0': resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} engines: {node: '>=18.18.0'} @@ -2336,9 +2345,6 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} - '@popperjs/core@2.11.8': - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -2988,177 +2994,219 @@ packages: '@tanstack/store@0.7.0': resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} - '@tiptap/core@2.11.7': - resolution: {integrity: sha512-zN+NFFxLsxNEL8Qioc+DL6b8+Tt2bmRbXH22Gk6F6nD30x83eaUSFlSv3wqvgyCq3I1i1NO394So+Agmayx6rQ==} + '@tiptap/core@3.6.5': + resolution: {integrity: sha512-CgXuhevQbBcPfxaXzGZgIY9+aVMSAd68Q21g3EONz1iZBw026QgiaLhGK6jgGTErZL4GoNL/P+gC5nFCvN7+cA==} peerDependencies: - '@tiptap/pm': ^2.7.0 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-blockquote@2.11.7': - resolution: {integrity: sha512-liD8kWowl3CcYCG9JQlVx1eSNc/aHlt6JpVsuWvzq6J8APWX693i3+zFqyK2eCDn0k+vW62muhSBe3u09hA3Zw==} + '@tiptap/extension-blockquote@3.6.5': + resolution: {integrity: sha512-FOOgkLHXQ3zTiL2V1js5+PfaOHXuyr/GjeFZe+W1AUk58X/qJNOVGvKT1xlMOy9gy2ySgWmco7PhNXRRTimkWg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-bold@2.11.7': - resolution: {integrity: sha512-VTR3JlldBixXbjpLTFme/Bxf1xeUgZZY3LTlt5JDlCW3CxO7k05CIa+kEZ8LXpog5annytZDUVtWqxrNjmsuHQ==} + '@tiptap/extension-bold@3.6.5': + resolution: {integrity: sha512-8JXC+K4DXtPDbClHxgRAZnXYO2an2I86PbpqUw+S7m17XCr4t39Sw9CeNBohOHS6Cl8uxOKAjSyCZzqdnYkn3g==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-bubble-menu@2.11.7': - resolution: {integrity: sha512-0vYqSUSSap3kk3/VT4tFE1/6StX70I3/NKQ4J68ZSFgkgyB3ZVlYv7/dY3AkEukjsEp3yN7m8Gw8ei2eEwyzwg==} + '@tiptap/extension-bubble-menu@3.6.5': + resolution: {integrity: sha512-RyCJghtkYZAljZQUfjk3B5tvVVCILsIYMR9XnC152uBiIuWsnz25qfdyBP+cOl6ONrQUvdscs0WmKvzN+nXZYw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-bullet-list@2.11.7': - resolution: {integrity: sha512-WbPogE2/Q3e3/QYgbT1Sj4KQUfGAJNc5pvb7GrUbvRQsAh7HhtuO8hqdDwH8dEdD/cNUehgt17TO7u8qV6qeBw==} + '@tiptap/extension-bullet-list@3.6.5': + resolution: {integrity: sha512-AP81hyN7oTyv5zbNVRK35cQA7zuLnI5ItFFyqMQKWh90vfftXi/zhC9C7FWvKtEH7Kk68B338G2mi4tlXDgBFQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.6.5 - '@tiptap/extension-code-block@2.11.7': - resolution: {integrity: sha512-To/y/2H04VWqiANy53aXjV7S6fA86c2759RsH1hTIe57jA1KyE7I5tlAofljOLZK/covkGmPeBddSPHGJbz++Q==} + '@tiptap/extension-code-block@3.6.5': + resolution: {integrity: sha512-VPPke3LqZYKPlbDBp8IcTJQwvYb1PP0L+2Qi2n3ebN4+gKn+KGhrjnkO+xNHCySWlqywQmMTIfWX1sxA0eVVdQ==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-code@2.11.7': - resolution: {integrity: sha512-VpPO1Uy/eF4hYOpohS/yMOcE1C07xmMj0/D989D9aS1x95jWwUVrSkwC+PlWMUBx9PbY2NRsg1ZDwVvlNKZ6yQ==} + '@tiptap/extension-code@3.6.5': + resolution: {integrity: sha512-U/cJFjE0hqBTbMb5J74e7ni5YReuJgS9NyJgTy94+Xt6vxR1vU4+qOl+3E0fOZtwDrxbLrsCQy3P3LvNb3HXdw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-document@2.11.7': - resolution: {integrity: sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==} + '@tiptap/extension-collaboration-caret@3.6.5': + resolution: {integrity: sha512-3tKnl4Y9zSYZcfQLKFhIg2QRUfSC5MHF11y8xKf7y04zuEnVuscAhaNkgjimt19EvG0LZ4JP5g7KoeoltBSqeQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + '@tiptap/y-tiptap': ^3.0.0-beta.3 - '@tiptap/extension-dropcursor@2.11.7': - resolution: {integrity: sha512-63mL+nxQILizsr5NbmgDeOjFEWi34BLt7evwL6UUZEVM15K8V1G8pD9Y0kCXrZYpHWz0tqFRXdrhDz0Ppu8oVw==} + '@tiptap/extension-collaboration@3.6.5': + resolution: {integrity: sha512-IbyZNGUo8xYYsZ09BJxuA/VHqpH8x+he9mUShfmT+PtBvAxiU3beq2B2yXIGBmiBW7At5C6JmDK9PvAGeBYvlw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + '@tiptap/y-tiptap': ^3.0.0-beta.3 + yjs: ^13 - '@tiptap/extension-file-handler@2.25.0': - resolution: {integrity: sha512-8qALTIz8rRumHP1vXYwCJ8IflfWJ8b9PMc/pcTOmyMfpUzwSn9tO6iVjuvWr5VgrcSgWErJB3YUq/1JiyxiCDA==} + '@tiptap/extension-document@3.6.5': + resolution: {integrity: sha512-0c7kxWBIEIcoHUG89vpHOF2h4CMa0q6VWXhZ+6iqcI5uyqaKwgcW/TbHZR0nAwEsZLdRCKaryn2kO7jXiCjfnA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/extension-text-style': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-floating-menu@2.14.0': - resolution: {integrity: sha512-Khx7M7RfZlD1/T/PUlpJmao6FtEBa2L6td2hhaW1USflwGJGk0U/ud4UEqh+aZoJZrkot/EMhEvzmORF3nq+xw==} + '@tiptap/extension-dropcursor@3.6.5': + resolution: {integrity: sha512-BsO3ufLHsdeV1ddChwQfi2Q4UkeqOF4LeUYPYBKfSg59aRKTSoxj3gZrAsaAm/0O3DmAiKNBiCtNRTJSApPEBQ==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/extensions': ^3.6.5 - '@tiptap/extension-gapcursor@2.11.7': - resolution: {integrity: sha512-EceesmPG7FyjXZ8EgeJPUov9G1mAf2AwdypxBNH275g6xd5dmU/KvjoFZjmQ0X1ve7mS+wNupVlGxAEUYoveew==} + '@tiptap/extension-file-handler@3.6.5': + resolution: {integrity: sha512-r0cR6ZbdtEkGG7V5taRm9TcMCXwIOFHC0niER2MxWVw+KsQdAeZEtTBf8YeIu5CoI5z7j95X9d2o4AaavYjIUw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/extension-text-style': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-hard-break@2.11.7': - resolution: {integrity: sha512-zTkZSA6q+F5sLOdCkiC2+RqJQN0zdsJqvFIOVFL/IDVOnq6PZO5THzwRRLvOSnJJl3edRQCl/hUgS0L5sTInGQ==} + '@tiptap/extension-floating-menu@3.6.5': + resolution: {integrity: sha512-ASKb5vHkYyB9g3vOAr2E2U+b6MbHk4Ff4PqngafGlWRAmOAmFxTcw9fLa3HKnj4pokSsYAEvYGOso99/W3GzhA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-heading@2.11.7': - resolution: {integrity: sha512-8kWh7y4Rd2fwxfWOhFFWncHdkDkMC1Z60yzIZWjIu72+6yQxvo8w3yeb7LI7jER4kffbMmadgcfhCHC/fkObBA==} + '@tiptap/extension-gapcursor@3.6.5': + resolution: {integrity: sha512-SHtp71zhV2bAQS8kaJ/otb2podGusDREZ9/SQ1rZi6yPcDFLS2KvIvsLssDwbjTuH6KefnsN6Vx01tzmXRAQig==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extensions': ^3.6.5 - '@tiptap/extension-history@2.11.7': - resolution: {integrity: sha512-Cu5x3aS13I040QSRoLdd+w09G4OCVfU+azpUqxufZxeNs9BIJC+0jowPLeOxKDh6D5GGT2A8sQtxc6a/ssbs8g==} + '@tiptap/extension-hard-break@3.6.5': + resolution: {integrity: sha512-6iMS6SzIn7+X95okRX8y3l/4f1G3lTrq24sbcAX4MHITncDC6g3TrdAxdA67Tqn5NI/OQx0LwF3kFJDO8QTAUg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-horizontal-rule@2.11.7': - resolution: {integrity: sha512-uVmQwD2dzZ5xwmvUlciy0ItxOdOfQjH6VLmu80zyJf8Yu7mvwP8JyxoXUX0vd1xHpwAhgQ9/ozjIWYGIw79DPQ==} + '@tiptap/extension-heading@3.6.5': + resolution: {integrity: sha512-jFS5saqTtfG6MM0sW4X6mZlLycT2ud0Oo1GOZkCyBClwSOpZI/EBLNRIgoXgNtWrY917vB7xTQgCpTVHbvVRsQ==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-image@2.11.7': - resolution: {integrity: sha512-YvCmTDB7Oo+A56tR4S/gcNaYpqU4DDlSQcRp5IQvmQV5EekSe0lnEazGDoqOCwsit9qQhj4MPQJhKrnaWrJUrg==} + '@tiptap/extension-horizontal-rule@3.6.5': + resolution: {integrity: sha512-yNxcejI25j6NQMQuKQMTVmNYLnrHFCpzGAz1Ndzyar+gItYZXI9BLmMlwpLkIaJMpIKChj+2qHz25fPS5FlNFw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-italic@2.11.7': - resolution: {integrity: sha512-r985bkQfG0HMpmCU0X0p/Xe7U1qgRm2mxvcp6iPCuts2FqxaCoyfNZ8YnMsgVK1mRhM7+CQ5SEg2NOmQNtHvPw==} + '@tiptap/extension-image@3.6.5': + resolution: {integrity: sha512-Tzej5vSjiIPmr+3zeFYIGOdZ7T+tnOMMuFuduiitynTsVY2oG34Y/oBnwBfD+jLq8v3SBFF55J972Ga6+vBvrA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-link@2.11.7': - resolution: {integrity: sha512-qKIowE73aAUrnQCIifYP34xXOHOsZw46cT/LBDlb0T60knVfQoKVE4ku08fJzAV+s6zqgsaaZ4HVOXkQYLoW7g==} + '@tiptap/extension-italic@3.6.5': + resolution: {integrity: sha512-2EtO2uffw5YnTQ1cieLPv9t7OKCfJFbgHRJPXf7Nnfh8XFh5AEyzw0qBNXZyLtlB28+HHSWLc/OHS6xMfwUy0A==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-list-item@2.11.7': - resolution: {integrity: sha512-6ikh7Y+qAbkSuIHXPIINqfzmWs5uIGrylihdZ9adaIyvrN1KSnWIqrZIk/NcZTg5YFIJlXrnGSRSjb/QM3WUhw==} + '@tiptap/extension-link@3.6.5': + resolution: {integrity: sha512-VLCDNwxLC1IPnWT3HLLJUg1Hflf8A2jfs7aNF4vyMTWmKnrk1zmN+VyXQTAkrqr27qE5FnmLhHOYF3SNolNucw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-mention@2.11.7': - resolution: {integrity: sha512-Q/fkceDOug4VjiqrCRLzBnOL9Oj+XugWwDgwfucJJMBOJxZ3++3eZGZ54dri/xK39A4ZD+xuMBF7PrJIy+Z5dw==} + '@tiptap/extension-list-item@3.6.5': + resolution: {integrity: sha512-A5JKf2dNG6IRrHmkaqroq/VcD5SnXYXgpQpsF7HrPGIzUSIjvjQu088980NQPHyMuTanDMml+nZgd8RzHhRISA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - '@tiptap/suggestion': ^2.7.0 + '@tiptap/extension-list': ^3.6.5 - '@tiptap/extension-ordered-list@2.11.7': - resolution: {integrity: sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==} + '@tiptap/extension-list-keymap@3.6.5': + resolution: {integrity: sha512-OHGGTJMdUOBincMgYGEN4WzHrTB/GFeCxLDJraDknPx4VJVa3UVZS8F8xd5cb2WnACEF33Ud/0yK3aN6kHrbtQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.6.5 - '@tiptap/extension-paragraph@2.11.7': - resolution: {integrity: sha512-Pl3B4q6DJqTvvAdraqZaNP9Hh0UWEHL5nNdxhaRNuhKaUo7lq8wbDSIxIW3lvV0lyCs0NfyunkUvSm1CXb6d4Q==} + '@tiptap/extension-list@3.6.5': + resolution: {integrity: sha512-2S6wNeaGvvYzJygBhHRLP0YubJAzY00WxQSO3NvHFeLFRFvilCnmh0JGMAqsNU+Owpz0iVrWY0YZskN5gPeR9w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 - '@tiptap/extension-placeholder@2.11.7': - resolution: {integrity: sha512-/06zXV4HIjYoiaUq1fVJo/RcU8pHbzx21evOpeG/foCfNpMI4xLU/vnxdUi6/SQqpZMY0eFutDqod1InkSOqsg==} + '@tiptap/extension-mention@3.6.5': + resolution: {integrity: sha512-ACElkBvemEJGm8gVYI4QKjf6tfNj3m5dC9MkZL4rwZo4CAwjiNQ8oFhj1x7sPO1OVlnjt+FhnItBix5ztTF8Ng==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + '@tiptap/suggestion': ^3.6.5 - '@tiptap/extension-strike@2.11.7': - resolution: {integrity: sha512-D6GYiW9F24bvAY7XMOARNZbC8YGPzdzWdXd8VOOJABhf4ynMi/oW4NNiko+kZ67jn3EGaKoz32VMJzNQgYi1HA==} + '@tiptap/extension-ordered-list@3.6.5': + resolution: {integrity: sha512-RiBl0Dkw8QtzS7OqUGm84BOyemw/N+hf8DYWsIqVysMRQAGBGhuklbw+DGpCL0nMHW4lh7WtvfKcb0yxLmhbbA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.6.5 + + '@tiptap/extension-paragraph@3.6.5': + resolution: {integrity: sha512-AfuaBu+DKrRPspaLsXgo17dhuneISS6QsZTIzPeX21jFJcq3TjtD8wSzS4yRgzAQCEbupkI7t4JbtgxAIBNQHA==} + peerDependencies: + '@tiptap/core': ^3.6.5 + + '@tiptap/extension-placeholder@3.6.5': + resolution: {integrity: sha512-9CLixogEb/4UkEyuDr4JdOlLvphcOVfZMdNMKmUVQdqo4MuZCdTDyK5ypfTPQJl8aUo0oCiEhqE0bQerYlueJQ==} + peerDependencies: + '@tiptap/extensions': ^3.6.5 + + '@tiptap/extension-strike@3.6.5': + resolution: {integrity: sha512-QR7CUmRJ7fJkHtxqKajKIaX/B4xpKFOsAOJHbnqZ8wzOtnEL5IlsmoUnbKBoVn0+2R2YKKvMK3lepGtAcVCfIQ==} + peerDependencies: + '@tiptap/core': ^3.6.5 '@tiptap/extension-text-style@2.11.7': resolution: {integrity: sha512-LHO6DBg/9SkCQFdWlVfw9nolUmw+Cid94WkTY+7IwrpyG2+ZGQxnKpCJCKyeaFNbDoYAtvu0vuTsSXeCkgShcA==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-text@2.11.7': - resolution: {integrity: sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==} + '@tiptap/extension-text@3.6.5': + resolution: {integrity: sha512-PVZDWUa25xPzmEN6WWA103yvYJn+NBvWb7WrQwWu9LkKUgd98ZgV3yFaEem/Ybugl/NDPV7q8GGaH+2wEg/VeA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/extension-typography@2.11.7': - resolution: {integrity: sha512-qyYROxuXuMAMw30RXFYjr9mfZv+7avD3BW+fVEIa3lwnUMFNExHj6j2HMgYvrPVByGXlQU/4uHWcB0uiG0Bf1w==} + '@tiptap/extension-typography@3.6.5': + resolution: {integrity: sha512-xHJzMGpWVH0pL+iZUjH4cMlc8DdNQz+r07NcGlPWYXqP4KJ/feyfxRVmnO9M7ods8zeOTSNdCs1npkMAy0nfxQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.6.5 - '@tiptap/pm@2.11.7': - resolution: {integrity: sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==} + '@tiptap/extension-underline@3.6.5': + resolution: {integrity: sha512-Ul1mO0H1e2vfvN5g48X/YQ8w1xFTpLqce+GUhi0OmXaZnVOTIMtLuN/zAAPjD+uw+79JVGjYa53lbo1dyhOfAw==} + peerDependencies: + '@tiptap/core': ^3.6.5 - '@tiptap/react@2.11.7': - resolution: {integrity: sha512-gQZEUkAoPsBptnB4T2gAtiUxswjVGhfsM9vOElQco+b11DYmy110T2Zuhg+2YGvB/CG3RoWJx34808P0FX1ijA==} + '@tiptap/extensions@3.6.5': + resolution: {integrity: sha512-7aadEaRjSbFAIp3WGYR1LXrvtVprmBNxw3FakEUMJ+XKmGNErDJgDMZh+siAYw5MWwCCGa5kKu8Qi/i+DU+ILg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/pm@3.6.5': + resolution: {integrity: sha512-S+j6MPgUXRIQd5/mdaLjaJnOt4ptFwjqGjGMUfBbf9a3uKpXUXaCCzfuC6ZikwaUtoVh4KN9BU3HCYDtgtENPA==} + + '@tiptap/react@3.6.5': + resolution: {integrity: sha512-kum9fYzY6qmHuabcXDUTX2sVLdtJtZS0kN91mwD29Ue8HUkjVvEX92PwV2HtgNw3WFMaVxgm/dtm3XPTAlUEwg==} + peerDependencies: + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tiptap/starter-kit@2.11.7': - resolution: {integrity: sha512-K+q51KwNU/l0kqRuV5e1824yOLVftj6kGplGQLvJG56P7Rb2dPbM/JeaDbxQhnHT/KDGamG0s0Po0M3pPY163A==} + '@tiptap/starter-kit@3.6.5': + resolution: {integrity: sha512-LNAJQstB/VazmMlRbUyu3rCNVQ9af25Ywkn3Uyuwt3Ks9ZlliIm/x/zertdXTY2adoig+b36zT5Xcx1O4IdJ3A==} - '@tiptap/suggestion@2.11.7': - resolution: {integrity: sha512-I1ckVAEErpErPn/H9ZdDmTb5zuPNPiKj3krxCtJDUU4+3we0cgJY9NQFXl9//mrug3UIngH0ZQO+arbZfIk75A==} + '@tiptap/suggestion@3.6.5': + resolution: {integrity: sha512-KduN9qEx2MlEjL1Hfnj7PbdkwHZjjJfLldglQkntB6GhNaDGBa/M7l6hbBEKsu350UtyAnc5YdI6pG+sWFKEfg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.6.5 + '@tiptap/pm': ^3.6.5 + + '@tiptap/y-tiptap@3.0.0': + resolution: {integrity: sha512-HIeJZCj+KYJde2x6fONzo4o6kd7gW7eonwhQsv2p2VQnUgwNXMVhN+D6Z3AH/2i541Sq33y1PO4U/1ThCPjqbA==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -3375,6 +3423,9 @@ packages: '@types/node@20.17.0': resolution: {integrity: sha512-a7zRo0f0eLo9K5X9Wp5cAqTUNGzuFLDG2R7C4HY2BhcMAsxgSPuRvAC1ZB6QkuUQXf0YZAgfOX2ZyrBa2n4nHQ==} + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -4091,10 +4142,6 @@ packages: bare-stream@2.3.2: resolution: {integrity: sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==} - base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6551,6 +6598,9 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + iterator.prototype@1.1.3: resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} engines: {node: '>= 0.4'} @@ -6728,6 +6778,11 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lib0@0.2.114: + resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} + engines: {node: '>=16'} + hasBin: true + light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} @@ -6745,8 +6800,8 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - linkifyjs@4.2.0: - resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} lint-staged@10.5.4: resolution: {integrity: sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==} @@ -8053,8 +8108,8 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - prosemirror-changeset@2.2.1: - resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + prosemirror-changeset@2.3.1: + resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} prosemirror-collab@1.3.1: resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} @@ -8083,17 +8138,14 @@ packages: prosemirror-menu@1.2.4: resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} - prosemirror-model@1.23.0: - resolution: {integrity: sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==} - prosemirror-model@1.25.0: resolution: {integrity: sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==} prosemirror-schema-basic@1.2.3: resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} - prosemirror-schema-list@1.4.1: - resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} prosemirror-state@1.4.3: resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} @@ -8108,9 +8160,6 @@ packages: prosemirror-state: ^1.4.2 prosemirror-view: ^1.33.8 - prosemirror-transform@1.10.2: - resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==} - prosemirror-transform@1.10.3: resolution: {integrity: sha512-Nhh/+1kZGRINbEHmVu39oynhcap4hWTs/BlU7NnxWj3+l0qi8I1mu67v6mMdEe/ltD8hHvU4FV6PHiCw2VSpMw==} @@ -8218,6 +8267,11 @@ packages: peerDependencies: react: ^19.0.0 + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + react-dropzone@11.7.1: resolution: {integrity: sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==} engines: {node: '>= 10.13'} @@ -8330,6 +8384,10 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + reactflow@11.11.4: resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==} peerDependencies: @@ -8605,6 +8663,9 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -9262,9 +9323,6 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - tippy.js@6.3.7: - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} - tiptap-markdown@0.8.10: resolution: {integrity: sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==} peerDependencies: @@ -9517,6 +9575,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -9551,6 +9614,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + unenv@1.10.0: resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} @@ -10069,6 +10135,12 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y-protocols@1.0.6: + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -10107,6 +10179,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yjs@13.6.27: + resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -10183,12 +10259,12 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.29(react@19.0.0)(zod@4.1.5)': + '@ai-sdk/react@2.0.29(react@19.2.0)(zod@4.1.5)': dependencies: '@ai-sdk/provider-utils': 3.0.7(zod@4.1.5) ai: 5.0.29(zod@4.1.5) - react: 19.0.0 - swr: 2.3.6(react@19.0.0) + react: 19.2.0 + swr: 2.3.6(react@19.2.0) throttleit: 2.1.0 optionalDependencies: zod: 4.1.5 @@ -11053,35 +11129,35 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 6.0.2 - '@dnd-kit/accessibility@3.1.0(react@19.0.0)': + '@dnd-kit/accessibility@3.1.0(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 tslib: 2.8.0 - '@dnd-kit/core@6.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@dnd-kit/core@6.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@dnd-kit/accessibility': 3.1.0(react@19.0.0) - '@dnd-kit/utilities': 3.2.2(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@dnd-kit/accessibility': 3.1.0(react@19.2.0) + '@dnd-kit/utilities': 3.2.2(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) tslib: 2.8.0 - '@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': dependencies: - '@dnd-kit/core': 6.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@dnd-kit/utilities': 3.2.2(react@19.0.0) - react: 19.0.0 + '@dnd-kit/core': 6.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@dnd-kit/utilities': 3.2.2(react@19.2.0) + react: 19.2.0 tslib: 2.8.0 - '@dnd-kit/utilities@3.2.2(react@19.0.0)': + '@dnd-kit/utilities@3.2.2(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 tslib: 2.8.0 - '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.0.0)': + '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.0)': dependencies: emoji-mart: 5.6.0 - react: 19.0.0 + react: 19.2.0 '@emotion/is-prop-valid@1.2.2': dependencies: @@ -11471,34 +11547,23 @@ snapshots: fastq: 1.17.1 glob: 10.4.5 - '@floating-ui/core@1.6.8': - dependencies: - '@floating-ui/utils': 0.2.8 - - '@floating-ui/core@1.7.2': + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.6.11': - dependencies: - '@floating-ui/core': 1.6.8 - '@floating-ui/utils': 0.2.8 - - '@floating-ui/dom@1.7.2': + '@floating-ui/dom@1.7.4': dependencies: - '@floating-ui/core': 1.7.2 + '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@floating-ui/react-dom@2.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@floating-ui/dom': 1.6.11 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@floating-ui/dom': 1.7.4 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) '@floating-ui/utils@0.2.10': {} - '@floating-ui/utils@0.2.8': {} - '@humanfs/core@0.19.0': {} '@humanfs/node@0.16.5': @@ -11539,7 +11604,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.0 + '@types/node': 24.7.0 '@types/yargs': 16.0.9 chalk: 4.1.2 @@ -11619,23 +11684,23 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@microsoft/api-extractor-model@7.30.0(@types/node@20.17.0)': + '@microsoft/api-extractor-model@7.30.0(@types/node@24.7.0)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@20.17.0) + '@rushstack/node-core-library': 5.10.0(@types/node@24.7.0) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.48.0(@types/node@20.17.0)': + '@microsoft/api-extractor@7.48.0(@types/node@24.7.0)': dependencies: - '@microsoft/api-extractor-model': 7.30.0(@types/node@20.17.0) + '@microsoft/api-extractor-model': 7.30.0(@types/node@24.7.0) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@20.17.0) + '@rushstack/node-core-library': 5.10.0(@types/node@24.7.0) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.3(@types/node@20.17.0) - '@rushstack/ts-command-line': 4.23.1(@types/node@20.17.0) + '@rushstack/terminal': 0.14.3(@types/node@24.7.0) + '@rushstack/ts-command-line': 4.23.1(@types/node@24.7.0) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -11689,7 +11754,7 @@ snapshots: yaml: 2.6.0 yargs: 17.7.2 - '@netlify/build@29.55.2(@opentelemetry/api@1.8.0)(@swc/core@1.7.39)(@types/node@20.17.0)(picomatch@4.0.3)': + '@netlify/build@29.55.2(@opentelemetry/api@1.8.0)(@swc/core@1.7.39)(@types/node@24.7.0)(picomatch@4.0.3)': dependencies: '@bugsnag/js': 7.25.0 '@netlify/blobs': 7.4.0 @@ -11746,8 +11811,8 @@ snapshots: strip-ansi: 7.1.0 supports-color: 9.4.0 terminal-link: 3.0.0 - ts-node: 10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3) - typescript: 5.6.3 + ts-node: 10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.9.3) + typescript: 5.9.3 uuid: 9.0.1 yargs: 17.7.2 transitivePeerDependencies: @@ -12087,7 +12152,7 @@ snapshots: '@oddbird/css-anchor-positioning@0.6.1': dependencies: - '@floating-ui/dom': 1.7.2 + '@floating-ui/dom': 1.7.4 '@types/css-tree': 2.3.10 css-tree: 3.1.0 nanoid: 5.1.5 @@ -12185,286 +12250,284 @@ snapshots: '@polka/url@1.0.0-next.28': {} - '@popperjs/core@2.11.8': {} - '@radix-ui/number@1.1.0': {} '@radix-ui/primitive@1.1.0': {} - '@radix-ui/react-arrow@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-arrow@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-collection@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-collection@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-compose-refs@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-compose-refs@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-context@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-context@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-context@1.1.1(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-context@1.1.1(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-direction@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-direction@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-id@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-id@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-popover@1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-popover@1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-portal': 1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.2.0) aria-hidden: 1.2.4 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - react-remove-scroll: 2.6.0(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.6.0(@types/react@19.0.1)(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-popper@1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': - dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-arrow': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.1)(react@19.0.0) + '@radix-ui/react-popper@1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.1)(react@19.2.0) '@radix-ui/rect': 1.1.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-portal@1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-portal@1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-presence@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-presence@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-primitive@2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-primitive@2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-collection': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-scroll-area@1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-scroll-area@1.2.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/number': 1.1.0 '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-slot@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-slot@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-tabs@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@radix-ui/react-tabs@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0) - '@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.2.0) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: '@radix-ui/rect': 1.1.0 - react: 19.0.0 + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 - '@radix-ui/react-use-size@1.1.0(@types/react@19.0.1)(react@19.0.0)': + '@radix-ui/react-use-size@1.1.0(@types/react@19.0.1)(react@19.2.0)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0) - react: 19.0.0 + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.2.0) + react: 19.2.0 optionalDependencies: '@types/react': 19.0.1 '@radix-ui/rect@1.1.0': {} - '@reactflow/background@11.3.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/background@11.3.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) classcat: 5.0.5 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.5(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.5(@types/react@19.0.1)(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/controls@11.2.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) classcat: 5.0.5 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.5(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.5(@types/react@19.0.1)(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/core@11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -12474,48 +12537,48 @@ snapshots: d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.5(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.5(@types/react@19.0.1)(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/minimap@11.7.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/d3-selection': 3.0.11 '@types/d3-zoom': 3.0.8 classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.5(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.5(@types/react@19.0.1)(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/node-resizer@2.2.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.5(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.5(@types/react@19.0.1)(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/node-toolbar@1.3.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) classcat: 5.0.5 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.5(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.5(@types/react@19.0.1)(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer @@ -12627,7 +12690,7 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/node-core-library@5.10.0(@types/node@20.17.0)': + '@rushstack/node-core-library@5.10.0(@types/node@24.7.0)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -12638,23 +12701,23 @@ snapshots: resolve: 1.22.10 semver: 7.5.4 optionalDependencies: - '@types/node': 20.17.0 + '@types/node': 24.7.0 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.14.3(@types/node@20.17.0)': + '@rushstack/terminal@0.14.3(@types/node@24.7.0)': dependencies: - '@rushstack/node-core-library': 5.10.0(@types/node@20.17.0) + '@rushstack/node-core-library': 5.10.0(@types/node@24.7.0) supports-color: 8.1.1 optionalDependencies: - '@types/node': 20.17.0 + '@types/node': 24.7.0 - '@rushstack/ts-command-line@4.23.1(@types/node@20.17.0)': + '@rushstack/ts-command-line@4.23.1(@types/node@24.7.0)': dependencies: - '@rushstack/terminal': 0.14.3(@types/node@20.17.0) + '@rushstack/terminal': 0.14.3(@types/node@24.7.0) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -12687,14 +12750,14 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@3.3.0(@sveltejs/kit@2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))': + '@sveltejs/adapter-auto@3.3.0(@sveltejs/kit@2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))': dependencies: - '@sveltejs/kit': 2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + '@sveltejs/kit': 2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) import-meta-resolve: 4.1.0 - '@sveltejs/kit@2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))': + '@sveltejs/kit@2.7.2(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + '@sveltejs/vite-plugin-svelte': 4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -12708,7 +12771,7 @@ snapshots: sirv: 3.0.0 svelte: 5.1.4 tiny-glob: 0.2.9 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) '@sveltejs/package@2.3.6(svelte@5.1.4)(typescript@5.6.3)': dependencies: @@ -12721,25 +12784,25 @@ snapshots: transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + '@sveltejs/vite-plugin-svelte': 4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) debug: 4.4.1(supports-color@9.4.0) svelte: 5.1.4 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))': + '@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.0(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)))(svelte@5.1.4)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) debug: 4.4.1(supports-color@9.4.0) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.12 svelte: 5.1.4 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) - vitefu: 1.0.3(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) + vitefu: 1.0.3(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) transitivePeerDependencies: - supports-color @@ -12804,165 +12867,191 @@ snapshots: '@tanstack/history@1.95.0': {} - '@tanstack/react-router@1.95.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-router@1.95.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@tanstack/history': 1.95.0 - '@tanstack/react-store': 0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-store': 0.7.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) jsesc: 3.0.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-store@0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-store@0.7.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@tanstack/store': 0.7.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - use-sync-external-store: 1.4.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + use-sync-external-store: 1.4.0(react@19.2.0) - '@tanstack/router-devtools@1.95.1(@tanstack/react-router@1.95.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/router-devtools@1.95.1(@tanstack/react-router@1.95.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(csstype@3.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@tanstack/react-router': 1.95.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-router': 1.95.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - csstype '@tanstack/store@0.7.0': {} - '@tiptap/core@2.11.7(@tiptap/pm@2.11.7)': + '@tiptap/core@3.6.5(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/pm': 2.11.7 + '@tiptap/pm': 3.6.5 - '@tiptap/extension-blockquote@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-blockquote@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-bold@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-bold@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-bubble-menu@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-bubble-menu@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 - tippy.js: 6.3.7 + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + optional: true + + '@tiptap/extension-bullet-list@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-bullet-list@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-code-block@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 - '@tiptap/extension-code-block@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-code@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-code@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-collaboration-caret@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + '@tiptap/y-tiptap': 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - '@tiptap/extension-document@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-collaboration@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + '@tiptap/y-tiptap': 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + yjs: 13.6.27 + + '@tiptap/extension-document@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + + '@tiptap/extension-dropcursor@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + dependencies: + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + + '@tiptap/extension-file-handler@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/extension-text-style@2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/extension-text-style': 2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/pm': 3.6.5 + + '@tiptap/extension-floating-menu@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + optional: true - '@tiptap/extension-dropcursor@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-gapcursor@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-file-handler@2.25.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/extension-text-style@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)))': + '@tiptap/extension-hard-break@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/extension-text-style': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-floating-menu@2.14.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-heading@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 - tippy.js: 6.3.7 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-gapcursor@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-horizontal-rule@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 - '@tiptap/extension-hard-break@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-image@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-heading@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-italic@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-history@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-link@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + linkifyjs: 4.3.2 - '@tiptap/extension-horizontal-rule@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-list-item@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-image@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-list-keymap@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-italic@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 - '@tiptap/extension-link@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-mention@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/suggestion@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 - linkifyjs: 4.2.0 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + '@tiptap/suggestion': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-list-item@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-ordered-list@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-mention@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-paragraph@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 - '@tiptap/suggestion': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-ordered-list@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-placeholder@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-paragraph@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-strike@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-placeholder@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': + '@tiptap/extension-text-style@2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-strike@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-text@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-text-style@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-typography@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-text@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extension-underline@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-typography@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))': + '@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 - '@tiptap/pm@2.11.7': + '@tiptap/pm@3.6.5': dependencies: - prosemirror-changeset: 2.2.1 + prosemirror-changeset: 2.3.1 prosemirror-collab: 1.3.1 prosemirror-commands: 1.6.2 prosemirror-dropcursor: 1.8.1 @@ -12972,55 +13061,72 @@ snapshots: prosemirror-keymap: 1.2.2 prosemirror-markdown: 1.13.1 prosemirror-menu: 1.2.4 - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-schema-basic: 1.2.3 - prosemirror-schema-list: 1.4.1 + prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.3 prosemirror-tables: 1.7.0 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1) - prosemirror-transform: 1.10.2 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1) + prosemirror-transform: 1.10.3 prosemirror-view: 1.39.1 - '@tiptap/react@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tiptap/react@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/extension-bubble-menu': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-floating-menu': 2.14.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + '@types/react': 19.0.1 + '@types/react-dom': 19.0.1 '@types/use-sync-external-store': 0.0.6 fast-deep-equal: 3.1.3 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - use-sync-external-store: 1.4.0(react@19.0.0) - - '@tiptap/starter-kit@2.11.7': - dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/extension-blockquote': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-bold': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-bullet-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-code': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-code-block': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-document': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-dropcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-gapcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-hard-break': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-heading': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-history': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-horizontal-rule': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-italic': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-list-item': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-ordered-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-paragraph': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-strike': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-text': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/extension-text-style': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)) - '@tiptap/pm': 2.11.7 - - '@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)': - dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) - '@tiptap/pm': 2.11.7 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + use-sync-external-store: 1.4.0(react@19.2.0) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-floating-menu': 3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.6.5': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/extension-blockquote': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-bold': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-bullet-list': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-code': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-code-block': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-document': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-dropcursor': 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-gapcursor': 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-hard-break': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-heading': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-horizontal-rule': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-italic': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-link': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list-item': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-list-keymap': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-ordered-list': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + '@tiptap/extension-paragraph': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-strike': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-text': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extension-underline': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + + '@tiptap/suggestion@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + dependencies: + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/pm': 3.6.5 + + '@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + dependencies: + lib0: 0.2.114 + prosemirror-model: 1.25.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.39.1 + y-protocols: 1.0.6(yjs@13.6.27) + yjs: 13.6.27 '@tokenizer/token@0.3.0': {} @@ -13213,7 +13319,7 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 20.17.0 + '@types/node': 24.7.0 '@types/istanbul-lib-coverage@2.0.6': {} @@ -13262,6 +13368,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@24.7.0': + dependencies: + undici-types: 7.14.0 + '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} @@ -13317,24 +13427,24 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.17.0 + '@types/node': 24.7.0 optional: true - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.11.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.18.0 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.3.0(typescript@5.9.3) optionalDependencies: - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -13356,16 +13466,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.3.7 eslint: 8.57.1 optionalDependencies: - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -13392,15 +13502,15 @@ snapshots: '@typescript-eslint/types': 8.11.0 '@typescript-eslint/visitor-keys': 8.11.0 - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3) debug: 4.4.0 eslint: 8.57.1 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.3.0(typescript@5.9.3) optionalDependencies: - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -13422,7 +13532,7 @@ snapshots: '@typescript-eslint/types@8.11.0': {} - '@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 @@ -13430,13 +13540,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 - tsutils: 3.21.0(typescript@5.6.3) + tsutils: 3.21.0(typescript@5.9.3) optionalDependencies: - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 @@ -13445,9 +13555,9 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.3.0(typescript@5.9.3) optionalDependencies: - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -13466,12 +13576,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color @@ -13527,7 +13637,7 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 - '@uiw/react-codemirror@4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@uiw/react-codemirror@4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.27.6 '@codemirror/commands': 6.8.1 @@ -13536,8 +13646,8 @@ snapshots: '@codemirror/view': 6.38.1 '@uiw/codemirror-extensions-basic-setup': 4.24.1(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1) codemirror: 6.0.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' @@ -13564,14 +13674,14 @@ snapshots: - encoding - supports-color - '@vitejs/plugin-react@4.3.4(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))': + '@vitejs/plugin-react@4.3.4(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) transitivePeerDependencies: - supports-color @@ -13582,13 +13692,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))': + '@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))': dependencies: '@vitest/spy': 2.1.3 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) '@vitest/pretty-format@2.1.3': dependencies: @@ -13615,13 +13725,13 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 - '@wuchale/jsx@0.7.4(react@19.0.0)': + '@wuchale/jsx@0.7.4(react@19.2.0)': dependencies: '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) acorn: 8.15.0 wuchale: 0.16.5 optionalDependencies: - react: 19.0.0 + react: 19.2.0 '@wuchale/vite-plugin@0.14.6': dependencies: @@ -14083,14 +14193,14 @@ snapshots: dependencies: '@babel/types': 7.27.7 - babel-plugin-styled-components@2.1.4(@babel/core@7.26.0)(styled-components@6.1.19(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): + babel-plugin-styled-components@2.1.4(@babel/core@7.26.0)(styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0)): dependencies: '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-module-imports': 7.25.9 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) lodash: 4.17.21 picomatch: 2.3.1 - styled-components: 6.1.19(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + styled-components: 6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -14126,8 +14236,6 @@ snapshots: streamx: 2.20.1 optional: true - base64-arraybuffer@1.0.2: {} - base64-js@1.5.1: {} before-after-hook@2.2.3: {} @@ -14989,10 +15097,10 @@ snapshots: detective-typescript@11.2.0(supports-color@9.4.0): dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.9.3) ast-module-types: 5.0.0 node-source-walk: 6.0.2 - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -15048,12 +15156,12 @@ snapshots: dotenv@16.4.5: {} - downshift@9.0.10(react@19.0.0): + downshift@9.0.10(react@19.2.0): dependencies: '@babel/runtime': 7.27.6 compute-scroll-into-view: 3.1.1 prop-types: 15.8.1 - react: 19.0.0 + react: 19.2.0 react-is: 18.2.0 tslib: 2.8.0 @@ -15442,17 +15550,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -15463,7 +15571,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -15475,7 +15583,7 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -15545,7 +15653,7 @@ snapshots: string.prototype.matchall: 4.0.11 string.prototype.repeat: 1.0.0 - eslint-plugin-svelte@2.46.0(eslint@9.13.0(jiti@2.3.3))(svelte@5.1.4)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)): + eslint-plugin-svelte@2.46.0(eslint@9.13.0(jiti@2.3.3))(svelte@5.1.4)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0(jiti@2.3.3)) '@jridgewell/sourcemap-codec': 1.5.2 @@ -15554,7 +15662,7 @@ snapshots: esutils: 2.0.3 known-css-properties: 0.35.0 postcss: 8.4.47 - postcss-load-config: 3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)) + postcss-load-config: 3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) postcss-safe-parser: 6.0.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 semver: 7.7.2 @@ -17058,6 +17166,8 @@ snapshots: isexe@3.1.1: {} + isomorphic.js@0.2.5: {} + iterator.prototype@1.1.3: dependencies: define-properties: 1.2.1 @@ -17231,6 +17341,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lib0@0.2.114: + dependencies: + isomorphic.js: 0.2.5 + light-my-request@5.14.0: dependencies: cookie: 0.7.2 @@ -17247,7 +17361,7 @@ snapshots: dependencies: uc.micro: 2.1.0 - linkifyjs@4.2.0: {} + linkifyjs@4.3.2: {} lint-staged@10.5.4: dependencies: @@ -18022,12 +18136,12 @@ snapshots: nested-error-stacks@2.1.1: {} - netlify-cli@17.37.1(@swc/core@1.7.39)(@types/node@20.17.0)(picomatch@4.0.3): + netlify-cli@17.37.1(@swc/core@1.7.39)(@types/node@24.7.0)(picomatch@4.0.3): dependencies: '@bugsnag/js': 7.25.0 '@fastify/static': 7.0.4 '@netlify/blobs': 8.1.0 - '@netlify/build': 29.55.2(@opentelemetry/api@1.8.0)(@swc/core@1.7.39)(@types/node@20.17.0)(picomatch@4.0.3) + '@netlify/build': 29.55.2(@opentelemetry/api@1.8.0)(@swc/core@1.7.39)(@types/node@24.7.0)(picomatch@4.0.3) '@netlify/build-info': 7.15.1 '@netlify/config': 20.19.0 '@netlify/edge-bundler': 12.2.3(supports-color@9.4.0) @@ -18719,13 +18833,13 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)): + postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3) + ts-node: 10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3) postcss-load-config@6.0.1(jiti@2.3.3)(postcss@8.4.49)(yaml@2.6.0): dependencies: @@ -18866,9 +18980,9 @@ snapshots: property-information@6.5.0: {} - prosemirror-changeset@2.2.1: + prosemirror-changeset@2.3.1: dependencies: - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 prosemirror-collab@1.3.1: dependencies: @@ -18876,34 +18990,34 @@ snapshots: prosemirror-commands@1.6.2: dependencies: - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 prosemirror-dropcursor@1.8.1: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 prosemirror-view: 1.39.1 prosemirror-gapcursor@1.3.2: dependencies: prosemirror-keymap: 1.2.2 - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-state: 1.4.3 prosemirror-view: 1.39.1 prosemirror-history@1.4.1: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 prosemirror-view: 1.39.1 rope-sequence: 1.3.4 prosemirror-inputrules@1.4.0: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 prosemirror-keymap@1.2.2: dependencies: @@ -18914,7 +19028,7 @@ snapshots: dependencies: '@types/markdown-it': 14.1.2 markdown-it: 14.1.0 - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-menu@1.2.4: dependencies: @@ -18923,28 +19037,24 @@ snapshots: prosemirror-history: 1.4.1 prosemirror-state: 1.4.3 - prosemirror-model@1.23.0: - dependencies: - orderedmap: 2.1.1 - prosemirror-model@1.25.0: dependencies: orderedmap: 2.1.1 prosemirror-schema-basic@1.2.3: dependencies: - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 - prosemirror-schema-list@1.4.1: + prosemirror-schema-list@1.5.1: dependencies: - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 prosemirror-state@1.4.3: dependencies: - prosemirror-model: 1.23.0 - prosemirror-transform: 1.10.2 + prosemirror-model: 1.25.0 + prosemirror-transform: 1.10.3 prosemirror-view: 1.39.1 prosemirror-tables@1.7.0: @@ -18955,27 +19065,23 @@ snapshots: prosemirror-transform: 1.10.3 prosemirror-view: 1.39.1 - prosemirror-trailing-node@3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1): + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1): dependencies: '@remirror/core-constants': 3.0.0 escape-string-regexp: 4.0.0 - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-state: 1.4.3 prosemirror-view: 1.39.1 - prosemirror-transform@1.10.2: - dependencies: - prosemirror-model: 1.23.0 - prosemirror-transform@1.10.3: dependencies: prosemirror-model: 1.25.0 prosemirror-view@1.39.1: dependencies: - prosemirror-model: 1.23.0 + prosemirror-model: 1.25.0 prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.2 + prosemirror-transform: 1.10.3 proto-list@1.2.4: {} @@ -19063,46 +19169,51 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-colorful@5.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-colorful@5.6.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) react-dom@19.0.0(react@19.0.0): dependencies: react: 19.0.0 scheduler: 0.25.0 - react-dropzone@11.7.1(react@19.0.0): + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react-dropzone@11.7.1(react@19.2.0): dependencies: attr-accept: 2.2.4 file-selector: 0.4.0 prop-types: 15.8.1 - react: 19.0.0 + react: 19.2.0 - react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: goober: 2.1.16(csstype@3.1.3) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - csstype - react-hotkeys-hook@3.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-hotkeys-hook@3.4.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: hotkeys-js: 3.9.4 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react-icons@4.12.0(react@19.0.0): + react-icons@4.12.0(react@19.2.0): dependencies: - react: 19.0.0 + react: 19.2.0 - react-intersection-observer@9.13.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-intersection-observer@9.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.0.0 + react: 19.2.0 optionalDependencies: - react-dom: 19.0.0(react@19.0.0) + react-dom: 19.2.0(react@19.2.0) react-is@16.13.1: {} @@ -19112,7 +19223,7 @@ snapshots: react-is@19.0.0: {} - react-markdown@9.0.3(@types/react@19.0.1)(react@19.0.0): + react-markdown@9.0.3(@types/react@19.0.1)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/react': 19.0.1 @@ -19120,7 +19231,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.2 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 - react: 19.0.0 + react: 19.2.0 remark-parse: 11.0.0 remark-rehype: 11.1.1 unified: 11.0.5 @@ -19129,7 +19240,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-pdf@9.1.1(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-pdf@9.1.1(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: clsx: 2.1.1 dequal: 2.0.3 @@ -19137,8 +19248,8 @@ snapshots: make-event-props: 1.6.2 merge-refs: 1.3.0(@types/react@19.0.1) pdfjs-dist: 4.4.168 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) tiny-invariant: 1.3.3 warning: 4.0.3 optionalDependencies: @@ -19149,58 +19260,60 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.6(@types/react@19.0.1)(react@19.0.0): + react-remove-scroll-bar@2.3.6(@types/react@19.0.1)(react@19.2.0): dependencies: - react: 19.0.0 - react-style-singleton: 2.2.1(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-style-singleton: 2.2.1(@types/react@19.0.1)(react@19.2.0) tslib: 2.8.0 optionalDependencies: '@types/react': 19.0.1 - react-remove-scroll@2.6.0(@types/react@19.0.1)(react@19.0.0): + react-remove-scroll@2.6.0(@types/react@19.0.1)(react@19.2.0): dependencies: - react: 19.0.0 - react-remove-scroll-bar: 2.3.6(@types/react@19.0.1)(react@19.0.0) - react-style-singleton: 2.2.1(@types/react@19.0.1)(react@19.0.0) + react: 19.2.0 + react-remove-scroll-bar: 2.3.6(@types/react@19.0.1)(react@19.2.0) + react-style-singleton: 2.2.1(@types/react@19.0.1)(react@19.2.0) tslib: 2.8.0 - use-callback-ref: 1.3.2(@types/react@19.0.1)(react@19.0.0) - use-sidecar: 1.1.2(@types/react@19.0.1)(react@19.0.0) + use-callback-ref: 1.3.2(@types/react@19.0.1)(react@19.2.0) + use-sidecar: 1.1.2(@types/react@19.0.1)(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 - react-style-singleton@2.2.1(@types/react@19.0.1)(react@19.0.0): + react-style-singleton@2.2.1(@types/react@19.0.1)(react@19.2.0): dependencies: get-nonce: 1.0.1 invariant: 2.2.4 - react: 19.0.0 + react: 19.2.0 tslib: 2.8.0 optionalDependencies: '@types/react': 19.0.1 - react-virtualized-auto-sizer@1.0.24(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-virtualized-auto-sizer@1.0.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react-window@1.8.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-window@1.8.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.25.9 memoize-one: 5.2.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) react@19.0.0: {} - reactflow@11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react@19.2.0: {} + + reactflow@11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@reactflow/background': 11.3.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/controls': 11.2.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/minimap': 11.7.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/node-resizer': 2.2.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/node-toolbar': 1.3.14(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@reactflow/background': 11.3.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@reactflow/controls': 11.2.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@reactflow/core': 11.11.4(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@reactflow/minimap': 11.7.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@reactflow/node-resizer': 2.2.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@reactflow/node-toolbar': 1.3.14(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - '@types/react' - immer @@ -19556,6 +19669,8 @@ snapshots: scheduler@0.25.0: {} + scheduler@0.27.0: {} + secure-json-parse@2.7.0: {} seek-bzip@1.0.6: @@ -20057,7 +20172,7 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-components@6.1.19(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + styled-components@6.1.19(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@emotion/is-prop-valid': 1.2.2 '@emotion/unitless': 0.8.1 @@ -20065,8 +20180,8 @@ snapshots: css-to-react-native: 3.2.0 csstype: 3.1.3 postcss: 8.4.49 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) shallowequal: 1.1.0 stylis: 4.3.2 tslib: 2.6.2 @@ -20111,15 +20226,15 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@3.8.6(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4): + svelte-check@3.8.6(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 picocolors: 1.1.1 sade: 1.8.1 svelte: 5.1.4 - svelte-preprocess: 5.1.4(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4)(typescript@5.6.3) - typescript: 5.6.3 + svelte-preprocess: 5.1.4(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4)(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -20141,7 +20256,7 @@ snapshots: optionalDependencies: svelte: 5.1.4 - svelte-preprocess@5.1.4(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4)(typescript@5.6.3): + svelte-preprocess@5.1.4(@babel/core@7.26.0)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4)(typescript@5.9.3): dependencies: '@types/pug': 2.0.10 detect-indent: 6.1.0 @@ -20152,8 +20267,8 @@ snapshots: optionalDependencies: '@babel/core': 7.26.0 postcss: 8.4.47 - postcss-load-config: 3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3)) - typescript: 5.6.3 + postcss-load-config: 3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) + typescript: 5.9.3 svelte2tsx@0.7.22(svelte@5.1.4)(typescript@5.6.3): dependencies: @@ -20188,11 +20303,11 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swr@2.3.6(react@19.0.0): + swr@2.3.6(react@19.2.0): dependencies: dequal: 2.0.3 - react: 19.0.0 - use-sync-external-store: 1.4.0(react@19.0.0) + react: 19.2.0 + use-sync-external-store: 1.4.0(react@19.2.0) synckit@0.9.2: dependencies: @@ -20350,13 +20465,9 @@ snapshots: tinyspy@3.0.2: {} - tippy.js@6.3.7: - dependencies: - '@popperjs/core': 2.11.8 - - tiptap-markdown@0.8.10(@tiptap/core@2.11.7(@tiptap/pm@2.11.7)): + tiptap-markdown@0.8.10(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)): dependencies: - '@tiptap/core': 2.11.7(@tiptap/pm@2.11.7) + '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) '@types/markdown-it': 13.0.9 markdown-it: 14.1.0 markdown-it-task-lists: 2.1.1 @@ -20419,16 +20530,20 @@ snapshots: dependencies: typescript: 5.6.3 + ts-api-utils@1.3.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@swc/core@1.7.39)(@types/node@20.17.0)(typescript@5.6.3): + ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.0 + '@types/node': 24.7.0 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -20440,6 +20555,27 @@ snapshots: yn: 3.1.1 optionalDependencies: '@swc/core': 1.7.39 + optional: true + + ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.7.0 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.7.39 tsconfig-paths@3.15.0: dependencies: @@ -20454,7 +20590,7 @@ snapshots: tslib@2.8.0: {} - tsup@8.3.5(@microsoft/api-extractor@7.48.0(@types/node@20.17.0))(@swc/core@1.7.39)(jiti@2.3.3)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.0): + tsup@8.3.5(@microsoft/api-extractor@7.48.0(@types/node@24.7.0))(@swc/core@1.7.39)(jiti@2.3.3)(postcss@8.4.49)(typescript@5.9.3)(yaml@2.6.0): dependencies: bundle-require: 5.0.0(esbuild@0.24.0) cac: 6.7.14 @@ -20473,20 +20609,20 @@ snapshots: tinyglobby: 0.2.9 tree-kill: 1.2.2 optionalDependencies: - '@microsoft/api-extractor': 7.48.0(@types/node@20.17.0) + '@microsoft/api-extractor': 7.48.0(@types/node@24.7.0) '@swc/core': 1.7.39 postcss: 8.4.49 - typescript: 5.6.3 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - tsutils@3.21.0(typescript@5.6.3): + tsutils@3.21.0(typescript@5.9.3): dependencies: tslib: 1.14.1 - typescript: 5.6.3 + typescript: 5.9.3 tunnel-agent@0.6.0: dependencies: @@ -20590,17 +20726,17 @@ snapshots: dependencies: is-typedarray: 1.0.0 - typedoc-plugin-missing-exports@2.3.0(typedoc@0.25.13(typescript@5.6.3)): + typedoc-plugin-missing-exports@2.3.0(typedoc@0.25.13(typescript@5.9.3)): dependencies: - typedoc: 0.25.13(typescript@5.6.3) + typedoc: 0.25.13(typescript@5.9.3) - typedoc@0.25.13(typescript@5.6.3): + typedoc@0.25.13(typescript@5.9.3): dependencies: lunr: 2.3.9 marked: 4.3.0 minimatch: 9.0.5 shiki: 0.14.7 - typescript: 5.6.3 + typescript: 5.9.3 types-wm@1.1.0: {} @@ -20621,6 +20757,8 @@ snapshots: typescript@5.6.3: {} + typescript@5.9.3: {} + uc.micro@2.1.0: {} ufo@1.5.4: {} @@ -20658,6 +20796,8 @@ snapshots: undici-types@6.19.8: {} + undici-types@7.14.0: {} + unenv@1.10.0: dependencies: consola: 3.2.3 @@ -20792,28 +20932,28 @@ snapshots: urlpattern-polyfill@8.0.2: {} - use-callback-ref@1.3.2(@types/react@19.0.1)(react@19.0.0): + use-callback-ref@1.3.2(@types/react@19.0.1)(react@19.2.0): dependencies: - react: 19.0.0 + react: 19.2.0 tslib: 2.8.0 optionalDependencies: '@types/react': 19.0.1 - use-sidecar@1.1.2(@types/react@19.0.1)(react@19.0.0): + use-sidecar@1.1.2(@types/react@19.0.1)(react@19.2.0): dependencies: detect-node-es: 1.1.0 - react: 19.0.0 + react: 19.2.0 tslib: 2.8.0 optionalDependencies: '@types/react': 19.0.1 - use-sync-external-store@1.2.2(react@19.0.0): + use-sync-external-store@1.2.2(react@19.2.0): dependencies: - react: 19.0.0 + react: 19.2.0 - use-sync-external-store@1.4.0(react@19.0.0): + use-sync-external-store@1.4.0(react@19.2.0): dependencies: - react: 19.0.0 + react: 19.2.0 util-deprecate@1.0.2: {} @@ -20846,12 +20986,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.3(@types/node@20.17.0)(terser@5.43.1): + vite-node@2.1.3(@types/node@24.7.0)(terser@5.43.1): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@9.4.0) pathe: 1.1.2 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) transitivePeerDependencies: - '@types/node' - less @@ -20871,45 +21011,45 @@ snapshots: - prismjs - supports-color - vite-plugin-pwa@0.20.5(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0): + vite-plugin-pwa@0.20.5(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0): dependencies: debug: 4.3.7 pretty-bytes: 6.1.1 tinyglobby: 0.2.9 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) workbox-build: 7.1.1(@types/babel__core@7.20.5) workbox-window: 7.1.0 transitivePeerDependencies: - supports-color - vite-plugin-webfont-dl@3.9.5(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)): + vite-plugin-webfont-dl@3.9.5(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)): dependencies: axios: 1.7.7 clean-css: 5.3.3 flat-cache: 5.0.0 picocolors: 1.1.1 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) transitivePeerDependencies: - debug - vite@5.4.10(@types/node@20.17.0)(terser@5.43.1): + vite@5.4.10(@types/node@24.7.0)(terser@5.43.1): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: - '@types/node': 20.17.0 + '@types/node': 24.7.0 fsevents: 2.3.3 terser: 5.43.1 - vitefu@1.0.3(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)): + vitefu@1.0.3(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)): optionalDependencies: - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) - vitest@2.1.3(@types/node@20.17.0)(terser@5.43.1): + vitest@2.1.3(@types/node@24.7.0)(terser@5.43.1): dependencies: '@vitest/expect': 2.1.3 - '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.10(@types/node@20.17.0)(terser@5.43.1)) + '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.10(@types/node@24.7.0)(terser@5.43.1)) '@vitest/pretty-format': 2.1.3 '@vitest/runner': 2.1.3 '@vitest/snapshot': 2.1.3 @@ -20924,11 +21064,11 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.10(@types/node@20.17.0)(terser@5.43.1) - vite-node: 2.1.3(@types/node@20.17.0)(terser@5.43.1) + vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) + vite-node: 2.1.3(@types/node@24.7.0)(terser@5.43.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.17.0 + '@types/node': 24.7.0 transitivePeerDependencies: - less - lightningcss @@ -21275,6 +21415,11 @@ snapshots: xtend@4.0.2: {} + y-protocols@1.0.6(yjs@13.6.27): + dependencies: + lib0: 0.2.114 + yjs: 13.6.27 + y18n@5.0.8: {} yallist@3.1.1: {} @@ -21314,6 +21459,10 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yjs@13.6.27: + dependencies: + lib0: 0.2.114 + yn@3.1.1: {} yocto-queue@0.1.0: {} @@ -21342,11 +21491,11 @@ snapshots: zod@4.1.5: {} - zustand@4.5.5(@types/react@19.0.1)(react@19.0.0): + zustand@4.5.5(@types/react@19.0.1)(react@19.2.0): dependencies: - use-sync-external-store: 1.2.2(react@19.0.0) + use-sync-external-store: 1.2.2(react@19.2.0) optionalDependencies: '@types/react': 19.0.1 - react: 19.0.0 + react: 19.2.0 zwitch@2.0.4: {} diff --git a/browser/react/package.json b/browser/react/package.json index 38c12a5db..6214f44d9 100644 --- a/browser/react/package.json +++ b/browser/react/package.json @@ -22,7 +22,8 @@ "@types/react-dom": "^19.0.0", "@types/react-router-dom": "^5.3.3", "tsup": "^8.3.5", - "typescript": "^5.6.3" + "typescript": "^5.9.3", + "yjs": "^13.6.27" }, "peerDependencies": { "react": ">18.3.0", diff --git a/browser/react/src/hooks.ts b/browser/react/src/hooks.ts index cf670310d..f72306bb1 100644 --- a/browser/react/src/hooks.ts +++ b/browser/react/src/hooks.ts @@ -30,6 +30,7 @@ import { core, server, } from '@tomic/lib'; +import type * as Y from 'yjs'; /** * Hook for getting a Resource in a React component. Will try to fetch the @@ -534,6 +535,36 @@ export function useDate( } } +/** + * Gets or creates a Yjs document for the given property. returns undefined if the resource is still loading. + */ +export function useYDoc( + resource: Resource, + propertyURL: string, +): Y.Doc | undefined { + const [doc, setDoc] = useState(() => + resource.loading ? undefined : resource.getYDoc(propertyURL), + ); + + useEffect(() => { + if (resource.loading) { + return; + } + + setDoc(resource.getYDoc(propertyURL)); + + return resource.on(ResourceEvents.LocalChange, prop => { + if (prop !== propertyURL) { + return; + } + + setDoc(resource.getYDoc(propertyURL)); + }); + }, [resource]); + + return doc; +} + /** Preferred way of using the store in a Component or Hook */ export function useStore(): Store { const store = useContext(StoreContext); diff --git a/browser/react/src/useMarkdown.ts b/browser/react/src/useMarkdown.ts index a51e3c9e1..bbe64529e 100644 --- a/browser/react/src/useMarkdown.ts +++ b/browser/react/src/useMarkdown.ts @@ -1,12 +1,12 @@ import { Datatype, - JSONValue, properties, Resource, Store, urls, valToArray, valToDate, + type AtomicValue, } from '@tomic/lib'; import { useEffect, useState } from 'react'; import { useStore, useString, useTitle } from './index.js'; @@ -69,7 +69,7 @@ export function useMarkdown(resource: Resource): string { /** Renders a single Atomic Property + Value as a single Markdown line */ async function propertyLine( propertySubject: string, - value: JSONValue, + value: AtomicValue, store: Store, ): Promise { const property = await store.getProperty(propertySubject); diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6aa81ddbe..25832780b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -13,6 +13,7 @@ atomic_lib = { version = "0.40.0", path = "../lib", features = [ "config", "rdf", ] } +base64 = "0.21" clap = { version = "4", features = ["cargo", "derive"] } colored = "2" dirs = "4" diff --git a/cli/src/new.rs b/cli/src/new.rs index 22694fac6..fda14b6b8 100644 --- a/cli/src/new.rs +++ b/cli/src/new.rs @@ -8,6 +8,7 @@ use atomic_lib::{ schema::{Class, Property}, Resource, Storelike, Value, }; +use base64::engine::{general_purpose, Engine}; use colored::Colorize; use promptly::prompt_opt; use regex::Regex; @@ -168,6 +169,15 @@ fn prompt_field( check_valid_json(&json).unwrap(); return Ok(Some(json)); } + DataType::YDoc => { + let msg = format!("YDoc{}", msg_appendix); + let Some(ydoc) = prompt_opt::(msg)? else { + return Ok(None); + }; + // Check if it is a valid Base64 string + general_purpose::STANDARD.decode(&ydoc).unwrap(); + return Ok(Some(ydoc)); + } DataType::Integer => { let msg = format!("integer{}", msg_appendix); let number: Option = prompt_opt(msg)?; diff --git a/docs/src/commits/concepts.md b/docs/src/commits/concepts.md index 8584814be..072ec66e1 100644 --- a/docs/src/commits/concepts.md +++ b/docs/src/commits/concepts.md @@ -22,6 +22,7 @@ The **optional method fields** describe how the data must be changed: - `remove` - an array of Properties that need to be removed (including their values). - `set` - a Nested Resource which contains all the new or edited fields. - `push` - a Nested Resource which contains all the fields that are _appended_ to. This means adding items to a new or existing ResourceArray. +- `yUpdate` - a Nested Resource which contains Yjs updates (v2) for the given properties. These commands are executed in the order above. This means that you can set `destroy` to `true` and include `set`, which empties the existing resource and sets new values. @@ -84,7 +85,7 @@ Congratulations, you've just created a valid Commit! Here are currently working implementations of this process, including serialization and signing (links are permalinks). - [in Rust (atomic-lib)](https://github.com/atomicdata-dev/atomic-server/blob/ceb88c1ae58811f2a9e6bacb7eaa39a2a7aa1513/lib/src/commit.rs#L81). -- [in Typescript / Javascript (atomic-data-browser)](https://github.com/atomicdata-dev/atomic-data-browser/blob/fc899bb2cf54bdff593ee6b4debf52e20a85619e/src/atomic-lib/commit.ts#L51). +- [in Typescript / Javascript (atomic-data-browser)](https://github.com/atomicdata-dev/atomic-server/blob/6947650263d56e6c70a7f726ed0a51c0f4d8f25c/browser/lib/src/commit.ts#L299). If you want validate your implementation, check out the tests for these two projects. diff --git a/docs/src/core/json-ad.md b/docs/src/core/json-ad.md index 11913ec20..c9858017a 100644 --- a/docs/src/core/json-ad.md +++ b/docs/src/core/json-ad.md @@ -24,6 +24,7 @@ The types of values allowed are determined by the [datatype](../schema/datatypes - **atomic-url** datatype fields must be either a `string` (url) or an `object` (nested resource). - **resource-array** datatype fields must be an `array` of strings (must be a url) or objects (must be an nested resource). - **json** datatype fields can be any valid JSON value. +- **ydoc** datatype fields must be an `object` with a `type` field set to `"ydoc"` and a `data` field set to a base64-encoded [Yjs update v2](https://github.com/yjs/yjs). Named Resources are only allowed in the following places: diff --git a/docs/src/js-lib/agent.md b/docs/src/js-lib/agent.md index 9b98388da..d084fa270 100644 --- a/docs/src/js-lib/agent.md +++ b/docs/src/js-lib/agent.md @@ -4,14 +4,47 @@ An agent is an authenticated identity that can interact with Atomic Data resourc All writes in AtomicServer are signed by an agent and can therefore be proven to be authentic. Read more about agents in the [Atomic Data specification](../agents.md). -## Creating an Agent instance +## Agent Secret -Creating an agent can be done in two ways, either by using the `Agent` constructor or by using the `Agent.fromSecret` method. +Agents can be encoded into a single string called a secret. +This secret contains the private key and the subject of the agent. + +Encoding and decoding secrets is easy: + +```ts +// Encode as secret +const secret = agent.buildSecret(); + +// Decode from secret +const agent = Agent.fromSecret(secret); +``` + +## Manual creation + +It is recommended to use the `Agent.fromSecret` method to create an agent instance but you can also manually create an agent instance by passing in the private key and the subject. ```typescript const agent = new Agent('my-private-key', 'my-agent-subject'); ``` +## Advanced + +### Getting the public key + +If you need the agents public key you can use the async `getPublicKey` method. + +```typescript +const publicKey = await agent.getPublicKey(); +``` + +This will generate a public key from the private key and cache it on the agent instance. + +### Verifying the public key + +If you need to verify the public key of the agent you can use the `verifyPublicKeyWithServer` method. + ```typescript -const agent = Agent.fromSecret('my-long-secret-string'); +await agent.verifyPublicKeyWithServer(); ``` + +This will fetch the agent from the server and check if the public key matches the one on the agent instance. diff --git a/docs/src/js-lib/resource.md b/docs/src/js-lib/resource.md index 0377b2a26..cedf19db5 100644 --- a/docs/src/js-lib/resource.md +++ b/docs/src/js-lib/resource.md @@ -309,6 +309,46 @@ const version = userPicksVersion(versions); await resource.setVersion(version); ``` +## Yjs Documents + +AtomicServer supports Yjs documents as a datatype. +Using these you can build powerful collaborative editors. +Yjs documents are synced via atomic commits when you call `resource.save()`, just like regular properties. +This means that you don't have to use any provider server to sync the documents. + +To use any Yjs related feature you first need to install the `yjs` package using your package manager of choice. +You also need to tell @tomic/lib that Yjs is available by calling the following function somewhere early on in your application. + +```typescript +import { enableYjs } from '@tomic/lib'; + +await enableYjs(); +``` + +This will load the Yjs module and make it available to @tomic/lib. + +### Using Yjs documents + +To get a Yjs document from a resource, use the `.getYDoc` method and pass the property of the value containing the document. +If the value is still empty, a new document will be created and returned. +You can then use the Yjs doc like you would normally with Yjs. +Any change made to the document will be merged into the current commit. +When you call `resource.save()`, the changes will be synced to the server and with other clients. + +```typescript +const doc = resource.getYDoc('https://my-atomicserver.com/properties/yjs-document'); + +const text = doc.getText('content'); +const cursors = doc.getMap('cursors'); + +doc.transact(() => { + text.insert(0, 'Hello, world!'); + cursors.set(someClientId, 13); +}); + +await resource.save(); +``` + ## Useful methods and properties ### Subject diff --git a/docs/src/schema/datatypes.md b/docs/src/schema/datatypes.md index c16147c6d..0ac587706 100644 --- a/docs/src/schema/datatypes.md +++ b/docs/src/schema/datatypes.md @@ -138,3 +138,19 @@ example: 9883 ] ``` + +## YDoc + +_URL: `https://atomicdata.dev/datatypes/ydoc`_ + +A [Yjs document](https://github.com/yjs/yjs). +Stores a Yjs document state. (uses the update v2 format). +They are updated using commits via the [yUpdate](https://atomicdata.dev/properties/yUpdate) property. +When encoded into a JSON-AD value it will look like this: + +```json +{ + "type": "ydoc", + "data": "base64-encoded-updates" +} +``` diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 9f4ad688c..58f179eae 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -39,6 +39,7 @@ ureq = "2" url = "2" urlencoding = "2" ulid = "1.1.3" +yrs = "0.24.0" [dev-dependencies] criterion = "0.5" diff --git a/lib/defaults/default_store.json b/lib/defaults/default_store.json index 28bc191b1..9e6c06028 100644 --- a/lib/defaults/default_store.json +++ b/lib/defaults/default_store.json @@ -760,6 +760,16 @@ ], "https://atomicdata.dev/properties/shortname": "set" }, + { + "@id": "https://atomicdata.dev/properties/yUpdate", + "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/atomicURL", + "https://atomicdata.dev/properties/description": "A field in a commit.\\\nNested resource mapping properties to Yjs state updates.", + "https://atomicdata.dev/properties/isA": [ + "https://atomicdata.dev/classes/Property" + ], + "https://atomicdata.dev/properties/parent": "https://atomicdata.dev/properties", + "https://atomicdata.dev/properties/shortname": "y-update" + }, { "@id": "https://atomicdata.dev/properties/secret", "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/string", @@ -899,7 +909,9 @@ "https://atomicdata.dev/properties/recommends": [ "https://atomicdata.dev/properties/destroy", "https://atomicdata.dev/properties/remove", - "https://atomicdata.dev/properties/set" + "https://atomicdata.dev/properties/set", + "https://atomicdata.dev/properties/push", + "https://atomicdata.dev/properties/yUpdate" ], "https://atomicdata.dev/properties/requires": [ "https://atomicdata.dev/properties/createdAt", @@ -1148,6 +1160,15 @@ "https://atomicdata.dev/properties/parent": "https://atomicdata.dev/datatypes", "https://atomicdata.dev/properties/shortname": "json" }, + { + "@id": "https://atomicdata.dev/datatypes/ydoc", + "https://atomicdata.dev/properties/description": "A Yjs update-v2 encoded as base64", + "https://atomicdata.dev/properties/isA": [ + "https://atomicdata.dev/classes/Datatype" + ], + "https://atomicdata.dev/properties/parent": "https://atomicdata.dev/datatypes", + "https://atomicdata.dev/properties/shortname": "ydoc" + }, { "@id": "https://atomicdata.dev/classes/Folder", "https://atomicdata.dev/properties/description": "Acts as a parent for resources, useful for ordering data.", diff --git a/lib/src/commit.rs b/lib/src/commit.rs index 1ef735cb4..d2141d0bb 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -1,9 +1,5 @@ //! Describe changes / mutations to data -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use urls::{SET, SIGNER}; - use crate::{ agents::{decode_base64, encode_base64}, datatype::DataType, @@ -13,7 +9,10 @@ use crate::{ values::SubResource, Atom, Resource, Storelike, Value, }; - +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use urls::{SET, SIGNER}; +use yrs::updates::decoder::Decode; /// The `resource_new`, `resource_old` and `commit_resource` fields are only created if the Commit is persisted. /// When the Db is only notifying other of changes (e.g. if a new Message was added to a ChatRoom), these fields are not created. /// When deleting a resource, the `resource_new` field is None. @@ -90,8 +89,11 @@ pub struct Commit { /// Overwrites existing values #[serde(rename = "https://atomicdata.dev/properties/set")] pub set: Option>, - /// The set of property URLs that need to be removed + /// A map of properties and the Yjs updates to be applied to them (must be Value::YDoc) + #[serde(rename = "https://atomicdata.dev/properties/yUpdate")] + pub y_update: Option>, #[serde(rename = "https://atomicdata.dev/properties/remove")] + /// The set of property URLs that need to be removed pub remove: Option>, /// If set to true, deletes the entire resource #[serde(rename = "https://atomicdata.dev/properties/destroy")] @@ -352,6 +354,43 @@ impl Commit { } } } + if let Some(y_update) = self.y_update.clone() { + for (prop, update) in y_update.iter() { + let update_bin = match update { + Value::YDoc(bin) => bin, + _ => { + return Err( + format!("Value in y_update is not of type YDoc: {}", prop).into() + ) + } + }; + + let decode_update = yrs::Update::decode_v2(update_bin) + .map_err(|e| format!("Error decoding Yjs update: {}", e))?; + + match resource.get(prop) { + Ok(val) => match val { + Value::YDoc(bin) => { + // Resource already has state so we will merge the update into it. + // let decoded_state = yrs::Update::decode_v2(bin) + // .map_err(|e| format!("Error decoding Yjs state: {}", e))?; + + // We can merge the state (that is saved as an update) and the incoming update without having to create a Yjs doc. + let merged_update = yrs::merge_updates_v2(vec![bin, update_bin]) + .map_err(|e| format!("Error merging Yjs updates: {}", e))?; + + resource.set(prop.into(), Value::YDoc(merged_update), store)?; + } + _ => return Err(format!("Property is not of type YDoc: {}", prop).into()), + }, + _ => { + // The property was not set yet so we initialize it with the update. + resource.set(prop.into(), Value::YDoc(update_bin.clone()), store)?; + } + }; + // We don't create any atoms because indexing yjs updates doesn't make much sense. + } + } // Remove all atoms from index if destroy if let Some(destroy) = self.destroy { if destroy { @@ -383,6 +422,10 @@ impl Commit { Ok(found) => Some(found.to_nested()?.to_owned()), Err(_) => None, }; + let y_update = match resource.get(urls::Y_UPDATE) { + Ok(found) => Some(found.to_nested()?.to_owned()), + Err(_) => None, + }; let remove = match resource.get(urls::REMOVE) { Ok(found) => Some(found.to_subjects(None)?), Err(_) => None, @@ -404,6 +447,7 @@ impl Commit { signer, set, push, + y_update, remove, destroy, previous_commit, @@ -463,6 +507,13 @@ impl Commit { Value::AtomicUrl(previous_commit.into()), ); } + if let Some(y_update) = &self.y_update { + let mut newy_update = PropVals::new(); + for (prop, val) in y_update { + newy_update.insert(prop.into(), val.clone()); + } + resource.set_unsafe(urls::Y_UPDATE.into(), newy_update.into()); + } resource.set_unsafe( SIGNER.into(), Value::new(&self.signer, &DataType::AtomicUrl)?, @@ -513,6 +564,8 @@ pub struct CommitBuilder { set: std::collections::HashMap, /// The set of PropVals that need to be appended to resource arrays. push: std::collections::HashMap, + /// A map of Propvals containing Yjs updates to be applied to the YDocs + y_update: std::collections::HashMap, /// The set of property URLs that need to be removed /// https://atomicdata.dev/properties/remove remove: HashSet, @@ -532,6 +585,7 @@ impl CommitBuilder { push: HashMap::new(), subject, set: HashMap::new(), + y_update: HashMap::new(), remove: HashSet::new(), destroy: false, previous_commit: None, @@ -584,6 +638,16 @@ impl CommitBuilder { self.subject = subject; } + pub fn add_y_update(&mut self, prop: String, update: Value) -> AtomicResult<()> { + match update { + Value::YDoc(_) => { + self.y_update.insert(prop, update); + Ok(()) + } + _ => Err(format!("Expected YDoc in add_y_update, got {}", update).into()), + } + } + /// Set Property URLs which values to be removed pub fn remove(&mut self, prop: String) { self.remove.insert(prop); @@ -607,6 +671,7 @@ fn sign_at( subject: commitbuilder.subject, signer: agent.subject.clone(), set: Some(commitbuilder.set), + y_update: Some(commitbuilder.y_update), remove: Some(commitbuilder.remove.into_iter().collect()), destroy: Some(commitbuilder.destroy), created_at: sign_date, @@ -717,6 +782,7 @@ mod test { signer: String::from("https://localhost/author"), set: Some(set), push: None, + y_update: None, remove: Some(remove), previous_commit: None, destroy: Some(destroy), diff --git a/lib/src/datatype.rs b/lib/src/datatype.rs index 9be33e24b..4626c43d3 100644 --- a/lib/src/datatype.rs +++ b/lib/src/datatype.rs @@ -19,6 +19,7 @@ pub enum DataType { Timestamp, Uri, JSON, + YDoc, Unsupported(String), } @@ -36,6 +37,7 @@ pub fn match_datatype(string: &str) -> DataType { urls::TIMESTAMP => DataType::Timestamp, urls::URI => DataType::Uri, urls::JSON => DataType::JSON, + urls::YDOC => DataType::YDoc, unsupported_datatype => DataType::Unsupported(unsupported_datatype.into()), } } @@ -57,6 +59,7 @@ impl std::str::FromStr for DataType { urls::TIMESTAMP => DataType::Timestamp, urls::URI => DataType::Uri, urls::JSON => DataType::JSON, + urls::YDOC => DataType::YDoc, unsupported_datatype => DataType::Unsupported(unsupported_datatype.into()), }) } @@ -77,6 +80,7 @@ impl fmt::Display for DataType { DataType::Timestamp => write!(f, "{}", urls::TIMESTAMP), DataType::Uri => write!(f, "{}", urls::URI), DataType::JSON => write!(f, "{}", urls::JSON), + DataType::YDoc => write!(f, "{}", urls::YDOC), DataType::Unsupported(url) => write!(f, "{}", url), } } diff --git a/lib/src/parse.rs b/lib/src/parse.rs index fa84bbe0e..0e60376ef 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -426,6 +426,33 @@ fn parse_propval( Some(&prop), )); } + DataType::YDoc => { + let serde_json::Value::Object(map) = val else { + return Err(AtomicError::parse_error( + "Invalid value for YDoc, must be of shape { type: \"ydoc\", data: }", + subject.as_deref(), + Some(&prop), + )); + }; + + let Some(data) = map.get("data") else { + return Err(AtomicError::parse_error( + "Invalid value for YDoc, no data field", + subject.as_deref(), + Some(&prop), + )); + }; + + let serde_json::Value::String(data) = data else { + return Err(AtomicError::parse_error( + "Invalid value for YDoc, data field must be a string", + subject.as_deref(), + Some(&prop), + )); + }; + + Value::new(data.as_str(), &DataType::YDoc)? + } }; Ok((prop, atomic_val)) @@ -544,8 +571,8 @@ fn parse_json_ad_map_to_resource( let importer = parse_opts.importer.as_deref().unwrap(); if !orig.has_parent(store, importer) { Err( - format!("Cannot overwrite {subj} outside of importer! Enable `overwrite_outside`"), - )? + format!("Cannot overwrite {subj} outside of importer! Enable `overwrite_outside`"), + )? } }; orig diff --git a/lib/src/serialize.rs b/lib/src/serialize.rs index 62f363d63..88092ad98 100644 --- a/lib/src/serialize.rs +++ b/lib/src/serialize.rs @@ -1,5 +1,6 @@ //! Serialization / formatting / encoding (JSON, RDF, N-Triples) +use base64::engine::{general_purpose, Engine}; use serde_json::Map; use serde_json::Value as SerdeValue; use tracing::instrument; @@ -60,6 +61,15 @@ fn val_to_serde(value: Value) -> AtomicResult { } crate::values::SubResource::Subject(s) => SerdeValue::String(s), }, + Value::YDoc(val) => { + let mut obj = Map::new(); + obj.insert("type".to_string(), "ydoc".into()); + obj.insert( + "data".to_string(), + general_purpose::STANDARD.encode(val).into(), + ); + obj.into() + } }; Ok(json_val) } diff --git a/lib/src/urls.rs b/lib/src/urls.rs index cc821d733..8589ae11f 100644 --- a/lib/src/urls.rs +++ b/lib/src/urls.rs @@ -47,6 +47,7 @@ pub const SET: &str = "https://atomicdata.dev/properties/set"; pub const PUSH: &str = "https://atomicdata.dev/properties/push"; pub const REMOVE: &str = "https://atomicdata.dev/properties/remove"; pub const DESTROY: &str = "https://atomicdata.dev/properties/destroy"; +pub const Y_UPDATE: &str = "https://atomicdata.dev/properties/yUpdate"; pub const SIGNER: &str = "https://atomicdata.dev/properties/signer"; pub const CREATED_AT: &str = "https://atomicdata.dev/properties/createdAt"; pub const SIGNATURE: &str = "https://atomicdata.dev/properties/signature"; @@ -144,6 +145,7 @@ pub const DATE: &str = "https://atomicdata.dev/datatypes/date"; pub const TIMESTAMP: &str = "https://atomicdata.dev/datatypes/timestamp"; pub const URI: &str = "https://atomicdata.dev/datatypes/uri"; pub const JSON: &str = "https://atomicdata.dev/datatypes/json"; +pub const YDOC: &str = "https://atomicdata.dev/datatypes/ydoc"; // Methods pub const INSERT: &str = "https://atomicdata.dev/methods/insert"; diff --git a/lib/src/values.rs b/lib/src/values.rs index 8ce897cc0..ce2355477 100644 --- a/lib/src/values.rs +++ b/lib/src/values.rs @@ -7,6 +7,7 @@ use crate::{ utils::{check_valid_uri, check_valid_url}, Resource, }; +use base64::{engine::general_purpose, Engine}; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -29,6 +30,7 @@ pub enum Value { Boolean(bool), Uri(String), JSON(serde_json::Value), + YDoc(Vec), Unsupported(UnsupportedValue), } @@ -85,6 +87,7 @@ impl Value { Value::Boolean(_) => DataType::Boolean, Value::Uri(_) => DataType::Uri, Value::JSON(_) => DataType::JSON, + Value::YDoc(_) => DataType::YDoc, Value::Unsupported(s) => DataType::Unsupported(s.datatype.clone()), } } @@ -167,6 +170,12 @@ impl Value { }; Ok(Value::Boolean(bool)) } + DataType::YDoc => { + let bin = general_purpose::STANDARD + .decode(value) + .map_err(|e| format!("Not a valid Base64 string: {}. {}", value, e))?; + Ok(Value::YDoc(bin)) + } } } @@ -360,6 +369,7 @@ impl fmt::Display for Value { Value::Boolean(b) => write!(f, "{}", b), Value::Uri(s) => write!(f, "{}", s), Value::JSON(s) => write!(f, "{}", s), + Value::YDoc(s) => write!(f, "{}", general_purpose::STANDARD.encode(s)), Value::Unsupported(u) => write!(f, "{}", u.value), } } diff --git a/server/src/actor_messages.rs b/server/src/actor_messages.rs index 47ff7298f..9622c3a5b 100644 --- a/server/src/actor_messages.rs +++ b/server/src/actor_messages.rs @@ -2,6 +2,7 @@ //! In this case it's for communication between the CommitMonitor and the WebSocketConnection. use actix::{prelude::Message, Addr}; +use serde::{Deserialize, Serialize}; /// Subscribes a WebSocketConnection to a Subject. #[derive(Message)] @@ -12,6 +13,13 @@ pub struct Subscribe { pub agent: String, } +#[derive(Message)] +#[rtype(result = "()")] +pub struct Unsubscribe { + pub addr: Addr, + pub subject: String, +} + /// A message containing a Resource, which should be sent to subscribers #[derive(Message, Clone, Debug)] #[rtype(result = "()")] @@ -19,3 +27,10 @@ pub struct CommitMessage { /// Full resource of the Commit itself, the new resource, and the old one pub commit_response: atomic_lib::commit::CommitResponse, } + +#[derive(Message, Clone, Debug, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct YAwarenessUpdate { + pub subject: String, + pub update: String, +} diff --git a/server/src/appstate.rs b/server/src/appstate.rs index 25795e7b7..e203305b9 100644 --- a/server/src/appstate.rs +++ b/server/src/appstate.rs @@ -1,6 +1,7 @@ //! App state, which is accessible from handlers use crate::{ commit_monitor::CommitMonitor, config::Config, errors::AtomicServerResult, search::SearchState, + y_awareness_broadcaster::YAwarenessBroadcaster, }; use atomic_lib::{ agents::Agent, @@ -23,6 +24,7 @@ pub struct AppState { pub config: Config, /// The Actix Address of the CommitMonitor, which should receive updates when a commit is applied pub commit_monitor: actix::Addr, + pub y_awareness_broadcaster: actix::Addr, pub search_state: SearchState, } @@ -65,6 +67,9 @@ impl AppState { let commit_monitor_clone = commit_monitor.clone(); + let y_awareness_broadcaster = + crate::y_awareness_broadcaster::create_y_awareness_broadcaster(store.clone()); + // This closure is called every time a Commit is created let send_commit = move |commit_response: &CommitResponse| { commit_monitor_clone.do_send(crate::actor_messages::CommitMessage { @@ -98,6 +103,7 @@ impl AppState { store, config, commit_monitor, + y_awareness_broadcaster, search_state, }) } diff --git a/server/src/bin.rs b/server/src/bin.rs index e965c919b..ab9882c9b 100644 --- a/server/src/bin.rs +++ b/server/src/bin.rs @@ -15,6 +15,7 @@ mod https; mod jsonerrors; mod routes; pub mod serve; +mod y_awareness_broadcaster; // #[cfg(feature = "search")] mod search; #[cfg(test)] diff --git a/server/src/handlers/web_sockets.rs b/server/src/handlers/web_sockets.rs index 8aab868e4..a72cb265a 100644 --- a/server/src/handlers/web_sockets.rs +++ b/server/src/handlers/web_sockets.rs @@ -18,8 +18,12 @@ use atomic_lib::{ use std::time::{Duration, Instant}; use crate::{ - actor_messages::CommitMessage, appstate::AppState, commit_monitor::CommitMonitor, - errors::AtomicServerResult, helpers::get_auth_headers, + actor_messages::{CommitMessage, YAwarenessUpdate}, + appstate::AppState, + commit_monitor::CommitMonitor, + errors::AtomicServerResult, + helpers::get_auth_headers, + y_awareness_broadcaster::YAwarenessBroadcaster, }; /// Get an HTTP request, upgrade it to a Websocket connection @@ -40,6 +44,7 @@ pub async fn web_socket_handler( let result = ws::start( WebSocketConnection::new( appstate.commit_monitor.clone(), + appstate.y_awareness_broadcaster.clone(), for_agent, // We need to make sure this is easily clone-able appstate.store.clone(), @@ -61,6 +66,7 @@ pub struct WebSocketConnection { subscribed: std::collections::HashSet, /// The CommitMonitor Actor that receives and sends messages for Commits commit_monitor_addr: Addr, + y_awareness_broadcaster_addr: Addr, /// The Agent who is connected. /// If it's not specified, it's the Public Agent. agent: ForAgent, @@ -129,6 +135,35 @@ fn handle_ws_message( Err("UNSUBSCRIBE needs a subject".into()) } } + s if s.starts_with("Y_AWARENESS_SUBSCRIBE ") => { + let mut parts = s.split("Y_AWARENESS_SUBSCRIBE "); + if let Some(subject) = parts.nth(1) { + conn.y_awareness_broadcaster_addr.do_send( + crate::actor_messages::Subscribe { + addr: ctx.address(), + subject: subject.to_string(), + agent: conn.agent.to_string(), + }, + ); + Ok(()) + } else { + Err("Y_AWARENESS_SUBSCRIBE needs a subject".into()) + } + } + s if s.starts_with("Y_AWARENESS_UNSUBSCRIBE ") => { + let mut parts = s.split("Y_AWARENESS_UNSUBSCRIBE "); + if let Some(subject) = parts.nth(1) { + conn.y_awareness_broadcaster_addr.do_send( + crate::actor_messages::Unsubscribe { + addr: ctx.address(), + subject: subject.to_string(), + }, + ); + Ok(()) + } else { + Err("Y_AWARENESS_UNSUBSCRIBE needs a subject".into()) + } + } s if s.starts_with("GET ") => { let mut parts = s.split("GET "); if let Some(subject) = parts.nth(1) { @@ -179,6 +214,22 @@ fn handle_ws_message( Err("AUTHENTICATE needs a JSON object".into()) } } + s if s.starts_with("Y_AWARENESS_UPDATE ") => { + let mut parts = s.split("Y_AWARENESS_UPDATE "); + let Some(json) = parts.nth(1) else { + return Err("Y_AWARENESS_UPDATE needs a JSON object".into()); + }; + + let update: YAwarenessUpdate = match serde_json::from_str(json) { + Ok(update) => update, + Err(err) => { + return Err(format!("Invalid Y_AWARENESS_UPDATE JSON: {}", err).into()) + } + }; + + conn.y_awareness_broadcaster_addr.do_send(update); + Ok(()) + } other => { tracing::warn!("Unknown websocket message: {}", other); Err(format!("Unknown message: {}", other).into()) @@ -199,7 +250,12 @@ fn handle_ws_message( } impl WebSocketConnection { - fn new(commit_monitor_addr: Addr, agent: ForAgent, store: Db) -> Self { + fn new( + commit_monitor_addr: Addr, + y_awareness_broadcaster_addr: Addr, + agent: ForAgent, + store: Db, + ) -> Self { let size = std::mem::size_of::(); if size > 10000 { tracing::warn!( @@ -213,6 +269,7 @@ impl WebSocketConnection { // Maybe this should be stored only in the CommitMonitor, and not here. subscribed: std::collections::HashSet::new(), commit_monitor_addr, + y_awareness_broadcaster_addr, agent, store, } @@ -250,3 +307,15 @@ impl Handler for WebSocketConnection { ctx.text(formatted_commit); } } + +impl Handler for WebSocketConnection { + type Result = (); + + #[tracing::instrument(name = "handle_y_awareness_update", skip_all)] + fn handle(&mut self, msg: YAwarenessUpdate, ctx: &mut ws::WebsocketContext) { + ctx.text(format!( + "Y_AWARENESS_UPDATE {}", + serde_json::to_string(&msg).unwrap() + )); + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 9110712de..8b0acdfef 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -16,6 +16,7 @@ mod https; mod jsonerrors; mod routes; pub mod serve; +mod y_awareness_broadcaster; // #[cfg(feature = "search")] mod search; #[cfg(test)] diff --git a/server/src/y_awareness_broadcaster.rs b/server/src/y_awareness_broadcaster.rs new file mode 100644 index 000000000..58a05eeea --- /dev/null +++ b/server/src/y_awareness_broadcaster.rs @@ -0,0 +1,129 @@ +use crate::{ + actor_messages::{Subscribe, Unsubscribe, YAwarenessUpdate}, + errors::AtomicServerResult, + handlers::web_sockets::WebSocketConnection, +}; + +use actix::{ + prelude::{Actor, Context, Handler}, + Addr, +}; +use atomic_lib::{agents::ForAgent, Db, Storelike}; +use std::collections::{HashMap, HashSet}; + +pub struct YAwarenessBroadcaster { + subscriptions: HashMap>>, + store: Db, +} + +impl Actor for YAwarenessBroadcaster { + type Context = Context; + + fn started(&mut self, _ctx: &mut Context) { + tracing::debug!("YAwarenessBroadcaster started"); + } +} + +impl Handler for YAwarenessBroadcaster { + type Result = (); + + fn handle(&mut self, msg: Subscribe, _ctx: &mut Context) { + if !msg.subject.starts_with(&self.store.get_self_url().unwrap()) { + tracing::warn!("can't subscribe to external resource"); + return; + } + + match self.store.get_resource(&msg.subject) { + Ok(resource) => { + match atomic_lib::hierarchy::check_read( + &self.store, + &resource, + &ForAgent::AgentSubject(msg.agent.clone()), + ) { + Ok(_explanation) => { + let mut set = self + .subscriptions + .get(&msg.subject) + .unwrap_or(&HashSet::new()) + .clone(); + + set.insert(msg.addr); + tracing::debug!("handle subscribe {} ", msg.subject); + self.subscriptions.insert(msg.subject.clone(), set); + } + Err(unauthorized_err) => { + tracing::debug!( + "Not allowed {} to subscribe to {}: {}", + &msg.agent, + &msg.subject, + unauthorized_err + ); + } + } + } + Err(e) => { + tracing::debug!( + "Subscribe failed for {} by {}: {}", + &msg.subject, + msg.agent, + e + ); + } + } + } +} + +impl Handler for YAwarenessBroadcaster { + type Result = (); + + fn handle(&mut self, msg: Unsubscribe, _ctx: &mut Context) { + let Some(subscriber) = self.subscriptions.get(&msg.subject) else { + tracing::warn!("no subscribers for {}", msg.subject); + return; + }; + + let mut new_subscriber = subscriber.clone(); + new_subscriber.remove(&msg.addr); + self.subscriptions + .insert(msg.subject.clone(), new_subscriber); + } +} + +// impl YAwarenessBroadcaster { +// fn broadcast_awareness_update(&mut self, msg: YAwarenessUpdate) -> AtomicServerResult<()> { +// let Some(subscribers) = self.subscriptions.get(&msg.subject) else { +// tracing::warn!("no subscribers for {}", msg.subject); +// return Ok(()); +// }; + +// for subscriber in subscribers { +// subscriber.do_send(msg.clone()); +// } + +// Ok(()) +// } +// } + +impl Handler for YAwarenessBroadcaster { + type Result = (); + + fn handle(&mut self, msg: YAwarenessUpdate, _ctx: &mut Context) { + let Some(subscribers) = self.subscriptions.get(&msg.subject) else { + tracing::warn!("no subscribers for {}", msg.subject); + return (); + }; + + for subscriber in subscribers { + subscriber.do_send(msg.clone()); + } + } +} + +pub fn create_y_awareness_broadcaster(store: Db) -> Addr { + YAwarenessBroadcaster::create(|_ctx: &mut Context| { + YAwarenessBroadcaster { + subscriptions: HashMap::new(), + store, + } + }) +} From 8f372a2bdda8c6893617f077b16c2c98c5b1955e Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Wed, 22 Oct 2025 10:20:23 +0200 Subject: [PATCH 2/8] Add y syncing and new document editor #998 #720 #1111 #741 --- Cargo.lock | 1 + browser/data-browser/package.json | 30 +- .../RTE/AIChatInput/AsyncAIChatInput.tsx | 2 +- ...sourceSuggestions.ts => mcpSuggestions.ts} | 1 - .../src/chunks/RTE/BubbleMenu.tsx | 179 +++-- .../src/chunks/RTE/CollaborativeEditor.tsx | 147 +++- .../data-browser/src/chunks/RTE/ColorMenu.tsx | 264 ++++++++ .../src/chunks/RTE/EditorWrapperBase.tsx | 45 ++ .../src/chunks/RTE/FullBubbleMenu.tsx | 100 +++ .../src/chunks/RTE/NodeSelectMenu.tsx | 29 +- .../ResourceExtension/ResourceComponent.tsx | 65 ++ .../ResourceExtension/ResourceExtention.ts | 88 +++ .../RTE/ResourceExtension/ResourceNode.ts | 147 ++++ .../src/chunks/RTE/SlashMenu/CommandList.tsx | 15 +- .../chunks/RTE/SlashMenu/CommandsExtension.ts | 180 +++-- .../data-browser/src/chunks/RTE/TableRTE.tsx | 35 + .../src/chunks/RTE/TiptapContext.tsx | 18 +- browser/data-browser/src/chunks/RTE/types.ts | 9 + .../src/chunks/RTE/useAwareness.ts | 47 -- .../data-browser/src/chunks/RTE/useYSync.ts | 76 +++ .../src/components/AtomicLink.tsx | 10 +- .../src/components/ButtonGroup.tsx | 12 +- .../data-browser/src/components/Popover.tsx | 80 ++- .../data-browser/src/components/YDocValue.tsx | 5 +- .../BasicInstanceHandlers.ts | 15 + browser/data-browser/src/helpers/iconMap.ts | 1 + browser/data-browser/src/hooks/useIsInRTE.ts | 7 + browser/data-browser/src/locales/de.po | 123 ++-- browser/data-browser/src/locales/en.po | 101 ++- browser/data-browser/src/locales/es.po | 107 ++- browser/data-browser/src/locales/fr.po | 117 ++-- .../src/views/Card/DocumentV2Card.tsx | 73 ++ .../src/views/Card/ResourceCard.tsx | 3 + .../src/views/Document/DocumentV2FullPage.tsx | 57 ++ .../data-browser/src/views/ResourcePage.tsx | 3 + .../src/views/TablePage/TablePage.tsx | 178 +---- .../src/views/TablePage/TableResource.tsx | 159 +++++ browser/lib/src/ontologies/dataBrowser.ts | 13 + browser/lib/src/store.ts | 84 ++- browser/lib/src/websockets.ts | 4 +- browser/pnpm-lock.yaml | 626 ++++++++++-------- lib/src/commit.rs | 4 - lib/src/urls.rs | 3 + server/Cargo.toml | 1 + server/src/actor_messages.rs | 26 +- server/src/appstate.rs | 14 +- server/src/bin.rs | 2 +- server/src/handlers/web_sockets.rs | 91 +-- server/src/lib.rs | 2 +- server/src/search.rs | 52 +- server/src/y_awareness_broadcaster.rs | 129 ---- server/src/y_sync_broadcaster.rs | 131 ++++ 52 files changed, 2673 insertions(+), 1038 deletions(-) rename browser/data-browser/src/chunks/RTE/AIChatInput/{resourceSuggestions.ts => mcpSuggestions.ts} (99%) create mode 100644 browser/data-browser/src/chunks/RTE/ColorMenu.tsx create mode 100644 browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx create mode 100644 browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx create mode 100644 browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts create mode 100644 browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts create mode 100644 browser/data-browser/src/chunks/RTE/TableRTE.tsx create mode 100644 browser/data-browser/src/chunks/RTE/types.ts delete mode 100644 browser/data-browser/src/chunks/RTE/useAwareness.ts create mode 100644 browser/data-browser/src/chunks/RTE/useYSync.ts create mode 100644 browser/data-browser/src/hooks/useIsInRTE.ts create mode 100644 browser/data-browser/src/views/Card/DocumentV2Card.tsx create mode 100644 browser/data-browser/src/views/Document/DocumentV2FullPage.tsx create mode 100644 browser/data-browser/src/views/TablePage/TableResource.tsx delete mode 100644 server/src/y_awareness_broadcaster.rs create mode 100644 server/src/y_sync_broadcaster.rs diff --git a/Cargo.lock b/Cargo.lock index 1b765b78d..3d80476b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,7 @@ dependencies = [ "urlencoding", "walkdir", "webp", + "yrs", ] [[package]] diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index 1fa0a9fc9..7c000bb42 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -25,18 +25,24 @@ "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-tabs": "^1.1.1", "@tanstack/react-router": "^1.95.1", - "@tiptap/extension-collaboration": "^3.6.5", - "@tiptap/extension-collaboration-caret": "^3.6.5", - "@tiptap/extension-file-handler": "^3.6.5", - "@tiptap/extension-image": "^3.6.5", - "@tiptap/extension-link": "^3.6.5", - "@tiptap/extension-mention": "^3.6.5", - "@tiptap/extension-placeholder": "^3.6.5", - "@tiptap/extension-typography": "^3.6.5", - "@tiptap/pm": "^3.6.5", - "@tiptap/react": "^3.6.5", - "@tiptap/starter-kit": "^3.6.5", - "@tiptap/suggestion": "^3.6.5", + "@tiptap/core": "^3.7.2", + "@tiptap/extension-collaboration": "^3.7.2", + "@tiptap/extension-collaboration-caret": "^3.7.2", + "@tiptap/extension-drag-handle-react": "^3.7.2", + "@tiptap/extension-file-handler": "^3.7.2", + "@tiptap/extension-image": "^3.7.2", + "@tiptap/extension-link": "^3.7.2", + "@tiptap/extension-list": "^3.7.2", + "@tiptap/extension-mention": "^3.7.2", + "@tiptap/extension-placeholder": "^3.7.2", + "@tiptap/extension-text-align": "^3.7.2", + "@tiptap/extension-text-style": "^3.7.2", + "@tiptap/extension-typography": "^3.7.2", + "@tiptap/markdown": "^3.7.2", + "@tiptap/pm": "^3.7.2", + "@tiptap/react": "^3.7.2", + "@tiptap/starter-kit": "^3.7.2", + "@tiptap/suggestion": "^3.7.2", "@tiptap/y-tiptap": "^3.0.0", "@tomic/react": "workspace:*", "@uiw/codemirror-theme-github": "^4.24.1", diff --git a/browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx b/browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx index 373d2b34f..c93777887 100644 --- a/browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx +++ b/browser/data-browser/src/chunks/RTE/AIChatInput/AsyncAIChatInput.tsx @@ -5,7 +5,7 @@ import Mention from '@tiptap/extension-mention'; import FileHandler from '@tiptap/extension-file-handler'; import { TiptapContextProvider } from '../TiptapContext'; import { EditorWrapperBase } from '../EditorWrapperBase'; -import { searchSuggestionBuilder } from './resourceSuggestions'; +import { searchSuggestionBuilder } from './mcpSuggestions'; import { useRef, useState } from 'react'; import { EditorEvents } from '../EditorEvents'; import { Markdown } from 'tiptap-markdown'; diff --git a/browser/data-browser/src/chunks/RTE/AIChatInput/resourceSuggestions.ts b/browser/data-browser/src/chunks/RTE/AIChatInput/mcpSuggestions.ts similarity index 99% rename from browser/data-browser/src/chunks/RTE/AIChatInput/resourceSuggestions.ts rename to browser/data-browser/src/chunks/RTE/AIChatInput/mcpSuggestions.ts index b31abe56d..f3ee37ae0 100644 --- a/browser/data-browser/src/chunks/RTE/AIChatInput/resourceSuggestions.ts +++ b/browser/data-browser/src/chunks/RTE/AIChatInput/mcpSuggestions.ts @@ -198,7 +198,6 @@ export function searchSuggestionBuilder( onExit() { state = SuggestionState.PickingCategory; - // cleanup(); component.destroy(); }, }; diff --git a/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx b/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx index 0ff81192b..24a410fc1 100644 --- a/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx @@ -9,101 +9,136 @@ import { } from 'react-icons/fa6'; import { styled } from 'styled-components'; import * as RadixPopover from '@radix-ui/react-popover'; -import { Row } from '../../components/Row'; +import { Column, Row } from '../../components/Row'; import { Popover } from '../../components/Popover'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { transparentize } from 'polished'; import { EditLinkForm } from './EditLinkForm'; import { useTipTapEditor } from './TiptapContext'; import { ToggleButton } from './ToggleButton'; import { NodeSelectMenu } from './NodeSelectMenu'; +import { useEditorState } from '@tiptap/react'; -export function BubbleMenu(): React.JSX.Element { +interface BubbleMenuProps { + children?: React.ReactNode; + extraItems?: React.ReactNode; + onShow?: () => void; +} + +export function BubbleMenu({ + children, + extraItems, + onShow, +}: BubbleMenuProps): React.JSX.Element { + const bubbleMenuElement = useRef(null); const editor = useTipTapEditor(); const [linkMenuOpen, setLinkMenuOpen] = useState(false); - if (!editor) { + const { isBold, isItalic, isStrikethrough, isBlockquote, isCode, isLink } = + useEditorState({ + editor, + selector: snapshot => ({ + isBold: snapshot.editor.isActive('bold'), + isItalic: snapshot.editor.isActive('italic'), + isStrikethrough: snapshot.editor.isActive('strike'), + isBlockquote: snapshot.editor.isActive('blockquote'), + isCode: snapshot.editor.isActive('code'), + isLink: snapshot.editor.isActive('link'), + }), + }); + + if (!editor.view) { return <>; } return ( - - - - editor.chain().focus().toggleBold().run()} - disabled={!editor.can().chain().focus().toggleBold().run()} - type='button' - > - - - editor.chain().focus().toggleItalic().run()} - disabled={!editor.can().chain().focus().toggleItalic().run()} - type='button' - > - - - editor.chain().focus().toggleStrike().run()} - disabled={!editor.can().chain().focus().toggleStrike().run()} - type='button' - > - - - editor.chain().focus().toggleBlockquote().run()} - disabled={!editor.can().chain().focus().toggleBlockquote().run()} - type='button' - > - - - editor.chain().focus().toggleCode().run()} - disabled={!editor.can().chain().focus().toggleCode().run()} - type='button' - > - - - - - - } - > - setLinkMenuOpen(false)} /> - + + + + + editor.chain().focus().toggleBold().run()} + disabled={!editor.can().chain().focus().toggleBold().run()} + type='button' + > + + + editor.chain().focus().toggleItalic().run()} + disabled={!editor.can().chain().focus().toggleItalic().run()} + type='button' + > + + + editor.chain().focus().toggleStrike().run()} + disabled={!editor.can().chain().focus().toggleStrike().run()} + type='button' + > + + + editor.chain().focus().toggleBlockquote().run()} + disabled={!editor.can().chain().focus().toggleBlockquote().run()} + type='button' + > + + + editor.chain().focus().toggleCode().run()} + disabled={!editor.can().chain().focus().toggleCode().run()} + type='button' + > + + + + + + } + > + setLinkMenuOpen(false)} /> + + {children} + + {extraItems} ); } -const BubbleMenuInner = styled(Row)` +const BubbleMenuInner = styled(Column)` background-color: ${p => p.theme.colors.bg}; border-radius: ${p => p.theme.radius}; padding: ${p => p.theme.size(2)}; box-shadow: ${p => p.theme.boxShadowSoft}; - + border: ${p => + p.theme.darkMode ? `1px solid ${p.theme.colors.bg2}` : 'none'}; @supports (backdrop-filter: blur(5px)) { background-color: ${p => transparentize(0.15, p.theme.colors.bg)}; backdrop-filter: blur(5px); @@ -115,6 +150,8 @@ const StyledPopover = styled(Popover)` backdrop-filter: blur(5px); padding: ${p => p.theme.size()}; border-radius: ${p => p.theme.radius}; + border: ${p => + p.theme.darkMode ? `1px solid ${p.theme.colors.bg2}` : 'none'}; @supports (backdrop-filter: blur(5px)) { background-color: ${p => transparentize(0.15, p.theme.colors.bg)}; diff --git a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx index 06ccba788..59a3e6eef 100644 --- a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx +++ b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx @@ -6,26 +6,53 @@ import { Placeholder } from '@tiptap/extension-placeholder'; import { Typography } from '@tiptap/extension-typography'; import Collaboration from '@tiptap/extension-collaboration'; import CollaborationCaret from '@tiptap/extension-collaboration-caret'; -import { useState } from 'react'; -import { BubbleMenu } from './BubbleMenu'; +import TextAlign from '@tiptap/extension-text-align'; +import { TaskList, TaskItem } from '@tiptap/extension-list'; +import DragHandle from '@tiptap/extension-drag-handle-react'; +import { + Color, + BackgroundColor, + TextStyle, +} from '@tiptap/extension-text-style'; +import { useEffect, useState } from 'react'; import { TiptapContextProvider } from './TiptapContext'; import { SlashCommands, buildSuggestion } from './SlashMenu/CommandsExtension'; +import { + ResourceCommands, + buildResourceSuggestion, +} from './ResourceExtension/ResourceExtention'; import { ExtendedImage } from './ImagePicker'; import { usePopoverContainer } from '../../components/Popover'; -import { StyledEditorWrapper, FloatingMenuText } from './sharedEditorStyles'; +import { FloatingMenuText } from './sharedEditorStyles'; import * as Y from 'yjs'; -import { useDebouncedSave, type Resource } from '@tomic/react'; +import { + useDebouncedSave, + useResource, + useStore, + type Core, + type Resource, +} from '@tomic/react'; import { EditorEvents } from './EditorEvents'; -import { useAwareness } from './useAwareness'; +import { useYSync } from './useYSync'; import { randomItem } from '@helpers/randomItem'; +import { EditorWrapperBase } from './EditorWrapperBase'; +import styled from 'styled-components'; +import { transition } from '@helpers/transition'; +import { useSettings } from '@helpers/AppSettings'; +import { FullBubbleMenu } from './FullBubbleMenu'; +import { + ResourceNode, + ResourceNodeInline, +} from './ResourceExtension/ResourceNode'; +import { IsInRTEContex } from '@hooks/useIsInRTE'; +import { FaGripVertical } from 'react-icons/fa6'; export type CollaborativeEditorProps = { placeholder?: string; doc: Y.Doc; autoFocus?: boolean; - // onChange?: (content: string) => void; resource: Resource; - + property: string; id?: string; labelId?: string; onBlur?: () => void; @@ -37,24 +64,31 @@ export default function CollaborativeEditor({ placeholder, autoFocus, doc, + property, id, labelId, resource, onBlur, }: CollaborativeEditorProps): React.JSX.Element { - const [save] = useDebouncedSave(resource, 500); + const store = useStore(); + const [save] = useDebouncedSave(resource, 2000); + const { agent, drive } = useSettings(); + const agentResource = useResource(agent?.subject); const containerRef = usePopoverContainer(); - + const color = randomItem(COLORS); const container = containerRef.current ?? document.body; - const awareness = useAwareness(resource, doc); + const awareness = useYSync(resource, property, doc); const [extensions] = useState(() => [ StarterKit.configure({ undoRedo: false, + link: false, }), Typography, Link.configure({ + autolink: true, + openOnClick: true, protocols: [ 'http', 'https', @@ -81,6 +115,11 @@ export default function CollaborativeEditor({ SlashCommands.configure({ suggestion: buildSuggestion(container), }), + ResourceCommands.configure({ + suggestion: buildResourceSuggestion(container, store, drive), + }), + ResourceNode, + ResourceNodeInline, Collaboration.configure({ document: doc, field: 'content', @@ -90,36 +129,96 @@ export default function CollaborativeEditor({ awareness, }, user: { - name: 'Pieter Post', - color: randomItem(COLORS), + name: agentResource.title, + color, }, }), + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), + TaskList, + TaskItem.configure({ + nested: true, + }), + TextStyle, + Color, + BackgroundColor, ]); const editor = useEditor({ extensions, - // content: markdown, onBlur, autofocus: !!autoFocus, editorProps: { attributes: { ...(id && { id }), ...(labelId && { 'aria-labelledby': labelId }), + spellcheck: 'true', }, }, }); + useEffect(() => { + if (agentResource) { + editor.commands.updateUser({ + name: agentResource.props.name ?? 'Untitled Agent', + color, + }); + } + }, [agentResource]); + return ( - - - - - Type '/' for options - - - - - - + + + + + + + + + + Type '/' for options or '@' for resources + + + + + + + + ); } + +export const StyledEditorWrapper = styled(EditorWrapperBase)` + box-shadow: none; + min-height: 10rem; + border-radius: ${p => p.theme.radius}; + min-height: 10rem; + padding: ${p => p.theme.size()}; + width: 100%; + margin-bottom: 10rem; + ${transition('box-shadow')} + + & .tiptap { + width: 100%; + min-height: 10rem; + ::spelling-error { + text-decoration: wavy red underline; + } + } + .drag-handle { + align-items: center; + border-radius: 0.25rem; + cursor: grab; + display: flex; + height: 1.5rem; + justify-content: center; + width: 1.5rem; + color: ${p => p.theme.colors.textLight2}; + + /* svg { + width: 1.25rem; + height: 1.25rem; + } */ + } +`; diff --git a/browser/data-browser/src/chunks/RTE/ColorMenu.tsx b/browser/data-browser/src/chunks/RTE/ColorMenu.tsx new file mode 100644 index 000000000..b3e2d3a37 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/ColorMenu.tsx @@ -0,0 +1,264 @@ +import { Column, Row } from '@components/Row'; +import { useTipTapEditor } from './TiptapContext'; +import { MdFormatColorFill, MdFormatColorText } from 'react-icons/md'; +import { useLocalStorage } from '@hooks/useLocalStorage'; +import styled from 'styled-components'; +import { transition } from '@helpers/transition'; +import { useState, useRef } from 'react'; +import { useEditorState } from '@tiptap/react'; +import { FaPencil } from 'react-icons/fa6'; +import { desaturate, readableColor, setLightness } from 'polished'; + +const MAX_LAST_USED_COLORS = 9; +const defaultColors = [ + '#7c8c04', + '#333333', + '#000080', + '#800000', + '#014421', + '#008080', + '#4B0082', + '#eb3535', + '#148a12', +]; +const defaultBackgroundColors = defaultColors.map(color => + desaturate(0.5, setLightness(0.7, color)), +); + +// Add a good highlight color to the first position. +defaultBackgroundColors[0] = '#e9ff70'; + +export const ColorMenu: React.FC = () => { + const editor = useTipTapEditor(); + const { selectedTextColor, selectedBackgroundColor } = useEditorState({ + editor, + selector: snapshot => { + return { + selectedTextColor: snapshot.editor.getAttributes('textStyle').color, + selectedBackgroundColor: + snapshot.editor.getAttributes('textStyle').backgroundColor, + }; + }, + }); + + const [lastUsedTextColors = [], setLastUsedTextColors] = useLocalStorage< + string[] + >('atomic.rte.lastUsedTextColors', defaultColors); + + const [lastUsedBackgroundColor = [], setLastUsedBackgroundColor] = + useLocalStorage( + 'atomic.rte.lastUsedBackgroundColor', + defaultBackgroundColors, + ); + + const setTextColor = (color: string) => { + editor.chain().setColor(color).run(); + setLastUsedTextColors(prev => [ + color, + ...(prev.includes(color) + ? prev.filter(c => c !== color) + : prev.slice(0, MAX_LAST_USED_COLORS - 1)), + ]); + }; + + const setBackgroundColor = (color: string) => { + editor.chain().setBackgroundColor(color).run(); + setLastUsedBackgroundColor(prev => [ + color, + ...(prev.includes(color) + ? prev.filter(c => c !== color) + : prev.slice(0, MAX_LAST_USED_COLORS - 1)), + ]); + }; + + const [handleTextColorInputChange, handleTextColorInputBlur] = useColor( + selectedTextColor, + setTextColor, + ); + + const [handleBackgroundColorInputChange, handleBackgroundColorInputBlur] = + useColor(selectedBackgroundColor, setBackgroundColor); + + const preventDefault = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( + + + + + {lastUsedTextColors.map(color => ( + setTextColor(color)} + onMouseDown={preventDefault} + /> + ))} + editor.chain().focus().unsetColor().run()} + onMouseDown={preventDefault} + /> + + + + + {lastUsedBackgroundColor.map(color => ( + setBackgroundColor(color)} + onMouseDown={preventDefault} + /> + ))} + editor.chain().focus().unsetBackgroundColor().run()} + onMouseDown={preventDefault} + /> + + + ); +}; + +const useColor = (initialColor: string, onSelect: (color: string) => void) => { + const [isChanging, setIsChanging] = useState(false); + const colorRef = useRef(initialColor); + + const onInputChange = (event: React.ChangeEvent) => { + const color = event.target.value; + colorRef.current = color; + setIsChanging(true); + }; + + const onInputBlur = () => { + if (!isChanging) { + return; + } + + setIsChanging(false); + onSelect(colorRef.current); + }; + + return [onInputChange, onInputBlur]; +}; + +const ColorButton = styled.button<{ color: string }>` + background-color: ${p => p.color}; + border: none; + height: 1.5rem; + aspect-ratio: 1/1; + border-radius: 50%; + cursor: pointer; + ${transition('transform')}; + &:hover, + &:focus-visible { + outline: none; + transform: scale(1.3); + } + + &:active { + transform: scale(1.1); + } + + &.unset { + position: relative; + border: 1px solid ${p => p.theme.colors.textLight}; + display: grid; + place-items: center; + &::before { + content: ''; + position: absolute; + height: 100%; + width: 2px; + background-color: ${p => p.theme.colors.alert}; + transform: rotate(45deg); + transform-origin: center; + } + } +`; + +interface ColorInputProps { + label: string; + value: string; + onChange: (event: React.ChangeEvent) => void; + onBlur: (event: React.FocusEvent) => void; +} + +const ColorInput: React.FC = ({ + label, + value, + onChange, + onBlur, +}) => { + return ( + +
+ +
+ +
+ ); +}; + +const HiddenColorInput = styled.input` + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +`; + +const ColorInputLabel = styled.label<{ color: string }>` + --CIL_foreground: ${p => readableColor(p.color ?? p.theme.colors.bg)}; + cursor: pointer; + position: relative; + gap: 0.5rem; + background-color: ${p => p.color}; + height: 1.5rem; + width: 1.5rem; + border-radius: 50%; + border: 1px solid var(--CIL_foreground); + &:focus-within { + outline: solid ${p => p.theme.colors.main}; + } + div { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: grid; + place-items: center; + + svg { + fill: var(--CIL_foreground); + width: 0.75rem; + height: 0.75rem; + } + } +`; diff --git a/browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx b/browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx index d4bf417b3..412101051 100644 --- a/browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx +++ b/browser/data-browser/src/chunks/RTE/EditorWrapperBase.tsx @@ -15,6 +15,9 @@ export const EditorWrapperBase = styled.div<{ hideEditor: boolean }>` } & .tiptap { + :first-child { + margin-top: 0; + } display: ${p => (p.hideEditor ? 'none' : 'block')}; outline: none; width: min(100%, 75ch); @@ -72,5 +75,47 @@ export const EditorWrapperBase = styled.div<{ hideEditor: boolean }>` color: ${p => p.theme.colors.textLight}; padding-inline-start: 1rem; } + + /* List styles */ + ul, + ol { + padding: 0 1rem; + li { + margin-bottom: 0; + } + li p { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + } + /* Task list specific styles */ + ul[data-type='taskList'] { + list-style: none; + margin-left: 0; + padding: 0; + + li { + align-items: flex-start; + display: flex; + + > label { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; + } + + > div { + flex: 1 1 auto; + } + } + + input[type='checkbox'] { + cursor: pointer; + } + + ul[data-type='taskList'] { + margin: 0; + } + } } `; diff --git a/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx b/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx new file mode 100644 index 000000000..a15cdfa2d --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx @@ -0,0 +1,100 @@ +import { ButtonGroup } from '@components/ButtonGroup'; +import { + FaAlignLeft, + FaAlignCenter, + FaAlignRight, + FaPalette, +} from 'react-icons/fa6'; +import { BubbleMenu } from './BubbleMenu'; +import { styled } from 'styled-components'; +import { useTipTapEditor } from './TiptapContext'; +import { useEditorState } from '@tiptap/react'; +import { ToggleButton } from './ToggleButton'; +import { useState } from 'react'; +import { ColorMenu } from './ColorMenu'; +import { flushSync } from 'react-dom'; + +export const FullBubbleMenu: React.FC = () => { + const editor = useTipTapEditor(); + const [colorMenuOpen, setColorMenuOpen] = useState(false); + const { alignedLeft, alignedCenter, alignedRight } = useEditorState({ + editor, + selector: snapshot => ({ + alignedLeft: snapshot.editor.isActive({ textAlign: 'left' }), + alignedCenter: snapshot.editor.isActive({ textAlign: 'center' }), + alignedRight: snapshot.editor.isActive({ textAlign: 'right' }), + }), + }); + + const alignTextOptions = [ + { + icon: , + label: 'Left', + value: 'left', + checked: alignedLeft, + }, + { + icon: , + label: 'Center', + value: 'center', + checked: alignedCenter, + }, + { + icon: , + label: 'Right', + value: 'right', + checked: alignedRight, + }, + ]; + + return ( + {colorMenuOpen && }} + onShow={() => { + flushSync(() => { + const style = editor.getAttributes('textStyle'); + setColorMenuOpen(!!style.color || !!style.backgroundColor); + + editor.commands.setMeta('bubbleMenu', 'updatePosition'); + }); + }} + > + + { + editor.chain().focus().setTextAlign(value).run(); + }} + value={ + alignedLeft + ? 'left' + : alignedCenter + ? 'center' + : alignedRight + ? 'right' + : 'left' + } + /> + + { + setColorMenuOpen(!colorMenuOpen); + requestAnimationFrame(() => { + editor.commands.setMeta('bubbleMenu', 'updatePosition'); + }); + }} + $active={colorMenuOpen} + type='button' + > + + + + ); +}; + +const Separator = styled.div` + width: 1px; + height: 2rem; + background-color: ${p => p.theme.colors.bg2}; +`; diff --git a/browser/data-browser/src/chunks/RTE/NodeSelectMenu.tsx b/browser/data-browser/src/chunks/RTE/NodeSelectMenu.tsx index 5c2b60c75..61cb01320 100644 --- a/browser/data-browser/src/chunks/RTE/NodeSelectMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/NodeSelectMenu.tsx @@ -1,9 +1,12 @@ import { BasicSelect } from '../../components/forms/BasicSelect'; import { useTipTapEditor } from './TiptapContext'; -import type { Editor } from '@tiptap/react'; +import { useEditorState, type Editor } from '@tiptap/react'; const getSelectedNode = (editor: Editor): string => { if (editor.isActive('codeBlock')) return 'codeBlock'; + if (editor.isActive('orderedList')) return 'orderedList'; + if (editor.isActive('bulletList')) return 'bulletList'; + if (editor.isActive('taskList')) return 'taskList'; if (editor.isActive('heading', { level: 1 })) return 'heading-1'; if (editor.isActive('heading', { level: 2 })) return 'heading-2'; if (editor.isActive('heading', { level: 3 })) return 'heading-3'; @@ -24,24 +27,40 @@ const nodeData = (name: string): [title: string, level?: number] => { export function NodeSelectMenu(): React.JSX.Element { const editor = useTipTapEditor(); + const { activeNode } = useEditorState({ + editor, + selector: snapshot => ({ + activeNode: getSelectedNode(snapshot.editor), + }), + }); if (!editor) return <>; - const selectedNode = getSelectedNode(editor); - const changeNodeType = (nodeType: string) => { const [targetNodeTitle, level] = nodeData(nodeType); - editor.commands.setNode(targetNodeTitle, level ? { level } : undefined); + + if (nodeType === 'orderedList') { + editor.commands.toggleOrderedList(); + } else if (nodeType === 'bulletList') { + editor.commands.toggleBulletList(); + } else if (nodeType === 'taskList') { + editor.commands.toggleTaskList(); + } else { + editor.commands.setNode(targetNodeTitle, level ? { level } : undefined); + } }; return ( changeNodeType(e.target.value)} > + + + diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx new file mode 100644 index 000000000..e29ee493d --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx @@ -0,0 +1,65 @@ +import { AtomicLink } from '@components/AtomicLink'; +import { getIconForClass } from '@helpers/iconMap'; +import type { ReactNodeViewProps } from '@tiptap/react'; +import { NodeViewWrapper } from '@tiptap/react'; +import { dataBrowser, useResource } from '@tomic/react'; +import ResourceCard from '@views/Card/ResourceCard'; +import { styled } from 'styled-components'; +import { TableRTE } from '../TableRTE'; + +const stopPropagation = (e: React.MouseEvent) => + e.stopPropagation(); + +export const ResourceComponent = ( + props: ReactNodeViewProps, +) => { + const resource = useResource(props.node.attrs.subject); + + const Component = resource.matchClass( + { + [dataBrowser.classes.table]: TableRTE, + }, + ResourceCard, + ); + + return ( + + + + ); +}; + +const StyledNodeViewWrapper = styled(NodeViewWrapper)` + margin-bottom: 1rem; +`; + +export const ResourceInlineComponent = ( + props: ReactNodeViewProps, +) => { + const resource = useResource(props.node.attrs.subject); + const Icon = getIconForClass(resource.getClasses()[0]); + + return ( + + + + {resource.title} + + + ); +}; + +const StyledAtomicLink = styled(AtomicLink)` + display: inline-flex; + align-items: center; + gap: 0.5ch; + color: ${props => props.theme.colors.mainSelectedFg}; + background-color: ${props => props.theme.colors.mainSelectedBg}; + padding: 0rem 0.4rem; + border-radius: ${props => props.theme.radius}; + border: 1px solid ${props => props.theme.colors.mainSelectedFg}; + user-select: none; +`; diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts new file mode 100644 index 000000000..1dde04b2f --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts @@ -0,0 +1,88 @@ +import { Extension, type Editor, type Range } from '@tiptap/react'; +import { Suggestion, type SuggestionOptions } from '@tiptap/suggestion'; +import type { Store } from '@tomic/react'; +import type { SuggestionItem } from '../types'; +import { getIconForClass } from '@helpers/iconMap'; +import { PluginKey } from '@tiptap/pm/state'; +import { createRenderFunction } from '../SlashMenu/CommandsExtension'; + +const resourceSuggestionPluginKey = new PluginKey('resourceSuggestion'); + +export const ResourceCommands = Extension.create({ + name: 'resourceCommands', + addOptions() { + return { + suggestion: { + char: '@', + // @ts-expect-error I'm not really sure how to type this. + command: ({ editor, range, props }) => { + props.command({ editor, range }); + }, + }, + }; + }, + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + pluginKey: resourceSuggestionPluginKey, + ...this.options.suggestion, + }), + ]; + }, +}); + +export const buildResourceSuggestion = ( + container: HTMLElement, + store: Store, + drive: string, +): Partial => ({ + items: async ({ query }: { query: string }): Promise => { + const results = await store.search(query, { + limit: 10, + // Including the results could lead to weird behavior when the document itself is returned from the server. + include: false, + parents: [drive], + }); + + const resources = await Promise.all(results.map(x => store.getResource(x))); + + return resources.map(r => ({ + title: r.title, + id: r.subject, + icon: getIconForClass(r.getClasses()[0]), + command: ({ editor, range }) => { + const subject = r.subject; + const textBeforeQuery = getTextBeforeQuery(editor, range); + + // If there is text before the query we are in not in a block context and the resource should be inserted inline. + const isBlockContext = textBeforeQuery.length === 0; + + const command = editor.chain().focus().deleteRange(range); + + if (isBlockContext) { + command.setResource({ subject }).run(); + } else { + command.setResourceInline({ subject }).insertContent(' ').run(); + } + }, + })); + }, + + render: createRenderFunction(container), +}); + +const getTextBeforeQuery = (editor: Editor, range: Range) => { + const { from } = range; + + const queryText = editor.state.doc.textBetween(range.from, range.to); + + // Resolve the position and the parent node + const $pos = editor.state.doc.resolve(from); + const parentNode = $pos.parent; + + // Calculate the offset within the parent node where the query starts + const startOfQueryOffset = $pos.parentOffset - queryText.length; + + return parentNode.textContent.substring(0, startOfQueryOffset).trim(); +}; diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts new file mode 100644 index 000000000..742b7d912 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts @@ -0,0 +1,147 @@ +import { mergeAttributes, Node } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import { unknownSubject } from '@tomic/react'; +import { + ResourceComponent, + ResourceInlineComponent, +} from './ResourceComponent'; + +export interface ResourceNodeOptions { + subject: string; +} + +declare module '@tiptap/core' { + interface Commands { + resource: { + /** + * Add a resource view to the document. + * @param options Object containing the subject. + */ + setResource: (options: ResourceNodeOptions) => ReturnType; + }; + resourceInline: { + setResourceInline: (options: ResourceNodeOptions) => ReturnType; + }; + } +} + +export const ResourceNode = Node.create({ + name: 'atomic-data-resource', + group: 'block', + + parseHTML() { + return [ + { + tag: 'a', + getAttrs: node => { + const dataType = node.getAttribute('data-type'); + + if (dataType !== 'resource-block') { + return false; // Not a resource-block, ignore + } + + return { + subject: node.getAttribute('data-subject'), // Extract the attribute + }; + }, + }, + ]; + }, + + renderHTML({ HTMLAttributes, node }) { + return [ + 'a', + mergeAttributes(HTMLAttributes, { + 'data-type': 'resource-block', + 'data-subject': node.attrs['subject'], + }), + ]; + }, + + addOptions() { + return { + subject: unknownSubject, + }; + }, + + addCommands() { + return { + setResource: + options => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }); + }, + }; + }, + + addAttributes() { + return { + subject: { + default: unknownSubject, + parseHTML: e => e.getAttribute('data-subject'), + }, + }; + }, + addNodeView() { + if (this.options.inline) { + return ReactNodeViewRenderer(ResourceInlineComponent); + } + + return ReactNodeViewRenderer(ResourceComponent); + }, +}); + +export const ResourceNodeInline = ResourceNode.extend({ + name: 'atomic-data-resource-inline', + group: 'inline', + inline: true, + parseHTML() { + return [ + { + tag: 'a', + getAttrs: node => { + const dataType = node.getAttribute('data-type'); + + if (dataType !== 'resource-inline') { + return false; // Not a resource-block, ignore + } + + return { + 'data-type': 'resource-inline', + subject: node.getAttribute('data-subject'), + }; + }, + }, + ]; + }, + + renderHTML({ HTMLAttributes, node }) { + return [ + 'a', + mergeAttributes(HTMLAttributes, { + 'data-type': 'resource-inline', + 'data-subject': node.attrs['subject'], + }), + ]; + }, + + addCommands() { + return { + setResourceInline: + options => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }); + }, + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(ResourceInlineComponent); + }, +}); diff --git a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx index 3f9a38141..8b7166b1d 100644 --- a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx +++ b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx @@ -1,4 +1,3 @@ -import type { Editor, Range } from '@tiptap/react'; import { transparentize } from 'polished'; import { forwardRef, @@ -8,23 +7,17 @@ import { useId, useCallback, } from 'react'; -import type { IconType } from 'react-icons'; import { styled } from 'styled-components'; import { ScrollArea } from '../../../components/ScrollArea'; +import type { SuggestionItem } from '../types'; export type CommandListRefType = { onKeyDown: (event: KeyboardEvent) => boolean; }; -export type CommandItem = { - title: string; - icon: IconType; - command: (props: { editor: Editor; range: Range }) => void; -}; - export interface CommandListProps { - items: CommandItem[]; - command: (item: CommandItem) => void; + items: SuggestionItem[]; + command: (item: SuggestionItem) => void; } const buildItemId = (compId: string, index: number) => @@ -95,7 +88,7 @@ export const CommandList = forwardRef( return ( selectItem(index)} onMouseEnter={() => setSelectedIndex(index)} diff --git a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts index 2505f7aa8..5d307ad81 100644 --- a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts +++ b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts @@ -1,22 +1,29 @@ import { Extension, ReactRenderer } from '@tiptap/react'; -import { Suggestion, type SuggestionOptions } from '@tiptap/suggestion'; -import { computePosition } from '@floating-ui/dom'; +import { + Suggestion, + type SuggestionOptions, + type SuggestionProps, +} from '@tiptap/suggestion'; +import { computePosition, flip, inline, shift } from '@floating-ui/dom'; import styles from '../floatingMenu.module.css'; import { CommandList, - type CommandItem, type CommandListProps, type CommandListRefType, } from './CommandList'; import { + FaCheck, FaCode, FaHeading, FaImage, + FaLink, + FaListOl, FaListUl, FaParagraph, FaQuoteLeft, } from 'react-icons/fa6'; +import type { SuggestionItem } from '../types'; export const SlashCommands = Extension.create({ name: 'slashCommands', @@ -41,37 +48,124 @@ export const SlashCommands = Extension.create({ }, }); +export const createRenderFunction = + (container: HTMLElement): SuggestionOptions['render'] => + () => { + let component: ReactRenderer; + + const updatePosition = (props: SuggestionProps) => { + if (!props.decorationNode) { + return; + } + + computePosition(props.decorationNode, component.element, { + placement: 'bottom-start', + middleware: [flip(), shift(), inline()], + }).then(({ x, y }) => { + component.element.style.setProperty('--left', `${x}px`); + component.element.style.setProperty('--top', `${y}px`); + container.appendChild(component.element); + }); + }; + + return { + onStart(props) { + component = new ReactRenderer(CommandList, { + props, + editor: props.editor, + className: styles.renderer, + }); + + // Set the initial position, this position might be obstructed so we update the position again after we render the elements. + updatePosition(props); + + requestAnimationFrame(() => { + updatePosition(props); + }); + }, + + onUpdate(props) { + component.updateProps(props); + updatePosition(props); + }, + + onKeyDown(props) { + if (props.event.key === 'Escape') { + component.destroy(); + + return true; + } + + if (!component.ref) { + return false; + } + + return component.ref.onKeyDown(props.event); + }, + + onExit() { + component.destroy(); + }, + }; + }; + export const buildSuggestion = ( container: HTMLElement, -): Partial => ({ - items: ({ query }: { query: string }): CommandItem[] => +): Partial> => ({ + items: async ({ query }: { query: string }): Promise => [ { title: 'Bullet List', + id: 'bullet-list', icon: FaListUl, command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBulletList().run(), - } as CommandItem, + } as SuggestionItem, + { + title: 'Ordered List', + id: 'ordered-list', + icon: FaListOl, + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).toggleOrderedList().run(), + } as SuggestionItem, + { + title: 'Task List', + id: 'task-list', + icon: FaCheck, + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).toggleTaskList().run(), + } as SuggestionItem, { title: 'Codeblock', + id: 'codeblock', icon: FaCode, command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setNode('codeBlock').run(), - } as CommandItem, + } as SuggestionItem, { title: 'Quote', + id: 'quote', icon: FaQuoteLeft, command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setBlockquote().run(), - } as CommandItem, + } as SuggestionItem, { title: 'Image', + id: 'image', icon: FaImage, command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setImage({ src: '' }).run(), - } as CommandItem, + } as SuggestionItem, + { + title: 'Resource', + id: 'resource', + icon: FaLink, + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).insertContent('@').run(), + } as SuggestionItem, { title: 'Heading 1', + id: 'heading-1', icon: FaHeading, command: ({ editor, range }) => editor @@ -80,9 +174,10 @@ export const buildSuggestion = ( .deleteRange(range) .setNode('heading', { level: 1 }) .run(), - } as CommandItem, + } as SuggestionItem, { title: 'Heading 2', + id: 'heading-2', icon: FaHeading, command: ({ editor, range }) => editor @@ -91,9 +186,10 @@ export const buildSuggestion = ( .deleteRange(range) .setNode('heading', { level: 2 }) .run(), - } as CommandItem, + } as SuggestionItem, { title: 'Heading 3', + id: 'heading-3', icon: FaHeading, command: ({ editor, range }) => editor @@ -102,9 +198,10 @@ export const buildSuggestion = ( .deleteRange(range) .setNode('heading', { level: 3 }) .run(), - } as CommandItem, + } as SuggestionItem, { title: 'Heading 4', + id: 'heading-4', icon: FaHeading, command: ({ editor, range }) => editor @@ -113,9 +210,10 @@ export const buildSuggestion = ( .deleteRange(range) .setNode('heading', { level: 4 }) .run(), - } as CommandItem, + } as SuggestionItem, { title: 'Heading 5', + id: 'heading-5', icon: FaHeading, command: ({ editor, range }) => editor @@ -124,9 +222,10 @@ export const buildSuggestion = ( .deleteRange(range) .setNode('heading', { level: 5 }) .run(), - } as CommandItem, + } as SuggestionItem, { title: 'Heading 6', + id: 'heading-6', icon: FaHeading, command: ({ editor, range }) => editor @@ -135,60 +234,15 @@ export const buildSuggestion = ( .deleteRange(range) .setNode('heading', { level: 6 }) .run(), - } as CommandItem, + } as SuggestionItem, { title: 'Paragraph', + id: 'paragraph', icon: FaParagraph, command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setNode('paragraph').run(), - } as CommandItem, + } as SuggestionItem, ].filter(item => item.title.toLowerCase().includes(query.toLowerCase())), - render: () => { - let component: ReactRenderer; - - return { - onStart: props => { - component = new ReactRenderer(CommandList, { - props, - editor: props.editor, - className: styles.renderer, - }); - - if (!props.decorationNode) { - return; - } - - computePosition(props.decorationNode, component.element, { - placement: 'bottom', - }).then(({ x, y }) => { - component.element.style.setProperty('--left', `${x}px`); - component.element.style.setProperty('--top', `${y}px`); - container.appendChild(component.element); - }); - }, - - onUpdate(props) { - component.updateProps(props); - }, - - onKeyDown(props) { - if (props.event.key === 'Escape') { - component.destroy(); - - return true; - } - - if (!component.ref) { - return false; - } - - return component.ref.onKeyDown(props.event); - }, - - onExit() { - component.destroy(); - }, - }; - }, + render: createRenderFunction(container), }); diff --git a/browser/data-browser/src/chunks/RTE/TableRTE.tsx b/browser/data-browser/src/chunks/RTE/TableRTE.tsx new file mode 100644 index 000000000..c830970bc --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/TableRTE.tsx @@ -0,0 +1,35 @@ +import { AtomicLink } from '@components/AtomicLink'; +import { useResource, type DataBrowser } from '@tomic/react'; +import { TableResource } from '@views/TablePage/TableResource'; +import { FaArrowUpRightFromSquare } from 'react-icons/fa6'; +import { styled } from 'styled-components'; + +interface TableRTEProps { + subject: string; +} + +export const TableRTE: React.FC = ({ subject }) => { + const resource = useResource(subject); + + return ( + + + + {resource.title} + + + ); +}; + +const Wrapper = styled.div` + width: 1100px; + margin-left: -150px; +`; + +const TableTitle = styled(AtomicLink)` + display: flex; + align-items: center; + gap: 1ch; + color: ${p => p.theme.colors.textLight}; + padding-inline-start: 0.5rem; +`; diff --git a/browser/data-browser/src/chunks/RTE/TiptapContext.tsx b/browser/data-browser/src/chunks/RTE/TiptapContext.tsx index 1ac48b7e3..a861d627d 100644 --- a/browser/data-browser/src/chunks/RTE/TiptapContext.tsx +++ b/browser/data-browser/src/chunks/RTE/TiptapContext.tsx @@ -1,20 +1,26 @@ import type { Editor } from '@tiptap/react'; import { createContext, useContext } from 'react'; -type TiptapContextType = Editor | null; +type TiptapContextType = Editor; -export const TiptapContext = createContext(null); +export const TiptapContext = createContext({} as Editor); export const useTipTapEditor = (): TiptapContextType => useContext(TiptapContext); interface TipTapContextProviderProps { - editor: Editor | null; + editor: Editor; } export const TiptapContextProvider = ({ editor, children, -}: React.PropsWithChildren) => ( - {children} -); +}: React.PropsWithChildren) => { + if (!editor) { + return null; + } + + return ( + {children} + ); +}; diff --git a/browser/data-browser/src/chunks/RTE/types.ts b/browser/data-browser/src/chunks/RTE/types.ts new file mode 100644 index 000000000..54238eff1 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/types.ts @@ -0,0 +1,9 @@ +import type { Editor, Range } from '@tiptap/react'; +import type { IconType } from 'react-icons'; + +export type SuggestionItem = { + id: string; + title: string; + icon: IconType; + command: (props: { editor: Editor; range: Range }) => void; +}; diff --git a/browser/data-browser/src/chunks/RTE/useAwareness.ts b/browser/data-browser/src/chunks/RTE/useAwareness.ts deleted file mode 100644 index 4e0e05fca..000000000 --- a/browser/data-browser/src/chunks/RTE/useAwareness.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useStore, type Resource } from '@tomic/react'; -import { useEffect } from 'react'; -import * as awarenessProtocol from 'y-protocols/awareness'; -import type * as Y from 'yjs'; - -type AwarenessUpdate = { - added: number[]; - removed: number[]; - updated: number[]; -}; - -export function useAwareness( - resource: Resource, - doc: Y.Doc, -): awarenessProtocol.Awareness { - const store = useStore(); - const awareness = new awarenessProtocol.Awareness(doc); - - useEffect(() => { - // store.subscribeAwareness(resource.subject); - - awareness.on( - 'update', - ({ added, updated, removed }: AwarenessUpdate, origin: string) => { - if (origin !== 'local') { - // Only send local updates to the server. - return; - } - - const changedClients = [...updated, ...added, ...removed]; - - const encodedUpdate = awarenessProtocol.encodeAwarenessUpdate( - awareness, - changedClients, - ); - - store.notifyAwarenessUpdate(resource.subject, encodedUpdate); - }, - ); - - return store.subscribeAwareness(resource.subject, update => { - awarenessProtocol.applyAwarenessUpdate(awareness, update, 'server'); - }); - }, [awareness, resource.subject]); - - return awareness; -} diff --git a/browser/data-browser/src/chunks/RTE/useYSync.ts b/browser/data-browser/src/chunks/RTE/useYSync.ts new file mode 100644 index 000000000..3b04dcd8d --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/useYSync.ts @@ -0,0 +1,76 @@ +import { useStore, type Resource } from '@tomic/react'; +import { useEffect } from 'react'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import * as Y from 'yjs'; + +type AwarenessUpdate = { + added: number[]; + removed: number[]; + updated: number[]; +}; + +export function useYSync( + resource: Resource, + property: string, + doc: Y.Doc, +): awarenessProtocol.Awareness { + const store = useStore(); + const awareness = new awarenessProtocol.Awareness(doc); + + useEffect(() => { + awareness.on( + 'update', + ({ added, updated, removed }: AwarenessUpdate, origin: string) => { + if (origin !== 'local') { + // Only send local updates to the server. + return; + } + + const changedClients = [...updated, ...added, ...removed]; + + const encodedUpdate = awarenessProtocol.encodeAwarenessUpdate( + awareness, + changedClients, + ); + + store.broadcastYSyncUpdate(resource.subject, property, { + awarenessUpdate: encodedUpdate, + }); + }, + ); + + return store.subscribeYSync( + resource.subject, + property, + ({ awarenessUpdate, docUpdate }) => { + if (awarenessUpdate) { + awarenessProtocol.applyAwarenessUpdate( + awareness, + awarenessUpdate, + 'server', + ); + } + + if (docUpdate) { + Y.applyUpdateV2(doc, docUpdate); + } + }, + ); + }, [awareness, resource.subject, property, store, doc]); + + useEffect(() => { + const cb = doc.on('updateV2', (udpate, _origin, _doc, transaction) => { + if (transaction.local) { + store.broadcastYSyncUpdate(resource.subject, property, { + docUpdate: udpate, + }); + } + }); + + return () => { + doc.off('updateV2', cb); + }; + }, [resource.subject, property, store, doc]); + + return awareness; +} diff --git a/browser/data-browser/src/components/AtomicLink.tsx b/browser/data-browser/src/components/AtomicLink.tsx index bc65d6f39..9714f8e98 100644 --- a/browser/data-browser/src/components/AtomicLink.tsx +++ b/browser/data-browser/src/components/AtomicLink.tsx @@ -6,6 +6,7 @@ import { ErrorLook } from '../components/ErrorLook'; import { isRunningInTauri } from '../helpers/tauri'; import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; import clsx from 'clsx'; +import { useIsInRTE } from '@hooks/useIsInRTE'; export interface AtomicLinkProps extends React.AnchorHTMLAttributes { @@ -33,6 +34,7 @@ export const AtomicLink = forwardRef( ref, ): JSX.Element => { const navigate = useNavigateWithTransition(); + const isInRTE = useIsInRTE(); if (subject === undefined && href === undefined && path === undefined) { return ( @@ -75,7 +77,13 @@ export const AtomicLink = forwardRef( } }; - const hrefConstructed = href || subject || pathToURL(path!); + let hrefConstructed: string | undefined = + href || subject || pathToURL(path!); + + if (isInRTE) { + // HACK: The Tiptap editor has an event handler that always opens links in new tabs. We can't disable it so we have to remove the href from links when inside the editor. + hrefConstructed = undefined; + } return ( void; + /** Setting value will make the button group controlled */ + value?: string; } export function ButtonGroup({ options, name, onChange, + value, }: ButtonGroupProps): JSX.Element { const [selected, setSelected] = useState( () => options.find(o => o.checked)?.value, ); const handleChange = useCallback( - (checked: boolean, value: string) => { + (checked: boolean, newVal: string) => { if (checked) { - onChange(value); - setSelected(value); + onChange(newVal); + setSelected(newVal); } }, [onChange], @@ -40,7 +43,7 @@ export function ButtonGroup({ {...option} key={option.value} onChange={handleChange} - checked={selected === option.value} + checked={(value ?? selected) === option.value} name={name} /> ))} @@ -115,6 +118,7 @@ const Label = styled.label` input:checked + & { background-color: ${p => p.theme.colors.bg1}; color: ${p => p.theme.colors.text}; + border: 1px solid ${p => p.theme.colors.bg2}; } :hover { diff --git a/browser/data-browser/src/components/Popover.tsx b/browser/data-browser/src/components/Popover.tsx index 29f2b25d3..d29179abd 100644 --- a/browser/data-browser/src/components/Popover.tsx +++ b/browser/data-browser/src/components/Popover.tsx @@ -16,6 +16,11 @@ import { styled, keyframes } from 'styled-components'; import { transparentize } from 'polished'; import { useDialogTreeInfo } from './Dialog/dialogContext'; import { useControlLock } from '../hooks/useControlLock'; +import { EventManager } from '@helpers/EventManager'; + +type PopoverEvents = { + interactionOutside: () => void; +}; export interface PopoverProps { Trigger: ReactNode; @@ -26,6 +31,7 @@ export interface PopoverProps { noArrow?: boolean; noLock?: boolean; modal?: boolean; + side?: 'top' | 'bottom' | 'left' | 'right'; } export function Popover({ @@ -38,7 +44,12 @@ export function Popover({ modal, onOpenChange, Trigger, + side = 'bottom', }: PropsWithChildren): JSX.Element { + const eventManagerRef = useRef( + new EventManager(), + ); + const { setHasOpenInnerPopup } = useDialogTreeInfo(); const containerRef = useContext(PopoverContainerContext); @@ -59,20 +70,30 @@ export function Popover({ }, [open, setHasOpenInnerPopup]); return ( - - {Trigger} - - - {children} - {!noArrow && } - - - + + + {Trigger} + + + eventManagerRef.current.emit('interactionOutside') + } + > + {children} + {!noArrow && } + + + + ); } @@ -132,3 +153,34 @@ export const PopoverContainer: FC = ({ children }) => { const ContainerDiv = styled.div` display: contents; `; + +const PopoverEventContext = createContext< + EventManager +>(new EventManager()); + +interface UsePopoverEventsProps { + onInteractionOutside: () => void; +} + +/** + * This hook allows children of a popover to listen to events emitted by the popover. + */ +export function usePopoverEvents({ + onInteractionOutside, +}: UsePopoverEventsProps) { + const eventManager = useContext(PopoverEventContext); + + useEffect(() => { + const unsubscribers: (() => void)[] = []; + + if (onInteractionOutside) { + unsubscribers.push( + eventManager.register('interactionOutside', onInteractionOutside), + ); + } + + return () => { + unsubscribers.forEach(unsubscribe => unsubscribe()); + }; + }, [eventManager, onInteractionOutside]); +} diff --git a/browser/data-browser/src/components/YDocValue.tsx b/browser/data-browser/src/components/YDocValue.tsx index 5826ea974..d2a9f88d7 100644 --- a/browser/data-browser/src/components/YDocValue.tsx +++ b/browser/data-browser/src/components/YDocValue.tsx @@ -24,10 +24,7 @@ export const YDocValue: React.FC = ({ value }) => { {showState ? 'Hide encoded state' : 'Show encoded state'} {showState && ( - + )} ); diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts index d4c24963d..6244a2de6 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts @@ -53,6 +53,21 @@ export const registerBasicInstanceHandlers = () => { }, ); + registerBasicInstanceHandler( + dataBrowser.classes.documentV2, + async (parent, createAndNavigate) => { + createAndNavigate( + dataBrowser.classes.documentV2, + { + [core.properties.name]: 'Untitled Document', + }, + { + parent, + }, + ); + }, + ); + registerBasicInstanceHandler( ai.classes.aiChat, async (parent, createAndNavigate) => { diff --git a/browser/data-browser/src/helpers/iconMap.ts b/browser/data-browser/src/helpers/iconMap.ts index 73267d7a1..e9162ac7a 100644 --- a/browser/data-browser/src/helpers/iconMap.ts +++ b/browser/data-browser/src/helpers/iconMap.ts @@ -42,6 +42,7 @@ const iconMap = new Map([ [dataBrowser.classes.bookmark, FaBook], [dataBrowser.classes.chatroom, FaComment], [dataBrowser.classes.document, FaFileLines], + [dataBrowser.classes.documentV2, FaFileLines], [server.classes.file, FaFile], [server.classes.drive, FaHardDrive], [commits.classes.commit, FaClock], diff --git a/browser/data-browser/src/hooks/useIsInRTE.ts b/browser/data-browser/src/hooks/useIsInRTE.ts new file mode 100644 index 000000000..976d424ac --- /dev/null +++ b/browser/data-browser/src/hooks/useIsInRTE.ts @@ -0,0 +1,7 @@ +import React from 'react'; + +export const IsInRTEContex = React.createContext(false); + +export function useIsInRTE(): boolean { + return React.useContext(IsInRTEContex); +} diff --git a/browser/data-browser/src/locales/de.po b/browser/data-browser/src/locales/de.po index 5c6756687..30cfb4f30 100644 --- a/browser/data-browser/src/locales/de.po +++ b/browser/data-browser/src/locales/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-29T10:47:38.272Z\n" -"PO-Revision-Date: 2025-10-14T09:30:37.541Z\n" +"PO-Revision-Date: 2025-10-22T08:13:10.872Z\n" "Last-Translator: \n" "Language: de\n" "Language-Team: \n" @@ -27,8 +27,8 @@ msgstr "Keine Klassen" #: src/components/ComboBox.tsx #: src/views/Element.tsx -#: src/views/TablePage/EditorCells/MultiRelationCell.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx +#: src/views/TablePage/EditorCells/MultiRelationCell.tsx msgid "No results" msgstr "Keine Ergebnisse" @@ -38,17 +38,17 @@ msgstr "Keine Ergebnisse" #: src/routes/History/HistoryMobileView.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx +#: src/views/OntologyPage/NewPropertyButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Abbrechen" @@ -82,14 +82,17 @@ msgid "Copy to clipboard" msgstr "In die Zwischenablage kopieren" #: src/components/HighlightedCodeBlock.tsx -#: src/chunks/AI/AIChatPage.tsx -#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/ResourcePage.tsx +#: src/chunks/AI/AIChatPage.tsx #: src/views/ResourceLine.tsx -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/Card/ResourceCard.tsx -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/views/Card/DocumentV2Card.tsx +#: src/views/Document/DocumentV2FullPage.tsx +#: src/views/Document/DocumentV2FullPage.tsx msgid "Loading..." msgstr "Laden..." @@ -112,8 +115,8 @@ msgstr "Nutzungen beschränken (optional)" #: src/components/InviteForm.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Create" msgstr "Erstellen" @@ -140,7 +143,7 @@ msgid "Go forward" msgstr "Vorwärts" #: src/components/MetaSetter.tsx -#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/mcpSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -405,15 +408,15 @@ msgstr "<0/>{0} Tastatur-Drag & Drop in der Seitenleiste aktivieren" #. placeholder {0}: resource.title #. placeholder {0}: resource.title -#: src/routes/DataRoute.tsx #: src/routes/EditRoute.tsx +#: src/routes/DataRoute.tsx msgid "Back to {0}" msgstr "Zurück zu {0}" #: src/routes/EditRoute.tsx +#: src/views/ResourcePageDefault.tsx #: src/chunks/AI/AgentConfigItem.tsx #: src/components/ResourceContextMenu/index.tsx -#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Bearbeiten" @@ -603,8 +606,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "Wenn Sie sich abmelden, wird Ihr Geheimnis entfernt. Wenn Sie Ihr Geheimnis nicht gespeichert haben, verlieren Sie den Zugriff auf diesen Benutzer. Möchten Sie sich wirklich abmelden?" #: src/routes/SettingsAgent.tsx -#: src/components/SideBar/AppMenu.tsx #: src/views/InvitePage.tsx +#: src/components/SideBar/AppMenu.tsx msgid "User Settings" msgstr "Benutzereinstellungen" @@ -1691,19 +1694,19 @@ msgstr "{0} um {1}" #. placeholder {0}: prop.shortname #. placeholder {0}: prop.shortname #: src/components/forms/EditFormDialog.tsx -#: src/views/TablePage/EditorCells/MarkdownCell.tsx #: src/views/TablePage/EditorCells/JSONCell.tsx +#: src/views/TablePage/EditorCells/MarkdownCell.tsx msgid "Edit {0}" msgstr "{0} bearbeiten" #: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx -#: src/components/forms/ResourceForm.tsx #: src/routes/SettingsServer/index.tsx +#: src/components/forms/ResourceForm.tsx #: src/routes/Share/ShareRoute.tsx -#: src/views/OntologyPage/NewPropertyButton.tsx -#: src/views/OntologyPage/NewClassButton.tsx #: src/views/Article/ArticleDescription.tsx +#: src/views/OntologyPage/NewClassButton.tsx +#: src/views/OntologyPage/NewPropertyButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -1730,23 +1733,23 @@ msgstr "Diese Eigenschaft löschen" msgid "Required field." msgstr "Pflichtfeld." -#: src/components/forms/InputMarkdown.tsx -#: src/components/forms/InputSlug.tsx +#: src/components/forms/InputDate.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx +#: src/components/forms/InputMarkdown.tsx #: src/components/forms/InputURI.tsx -#: src/components/forms/InputTimestamp.tsx -#: src/components/forms/InputDate.tsx #: src/components/forms/InputString.tsx +#: src/components/forms/InputSlug.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx -#: src/components/forms/FilePicker/FilePicker.tsx +#: src/components/forms/InputTimestamp.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx -#: src/views/TablePage/PropertyForm/PropertyForm.tsx #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts +#: src/components/forms/FilePicker/FilePicker.tsx +#: src/views/TablePage/PropertyForm/PropertyForm.tsx msgid "Required" msgstr "Erforderlich" @@ -1856,8 +1859,8 @@ msgid "Upload file(s)..." msgstr "Datei(en) hochladen..." #: src/components/forms/UploadForm.tsx -#: src/components/forms/FileDropzone/FileDropzoneInput.tsx #: src/components/forms/FileDropzone/FileDropzone.tsx +#: src/components/forms/FileDropzone/FileDropzoneInput.tsx msgid "Uploading..." msgstr "Wird hochgeladen..." @@ -2032,8 +2035,8 @@ msgstr "<0/> Herunterladen" msgid "Sorry, your browser doesn't support embedded videos." msgstr "Entschuldigung, Ihr Browser unterstützt keine eingebetteten Videos." -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx msgid "No preview available" msgstr "Keine Vorschau verfügbar" @@ -2188,10 +2191,10 @@ msgstr "Keine Instanzen" msgid "Use <0/> in code" msgstr "<0/> im Code verwenden" -#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/views/ResourceInline/ResourceInline.tsx -#: src/components/forms/FilePicker/FilePickerItem.tsx +#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/components/forms/NewForm/NewFormDialog.tsx +#: src/components/forms/FilePicker/FilePickerItem.tsx msgid "loading" msgstr "lädt" @@ -2403,14 +2406,13 @@ msgstr "<0/> Eigenschaft hinzufügen" msgid "New Property" msgstr "Neue Eigenschaft" +#: src/chunks/RTE/CollaborativeEditor.tsx #: src/chunks/RTE/AsyncMarkdownEditor.tsx #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Beginne zu tippen..." #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Gib '/' für Optionen ein" @@ -2511,8 +2513,8 @@ msgstr "Servername" msgid "Enter server name" msgstr "Servernamen eingeben" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Server URL" msgstr "Server-URL" @@ -2524,8 +2526,8 @@ msgstr "Server-URL eingeben" msgid "Type" msgstr "Typ" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Select transport type" msgstr "Transporttyp auswählen" @@ -2755,13 +2757,13 @@ msgstr "Leerer Chat" #. placeholder {0}: classType.title #. placeholder {0}: classType.title -#: src/views/TablePage/EditorCells/MultiRelationCell.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx +#: src/views/TablePage/EditorCells/MultiRelationCell.tsx msgid "Search {0}" msgstr "{0} suchen" -#: src/views/TablePage/EditorCells/MultiRelationCell.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx +#: src/views/TablePage/EditorCells/MultiRelationCell.tsx msgid "Search..." msgstr "Suchen..." @@ -2778,8 +2780,8 @@ msgstr "Einzelne Instanz" msgid "Table" msgstr "Tabelle" -#: src/views/TablePage/EditorCells/MarkdownCell.tsx #: src/views/TablePage/EditorCells/JSONCell.tsx +#: src/views/TablePage/EditorCells/MarkdownCell.tsx msgid "Open edit dialog" msgstr "Öffne Bearbeitungsdialog" @@ -2803,8 +2805,8 @@ msgstr "Datentyp" msgid "Classtype" msgstr "Klassentyp" -#: src/views/OntologyPage/Property/PropertyFormCommon.tsx #: src/views/OntologyPage/Property/EnumFormPart.tsx +#: src/views/OntologyPage/Property/PropertyFormCommon.tsx msgid "Allows Only" msgstr "Erlaubt nur" @@ -2934,6 +2936,7 @@ msgstr "Unbenannter Ordner" msgid "Untitled ChatRoom" msgstr "Unbenannter Chatraum" +#: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts #: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts msgid "Untitled Document" msgstr "Unbenanntes Dokument" @@ -2964,8 +2967,8 @@ msgstr "Mein Laufwerk" msgid "New Bookmark" msgstr "Neues Lesezeichen" -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx msgid "Ok" msgstr "Ok" @@ -3058,5 +3061,45 @@ msgid "Editing YDoc directly is not supported" msgstr "Das direkte Bearbeiten von YDoc wird nicht unterstützt" #: src/chunks/RTE/CollaborativeEditor.tsx -msgid "Pieter Post" -msgstr "Peter Post" +msgid "Untitled Agent" +msgstr "Unbenannter Agent" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Left" +msgstr "Links" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Center" +msgstr "Zentriert" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Right" +msgstr "Rechts" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Ordered List" +msgstr "Geordnete Liste" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Bullet List" +msgstr "Aufzählungsliste" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Task List" +msgstr "Aufgabenliste" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit text color" +msgstr "Textfarbe bearbeiten" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit background color" +msgstr "Hintergrundfarbe bearbeiten" + +#: src/views/Card/DocumentV2Card.tsx +msgid "document" +msgstr "Dokument" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Type '/' for options or '@' for resources" +msgstr "Geben Sie '/' für Optionen oder '@' für Ressourcen ein" diff --git a/browser/data-browser/src/locales/en.po b/browser/data-browser/src/locales/en.po index e6a2c55fd..47bdeb250 100644 --- a/browser/data-browser/src/locales/en.po +++ b/browser/data-browser/src/locales/en.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T12:18:27.636Z\n" -"PO-Revision-Date: 2025-10-14T09:30:37.525Z\n" +"PO-Revision-Date: 2025-10-22T08:13:10.867Z\n" "Last-Translator: \n" "Language: en\n" "Language-Team: \n" @@ -47,14 +47,17 @@ msgid "Resource is loading..." msgstr "Resource is loading..." #: src/components/HighlightedCodeBlock.tsx -#: src/chunks/AI/AIChatPage.tsx -#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/ResourcePage.tsx +#: src/chunks/AI/AIChatPage.tsx #: src/views/ResourceLine.tsx -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/components/Searchbar/TagSuggestionOverlay.tsx +#: src/views/Document/DocumentV2FullPage.tsx +#: src/views/Document/DocumentV2FullPage.tsx #: src/views/Card/ResourceCard.tsx -#: src/views/File/FilePreviewThumbnail.tsx +#: src/views/Card/DocumentV2Card.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx msgid "Loading..." msgstr "Loading..." @@ -86,6 +89,7 @@ msgstr "Untitled Folder" msgid "Untitled ChatRoom" msgstr "Untitled ChatRoom" +#: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts #: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts msgid "Untitled Document" msgstr "Untitled Document" @@ -155,15 +159,15 @@ msgstr "Templates" #. placeholder {0}: resource.title #. placeholder {0}: resource.title -#: src/routes/EditRoute.tsx #: src/routes/DataRoute.tsx +#: src/routes/EditRoute.tsx msgid "Back to {0}" msgstr "Back to {0}" #: src/routes/EditRoute.tsx +#: src/views/ResourcePageDefault.tsx #: src/chunks/AI/AgentConfigItem.tsx #: src/components/ResourceContextMenu/index.tsx -#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Edit" @@ -314,9 +318,9 @@ msgstr "Current Drive" #: src/components/forms/ResourceForm.tsx #: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx +#: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx -#: src/views/Article/ArticleDescription.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -364,8 +368,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "If you sign out, your secret will be removed. If you haven't saved your secret somewhere, you will lose access to this User. Are you sure you want to sign out?" #: src/routes/SettingsAgent.tsx -#: src/components/SideBar/AppMenu.tsx #: src/views/InvitePage.tsx +#: src/components/SideBar/AppMenu.tsx msgid "User Settings" msgstr "User Settings" @@ -762,11 +766,11 @@ msgstr "Name" #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Cancel" @@ -774,8 +778,8 @@ msgstr "Cancel" #: src/components/InviteForm.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Create" msgstr "Create" @@ -1119,8 +1123,8 @@ msgid "Drop files or click here to upload." msgstr "Drop files or click here to upload." #: src/components/forms/UploadForm.tsx -#: src/components/forms/FileDropzone/FileDropzoneInput.tsx #: src/components/forms/FileDropzone/FileDropzone.tsx +#: src/components/forms/FileDropzone/FileDropzoneInput.tsx msgid "Uploading..." msgstr "Uploading..." @@ -1365,7 +1369,7 @@ msgid "Edit tag" msgstr "Edit tag" #: src/components/MetaSetter.tsx -#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/mcpSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -1435,8 +1439,8 @@ msgstr "Server Name" msgid "Enter server name" msgstr "Enter server name" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Server URL" msgstr "Server URL" @@ -1448,8 +1452,8 @@ msgstr "Enter server URL" msgid "Type" msgstr "Type" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Select transport type" msgstr "Select transport type" @@ -1467,10 +1471,10 @@ msgstr "Remove drive from list" msgid "Select" msgstr "Select" -#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/views/ResourceInline/ResourceInline.tsx -#: src/components/forms/FilePicker/FilePickerItem.tsx +#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/components/forms/NewForm/NewFormDialog.tsx +#: src/components/forms/FilePicker/FilePickerItem.tsx msgid "loading" msgstr "loading" @@ -1540,17 +1544,17 @@ msgstr "Sandbox, test components in isolation" msgid "Invalid Resource" msgstr "Invalid Resource" -#: src/components/forms/InputSlug.tsx -#: src/components/forms/InputMarkdown.tsx -#: src/components/forms/InputNumber.tsx -#: src/components/forms/InputNumber.tsx #: src/components/forms/InputDate.tsx -#: src/components/forms/InputString.tsx -#: src/components/forms/InputTimestamp.tsx +#: src/components/forms/InputMarkdown.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx +#: src/components/forms/InputNumber.tsx +#: src/components/forms/InputNumber.tsx +#: src/components/forms/InputString.tsx +#: src/components/forms/InputSlug.tsx #: src/components/forms/InputURI.tsx +#: src/components/forms/InputTimestamp.tsx #: src/components/forms/FilePicker/FilePicker.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/components/forms/formValidation/useValidation.ts @@ -1836,8 +1840,8 @@ msgstr "<0/> Download" msgid "Sorry, your browser doesn't support embedded videos." msgstr "Sorry, your browser doesn't support embedded videos." -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx msgid "No preview available" msgstr "No preview available" @@ -2337,8 +2341,8 @@ msgstr "Datatype" msgid "Classtype" msgstr "Classtype" -#: src/views/OntologyPage/Property/PropertyFormCommon.tsx #: src/views/OntologyPage/Property/EnumFormPart.tsx +#: src/views/OntologyPage/Property/PropertyFormCommon.tsx msgid "Allows Only" msgstr "Allows Only" @@ -2904,7 +2908,6 @@ msgid "Start typing..." msgstr "Start typing..." #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Type '/' for options" @@ -3065,5 +3068,45 @@ msgid "Editing YDoc directly is not supported" msgstr "Editing YDoc directly is not supported" #: src/chunks/RTE/CollaborativeEditor.tsx -msgid "Pieter Post" -msgstr "Pieter Post" +msgid "Untitled Agent" +msgstr "Untitled Agent" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Left" +msgstr "Left" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Center" +msgstr "Center" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Right" +msgstr "Right" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Ordered List" +msgstr "Ordered List" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Bullet List" +msgstr "Bullet List" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Task List" +msgstr "Task List" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit text color" +msgstr "Edit text color" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit background color" +msgstr "Edit background color" + +#: src/views/Card/DocumentV2Card.tsx +msgid "document" +msgstr "document" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Type '/' for options or '@' for resources" +msgstr "Type '/' for options or '@' for resources" diff --git a/browser/data-browser/src/locales/es.po b/browser/data-browser/src/locales/es.po index dc917e5f4..9bc11da6b 100644 --- a/browser/data-browser/src/locales/es.po +++ b/browser/data-browser/src/locales/es.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T09:59:41.856Z\n" -"PO-Revision-Date: 2025-10-14T09:30:37.529Z\n" +"PO-Revision-Date: 2025-10-22T08:13:10.870Z\n" "Last-Translator: \n" "Language: es\n" "Language-Team: \n" @@ -37,11 +37,11 @@ msgstr "No hay clases" #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Cancelar" @@ -91,9 +91,9 @@ msgstr "Limitar usos (opcional)" #: src/components/InviteForm.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Create" msgstr "Crear" @@ -103,19 +103,22 @@ msgid "Invite created and copied to clipboard! 🚀" msgstr "¡Invitación creada y copiada al portapapeles! 🚀" #: src/components/HighlightedCodeBlock.tsx -#: src/chunks/AI/AIChatPage.tsx -#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/ResourcePage.tsx +#: src/chunks/AI/AIChatPage.tsx #: src/views/ResourceLine.tsx -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/components/Searchbar/TagSuggestionOverlay.tsx +#: src/views/Document/DocumentV2FullPage.tsx +#: src/views/Document/DocumentV2FullPage.tsx +#: src/views/Card/DocumentV2Card.tsx #: src/views/Card/ResourceCard.tsx -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx msgid "Loading..." msgstr "Cargando..." #: src/components/MetaSetter.tsx -#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/mcpSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -402,8 +405,8 @@ msgstr "Aceptar" #. placeholder {0}: resource.title #. placeholder {0}: resource.title -#: src/routes/EditRoute.tsx #: src/routes/DataRoute.tsx +#: src/routes/EditRoute.tsx msgid "Back to {0}" msgstr "Volver a {0}" @@ -453,9 +456,9 @@ msgid "Usage" msgstr "Uso" #: src/routes/EditRoute.tsx +#: src/views/ResourcePageDefault.tsx #: src/chunks/AI/AgentConfigItem.tsx #: src/components/ResourceContextMenu/index.tsx -#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Editar" @@ -584,8 +587,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "Si cierras sesión, tu secreto será eliminado. Si no has guardado tu secreto en algún lugar, perderás el acceso a este Usuario. ¿Estás seguro de que quieres cerrar sesión?" #: src/routes/SettingsAgent.tsx -#: src/components/SideBar/AppMenu.tsx #: src/views/InvitePage.tsx +#: src/components/SideBar/AppMenu.tsx msgid "User Settings" msgstr "Configuración de usuario" @@ -1171,14 +1174,13 @@ msgstr "Elige un emoji" msgid "Copy code" msgstr "Copiar código" +#: src/chunks/RTE/CollaborativeEditor.tsx #: src/chunks/RTE/AsyncMarkdownEditor.tsx #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Empieza a escribir..." #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Escribe '/' para ver las opciones" @@ -1200,12 +1202,12 @@ msgstr "Texto alternativo" #: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx -#: src/components/forms/ResourceForm.tsx #: src/routes/SettingsServer/index.tsx +#: src/components/forms/ResourceForm.tsx #: src/routes/Share/ShareRoute.tsx +#: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx -#: src/views/Article/ArticleDescription.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -1859,17 +1861,17 @@ msgstr "Campo obligatorio." msgid "Invalid JSON" msgstr "JSON no válido" -#: src/components/forms/InputSlug.tsx #: src/components/forms/InputMarkdown.tsx +#: src/components/forms/InputDate.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx -#: src/components/forms/InputDate.tsx #: src/components/forms/InputString.tsx +#: src/components/forms/InputSlug.tsx #: src/components/forms/InputURI.tsx -#: src/components/forms/InputTimestamp.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx +#: src/components/forms/InputTimestamp.tsx #: src/components/forms/FilePicker/FilePicker.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx #: src/components/forms/formValidation/useValidation.ts @@ -2144,8 +2146,8 @@ msgstr "No se puede mostrar el archivo debido a datos inválidos." msgid "Sorry, your browser doesn't support embedded videos." msgstr "Lo sentimos, tu navegador no soporta videos incrustados." -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx msgid "No preview available" msgstr "No hay vista previa disponible" @@ -2208,10 +2210,10 @@ msgstr "Crear nuevo recurso" msgid "New Resource" msgstr "Nuevo recurso" -#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/views/ResourceInline/ResourceInline.tsx -#: src/components/forms/FilePicker/FilePickerItem.tsx +#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/components/forms/NewForm/NewFormDialog.tsx +#: src/components/forms/FilePicker/FilePickerItem.tsx msgid "loading" msgstr "cargando" @@ -2413,10 +2415,10 @@ msgstr "Proveedor" msgid "OpenRouter is not enabled" msgstr "OpenRouter no está habilitado" -#. placeholder {0}: models.length #. placeholder {0}: modelList.length -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#. placeholder {0}: models.length #: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx msgid "{0} Models" msgstr "{0} Modelos" @@ -2443,8 +2445,8 @@ msgstr "{0} /M tokens de salida" msgid "{0} /1K web search results" msgstr "{0} /1K resultados de búsqueda web" -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx #: src/chunks/AI/ModelSelect/OpenRouterModelSelector.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx msgid "Select a model" msgstr "Selecciona un modelo" @@ -2496,8 +2498,8 @@ msgstr "Nombre del Servidor" msgid "Enter server name" msgstr "Introduce el nombre del servidor" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Server URL" msgstr "URL del Servidor" @@ -2509,8 +2511,8 @@ msgstr "Introduce la URL del servidor" msgid "Type" msgstr "Tipo" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Select transport type" msgstr "Selecciona el tipo de transporte" @@ -2689,8 +2691,8 @@ msgstr "Instancia única" msgid "Table" msgstr "Tabla" -#: src/views/OntologyPage/Property/PropertyFormCommon.tsx #: src/views/OntologyPage/Property/EnumFormPart.tsx +#: src/views/OntologyPage/Property/PropertyFormCommon.tsx msgid "Allows Only" msgstr "Permitir solo" @@ -2845,6 +2847,7 @@ msgstr "Carpeta sin título" msgid "Untitled ChatRoom" msgstr "Sala de chat sin título" +#: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts #: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts msgid "Untitled Document" msgstr "Documento sin título" @@ -2882,8 +2885,8 @@ msgstr "Decimales" msgid "New Bookmark" msgstr "Nuevo marcador" -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx msgid "Ok" msgstr "Aceptar" @@ -3033,5 +3036,45 @@ msgid "Editing YDoc directly is not supported" msgstr "La edición directa de YDoc no es compatible" #: src/chunks/RTE/CollaborativeEditor.tsx -msgid "Pieter Post" -msgstr "Pedro Cartero" +msgid "Untitled Agent" +msgstr "Agente sin título" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Left" +msgstr "Izquierda" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Center" +msgstr "Centrar" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Right" +msgstr "Derecha" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Ordered List" +msgstr "Lista ordenada" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Bullet List" +msgstr "Lista de viñetas" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Task List" +msgstr "Lista de tareas" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit text color" +msgstr "Editar el color del texto" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit background color" +msgstr "Editar el color de fondo" + +#: src/views/Card/DocumentV2Card.tsx +msgid "document" +msgstr "documento" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Type '/' for options or '@' for resources" +msgstr "Escribe '/' para ver las opciones o '@' para ver los recursos" diff --git a/browser/data-browser/src/locales/fr.po b/browser/data-browser/src/locales/fr.po index 685cee2d3..787af015e 100644 --- a/browser/data-browser/src/locales/fr.po +++ b/browser/data-browser/src/locales/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T10:06:11.465Z\n" -"PO-Revision-Date: 2025-10-14T09:30:37.538Z\n" +"PO-Revision-Date: 2025-10-22T08:13:10.871Z\n" "Last-Translator: \n" "Language: fr\n" "Language-Team: \n" @@ -31,25 +31,25 @@ msgstr "Aucune classe" #: src/routes/History/HistoryMobileView.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/views/OntologyPage/NewPropertyButton.tsx #: src/views/OntologyPage/NewClassButton.tsx +#: src/views/OntologyPage/NewPropertyButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ValueForm/ValueFormEdit.tsx #: src/views/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Cancel" msgstr "Annuler" #: src/components/ComboBox.tsx #: src/views/Element.tsx -#: src/views/TablePage/EditorCells/MultiRelationCell.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx +#: src/views/TablePage/EditorCells/MultiRelationCell.tsx msgid "No results" msgstr "Aucun résultat" @@ -92,8 +92,8 @@ msgstr "Limiter les utilisations (facultatif)" #: src/components/InviteForm.tsx #: src/views/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx msgid "Create" msgstr "Créer" @@ -103,19 +103,22 @@ msgid "Invite created and copied to clipboard! 🚀" msgstr "Invitation créée et copiée dans le presse-papier ! 🚀" #: src/components/HighlightedCodeBlock.tsx -#: src/chunks/AI/AIChatPage.tsx -#: src/components/Searchbar/TagSuggestionOverlay.tsx #: src/views/ResourcePage.tsx +#: src/chunks/AI/AIChatPage.tsx #: src/views/ResourceLine.tsx -#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx +#: src/components/Searchbar/TagSuggestionOverlay.tsx +#: src/views/Document/DocumentV2FullPage.tsx +#: src/views/Document/DocumentV2FullPage.tsx +#: src/views/Card/DocumentV2Card.tsx #: src/views/Card/ResourceCard.tsx -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx +#: src/chunks/AI/ModelSelect/OllamaModelSelector.tsx msgid "Loading..." msgstr "Chargement..." #: src/components/MetaSetter.tsx -#: src/chunks/RTE/AIChatInput/resourceSuggestions.ts +#: src/chunks/RTE/AIChatInput/mcpSuggestions.ts msgid "Atomic Data" msgstr "Atomic Data" @@ -420,8 +423,8 @@ msgstr "Accepter" #. placeholder {0}: resource.title #. placeholder {0}: resource.title -#: src/routes/DataRoute.tsx #: src/routes/EditRoute.tsx +#: src/routes/DataRoute.tsx msgid "Back to {0}" msgstr "Retour à {0}" @@ -471,9 +474,9 @@ msgid "Usage" msgstr "Usage" #: src/routes/EditRoute.tsx +#: src/views/ResourcePageDefault.tsx #: src/chunks/AI/AgentConfigItem.tsx #: src/components/ResourceContextMenu/index.tsx -#: src/views/ResourcePageDefault.tsx #: src/views/TablePage/TableHeadingMenu.tsx msgid "Edit" msgstr "Modifier" @@ -602,8 +605,8 @@ msgid "If you sign out, your secret will be removed. If you haven't saved your s msgstr "Si vous vous déconnectez, votre secret sera supprimé. Si vous n'avez pas enregistré votre secret quelque part, vous perdrez l'accès à cet utilisateur. Êtes-vous sûr de vouloir vous déconnecter ?" #: src/routes/SettingsAgent.tsx -#: src/components/SideBar/AppMenu.tsx #: src/views/InvitePage.tsx +#: src/components/SideBar/AppMenu.tsx msgid "User Settings" msgstr "Paramètres utilisateur" @@ -1193,14 +1196,13 @@ msgstr "Choisissez un emoji" msgid "Copy code" msgstr "Copier le code" +#: src/chunks/RTE/CollaborativeEditor.tsx #: src/chunks/RTE/AsyncMarkdownEditor.tsx #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Start typing..." msgstr "Commencez à taper..." #: src/chunks/RTE/AsyncMarkdownEditor.tsx -#: src/chunks/RTE/CollaborativeEditor.tsx msgid "Type '/' for options" msgstr "Tapez « / » pour les options" @@ -1222,12 +1224,12 @@ msgstr "Texte alternatif" #: src/chunks/RTE/ImagePicker.tsx #: src/components/forms/EditFormDialog.tsx -#: src/components/forms/ResourceForm.tsx #: src/routes/SettingsServer/index.tsx +#: src/components/forms/ResourceForm.tsx #: src/routes/Share/ShareRoute.tsx -#: src/views/OntologyPage/NewPropertyButton.tsx -#: src/views/OntologyPage/NewClassButton.tsx #: src/views/Article/ArticleDescription.tsx +#: src/views/OntologyPage/NewClassButton.tsx +#: src/views/OntologyPage/NewPropertyButton.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/views/TablePage/PropertyForm/EditPropertyDialog.tsx msgid "Save" @@ -1877,19 +1879,19 @@ msgstr "Champ obligatoire." msgid "Invalid JSON" msgstr "JSON non valide" -#: src/components/forms/InputSlug.tsx -#: src/components/forms/InputMarkdown.tsx +#: src/components/forms/InputDate.tsx #: src/components/forms/InputNumber.tsx #: src/components/forms/InputNumber.tsx +#: src/components/forms/InputMarkdown.tsx #: src/components/forms/InputURI.tsx -#: src/components/forms/InputTimestamp.tsx -#: src/components/forms/InputDate.tsx #: src/components/forms/InputString.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx #: src/components/forms/InputResourceArray.tsx -#: src/components/forms/FilePicker/FilePicker.tsx +#: src/components/forms/InputSlug.tsx +#: src/components/forms/InputTimestamp.tsx #: src/components/forms/ResourceSelector/ResourceSelector.tsx +#: src/components/forms/FilePicker/FilePicker.tsx #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts #: src/components/forms/formValidation/useValidation.ts @@ -1948,8 +1950,8 @@ msgid "Upload file(s)..." msgstr "Téléverser le(s) fichier(s)..." #: src/components/forms/UploadForm.tsx -#: src/components/forms/FileDropzone/FileDropzoneInput.tsx #: src/components/forms/FileDropzone/FileDropzone.tsx +#: src/components/forms/FileDropzone/FileDropzoneInput.tsx msgid "Uploading..." msgstr "Téléversement..." @@ -2162,8 +2164,8 @@ msgstr "Impossible d'afficher le fichier en raison de données non valides." msgid "Sorry, your browser doesn't support embedded videos." msgstr "Désolé, votre navigateur ne prend pas en charge les vidéos intégrées." -#: src/views/File/FilePreviewThumbnail.tsx #: src/views/File/FilePreview.tsx +#: src/views/File/FilePreviewThumbnail.tsx msgid "No preview available" msgstr "Aucun aperçu disponible" @@ -2226,10 +2228,10 @@ msgstr "Créer une nouvelle ressource" msgid "New Resource" msgstr "Nouvelle ressource" -#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/views/ResourceInline/ResourceInline.tsx -#: src/components/forms/FilePicker/FilePickerItem.tsx +#: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx #: src/components/forms/NewForm/NewFormDialog.tsx +#: src/components/forms/FilePicker/FilePickerItem.tsx msgid "loading" msgstr "chargement" @@ -2514,8 +2516,8 @@ msgstr "Nom du serveur" msgid "Enter server name" msgstr "Entrer le nom du serveur" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Server URL" msgstr "URL du serveur" @@ -2527,8 +2529,8 @@ msgstr "Entrer l'URL du serveur" msgid "Type" msgstr "Type" -#: src/components/AI/MCP/MCPServersManager.tsx #: src/components/AI/MCP/ServerItem.tsx +#: src/components/AI/MCP/MCPServersManager.tsx msgid "Select transport type" msgstr "Sélectionner le type de transport" @@ -2707,8 +2709,8 @@ msgstr "Instance unique" msgid "Table" msgstr "Tableau" -#: src/views/OntologyPage/Property/PropertyFormCommon.tsx #: src/views/OntologyPage/Property/EnumFormPart.tsx +#: src/views/OntologyPage/Property/PropertyFormCommon.tsx msgid "Allows Only" msgstr "Autoriser seulement" @@ -2756,13 +2758,13 @@ msgstr "Configurer {0}" #. placeholder {0}: classType.title #. placeholder {0}: classType.title -#: src/views/TablePage/EditorCells/MultiRelationCell.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx +#: src/views/TablePage/EditorCells/MultiRelationCell.tsx msgid "Search {0}" msgstr "Rechercher {0}" -#: src/views/TablePage/EditorCells/MultiRelationCell.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx +#: src/views/TablePage/EditorCells/MultiRelationCell.tsx msgid "Search..." msgstr "Rechercher..." @@ -2863,6 +2865,7 @@ msgstr "Dossier sans titre" msgid "Untitled ChatRoom" msgstr "Salon de discussion sans titre" +#: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts #: src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts msgid "Untitled Document" msgstr "Document sans titre" @@ -2900,8 +2903,8 @@ msgstr "Nombre de décimales" msgid "New Bookmark" msgstr "Nouveau signet" -#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx msgid "Ok" msgstr "Ok" @@ -3053,5 +3056,45 @@ msgid "Editing YDoc directly is not supported" msgstr "La modification directe de YDoc n'est pas prise en charge" #: src/chunks/RTE/CollaborativeEditor.tsx -msgid "Pieter Post" -msgstr "Pierre Postier" +msgid "Untitled Agent" +msgstr "Agent sans titre" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Left" +msgstr "Gauche" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Center" +msgstr "Centrer" + +#: src/chunks/RTE/FullBubbleMenu.tsx +msgid "Right" +msgstr "Droite" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Ordered List" +msgstr "Liste ordonnée" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Bullet List" +msgstr "Liste à puces" + +#: src/chunks/RTE/NodeSelectMenu.tsx +msgid "Task List" +msgstr "Liste de tâches" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit text color" +msgstr "Modifier la couleur du texte" + +#: src/chunks/RTE/ColorMenu.tsx +msgid "Edit background color" +msgstr "Modifier la couleur de fond" + +#: src/views/Card/DocumentV2Card.tsx +msgid "document" +msgstr "document" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Type '/' for options or '@' for resources" +msgstr "Tapez « / » pour les options ou « @ » pour les ressources" diff --git a/browser/data-browser/src/views/Card/DocumentV2Card.tsx b/browser/data-browser/src/views/Card/DocumentV2Card.tsx new file mode 100644 index 000000000..f5136f5d5 --- /dev/null +++ b/browser/data-browser/src/views/Card/DocumentV2Card.tsx @@ -0,0 +1,73 @@ +import { Column, Row } from '@components/Row'; +import type { CardViewProps } from './CardViewProps'; +import { ResourceCardTitle } from './ResourceCardTitle'; +import { + dataBrowser, + useArray, + useYDoc, + type DataBrowser, + type Resource, +} from '@tomic/react'; +import * as Y from 'yjs'; +import { Tag } from '@components/Tag'; +import { ResourceContextMenu } from '@components/ResourceContextMenu'; + +export const DocumentV2Card: React.FC = ({ resource }) => { + const [tags] = useArray(resource, dataBrowser.properties.tags); + + return ( + + + + document + + + + + {tags.map(tag => ( + + ))} + + + + ); +}; + +interface AsyncDocMarkdownRendererProps { + resource: Resource; +} + +const extractText = (doc: Y.Doc) => { + const fragment = doc.getXmlFragment('content'); + let text = ''; + + for (const node of fragment.createTreeWalker(() => true)) { + if (node instanceof Y.XmlText) { + text += node.toString().replace(/<[^>]*>?/g, ''); + } + + if (node instanceof Y.XmlElement) { + text += ' '; + } + + if (text.length > 300) { + break; + } + } + + return text + '...'; +}; + +const YdocTextRenderer: React.FC = ({ + resource, +}) => { + const doc = useYDoc(resource, dataBrowser.properties.documentContent); + + if (!doc) { + return
Loading...
; + } + + const text = extractText(doc); + + return
{text}
; +}; diff --git a/browser/data-browser/src/views/Card/ResourceCard.tsx b/browser/data-browser/src/views/Card/ResourceCard.tsx index c3a6e1cd9..6ab00a0b5 100644 --- a/browser/data-browser/src/views/Card/ResourceCard.tsx +++ b/browser/data-browser/src/views/Card/ResourceCard.tsx @@ -30,6 +30,7 @@ import { Column, Row } from '../../components/Row'; import { Tag } from '../../components/Tag'; import { ResourceContextMenu } from '../../components/ResourceContextMenu'; import { AIChatContentCard } from './AIChatContentCard'; +import { DocumentV2Card } from './DocumentV2Card'; interface ResourceCardProps extends CardViewPropsBase { /** The subject URL - the identifier of the resource. */ @@ -117,6 +118,8 @@ function ResourceCardInner(props: ResourceCardProps): JSX.Element { return ; case ai.classes.textPart: return ; + case dataBrowser.classes.documentV2: + return ; default: return ; } diff --git a/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx b/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx new file mode 100644 index 000000000..eaa329fb6 --- /dev/null +++ b/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx @@ -0,0 +1,57 @@ +import { EditableTitle } from '@components/EditableTitle'; +import { TagBar } from '@components/Tag/TagBar'; +import { dataBrowser, useYDoc } from '@tomic/react'; +import type { ResourcePageProps } from '@views/ResourcePage'; +import { lazy, Suspense } from 'react'; +import styled from 'styled-components'; + +const CollaborativeEditor = lazy( + () => import('@chunks/RTE/CollaborativeEditor'), +); + +export const DocumentV2FullPage: React.FC = ({ + resource, +}) => { + const doc = useYDoc(resource, dataBrowser.properties.documentContent); + + if (!doc) { + return
Loading...
; + } + + return ( + + + + + Loading...}> + + + + + ); +}; + +const FullPageWrapper = styled.div` + background-color: ${p => p.theme.colors.bg}; + display: flex; + flex: 1; + flex-direction: column; + min-height: ${p => p.theme.heights.fullPage}; + box-sizing: border-box; +`; + +const DocumentContainer = styled.div` + width: min(100%, ${p => p.theme.containerWidthWide}); + margin: auto; + display: flex; + flex: 1; + flex-direction: column; + padding: ${p => p.theme.size(7)}; + @media (max-width: ${props => props.theme.containerWidthWide}) { + padding: ${p => p.theme.size()}; + } +`; diff --git a/browser/data-browser/src/views/ResourcePage.tsx b/browser/data-browser/src/views/ResourcePage.tsx index 02eb05573..16d6205a4 100644 --- a/browser/data-browser/src/views/ResourcePage.tsx +++ b/browser/data-browser/src/views/ResourcePage.tsx @@ -33,6 +33,7 @@ import { Main } from '../components/Main'; import { OntologyPage } from './OntologyPage'; import { TagPage } from './TagPage/TagPage'; import { AIChatPage } from '@views/AIChat/AIChatPage'; +import { DocumentV2FullPage } from './Document/DocumentV2FullPage'; /** These properties are passed to every View at Page level */ export type ResourcePageProps = { @@ -123,6 +124,8 @@ function selectComponent(klass: string) { return TagPage; case ai.classes.aiChat: return AIChatPage; + case dataBrowser.classes.documentV2: + return DocumentV2FullPage; default: return ResourcePageDefault; } diff --git a/browser/data-browser/src/views/TablePage/TablePage.tsx b/browser/data-browser/src/views/TablePage/TablePage.tsx index cc3e73d5b..ffb6d6487 100644 --- a/browser/data-browser/src/views/TablePage/TablePage.tsx +++ b/browser/data-browser/src/views/TablePage/TablePage.tsx @@ -1,178 +1,44 @@ -import { Property, unknownSubject, useCanWrite, useStore } from '@tomic/react'; -import { useCallback, useId, useMemo, useState, type JSX } from 'react'; +import { useId, useState, type JSX } from 'react'; import { ContainerFull } from '../../components/Containers'; import { EditableTitle } from '../../components/EditableTitle'; -import { FancyTable } from '../../components/TableEditor'; import type { ResourcePageProps } from '../ResourcePage'; -import { TableHeading } from './TableHeading'; -import { useTableColumns } from './useTableColumns'; -import { TableNewRow, TableRow } from './TableRow'; -import { useTableData } from './useTableData'; -import { NewColumnButton } from './NewColumnButton'; -import { TablePageContext, TablePageContextType } from './tablePageContext'; -import { useHandlePaste } from './helpers/useHandlePaste'; -import { useHandleColumnResize } from './helpers/useHandleColumnResize'; -import { - createResourceDeletedHistoryItem, - useTableHistory, -} from './helpers/useTableHistory'; import { Row as FlexRow, Column } from '../../components/Row'; -import { useHandleClearCells } from './helpers/useHandleClearCells'; -import { useHandleCopyCommand } from './helpers/useHandleCopyCommand'; -import { ExpandedRowDialog } from './ExpandedRowDialog'; import { IconButton } from '../../components/IconButton/IconButton'; import { FaCode, FaFileCsv } from 'react-icons/fa6'; import { ResourceCodeUsageDialog } from '../CodeUsage/ResourceCodeUsageDialog'; import { TableExportDialog } from './TableExportDialog'; import { TagBar } from '../../components/Tag/TagBar'; - -const columnToKey = (column: Property) => column.subject; +import { TableResource } from './TableResource'; export function TablePage({ resource }: ResourcePageProps): JSX.Element { - const store = useStore(); const titleId = useId(); - const canWrite = useCanWrite(resource); - const [showCodeUsageDialog, setShowCodeUsageDialog] = useState(false); const [showExportDialog, setShowExportDialog] = useState(false); - const { tableClass, sorting, setSortBy, collection, invalidateCollection } = - useTableData(resource); - - const { columns, reorderColumns } = useTableColumns(tableClass); - - const { undoLastItem, addItemsToHistoryStack } = - useTableHistory(invalidateCollection); - - const handlePaste = useHandlePaste( - resource, - collection, - tableClass, - invalidateCollection, - addItemsToHistoryStack, - ); - - const [showExpandedRowDialog, setShowExpandedRowDialog] = useState(false); - const [expandedRowSubject, setExpandedRowSubject] = useState(); - - const handleRowExpand = useCallback( - async (index: number) => { - const row = await collection.getMemberWithIndex(index); - setExpandedRowSubject(row); - setShowExpandedRowDialog(true); - }, - [collection], - ); - - const tablePageContext: TablePageContextType = useMemo( - () => ({ - tableClassSubject: tableClass.subject, - sorting, - setSortBy, - addItemsToHistoryStack, - }), - [tableClass, setSortBy, sorting, addItemsToHistoryStack], - ); - - const handleDeleteRow = useCallback( - async (index: number) => { - const row = await collection.getMemberWithIndex(index); - - if (!row) { - return; - } - - const rowResource = store.getResourceLoading(row); - addItemsToHistoryStack(createResourceDeletedHistoryItem(rowResource)); - - await rowResource.destroy(); - - invalidateCollection(); - }, - [collection, store, invalidateCollection, addItemsToHistoryStack], - ); - - const handleClearCells = useHandleClearCells( - collection, - addItemsToHistoryStack, - ); - - const handleCopyCommand = useHandleCopyCommand(collection); - - const [columnSizes, handleColumnResize] = useHandleColumnResize(resource); - - const Row = useCallback( - ({ index }: { index: number }) => { - if (index < collection.totalMembers) { - return ( - - ); - } - - return ( - - ); - }, - - // Resource can update a lot but its internals are stable so removing it from the array saves a lot of rerenders and shouldn't cause issues. - // eslint-disable-next-line react-hooks/react-compiler, react-hooks/exhaustive-deps - [collection, columns, invalidateCollection, resource.subject], - ); return ( - - - - - - setShowCodeUsageDialog(true)} - > - - - setShowExportDialog(true)} - > - - - + + + + + setShowCodeUsageDialog(true)} + > + + + setShowExportDialog(true)} + > + + - - - {Row} - - - - + + + + ; +} + +const columnToKey = (column: Property) => column.subject; + +export const TableResource: React.FC = ({ resource }) => { + const store = useStore(); + const titleId = useId(); + + const canWrite = useCanWrite(resource); + + const { tableClass, sorting, setSortBy, collection, invalidateCollection } = + useTableData(resource); + + const { columns, reorderColumns } = useTableColumns(tableClass); + + const { undoLastItem, addItemsToHistoryStack } = + useTableHistory(invalidateCollection); + + const handlePaste = useHandlePaste( + resource, + collection, + tableClass, + invalidateCollection, + addItemsToHistoryStack, + ); + + const [showExpandedRowDialog, setShowExpandedRowDialog] = useState(false); + const [expandedRowSubject, setExpandedRowSubject] = useState(); + + const handleRowExpand = useCallback( + async (index: number) => { + const row = await collection.getMemberWithIndex(index); + setExpandedRowSubject(row); + setShowExpandedRowDialog(true); + }, + [collection], + ); + + const tablePageContext: TablePageContextType = useMemo( + () => ({ + tableClassSubject: tableClass.subject, + sorting, + setSortBy, + addItemsToHistoryStack, + }), + [tableClass, setSortBy, sorting, addItemsToHistoryStack], + ); + + const handleDeleteRow = useCallback( + async (index: number) => { + const row = await collection.getMemberWithIndex(index); + + if (!row) { + return; + } + + const rowResource = store.getResourceLoading(row); + addItemsToHistoryStack(createResourceDeletedHistoryItem(rowResource)); + + await rowResource.destroy(); + + invalidateCollection(); + }, + [collection, store, invalidateCollection, addItemsToHistoryStack], + ); + + const handleClearCells = useHandleClearCells( + collection, + addItemsToHistoryStack, + ); + + const handleCopyCommand = useHandleCopyCommand(collection); + + const [columnSizes, handleColumnResize] = useHandleColumnResize(resource); + + const Row = useCallback( + ({ index }: { index: number }) => { + if (index < collection.totalMembers) { + return ( + + ); + } + + return ( + + ); + }, + + // Resource can update a lot but its internals are stable so removing it from the array saves a lot of rerenders and shouldn't cause issues. + // eslint-disable-next-line react-hooks/react-compiler, react-hooks/exhaustive-deps + [collection, columns, invalidateCollection, resource.subject], + ); + + return ( + + + {Row} + + + + ); +}; diff --git a/browser/lib/src/ontologies/dataBrowser.ts b/browser/lib/src/ontologies/dataBrowser.ts index 557ec5645..e14d6186a 100644 --- a/browser/lib/src/ontologies/dataBrowser.ts +++ b/browser/lib/src/ontologies/dataBrowser.ts @@ -28,6 +28,7 @@ export const dataBrowser = { table: 'https://atomicdata.dev/classes/Table', tag: 'https://atomicdata.dev/classes/Tag', template: 'https://atomicdata.dev/ontology/data-browser/class/template', + documentV2: 'https://atomicdata.dev/classes/DocumentV2', }, properties: { color: 'https://atomicdata.dev/properties/color', @@ -58,6 +59,7 @@ export const dataBrowser = { tags: 'https://atomicdata.dev/properties/tags', tagList: 'https://atomicdata.dev/ontology/data-browser/property/tag-list', url: 'https://atomicdata.dev/property/url', + documentContent: 'https://atomicdata.dev/properties/documentContent', }, __classDefs: { ['https://atomicdata.dev/classes/Article']: [ @@ -141,6 +143,10 @@ export const dataBrowser = { 'https://atomicdata.dev/ontology/data-browser/property/image', 'https://atomicdata.dev/ontology/data-browser/property/resources', ], + ['https://atomicdata.dev/classes/DocumentV2']: [ + 'https://atomicdata.dev/properties/name', + 'https://atomicdata.dev/properties/documentContent', + ], }, } as const satisfies OntologyBaseObject; @@ -167,6 +173,7 @@ export namespace DataBrowser { export type Table = typeof dataBrowser.classes.table; export type Tag = typeof dataBrowser.classes.tag; export type Template = typeof dataBrowser.classes.template; + export type DocumentV2 = typeof dataBrowser.classes.documentV2; } declare module '../index.js' { @@ -287,6 +294,10 @@ declare module '../index.js' { | typeof dataBrowser.properties.resources; recommends: never; }; + [dataBrowser.classes.documentV2]: { + requires: BaseProps | 'https://atomicdata.dev/properties/name'; + recommends: typeof dataBrowser.properties.documentContent; + }; } interface PropTypeMapping { @@ -316,6 +327,7 @@ declare module '../index.js' { [dataBrowser.properties.tags]: string[]; [dataBrowser.properties.tagList]: string[]; [dataBrowser.properties.url]: string; + [dataBrowser.properties.documentContent]: never; } interface PropSubjectToNameMapping { @@ -345,5 +357,6 @@ declare module '../index.js' { [dataBrowser.properties.tags]: 'tags'; [dataBrowser.properties.tagList]: 'tagList'; [dataBrowser.properties.url]: 'url'; + [dataBrowser.properties.documentContent]: 'documentContent'; } } diff --git a/browser/lib/src/store.ts b/browser/lib/src/store.ts index 4b91e1ee9..048040b2d 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -28,7 +28,10 @@ import { decodeB64, encodeB64 } from './base64.js'; type ResourceCallback = ( resource: Resource, ) => void; -type AwarenessCallback = (update: Uint8Array) => void; +type YSyncCallback = (update: { + docUpdate?: Uint8Array; + awarenessUpdate?: Uint8Array; +}) => void; type SubjectCallback = (subject: string) => void; /** Callback called when the stores agent changes */ type AgentCallback = (agent: Agent | undefined) => void; @@ -54,6 +57,13 @@ type CreateResourceOptions = { propVals?: Record; }; +type SerializedYSyncUpdate = { + subject: string; + property: string; + awareness_update?: string; + doc_update?: string; +}; + export interface StoreOpts { /** The default store URL, where to send commits and where to create new instances */ serverUrl?: string; @@ -118,7 +128,8 @@ const supportsWebSockets = () => typeof WebSocket !== 'undefined'; export class Store { /** A list of all functions that need to be called when a certain resource is updated */ public subscribers: Map; - private awarenessSubscribers: Map = new Map(); + private ySyncSubscribers: Map<`${string}+${string}`, YSyncCallback[]> = + new Map(); private injectedFetch: Fetch; /** * The base URL of an Atomic Server. This is where to send commits, create new @@ -819,36 +830,46 @@ export class Store { } /** - * Subscribe to Yjs Awareness updates for a resource. + * Subscribe to Yjs Sync messages send over the websocket connection. + * These sync messages can be used for realtime collaboration and are not persisted on the server. + * For regular updates to normal values an ydocs use `store.subscribe()` instead. * @param subject The subject of the resource that you want to subscribe to. - * @param callback The callback that will be called when the awareness state changes. You should apply the update to your awareness instance here. + * @param property The property that contains the ydoc. + * @param callback The callback that will be called when the doc or awareness state changes. * @returns A function that can be called to unsubscribe. */ - public subscribeAwareness( + public subscribeYSync( subject: string, - callback: (update: Uint8Array) => void, + property: string, + callback: YSyncCallback, ): () => void { const ws = this.getWebSocketForSubject(subject); + const key = `${subject}+${property}` as const; + + const messageBody = JSON.stringify({ + subject, + property, + }); const unsub = () => { - const subscribers = this.awarenessSubscribers.get(subject); + const subscribers = this.ySyncSubscribers.get(key); if (subscribers) { const afterUnsub = subscribers.filter(item => item !== callback); if (afterUnsub.length === 0) { - this.awarenessSubscribers.delete(subject); + this.ySyncSubscribers.delete(key); if (ws?.readyState === 1) { - ws?.send(`Y_AWARENESS_UNSUBSCRIBE ${subject}`); + ws?.send(`Y_SYNC_UNSUBSCRIBE ${messageBody}`); } } else { - this.awarenessSubscribers.set(subject, afterUnsub); + this.ySyncSubscribers.set(key, afterUnsub); } } }; - const subscribers = this.awarenessSubscribers.get(subject); + const subscribers = this.ySyncSubscribers.get(key); if (subscribers) { subscribers.push(callback); @@ -856,30 +877,41 @@ export class Store { return unsub; } - this.awarenessSubscribers.set(subject, [callback]); + this.ySyncSubscribers.set(key, [callback]); if (ws?.readyState === 1) { - ws?.send(`Y_AWARENESS_SUBSCRIBE ${subject}`); + ws?.send(`Y_SYNC_SUBSCRIBE ${messageBody}`); } return unsub; } /** - * Notify the store that your awareness state changed, the store will send the update to the server. - * @param subject The subject of the resource that your awareness state changed for. + * Broadcast a change to a ydoc or awareness state to all other listeners via the open websocket. + * These messages are not persisted and are meant for fast realtime collaboration. + * To persist changes call `resource.save()` instead. + * @param subject The subject of the resource. + * @param property The property that contains the ydoc. * @param update The binary encoded update to send to the server. */ - public notifyAwarenessUpdate(subject: string, update: Uint8Array): void { + public broadcastYSyncUpdate( + subject: string, + property: string, + update: { docUpdate?: Uint8Array; awarenessUpdate?: Uint8Array }, + ): void { const ws = this.getWebSocketForSubject(subject); + const { docUpdate, awarenessUpdate } = update; + const messageBody = { subject: subject, - update: encodeB64(update), + property: property, + ...(docUpdate && { doc_update: encodeB64(docUpdate) }), + ...(awarenessUpdate && { awareness_update: encodeB64(awarenessUpdate) }), }; if (ws?.readyState === 1) { - ws?.send(`Y_AWARENESS_UPDATE ${JSON.stringify(messageBody)}`); + ws?.send(`Y_SYNC_UPDATE ${JSON.stringify(messageBody)}`); } } @@ -887,13 +919,21 @@ export class Store { * @Internal */ public __handleAwarenessUpdateMessage(message: string): void { - const messageBody = JSON.parse(message); - const update = decodeB64(messageBody.update); + const messageBody: SerializedYSyncUpdate = JSON.parse(message); + + const subscribers = this.ySyncSubscribers.get( + `${messageBody.subject}+${messageBody.property}`, + ); - const subscribers = this.awarenessSubscribers.get(messageBody.subject); + const awarenessUpdate = messageBody.awareness_update + ? decodeB64(messageBody.awareness_update) + : undefined; + const docUpdate = messageBody.doc_update + ? decodeB64(messageBody.doc_update) + : undefined; if (subscribers) { - subscribers.forEach(callback => callback(update)); + subscribers.forEach(callback => callback({ docUpdate, awarenessUpdate })); } } diff --git a/browser/lib/src/websockets.ts b/browser/lib/src/websockets.ts index 946824267..e4d936af2 100644 --- a/browser/lib/src/websockets.ts +++ b/browser/lib/src/websockets.ts @@ -45,8 +45,8 @@ function handleMessage(ev: MessageEvent, store: Store) { } else if (ev.data.startsWith('RESOURCE ')) { const resources = parseResourceMessage(ev); store.addResources(resources); - } else if (ev.data.startsWith('Y_AWARENESS_UPDATE ')) { - const update = ev.data.slice(18); + } else if (ev.data.startsWith('Y_SYNC_UPDATE ')) { + const update = ev.data.slice(14); store.__handleAwarenessUpdateMessage(update); } else { console.warn('Unknown websocket message:', ev); diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 6f24bc0f7..2bcd1978e 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -162,42 +162,60 @@ importers: '@tanstack/react-router': specifier: ^1.95.1 version: 1.95.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tiptap/core': + specifier: ^3.7.2 + version: 3.7.2(@tiptap/pm@3.7.2) '@tiptap/extension-collaboration': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) '@tiptap/extension-collaboration-caret': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + '@tiptap/extension-drag-handle-react': + specifier: ^3.7.2 + version: 3.7.2(@tiptap/extension-drag-handle@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/extension-collaboration@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/extension-node-range@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/pm@3.7.2)(@tiptap/react@3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tiptap/extension-file-handler': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/extension-text-style@2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)))(@tiptap/pm@3.6.5) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/extension-text-style@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)))(@tiptap/pm@3.7.2) '@tiptap/extension-image': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) '@tiptap/extension-link': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/extension-list': + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) '@tiptap/extension-mention': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/suggestion@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/suggestion@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) '@tiptap/extension-placeholder': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-text-align': + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-text-style': + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) '@tiptap/extension-typography': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/markdown': + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) '@tiptap/pm': - specifier: ^3.6.5 - version: 3.6.5 + specifier: ^3.7.2 + version: 3.7.2 '@tiptap/react': - specifier: ^3.6.5 - version: 3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^3.7.2 + version: 3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tiptap/starter-kit': - specifier: ^3.6.5 - version: 3.6.5 + specifier: ^3.7.2 + version: 3.7.2 '@tiptap/suggestion': - specifier: ^3.6.5 - version: 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + specifier: ^3.7.2 + version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) '@tiptap/y-tiptap': specifier: ^3.0.0 version: 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) @@ -293,7 +311,7 @@ importers: version: 4.3.0 tiptap-markdown: specifier: ^0.8.10 - version: 0.8.10(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) + version: 0.8.10(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) wuchale: specifier: ^0.16.5 version: 0.16.5 @@ -2994,209 +3012,244 @@ packages: '@tanstack/store@0.7.0': resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} - '@tiptap/core@3.6.5': - resolution: {integrity: sha512-CgXuhevQbBcPfxaXzGZgIY9+aVMSAd68Q21g3EONz1iZBw026QgiaLhGK6jgGTErZL4GoNL/P+gC5nFCvN7+cA==} + '@tiptap/core@3.7.2': + resolution: {integrity: sha512-fJwNpTx0aq4UU0HNkxPvPYfNBcTHQ/q5xBUdOB5Mgu6clwGES38jVsNNSudB8g53APUmJIS+2fJbkxl3V+0jww==} peerDependencies: - '@tiptap/pm': ^3.6.5 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-blockquote@3.6.5': - resolution: {integrity: sha512-FOOgkLHXQ3zTiL2V1js5+PfaOHXuyr/GjeFZe+W1AUk58X/qJNOVGvKT1xlMOy9gy2ySgWmco7PhNXRRTimkWg==} + '@tiptap/extension-blockquote@3.7.2': + resolution: {integrity: sha512-8rNDh1E1ratex9KicvNNnjJGtF313Kpf5hXHOUcIm8FQwvA/0Tu6jq7r6VgESMyo95R3EmzRpnCYQef+zDm6OQ==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-bold@3.6.5': - resolution: {integrity: sha512-8JXC+K4DXtPDbClHxgRAZnXYO2an2I86PbpqUw+S7m17XCr4t39Sw9CeNBohOHS6Cl8uxOKAjSyCZzqdnYkn3g==} + '@tiptap/extension-bold@3.7.2': + resolution: {integrity: sha512-bwCn9lQEXnEi7LfIx3G/oaH4I0ZapAgrHzLCNJH/tNgRKVWym1H1Oa8PlkiFDbalWOdUkbgeAUqUaIB13k408Q==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-bubble-menu@3.6.5': - resolution: {integrity: sha512-RyCJghtkYZAljZQUfjk3B5tvVVCILsIYMR9XnC152uBiIuWsnz25qfdyBP+cOl6ONrQUvdscs0WmKvzN+nXZYw==} + '@tiptap/extension-bubble-menu@3.7.2': + resolution: {integrity: sha512-rCJu/X7sZEYWkOwLO342JP06f4giVBECPzr/SzG/fQdAidPW96eilPk3L82w5j24kS9odTlxSLlFlIf6UZ2b9w==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-bullet-list@3.6.5': - resolution: {integrity: sha512-AP81hyN7oTyv5zbNVRK35cQA7zuLnI5ItFFyqMQKWh90vfftXi/zhC9C7FWvKtEH7Kk68B338G2mi4tlXDgBFQ==} + '@tiptap/extension-bullet-list@3.7.2': + resolution: {integrity: sha512-OHYYXKjmxisLQws0tW8Dz14PcyIJmaed7eypZvIm/R3hxa/7lJY/2EM/Ti5g/w1U8WPBEH1hX3icRtiulserKw==} peerDependencies: - '@tiptap/extension-list': ^3.6.5 + '@tiptap/extension-list': ^3.7.2 - '@tiptap/extension-code-block@3.6.5': - resolution: {integrity: sha512-VPPke3LqZYKPlbDBp8IcTJQwvYb1PP0L+2Qi2n3ebN4+gKn+KGhrjnkO+xNHCySWlqywQmMTIfWX1sxA0eVVdQ==} + '@tiptap/extension-code-block@3.7.2': + resolution: {integrity: sha512-TfixutvvbGCrSSCsfDK/PBm6A5FIzcPTSVDrmmsiAfqldj/Woy1T42dads+wv9SjKG06GlWDwYtDGAk2Uun8NA==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-code@3.6.5': - resolution: {integrity: sha512-U/cJFjE0hqBTbMb5J74e7ni5YReuJgS9NyJgTy94+Xt6vxR1vU4+qOl+3E0fOZtwDrxbLrsCQy3P3LvNb3HXdw==} + '@tiptap/extension-code@3.7.2': + resolution: {integrity: sha512-J8FaCiKJJnHvQiPcbfbUtc5RNmGx/Gui/K5CDMPc17jhCiQ9JhR9idRPREV24Z2t7GujWX7LG6ZDDR82pSns+g==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-collaboration-caret@3.6.5': - resolution: {integrity: sha512-3tKnl4Y9zSYZcfQLKFhIg2QRUfSC5MHF11y8xKf7y04zuEnVuscAhaNkgjimt19EvG0LZ4JP5g7KoeoltBSqeQ==} + '@tiptap/extension-collaboration-caret@3.7.2': + resolution: {integrity: sha512-guOhgA2gYS4wRRbpOkkcaSpruqZVlJ3Xqb379n0lwXrZONorFTOHl7/kan4Da4RM2IoaTg73OSjQkChyEAcvuw==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 '@tiptap/y-tiptap': ^3.0.0-beta.3 - '@tiptap/extension-collaboration@3.6.5': - resolution: {integrity: sha512-IbyZNGUo8xYYsZ09BJxuA/VHqpH8x+he9mUShfmT+PtBvAxiU3beq2B2yXIGBmiBW7At5C6JmDK9PvAGeBYvlw==} + '@tiptap/extension-collaboration@3.7.2': + resolution: {integrity: sha512-eIwFBQca7hz8p+UXtn9/B9p45qCFuKLTdCgog9bJjLY6K1ObY0/9fmraxou59Qym57XLs5cm0tc2Db1O5rOxkw==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 '@tiptap/y-tiptap': ^3.0.0-beta.3 yjs: ^13 - '@tiptap/extension-document@3.6.5': - resolution: {integrity: sha512-0c7kxWBIEIcoHUG89vpHOF2h4CMa0q6VWXhZ+6iqcI5uyqaKwgcW/TbHZR0nAwEsZLdRCKaryn2kO7jXiCjfnA==} + '@tiptap/extension-document@3.7.2': + resolution: {integrity: sha512-OrHl402v2FWCUKR1Xi5MTNBAkKYQh7mtpw/WlJDFnk5z1qHLqz4UIcbGilDYzVPrNUZPhA1p3c+V5UUVUFzUfg==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-dropcursor@3.6.5': - resolution: {integrity: sha512-BsO3ufLHsdeV1ddChwQfi2Q4UkeqOF4LeUYPYBKfSg59aRKTSoxj3gZrAsaAm/0O3DmAiKNBiCtNRTJSApPEBQ==} + '@tiptap/extension-drag-handle-react@3.7.2': + resolution: {integrity: sha512-WCgbdHNGjtcWIo1CYQhrKE3vEW/tKSAar/0ezxp48UJiSN79mOXq7R/hoC+DXfBUkEO3dkJEuLgoT0XV4uymWQ==} peerDependencies: - '@tiptap/extensions': ^3.6.5 + '@tiptap/extension-drag-handle': ^3.7.2 + '@tiptap/pm': ^3.7.2 + '@tiptap/react': ^3.7.2 + react: ^16.8 || ^17 || ^18 || ^19 + react-dom: ^16.8 || ^17 || ^18 || ^19 - '@tiptap/extension-file-handler@3.6.5': - resolution: {integrity: sha512-r0cR6ZbdtEkGG7V5taRm9TcMCXwIOFHC0niER2MxWVw+KsQdAeZEtTBf8YeIu5CoI5z7j95X9d2o4AaavYjIUw==} + '@tiptap/extension-drag-handle@3.7.2': + resolution: {integrity: sha512-YFnknAu+yuaDwvNdRm/hdgxnIfqYw/dM3o0C32zztvrhd8CE7gINcloF+O+HLxyZ5ut+gjm33QTqQqP/l550pA==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/extension-text-style': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/extension-collaboration': ^3.7.2 + '@tiptap/extension-node-range': ^3.7.2 + '@tiptap/pm': ^3.7.2 + '@tiptap/y-tiptap': ^3.0.0-beta.3 + + '@tiptap/extension-dropcursor@3.7.2': + resolution: {integrity: sha512-79y6M9pJYwqcqBHIWoomfptJp0QB/TP3Y+2NOL09sMNeSdUgmz5pCVObA4H48YMkoB0EcUtux2IUOM66e4nsJA==} + peerDependencies: + '@tiptap/extensions': ^3.7.2 - '@tiptap/extension-floating-menu@3.6.5': - resolution: {integrity: sha512-ASKb5vHkYyB9g3vOAr2E2U+b6MbHk4Ff4PqngafGlWRAmOAmFxTcw9fLa3HKnj4pokSsYAEvYGOso99/W3GzhA==} + '@tiptap/extension-file-handler@3.7.2': + resolution: {integrity: sha512-tdWsrZO+InXcP3jpSJd8qlCa6uNcZ/q1yARPLGsY6RKcGAq3ZmuOVkquRTOE5181kL34WtptUbQb+qQorMTXdw==} + peerDependencies: + '@tiptap/core': ^3.7.2 + '@tiptap/extension-text-style': ^3.7.2 + '@tiptap/pm': ^3.7.2 + + '@tiptap/extension-floating-menu@3.7.2': + resolution: {integrity: sha512-g19ratrXlplYDS29VLQa1y/IM/ro0UFhSS4fQokiQKkazwnA1ZVnebjw8ERYg5lkMm/hiImqstpgdO0LtoivvQ==} peerDependencies: '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 + + '@tiptap/extension-gapcursor@3.7.2': + resolution: {integrity: sha512-vCLo2dL2SfeWjh/gJKDiu0/fz6OF7obGTJvHg/yStkoUqlAEiwKoyHP/NXeTGYJMzZzUi0kY9DtTEJdGFvphuQ==} + peerDependencies: + '@tiptap/extensions': ^3.7.2 - '@tiptap/extension-gapcursor@3.6.5': - resolution: {integrity: sha512-SHtp71zhV2bAQS8kaJ/otb2podGusDREZ9/SQ1rZi6yPcDFLS2KvIvsLssDwbjTuH6KefnsN6Vx01tzmXRAQig==} + '@tiptap/extension-hard-break@3.7.2': + resolution: {integrity: sha512-nNDo+5S1yRQ3JkBM+gwpEEVZ/Kw9qWoG/cpShyGYDHo1/y8MgO+VI0kSb/LuBTw7g+jmNXdf+ZaRRI/pXsUihg==} peerDependencies: - '@tiptap/extensions': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-hard-break@3.6.5': - resolution: {integrity: sha512-6iMS6SzIn7+X95okRX8y3l/4f1G3lTrq24sbcAX4MHITncDC6g3TrdAxdA67Tqn5NI/OQx0LwF3kFJDO8QTAUg==} + '@tiptap/extension-heading@3.7.2': + resolution: {integrity: sha512-eH/G66FIRlTQz4MhEmlNNNQgVTxhoqlkyFzgeG5aipIplYOdYa5Y6Wl0NF4xqr1jAHGLAK6LaYS4FXp3TE7LyA==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-heading@3.6.5': - resolution: {integrity: sha512-jFS5saqTtfG6MM0sW4X6mZlLycT2ud0Oo1GOZkCyBClwSOpZI/EBLNRIgoXgNtWrY917vB7xTQgCpTVHbvVRsQ==} + '@tiptap/extension-horizontal-rule@3.7.2': + resolution: {integrity: sha512-pN+1hJAVVP3uqtpZ5Rm7z5XUB/NGprK6wExJ04xG117E4rTVcaEb1FnMILY3J3A5XbdC3vHX+cblR8mOl1PAMw==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-horizontal-rule@3.6.5': - resolution: {integrity: sha512-yNxcejI25j6NQMQuKQMTVmNYLnrHFCpzGAz1Ndzyar+gItYZXI9BLmMlwpLkIaJMpIKChj+2qHz25fPS5FlNFw==} + '@tiptap/extension-image@3.7.2': + resolution: {integrity: sha512-GlFdoZULF9mEG3tMRqB1DDlyA75NIRHS5NKuoicQTAX9OegiZfTPYRmVOpLNaTunTt8mFL6Wx2Z9x5ZN2WdNBg==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-image@3.6.5': - resolution: {integrity: sha512-Tzej5vSjiIPmr+3zeFYIGOdZ7T+tnOMMuFuduiitynTsVY2oG34Y/oBnwBfD+jLq8v3SBFF55J972Ga6+vBvrA==} + '@tiptap/extension-italic@3.7.2': + resolution: {integrity: sha512-1tfF37LvKgA5hg09UBgOjdMLNRb1C6keIOBF0r5oHKeWPYOf4z3j5IU9PsFUoOn53XRMb1aiD/TNbGPyoT3Fyw==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-italic@3.6.5': - resolution: {integrity: sha512-2EtO2uffw5YnTQ1cieLPv9t7OKCfJFbgHRJPXf7Nnfh8XFh5AEyzw0qBNXZyLtlB28+HHSWLc/OHS6xMfwUy0A==} + '@tiptap/extension-link@3.7.2': + resolution: {integrity: sha512-9K54PxBiDSWAMfICqkb8jcQ6cL7vDAtjTk0zqBw4d+XuaUy0FC9QUdbx7r1Pkbf36K1/ApbvM9a7qpOirWk8Xw==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-link@3.6.5': - resolution: {integrity: sha512-VLCDNwxLC1IPnWT3HLLJUg1Hflf8A2jfs7aNF4vyMTWmKnrk1zmN+VyXQTAkrqr27qE5FnmLhHOYF3SNolNucw==} + '@tiptap/extension-list-item@3.7.2': + resolution: {integrity: sha512-962TFsx4eF5NMyLVhGFGF/btt5j3MipPhDiUsxzBgnlW8o5OonVepb9cDrqpEDQ2/wLvheWnCKuvmG7umasldQ==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/extension-list': ^3.7.2 - '@tiptap/extension-list-item@3.6.5': - resolution: {integrity: sha512-A5JKf2dNG6IRrHmkaqroq/VcD5SnXYXgpQpsF7HrPGIzUSIjvjQu088980NQPHyMuTanDMml+nZgd8RzHhRISA==} + '@tiptap/extension-list-keymap@3.7.2': + resolution: {integrity: sha512-1du9eo+NPIkuRT258yUn9bovhip556aJo/yDtRbswEVNScP1E8y/kFRWvw0HD7/YWcNqok1ZteoSwShWnKAXRQ==} peerDependencies: - '@tiptap/extension-list': ^3.6.5 + '@tiptap/extension-list': ^3.7.2 - '@tiptap/extension-list-keymap@3.6.5': - resolution: {integrity: sha512-OHGGTJMdUOBincMgYGEN4WzHrTB/GFeCxLDJraDknPx4VJVa3UVZS8F8xd5cb2WnACEF33Ud/0yK3aN6kHrbtQ==} + '@tiptap/extension-list@3.7.2': + resolution: {integrity: sha512-/tYHmEkOGcVweAc9ZgnAXkzua5aJfu7TjZcKTq5fmDt6x9MY1eY1+egS7D9hVR2sUSAC10VgXmYdYPDsKF3p2g==} peerDependencies: - '@tiptap/extension-list': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-list@3.6.5': - resolution: {integrity: sha512-2S6wNeaGvvYzJygBhHRLP0YubJAzY00WxQSO3NvHFeLFRFvilCnmh0JGMAqsNU+Owpz0iVrWY0YZskN5gPeR9w==} + '@tiptap/extension-mention@3.7.2': + resolution: {integrity: sha512-y8ldoGItWii6DY+db37BqdmHIbwrIV7b7Lz0uI3lhb3tNNkjaa84XRTUK7mySXrkzp/FMvw8MXCTUF44aQdFZQ==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 + '@tiptap/suggestion': ^3.7.2 - '@tiptap/extension-mention@3.6.5': - resolution: {integrity: sha512-ACElkBvemEJGm8gVYI4QKjf6tfNj3m5dC9MkZL4rwZo4CAwjiNQ8oFhj1x7sPO1OVlnjt+FhnItBix5ztTF8Ng==} + '@tiptap/extension-node-range@3.7.2': + resolution: {integrity: sha512-j4ZkxEhf1QF97OO/SiHcCceTzGstcjl4Bt4XtZoK++9N3tTKly8gUIYis+IjxAa0TiNyQPbnJvT3fon2iwtTLg==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 - '@tiptap/suggestion': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 - '@tiptap/extension-ordered-list@3.6.5': - resolution: {integrity: sha512-RiBl0Dkw8QtzS7OqUGm84BOyemw/N+hf8DYWsIqVysMRQAGBGhuklbw+DGpCL0nMHW4lh7WtvfKcb0yxLmhbbA==} + '@tiptap/extension-ordered-list@3.7.2': + resolution: {integrity: sha512-Tu61/JXh1RRd3Kb+s7A7jmpnB+w1pqGSRfMXBtYHDHDIGyXu255ru7soX44lJfHGq/zYcTFSHGSsi8o23QONJg==} peerDependencies: - '@tiptap/extension-list': ^3.6.5 + '@tiptap/extension-list': ^3.7.2 - '@tiptap/extension-paragraph@3.6.5': - resolution: {integrity: sha512-AfuaBu+DKrRPspaLsXgo17dhuneISS6QsZTIzPeX21jFJcq3TjtD8wSzS4yRgzAQCEbupkI7t4JbtgxAIBNQHA==} + '@tiptap/extension-paragraph@3.7.2': + resolution: {integrity: sha512-HmDuAixTcvP4A/v6OLkh/C6nB86i7/DRNswBf/Udak8TgWUIcSUK0iActxxm5+B3MZTSf3U87JzyI6IeuElLIQ==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-placeholder@3.6.5': - resolution: {integrity: sha512-9CLixogEb/4UkEyuDr4JdOlLvphcOVfZMdNMKmUVQdqo4MuZCdTDyK5ypfTPQJl8aUo0oCiEhqE0bQerYlueJQ==} + '@tiptap/extension-placeholder@3.7.2': + resolution: {integrity: sha512-YUr1rlxkgEBQDsMLpU8ruA4Uet37kXvwwFwIbDgaFd4NpfAD0fvX2zmPhHIBzsdH3e4V6eNp6IkmoYCWvugAAA==} peerDependencies: - '@tiptap/extensions': ^3.6.5 + '@tiptap/extensions': ^3.7.2 - '@tiptap/extension-strike@3.6.5': - resolution: {integrity: sha512-QR7CUmRJ7fJkHtxqKajKIaX/B4xpKFOsAOJHbnqZ8wzOtnEL5IlsmoUnbKBoVn0+2R2YKKvMK3lepGtAcVCfIQ==} + '@tiptap/extension-strike@3.7.2': + resolution: {integrity: sha512-I1G+4vZbCBTpAMmyVwaO8cLBJgXEf1DyEzc0B+HhTJiBa9qA9OKgRQEGFgisxu1kggjbzB6+d0+taHfjsZC1SQ==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-text-style@2.11.7': - resolution: {integrity: sha512-LHO6DBg/9SkCQFdWlVfw9nolUmw+Cid94WkTY+7IwrpyG2+ZGQxnKpCJCKyeaFNbDoYAtvu0vuTsSXeCkgShcA==} + '@tiptap/extension-text-align@3.7.2': + resolution: {integrity: sha512-tUdoatcxM8u16tFVfEURFZwmxvZQR33f9VLtkyR+1aXgy0Pi87cNoFC60pTjH7gNtktEuagNfPE00tGMvqIehg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-text@3.6.5': - resolution: {integrity: sha512-PVZDWUa25xPzmEN6WWA103yvYJn+NBvWb7WrQwWu9LkKUgd98ZgV3yFaEem/Ybugl/NDPV7q8GGaH+2wEg/VeA==} + '@tiptap/extension-text-style@3.7.2': + resolution: {integrity: sha512-afbEnk+Cf9tOfnM+dcKRtyAVb99JZRzUd2qTGqqoEJuySRk5KckVBZSkwGAt6TiIKpPlmwHHB5YTdMx9Fg+tbQ==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-typography@3.6.5': - resolution: {integrity: sha512-xHJzMGpWVH0pL+iZUjH4cMlc8DdNQz+r07NcGlPWYXqP4KJ/feyfxRVmnO9M7ods8zeOTSNdCs1npkMAy0nfxQ==} + '@tiptap/extension-text@3.7.2': + resolution: {integrity: sha512-sKaeGYNP1+bAe2rvmzWLW5qH9DsSFOJlOUEOFchR0OX0rC7bbGS6/KuyAq0w6UkL+cMJnDyAbv3KeD2WEA192w==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extension-underline@3.6.5': - resolution: {integrity: sha512-Ul1mO0H1e2vfvN5g48X/YQ8w1xFTpLqce+GUhi0OmXaZnVOTIMtLuN/zAAPjD+uw+79JVGjYa53lbo1dyhOfAw==} + '@tiptap/extension-typography@3.7.2': + resolution: {integrity: sha512-2yW3gRVm+9G7INEFj9jaL5otCw7I/271VJW25PNYNh3ERtV54rO0UjfSykMSqu70OkNzGQtYE7nixPGXpOrulQ==} peerDependencies: - '@tiptap/core': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/extensions@3.6.5': - resolution: {integrity: sha512-7aadEaRjSbFAIp3WGYR1LXrvtVprmBNxw3FakEUMJ+XKmGNErDJgDMZh+siAYw5MWwCCGa5kKu8Qi/i+DU+ILg==} + '@tiptap/extension-underline@3.7.2': + resolution: {integrity: sha512-GDpUZllTD7uIdHjTzYJ6i4jUgCeviW40SCpLVVv1xH0gj1t1xu0Rnxmk+bXkF2XNe8jPXkMCgYNr6DR6eO8roQ==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 - '@tiptap/pm@3.6.5': - resolution: {integrity: sha512-S+j6MPgUXRIQd5/mdaLjaJnOt4ptFwjqGjGMUfBbf9a3uKpXUXaCCzfuC6ZikwaUtoVh4KN9BU3HCYDtgtENPA==} + '@tiptap/extensions@3.7.2': + resolution: {integrity: sha512-FaToSdU9fhQk2swkaXrAQNgdaE0dwLbUHcvilW5F4xTpQfZ3J535u5U2TUYf+f9KKSV5fTmD4QGNY9qxY7ihTg==} + peerDependencies: + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 + + '@tiptap/markdown@3.7.2': + resolution: {integrity: sha512-0cdCYYHdBDXcwjZsTOSySbdHQuHZct6nxvcp4dSVpP25kbZL3ONSJvLY5Nsy3rkXlmhk9qbyFwsexGiSIdFy8Q==} + peerDependencies: + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 + + '@tiptap/pm@3.7.2': + resolution: {integrity: sha512-i2fvXDapwo/TWfHM6STYEbkYyF3qyfN6KEBKPrleX/Z80G5bLxom0gB79TsjLNxTLi6mdf0vTHgAcXMG1avc2g==} - '@tiptap/react@3.6.5': - resolution: {integrity: sha512-kum9fYzY6qmHuabcXDUTX2sVLdtJtZS0kN91mwD29Ue8HUkjVvEX92PwV2HtgNw3WFMaVxgm/dtm3XPTAlUEwg==} + '@tiptap/react@3.7.2': + resolution: {integrity: sha512-tka4ioSmsGI4TyGZ7jAUoIw8t8DVjr1It0B38vZVLqg8M/ZFgR1NkF50TJ6qAkhy8Uz12AO50so0v79tV2pmEA==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tiptap/starter-kit@3.6.5': - resolution: {integrity: sha512-LNAJQstB/VazmMlRbUyu3rCNVQ9af25Ywkn3Uyuwt3Ks9ZlliIm/x/zertdXTY2adoig+b36zT5Xcx1O4IdJ3A==} + '@tiptap/starter-kit@3.7.2': + resolution: {integrity: sha512-43GwI+2Mtc/ci7J4eJOE02wZxp5KIsDTMMb0peNksPcEGaURGdDhav9zbAW24NRjRxU7Auk/zaQu9O8+ZE0v0A==} - '@tiptap/suggestion@3.6.5': - resolution: {integrity: sha512-KduN9qEx2MlEjL1Hfnj7PbdkwHZjjJfLldglQkntB6GhNaDGBa/M7l6hbBEKsu350UtyAnc5YdI6pG+sWFKEfg==} + '@tiptap/suggestion@3.7.2': + resolution: {integrity: sha512-CYmIMeLqeGBotl7+4TrnGux/ov9IJoWTUQN/JcHp0aOoN3z8c/dQ6cziXXknr51jGHSdVYMWEyamLDZfcaGC1w==} peerDependencies: - '@tiptap/core': ^3.6.5 - '@tiptap/pm': ^3.6.5 + '@tiptap/core': ^3.7.2 + '@tiptap/pm': ^3.7.2 '@tiptap/y-tiptap@3.0.0': resolution: {integrity: sha512-HIeJZCj+KYJde2x6fONzo4o6kd7gW7eonwhQsv2p2VQnUgwNXMVhN+D6Z3AH/2i541Sq33y1PO4U/1ThCPjqbA==} @@ -6993,6 +7046,11 @@ packages: peerDependencies: marked: '>=1 <15' + marked@16.4.0: + resolution: {integrity: sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==} + engines: {node: '>= 20'} + hasBin: true + marked@4.3.0: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} engines: {node: '>= 12'} @@ -12896,160 +12954,192 @@ snapshots: '@tanstack/store@0.7.0': {} - '@tiptap/core@3.6.5(@tiptap/pm@3.6.5)': + '@tiptap/core@3.7.2(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/pm': 3.6.5 + '@tiptap/pm': 3.7.2 - '@tiptap/extension-blockquote@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-blockquote@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-bold@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-bold@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-bubble-menu@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-bubble-menu@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: '@floating-ui/dom': 1.7.4 - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 optional: true - '@tiptap/extension-bullet-list@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-bullet-list@3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-code-block@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-code-block@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 - '@tiptap/extension-code@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-code@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-collaboration-caret@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-collaboration-caret@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 '@tiptap/y-tiptap': 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - '@tiptap/extension-collaboration@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)': + '@tiptap/extension-collaboration@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 '@tiptap/y-tiptap': 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) yjs: 13.6.27 - '@tiptap/extension-document@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-document@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + + '@tiptap/extension-drag-handle-react@3.7.2(@tiptap/extension-drag-handle@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/extension-collaboration@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/extension-node-range@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)))(@tiptap/pm@3.7.2)(@tiptap/react@3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/extension-drag-handle': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/extension-collaboration@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/extension-node-range@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + '@tiptap/pm': 3.7.2 + '@tiptap/react': 3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@tiptap/extension-drag-handle@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/extension-collaboration@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27))(@tiptap/extension-node-range@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/extension-collaboration': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) + '@tiptap/extension-node-range': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 + '@tiptap/y-tiptap': 3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - '@tiptap/extension-dropcursor@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-dropcursor@3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extensions': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-file-handler@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/extension-text-style@2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)))(@tiptap/pm@3.6.5)': + '@tiptap/extension-file-handler@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/extension-text-style@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)))(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-text-style': 2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/extension-text-style': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/pm': 3.7.2 - '@tiptap/extension-floating-menu@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-floating-menu@3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: '@floating-ui/dom': 1.7.4 - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 optional: true - '@tiptap/extension-gapcursor@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-gapcursor@3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extensions': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-hard-break@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-hard-break@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-heading@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-heading@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-horizontal-rule@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-horizontal-rule@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 - '@tiptap/extension-image@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-image@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-italic@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-italic@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-link@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-link@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 linkifyjs: 4.3.2 - '@tiptap/extension-list-item@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-list-item@3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-list-keymap@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-list-keymap@3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-list': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 - '@tiptap/extension-mention@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@tiptap/suggestion@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-mention@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@tiptap/suggestion@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 - '@tiptap/suggestion': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 + '@tiptap/suggestion': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-ordered-list@3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-node-range@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': dependencies: - '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 - '@tiptap/extension-paragraph@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-ordered-list@3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/extension-list': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-placeholder@3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5))': + '@tiptap/extension-paragraph@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-strike@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-placeholder@3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/extensions': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) - '@tiptap/extension-text-style@2.11.7(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-strike@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-text@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-text-align@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-typography@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-text-style@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extension-underline@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))': + '@tiptap/extension-text@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': + '@tiptap/extension-typography@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) - '@tiptap/pm@3.6.5': + '@tiptap/extension-underline@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + + '@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 + + '@tiptap/markdown@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 + marked: 16.4.0 + + '@tiptap/pm@3.7.2': dependencies: prosemirror-changeset: 2.3.1 prosemirror-collab: 1.3.1 @@ -13070,10 +13160,10 @@ snapshots: prosemirror-transform: 1.10.3 prosemirror-view: 1.39.1 - '@tiptap/react@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tiptap/react@3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 '@types/react': 19.0.1 '@types/react-dom': 19.0.1 '@types/use-sync-external-store': 0.0.6 @@ -13082,42 +13172,42 @@ snapshots: react-dom: 19.2.0(react@19.2.0) use-sync-external-store: 1.4.0(react@19.2.0) optionalDependencies: - '@tiptap/extension-bubble-menu': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-floating-menu': 3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) + '@tiptap/extension-bubble-menu': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/extension-floating-menu': 3.7.2(@floating-ui/dom@1.7.4)(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) transitivePeerDependencies: - '@floating-ui/dom' - '@tiptap/starter-kit@3.6.5': - dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/extension-blockquote': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-bold': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-bullet-list': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) - '@tiptap/extension-code': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-code-block': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-document': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-dropcursor': 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) - '@tiptap/extension-gapcursor': 3.6.5(@tiptap/extensions@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) - '@tiptap/extension-hard-break': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-heading': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-horizontal-rule': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-italic': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-link': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-list': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/extension-list-item': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) - '@tiptap/extension-list-keymap': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) - '@tiptap/extension-ordered-list': 3.6.5(@tiptap/extension-list@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)) - '@tiptap/extension-paragraph': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-strike': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-text': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extension-underline': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)) - '@tiptap/extensions': 3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 - - '@tiptap/suggestion@3.6.5(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)': - dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) - '@tiptap/pm': 3.6.5 + '@tiptap/starter-kit@3.7.2': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/extension-blockquote': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-bold': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-bullet-list': 3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-code': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-code-block': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/extension-document': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-dropcursor': 3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-gapcursor': 3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-hard-break': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-heading': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-horizontal-rule': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/extension-italic': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-link': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/extension-list': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/extension-list-item': 3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-list-keymap': 3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-ordered-list': 3.7.2(@tiptap/extension-list@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-paragraph': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-strike': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-text': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extension-underline': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) + '@tiptap/extensions': 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 + + '@tiptap/suggestion@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 '@tiptap/y-tiptap@3.0.0(prosemirror-model@1.25.0)(prosemirror-state@1.4.3)(prosemirror-view@1.39.1)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': dependencies: @@ -17596,6 +17686,8 @@ snapshots: node-emoji: 2.1.3 supports-hyperlinks: 3.1.0 + marked@16.4.0: {} + marked@4.3.0: {} marked@9.1.6: {} @@ -20465,9 +20557,9 @@ snapshots: tinyspy@3.0.2: {} - tiptap-markdown@0.8.10(@tiptap/core@3.6.5(@tiptap/pm@3.6.5)): + tiptap-markdown@0.8.10(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)): dependencies: - '@tiptap/core': 3.6.5(@tiptap/pm@3.6.5) + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) '@types/markdown-it': 13.0.9 markdown-it: 14.1.0 markdown-it-task-lists: 2.1.1 diff --git a/lib/src/commit.rs b/lib/src/commit.rs index d2141d0bb..ba43c75eb 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -12,7 +12,6 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use urls::{SET, SIGNER}; -use yrs::updates::decoder::Decode; /// The `resource_new`, `resource_old` and `commit_resource` fields are only created if the Commit is persisted. /// When the Db is only notifying other of changes (e.g. if a new Message was added to a ChatRoom), these fields are not created. /// When deleting a resource, the `resource_new` field is None. @@ -365,9 +364,6 @@ impl Commit { } }; - let decode_update = yrs::Update::decode_v2(update_bin) - .map_err(|e| format!("Error decoding Yjs update: {}", e))?; - match resource.get(prop) { Ok(val) => match val { Value::YDoc(bin) => { diff --git a/lib/src/urls.rs b/lib/src/urls.rs index 8589ae11f..2aa52c800 100644 --- a/lib/src/urls.rs +++ b/lib/src/urls.rs @@ -19,6 +19,7 @@ pub const MESSAGE: &str = "https://atomicdata.dev/classes/Message"; pub const IMPORTER: &str = "https://atomicdata.dev/classes/Importer"; pub const ERROR: &str = "https://atomicdata.dev/classes/Error"; pub const BOOKMARK: &str = "https://atomicdata.dev/class/Bookmark"; +pub const DOCUMENT_V2: &str = "https://atomicdata.dev/classes/DocumentV2"; pub const ONTOLOGY: &str = "https://atomicdata.dev/class/ontology"; pub const ENDPOINT_RESPONSE: &str = "https://atomicdata.dev/ontology/server/class/endpoint-response"; @@ -118,6 +119,8 @@ pub const IMAGE_HEIGHT: &str = "https://atomicdata.dev/properties/imageHeight"; // ... for ChatRooms and Messages pub const MESSAGES: &str = "https://atomicdata.dev/properties/messages"; pub const NEXT_PAGE: &str = "https://atomicdata.dev/properties/nextPage"; +// ... for DocumentV2 +pub const DOCUMENT_CONTENT: &str = "https://atomicdata.dev/properties/documentContent"; // ... for Importers pub const IMPORTER_URL: &str = "https://atomicdata.dev/properties/importer/url"; pub const IMPORTER_JSON: &str = "https://atomicdata.dev/properties/importer/json"; diff --git a/server/Cargo.toml b/server/Cargo.toml index 27ca93915..63262f277 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -53,6 +53,7 @@ tracing-log = "0.2" ureq = "2" urlencoding = "2" ring = "0.17.14" +yrs = "0.24.0" [dependencies.instant-acme] optional = true diff --git a/server/src/actor_messages.rs b/server/src/actor_messages.rs index 9622c3a5b..9264b6a39 100644 --- a/server/src/actor_messages.rs +++ b/server/src/actor_messages.rs @@ -13,11 +13,27 @@ pub struct Subscribe { pub agent: String, } +#[derive(Deserialize, Serialize)] +pub struct YSubscriptionJSON { + pub subject: String, + pub property: String, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct SubscribeYSync { + pub addr: Addr, + pub subject: String, + pub property: String, + pub agent: String, +} + #[derive(Message)] #[rtype(result = "()")] -pub struct Unsubscribe { +pub struct UnsubscribeYSync { pub addr: Addr, pub subject: String, + pub property: String, } /// A message containing a Resource, which should be sent to subscribers @@ -28,9 +44,13 @@ pub struct CommitMessage { pub commit_response: atomic_lib::commit::CommitResponse, } +/// A message that can contain both a Yjs Doc update or a Yjs Awareness update. +/// It is used to enable live collaboration on Yjs Docs and does not store these updates on the server. #[derive(Message, Clone, Debug, Serialize, Deserialize)] #[rtype(result = "()")] -pub struct YAwarenessUpdate { +pub struct YSyncUpdate { pub subject: String, - pub update: String, + pub property: String, + pub awareness_update: Option, + pub doc_update: Option, } diff --git a/server/src/appstate.rs b/server/src/appstate.rs index e203305b9..1c6b1b0d3 100644 --- a/server/src/appstate.rs +++ b/server/src/appstate.rs @@ -1,7 +1,10 @@ //! App state, which is accessible from handlers use crate::{ - commit_monitor::CommitMonitor, config::Config, errors::AtomicServerResult, search::SearchState, - y_awareness_broadcaster::YAwarenessBroadcaster, + commit_monitor::CommitMonitor, + config::Config, + errors::AtomicServerResult, + search::SearchState, + y_sync_broadcaster::{self, YSyncBroadcaster}, }; use atomic_lib::{ agents::Agent, @@ -24,7 +27,7 @@ pub struct AppState { pub config: Config, /// The Actix Address of the CommitMonitor, which should receive updates when a commit is applied pub commit_monitor: actix::Addr, - pub y_awareness_broadcaster: actix::Addr, + pub y_sync_broadcaster: actix::Addr, pub search_state: SearchState, } @@ -67,8 +70,7 @@ impl AppState { let commit_monitor_clone = commit_monitor.clone(); - let y_awareness_broadcaster = - crate::y_awareness_broadcaster::create_y_awareness_broadcaster(store.clone()); + let y_sync_broadcaster = y_sync_broadcaster::create_y_sync_broadcaster(store.clone()); // This closure is called every time a Commit is created let send_commit = move |commit_response: &CommitResponse| { @@ -103,7 +105,7 @@ impl AppState { store, config, commit_monitor, - y_awareness_broadcaster, + y_sync_broadcaster, search_state, }) } diff --git a/server/src/bin.rs b/server/src/bin.rs index ab9882c9b..211bd3016 100644 --- a/server/src/bin.rs +++ b/server/src/bin.rs @@ -15,7 +15,7 @@ mod https; mod jsonerrors; mod routes; pub mod serve; -mod y_awareness_broadcaster; +mod y_sync_broadcaster; // #[cfg(feature = "search")] mod search; #[cfg(test)] diff --git a/server/src/handlers/web_sockets.rs b/server/src/handlers/web_sockets.rs index a72cb265a..e20d9216d 100644 --- a/server/src/handlers/web_sockets.rs +++ b/server/src/handlers/web_sockets.rs @@ -18,12 +18,12 @@ use atomic_lib::{ use std::time::{Duration, Instant}; use crate::{ - actor_messages::{CommitMessage, YAwarenessUpdate}, + actor_messages::{CommitMessage, YSubscriptionJSON, YSyncUpdate}, appstate::AppState, commit_monitor::CommitMonitor, errors::AtomicServerResult, helpers::get_auth_headers, - y_awareness_broadcaster::YAwarenessBroadcaster, + y_sync_broadcaster::YSyncBroadcaster, }; /// Get an HTTP request, upgrade it to a Websocket connection @@ -44,7 +44,7 @@ pub async fn web_socket_handler( let result = ws::start( WebSocketConnection::new( appstate.commit_monitor.clone(), - appstate.y_awareness_broadcaster.clone(), + appstate.y_sync_broadcaster.clone(), for_agent, // We need to make sure this is easily clone-able appstate.store.clone(), @@ -66,7 +66,7 @@ pub struct WebSocketConnection { subscribed: std::collections::HashSet, /// The CommitMonitor Actor that receives and sends messages for Commits commit_monitor_addr: Addr, - y_awareness_broadcaster_addr: Addr, + y_sync_broadcaster_addr: Addr, /// The Agent who is connected. /// If it's not specified, it's the Public Agent. agent: ForAgent, @@ -135,34 +135,41 @@ fn handle_ws_message( Err("UNSUBSCRIBE needs a subject".into()) } } - s if s.starts_with("Y_AWARENESS_SUBSCRIBE ") => { - let mut parts = s.split("Y_AWARENESS_SUBSCRIBE "); - if let Some(subject) = parts.nth(1) { - conn.y_awareness_broadcaster_addr.do_send( - crate::actor_messages::Subscribe { - addr: ctx.address(), - subject: subject.to_string(), - agent: conn.agent.to_string(), - }, - ); - Ok(()) - } else { - Err("Y_AWARENESS_SUBSCRIBE needs a subject".into()) - } + s if s.starts_with("Y_SYNC_SUBSCRIBE ") => { + let mut parts = s.split("Y_SYNC_SUBSCRIBE "); + + let Some(json) = parts.nth(1) else { + return Err("Y_SYNC_SUBSCRIBE needs a JSON object".into()); + }; + + let message: YSubscriptionJSON = serde_json::from_str(json)?; + + conn.y_sync_broadcaster_addr + .do_send(crate::actor_messages::SubscribeYSync { + addr: ctx.address(), + subject: message.subject.to_string(), + property: message.property.to_string(), + agent: conn.agent.to_string(), + }); + Ok(()) } - s if s.starts_with("Y_AWARENESS_UNSUBSCRIBE ") => { - let mut parts = s.split("Y_AWARENESS_UNSUBSCRIBE "); - if let Some(subject) = parts.nth(1) { - conn.y_awareness_broadcaster_addr.do_send( - crate::actor_messages::Unsubscribe { - addr: ctx.address(), - subject: subject.to_string(), - }, - ); - Ok(()) - } else { - Err("Y_AWARENESS_UNSUBSCRIBE needs a subject".into()) - } + s if s.starts_with("Y_SYNC_UNSUBSCRIBE ") => { + let mut parts = s.split("Y_SYNC_UNSUBSCRIBE "); + + let Some(json) = parts.nth(1) else { + return Err("Y_SYNC_UNSUBSCRIBE needs a JSON object".into()); + }; + + let message: YSubscriptionJSON = serde_json::from_str(json)?; + + conn.y_sync_broadcaster_addr + .do_send(crate::actor_messages::UnsubscribeYSync { + addr: ctx.address(), + subject: message.subject.to_string(), + property: message.property.to_string(), + }); + + Ok(()) } s if s.starts_with("GET ") => { let mut parts = s.split("GET "); @@ -214,20 +221,20 @@ fn handle_ws_message( Err("AUTHENTICATE needs a JSON object".into()) } } - s if s.starts_with("Y_AWARENESS_UPDATE ") => { - let mut parts = s.split("Y_AWARENESS_UPDATE "); + s if s.starts_with("Y_SYNC_UPDATE ") => { + let mut parts = s.split("Y_SYNC_UPDATE "); let Some(json) = parts.nth(1) else { - return Err("Y_AWARENESS_UPDATE needs a JSON object".into()); + return Err("Y_SYNC_UPDATE needs a JSON object".into()); }; - let update: YAwarenessUpdate = match serde_json::from_str(json) { + let update: YSyncUpdate = match serde_json::from_str(json) { Ok(update) => update, Err(err) => { - return Err(format!("Invalid Y_AWARENESS_UPDATE JSON: {}", err).into()) + return Err(format!("Invalid Y_SYNC_UPDATE JSON: {}", err).into()) } }; - conn.y_awareness_broadcaster_addr.do_send(update); + conn.y_sync_broadcaster_addr.do_send(update); Ok(()) } other => { @@ -252,7 +259,7 @@ fn handle_ws_message( impl WebSocketConnection { fn new( commit_monitor_addr: Addr, - y_awareness_broadcaster_addr: Addr, + y_sync_broadcaster_addr: Addr, agent: ForAgent, store: Db, ) -> Self { @@ -269,7 +276,7 @@ impl WebSocketConnection { // Maybe this should be stored only in the CommitMonitor, and not here. subscribed: std::collections::HashSet::new(), commit_monitor_addr, - y_awareness_broadcaster_addr, + y_sync_broadcaster_addr, agent, store, } @@ -308,13 +315,13 @@ impl Handler for WebSocketConnection { } } -impl Handler for WebSocketConnection { +impl Handler for WebSocketConnection { type Result = (); #[tracing::instrument(name = "handle_y_awareness_update", skip_all)] - fn handle(&mut self, msg: YAwarenessUpdate, ctx: &mut ws::WebsocketContext) { + fn handle(&mut self, msg: YSyncUpdate, ctx: &mut ws::WebsocketContext) { ctx.text(format!( - "Y_AWARENESS_UPDATE {}", + "Y_SYNC_UPDATE {}", serde_json::to_string(&msg).unwrap() )); } diff --git a/server/src/lib.rs b/server/src/lib.rs index 8b0acdfef..ee80bf544 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -16,7 +16,7 @@ mod https; mod jsonerrors; mod routes; pub mod serve; -mod y_awareness_broadcaster; +mod y_sync_broadcaster; // #[cfg(feature = "search")] mod search; #[cfg(test)] diff --git a/server/src/search.rs b/server/src/search.rs index 4a75677f5..60bd08eb7 100644 --- a/server/src/search.rs +++ b/server/src/search.rs @@ -1,9 +1,12 @@ //! Full-text search, powered by Tantivy. //! A folder for the index is stored in the config. //! You can see the Endpoint on `http://localhost/search` +use crate::config::Config; +use crate::errors::AtomicServerResult; use atomic_lib::Db; use atomic_lib::Resource; use atomic_lib::Storelike; +use regex::Regex; use tantivy::schema::Facet; use tantivy::schema::Field; use tantivy::schema::STORED; @@ -12,9 +15,11 @@ use tantivy::Index; use tantivy::IndexWriter; use tantivy::ReloadPolicy; -use crate::config::Config; -use crate::errors::AtomicServerResult; - +use yrs::updates::decoder::Decode; +use yrs::GetString; +use yrs::WriteTxn; +use yrs::XmlFragment; +use yrs::{Transact, Update}; /// The actual Schema used for search. /// It mimics a single Atom (or Triple). #[derive(Debug)] @@ -128,6 +133,23 @@ impl SearchState { doc.add_text(fields.description, description); }; + // If the resource has a document-content property, we extract the plain text and use that as the description instead. + // This way, documents can be indexed by search. + if let Ok(atomic_lib::Value::YDoc(state)) = resource.get(atomic_lib::urls::DOCUMENT_CONTENT) + { + let ydoc = yrs::Doc::new(); + let mut txn = ydoc.transact_mut(); + txn.apply_update( + Update::decode_v2(state) + .map_err(|e| format!("Failed to decode YDoc update: {}", e))?, + ) + .map_err(|e| format!("Failed to apply YDoc update: {}", e))?; + + let xml_content = txn.get_or_insert_xml_fragment("content"); + let content = extract_plain_text(&xml_content, &txn); + doc.add_text(fields.description, content); + } + let hierarchy = resource_to_facet(resource, store)?; doc.add_facet(fields.hierarchy, hierarchy); @@ -261,6 +283,30 @@ fn get_resource_title(resource: &Resource) -> String { } } +/// Recursively traverses the Yjs XmlFragment structure using a TreeWalker +/// and extracts all nested plain text content. +/// +/// This function requires a Transaction to read the text data correctly. +fn extract_plain_text(fragment: &yrs::XmlFragmentRef, txn: &yrs::TransactionMut) -> String { + let mut text_content = String::new(); + + for node in fragment.successors(txn) { + match node { + yrs::types::xml::XmlOut::Text(text) => { + text_content.push_str(&text.get_string(txn)); + } + _ => {} + } + } + + // Remove XML tags using regex + let xml_tag_regex = Regex::new(r"<[^>]*>").unwrap(); + let clean_text = xml_tag_regex.replace_all(&text_content, " "); + + // Clean up leading/trailing whitespace and return + clean_text.trim().to_string() +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/y_awareness_broadcaster.rs b/server/src/y_awareness_broadcaster.rs deleted file mode 100644 index 58a05eeea..000000000 --- a/server/src/y_awareness_broadcaster.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::{ - actor_messages::{Subscribe, Unsubscribe, YAwarenessUpdate}, - errors::AtomicServerResult, - handlers::web_sockets::WebSocketConnection, -}; - -use actix::{ - prelude::{Actor, Context, Handler}, - Addr, -}; -use atomic_lib::{agents::ForAgent, Db, Storelike}; -use std::collections::{HashMap, HashSet}; - -pub struct YAwarenessBroadcaster { - subscriptions: HashMap>>, - store: Db, -} - -impl Actor for YAwarenessBroadcaster { - type Context = Context; - - fn started(&mut self, _ctx: &mut Context) { - tracing::debug!("YAwarenessBroadcaster started"); - } -} - -impl Handler for YAwarenessBroadcaster { - type Result = (); - - fn handle(&mut self, msg: Subscribe, _ctx: &mut Context) { - if !msg.subject.starts_with(&self.store.get_self_url().unwrap()) { - tracing::warn!("can't subscribe to external resource"); - return; - } - - match self.store.get_resource(&msg.subject) { - Ok(resource) => { - match atomic_lib::hierarchy::check_read( - &self.store, - &resource, - &ForAgent::AgentSubject(msg.agent.clone()), - ) { - Ok(_explanation) => { - let mut set = self - .subscriptions - .get(&msg.subject) - .unwrap_or(&HashSet::new()) - .clone(); - - set.insert(msg.addr); - tracing::debug!("handle subscribe {} ", msg.subject); - self.subscriptions.insert(msg.subject.clone(), set); - } - Err(unauthorized_err) => { - tracing::debug!( - "Not allowed {} to subscribe to {}: {}", - &msg.agent, - &msg.subject, - unauthorized_err - ); - } - } - } - Err(e) => { - tracing::debug!( - "Subscribe failed for {} by {}: {}", - &msg.subject, - msg.agent, - e - ); - } - } - } -} - -impl Handler for YAwarenessBroadcaster { - type Result = (); - - fn handle(&mut self, msg: Unsubscribe, _ctx: &mut Context) { - let Some(subscriber) = self.subscriptions.get(&msg.subject) else { - tracing::warn!("no subscribers for {}", msg.subject); - return; - }; - - let mut new_subscriber = subscriber.clone(); - new_subscriber.remove(&msg.addr); - self.subscriptions - .insert(msg.subject.clone(), new_subscriber); - } -} - -// impl YAwarenessBroadcaster { -// fn broadcast_awareness_update(&mut self, msg: YAwarenessUpdate) -> AtomicServerResult<()> { -// let Some(subscribers) = self.subscriptions.get(&msg.subject) else { -// tracing::warn!("no subscribers for {}", msg.subject); -// return Ok(()); -// }; - -// for subscriber in subscribers { -// subscriber.do_send(msg.clone()); -// } - -// Ok(()) -// } -// } - -impl Handler for YAwarenessBroadcaster { - type Result = (); - - fn handle(&mut self, msg: YAwarenessUpdate, _ctx: &mut Context) { - let Some(subscribers) = self.subscriptions.get(&msg.subject) else { - tracing::warn!("no subscribers for {}", msg.subject); - return (); - }; - - for subscriber in subscribers { - subscriber.do_send(msg.clone()); - } - } -} - -pub fn create_y_awareness_broadcaster(store: Db) -> Addr { - YAwarenessBroadcaster::create(|_ctx: &mut Context| { - YAwarenessBroadcaster { - subscriptions: HashMap::new(), - store, - } - }) -} diff --git a/server/src/y_sync_broadcaster.rs b/server/src/y_sync_broadcaster.rs new file mode 100644 index 000000000..24af4771c --- /dev/null +++ b/server/src/y_sync_broadcaster.rs @@ -0,0 +1,131 @@ +use crate::{ + actor_messages::{SubscribeYSync, UnsubscribeYSync, YSyncUpdate}, + handlers::web_sockets::WebSocketConnection, +}; + +use actix::{ + prelude::{Actor, Context, Handler}, + Addr, +}; +use atomic_lib::{agents::ForAgent, Db, Storelike}; +use std::collections::{HashMap, HashSet}; + +pub struct YSyncBroadcaster { + subscriptions: HashMap<(String, String), HashSet>>, + store: Db, +} + +impl Actor for YSyncBroadcaster { + type Context = Context; + + fn started(&mut self, _ctx: &mut Context) { + tracing::debug!("YAwarenessBroadcaster started"); + } +} + +impl Handler for YSyncBroadcaster { + type Result = (); + + fn handle(&mut self, msg: SubscribeYSync, _ctx: &mut Context) { + if !msg.subject.starts_with(&self.store.get_self_url().unwrap()) { + tracing::warn!("can't subscribe to external resource"); + return; + } + let key = (msg.subject.clone(), msg.property.clone()); + + let resource = match self.store.get_resource(&msg.subject) { + Ok(resource) => resource, + Err(e) => { + tracing::debug!( + "Subscribe failed for {} by {}: {}", + &msg.subject, + msg.agent, + e + ); + return; + } + }; + + match atomic_lib::hierarchy::check_read( + &self.store, + &resource, + &ForAgent::AgentSubject(msg.agent.clone()), + ) { + Ok(_explanation) => { + let mut set = self + .subscriptions + .get(&key) + .unwrap_or(&HashSet::new()) + .clone(); + + set.insert(msg.addr); + tracing::debug!("handle subscribe {} ", msg.subject); + self.subscriptions.insert(key.clone(), set); + } + Err(unauthorized_err) => { + tracing::debug!( + "Not allowed {} to subscribe to {}: {}", + &msg.agent, + &msg.subject, + unauthorized_err + ); + } + } + } +} + +impl Handler for YSyncBroadcaster { + type Result = (); + + fn handle(&mut self, msg: UnsubscribeYSync, _ctx: &mut Context) { + let key = (msg.subject.clone(), msg.property.clone()); + + let Some(subscriber) = self.subscriptions.get(&key) else { + tracing::warn!("no subscribers for {}", msg.subject); + return; + }; + + let mut new_subscriber = subscriber.clone(); + new_subscriber.remove(&msg.addr); + self.subscriptions.insert(key.clone(), new_subscriber); + } +} + +// impl YAwarenessBroadcaster { +// fn broadcast_awareness_update(&mut self, msg: YAwarenessUpdate) -> AtomicServerResult<()> { +// let Some(subscribers) = self.subscriptions.get(&msg.subject) else { +// tracing::warn!("no subscribers for {}", msg.subject); +// return Ok(()); +// }; + +// for subscriber in subscribers { +// subscriber.do_send(msg.clone()); +// } + +// Ok(()) +// } +// } + +impl Handler for YSyncBroadcaster { + type Result = (); + + fn handle(&mut self, msg: YSyncUpdate, _ctx: &mut Context) { + let key = (msg.subject.clone(), msg.property.clone()); + + let Some(subscribers) = self.subscriptions.get(&key) else { + tracing::warn!("no subscribers for {}", msg.subject); + return (); + }; + + for subscriber in subscribers { + subscriber.do_send(msg.clone()); + } + } +} + +pub fn create_y_sync_broadcaster(store: Db) -> Addr { + YSyncBroadcaster::create(|_ctx: &mut Context| YSyncBroadcaster { + subscriptions: HashMap::new(), + store, + }) +} From f7acc6b1c15a580acf77c1728ae3d7af6e07fa0c Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 10 Nov 2025 09:55:33 +0100 Subject: [PATCH 3/8] Add tables to documents, update linter #741 --- .vscode/settings.json | 10 +- browser/.eslintrc.cjs | 119 - browser/.prettierignore | 5 +- browser/.prettierrc.json | 6 +- browser/cli/package.json | 3 +- browser/create-template/package.json | 5 +- browser/data-browser/.npmrc | 1 + browser/data-browser/package.json | 27 +- browser/data-browser/src/Providers.tsx | 11 +- .../src/chunks/PDFViewer/index.tsx | 10 +- .../chunks/RTE/AIChatInput/MentionList.tsx | 37 +- .../src/chunks/RTE/AsyncMarkdownEditor.tsx | 6 +- .../src/chunks/RTE/BubbleMenu.tsx | 11 +- .../src/chunks/RTE/CollaborativeEditor.tsx | 292 +- .../src/chunks/RTE/EditorEvents.tsx | 4 +- .../src/chunks/RTE/FullBubbleMenu.tsx | 4 + .../src/chunks/RTE/ImagePicker.tsx | 54 +- .../ResourceExtension/RTENodeViewWrapper.tsx | 29 + .../ResourceExtension/ResourceComponent.tsx | 26 +- .../ResourceExtension/ResourceNode.module.css | 18 + .../RTE/ResourceExtension/ResourceNode.ts | 96 +- .../src/chunks/RTE/SlashMenu/CommandList.tsx | 5 +- .../chunks/RTE/SlashMenu/CommandsExtension.ts | 10 +- .../data-browser/src/chunks/RTE/TableRTE.tsx | 20 +- .../src/chunks/RTE/TiptapContext.tsx | 4 - .../src/components/AllPropsSimple.tsx | 9 +- .../src/components/AtomicLink.tsx | 164 +- .../src/components/CustomPopover.tsx | 149 + .../src/components/Dialog/index.tsx | 2 +- .../src/components/HideInPrint.tsx | 8 + .../src/components/IconButton/IconButton.tsx | 3 +- browser/data-browser/src/components/Main.tsx | 11 + .../src/components/Navigation.tsx | 25 +- .../data-browser/src/components/Parent.tsx | 5 + .../CustomContextItemsContext.tsx | 95 + .../components/ResourceContextMenu/index.tsx | 18 +- .../components/Searchbar/SearchbarInput.tsx | 2 +- .../src/components/SideBar/SideBarDrive.tsx | 5 +- .../src/components/SideBar/index.tsx | 6 +- .../src/components/TableEditor/Cell.tsx | 20 +- .../components/TableEditor/TableEditor.tsx | 12 +- .../src/components/Tag/TagSelectPopover.tsx | 32 +- .../forms/FileDropzone/FileDropzoneInput.tsx | 4 +- .../forms/FilePicker/FilePickerDialog.tsx | 12 +- .../CustomForms/NewArticleDialog.tsx | 13 +- .../CustomForms/NewBookmarkDialog.tsx | 13 +- .../CustomForms/NewCollectionDialog.tsx | 15 +- .../CustomForms/NewDriveDialog.tsx | 19 +- .../CustomForms/NewOntologyDialog.tsx | 13 +- .../CustomForms/NewTableDialog.tsx | 9 +- .../forms/NewForm/useNewResourceUI.tsx | 43 +- .../data-browser/src/hooks/useCombineRefs.ts | 10 +- .../data-browser/src/hooks/useControlable.ts | 50 + .../src/hooks/useCreateAndNavigate.ts | 17 +- .../data-browser/src/hooks/useDocumentText.ts | 46 + .../src/hooks/useSelectedIndex.ts | 22 +- browser/data-browser/src/locales/de.po | 33 +- browser/data-browser/src/locales/en.po | 33 +- browser/data-browser/src/locales/es.po | 33 +- browser/data-browser/src/locales/fr.po | 33 +- .../src/routes/LinkOpenRouter.tsx | 36 +- .../src/routes/NewResource/BaseButtons.tsx | 2 +- .../src/routes/Search/SearchRoute.tsx | 7 +- .../src/routes/SettingsServer/WSIndicator.tsx | 18 +- browser/data-browser/src/styling.tsx | 6 +- .../src/views/BookmarkPage/usePreview.ts | 58 +- .../src/views/Card/DocumentV2Card.tsx | 52 +- .../src/views/Card/ResourceCard.tsx | 12 +- .../data-browser/src/views/ChatRoomPage.tsx | 50 +- .../src/views/Document/DocumentV2FullPage.tsx | 31 +- .../data-browser/src/views/DocumentPage.tsx | 114 +- browser/data-browser/src/views/Element.tsx | 10 +- .../data-browser/src/views/EndpointPage.tsx | 2 +- .../src/views/File/fileTypeUtils.ts | 4 +- .../GridItem/DocumentV2GridItem.tsx | 19 + .../FolderPage/GridItem/ResourceGridItem.tsx | 2 + .../TablePage/EditorCells/AtomicURLCell.tsx | 63 +- .../TablePage/EditorCells/CellComponents.tsx | 25 +- .../views/TablePage/EditorCells/DateCell.tsx | 7 +- .../TablePage/EditorCells/DateTimeCell.tsx | 24 +- .../views/TablePage/EditorCells/InputBase.ts | 1 + .../EditorCells/MultiRelationCell.tsx | 137 +- .../ResourceCells/SimpleResourceLink.tsx | 17 +- .../TablePage/EditorCells/SelectCell.tsx | 151 +- .../EditorCells/useResourceSearch.ts | 50 +- .../PropertyForm/ExternalPropertyDialog.tsx | 7 +- .../TablePage/PropertyForm/PropertyForm.tsx | 12 +- .../src/views/TablePage/TableCell.tsx | 33 +- .../src/views/TablePage/TableHeadingMenu.tsx | 36 +- .../src/views/TablePage/TablePage.tsx | 36 +- .../src/views/TablePage/TableResource.tsx | 3 +- .../src/views/TablePage/TableRow.tsx | 71 +- browser/data-browser/vite.config.ts | 1 + browser/eslint.config.js | 103 + browser/lib/package.json | 3 +- browser/lib/src/client.ts | 3 +- browser/lib/src/commit.ts | 10 +- browser/lib/src/ontology.ts | 10 +- browser/lib/src/parse.ts | 3 +- browser/lib/src/resource.ts | 51 +- browser/lib/src/search.ts | 8 +- browser/lib/src/store.ts | 37 +- browser/lib/src/websockets.ts | 27 +- browser/package.json | 18 +- browser/pnpm-lock.yaml | 3477 +++++++++++------ browser/react/package.json | 11 +- browser/react/src/hooks.ts | 67 +- browser/react/src/useDebounce.ts | 6 +- browser/react/src/useServerSearch.tsx | 87 +- browser/svelte/package.json | 4 +- server/src/actor_messages.rs | 2 + server/src/handlers/web_sockets.rs | 3 +- server/src/y_sync_broadcaster.rs | 96 +- 113 files changed, 4372 insertions(+), 2548 deletions(-) delete mode 100644 browser/.eslintrc.cjs create mode 100644 browser/data-browser/.npmrc create mode 100644 browser/data-browser/src/chunks/RTE/ResourceExtension/RTENodeViewWrapper.tsx create mode 100644 browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css create mode 100644 browser/data-browser/src/components/CustomPopover.tsx create mode 100644 browser/data-browser/src/components/HideInPrint.tsx create mode 100644 browser/data-browser/src/components/ResourceContextMenu/CustomContextItemsContext.tsx create mode 100644 browser/data-browser/src/hooks/useControlable.ts create mode 100644 browser/data-browser/src/hooks/useDocumentText.ts create mode 100644 browser/data-browser/src/views/FolderPage/GridItem/DocumentV2GridItem.tsx create mode 100644 browser/eslint.config.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 17448e0d7..f868713d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { // The linter in the CI is quite strict, so running `cargo fmt` on save is probably a good idea! "editor.formatOnSave": true, - "files.autoSave": "onFocusChange", "rust-analyzer.checkOnSave.command": "clippy", "search.exclude": { "**/.git": true, @@ -20,17 +19,10 @@ "eslint.alwaysShowStatus": true, "eslint.format.enable": true, "eslint.lintTask.enable": true, - "eslint.quiet": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "eslint.workingDirectories": [ - "./data-browser", - "./react", - "./lib", - "./cli", - "./svelte" - ], + "eslint.workingDirectories": [{ "directory": "browser" }], "typescript.preferences.preferTypeOnlyAutoImports": true, "rustTestExplorer.rootCargoManifestFilePath": "./Cargo.toml", // This won't work in multi-root workspaces, could be fixed by using a rust-analyzer.toml once there is some more documentation on that. diff --git a/browser/.eslintrc.cjs b/browser/.eslintrc.cjs deleted file mode 100644 index 9709c8ae5..000000000 --- a/browser/.eslintrc.cjs +++ /dev/null @@ -1,119 +0,0 @@ -module.exports = { - root: true, - ignorePatterns: ['./.eslint.cjs', '**/vite.config.ts'], - extends: [ - 'eslint:recommended', - 'plugin:prettier/recommended', - "plugin:import/recommended", - "plugin:import/typescript", - 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react - 'plugin:react/jsx-runtime', - 'plugin:@typescript-eslint/eslint-recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin - 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin - 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier - 'plugin:jsx-a11y/recommended', - ], - parser: '@typescript-eslint/parser', // Specifies the ESLint parser - env: { - browser: true, - es6: true, - node: true, - }, - parserOptions: { - ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports - ecmaFeatures: { - jsx: true, // Allows for the parsing of JSX - arrowFunctions: true, - }, - // Next two lines enable deeper TS type checking - // https://typescript-eslint.io/docs/linting/typed-linting/ - tsconfigRootDir: __dirname, - project: [ - 'lib/tsconfig.json', - 'cli/tsconfig.json', - 'react/tsconfig.json', - 'data-browser/tsconfig.json', - 'e2e/tsconfig.json', - 'create-template/tsconfig.json', - ], - }, - plugins: ['react', '@typescript-eslint', 'prettier', 'react-hooks', 'jsx-a11y'], - settings: { - react: { - version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use - }, - 'import/resolver': { - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - paths: ['./src'], - }, - }, - }, - rules: { - // Existing rules - 'comma-dangle': 'off', // https://eslint.org/docs/rules/comma-dangle - 'function-paren-newline': 'off', // https://eslint.org/docs/rules/function-paren-newline - 'global-require': 'off', // https://eslint.org/docs/rules/global-require - // Turn this on when we have migrated all import paths to use `.js` - // "import/extensions": ["error", "ignorePackages"], - "import/no-unresolved": "off", - 'import/no-dynamic-require': 'off', // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-dynamic-require.md - 'import/no-named-as-default': 'off', - 'no-inner-declarations': 'off', // https://eslint.org/docs/rules/no-inner-declarations// New rules - 'class-methods-use-this': 'off', - //Allow underscores https://stackoverflow.com/questions/57802057/eslint-configuring-no-unused-vars-for-typescript - '@typescript-eslint/no-unused-vars': ['error', { 'varsIgnorePattern': '^_', 'argsIgnorePattern': '^_' }], - 'react-hooks/exhaustive-deps': 'warn', - // 'no-unused-vars': ["error", { "ie": "^_" }], - 'import/prefer-default-export': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-explicit-any': 'error', - "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks - 'no-console': ['error', { allow: ['error', 'warn'] }], - "react/prop-types": "off", - "padding-line-between-statements": [ - "error", - { - "blankLine": "always", - "next": "return", - "prev": "*" - }, - { - "blankLine": "always", - "next": "export", - "prev": "*" - }, - { - "blankLine": "always", - "next": "multiline-block-like", - "prev": "*" - }, - { - "blankLine": "always", - "next": "*", - "prev": "multiline-block-like" - }, - { - "blankLine": "any", - "next": "export", - "prev": "export" - } - ], - "@typescript-eslint/explicit-member-accessibility": "error", - "eqeqeq": "error", - "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTaggedTemplates": true }], - "jsx-a11y/no-autofocus": "off", - // This has a bug, so we use typescripts version - "no-shadow": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "no-eval": "error", - "no-implied-eval": "error", - "@typescript-eslint/no-shadow": ["error"], - "@typescript-eslint/member-ordering": "error", - "react/no-unknown-property": ["error", { "ignore": ["about"] }], - 'react-hooks/react-compiler': 'error', - }, -}; diff --git a/browser/.prettierignore b/browser/.prettierignore index 264e8a7d8..c0fdc9ece 100644 --- a/browser/.prettierignore +++ b/browser/.prettierignore @@ -1,8 +1,9 @@ build -**/node_modules -**/dist +**/node_modules/** +**/dist/** **/package.json **/yarn.lock **/package-lock.json **/.eslintrc.js **/tsconfig.json +**/.svelte-kit/** diff --git a/browser/.prettierrc.json b/browser/.prettierrc.json index 3177c3a8e..80ab8b54c 100644 --- a/browser/.prettierrc.json +++ b/browser/.prettierrc.json @@ -1,4 +1,7 @@ { + "plugins": [ + "prettier-plugin-svelte" + ], "semi": true, "printWidth": 80, "tabWidth": 2, @@ -7,6 +10,5 @@ "useTabs": false, "arrowParens": "avoid", "jsxSingleQuote": true, - "trailingComma": "all", - "jsdocParser": true + "trailingComma": "all" } diff --git a/browser/cli/package.json b/browser/cli/package.json index d00efee7c..288530490 100644 --- a/browser/cli/package.json +++ b/browser/cli/package.json @@ -23,10 +23,11 @@ }, "scripts": { "build": "tsc", - "lint": "eslint ./src --ext .js,.ts", + "lint": "eslint ./src --ext .js,.ts && pnpm prettier-check", "lint-fix": "eslint ./src --ext .js,.ts --fix", "prepublishOnly": "pnpm run build && pnpm run lint && pnpm run lint-package", "lint-package": "pnpm dlx publint", + "prettier-check": "prettier --check ./src", "watch": "tsc --build --watch", "start": "pnpm watch", "tsc": "pnpm exec tsc --build", diff --git a/browser/create-template/package.json b/browser/create-template/package.json index 6771f0ba2..10eb7543e 100644 --- a/browser/create-template/package.json +++ b/browser/create-template/package.json @@ -26,14 +26,15 @@ }, "scripts": { "build": "tsc", - "lint": "eslint ./src --ext .js,.ts", + "lint": "eslint ./src --ext .js,.ts && pnpm prettier-check", "lint-fix": "eslint ./src --ext .js,.ts --fix", "prepublishOnly": "pnpm run build && pnpm run lint && pnpm run lint-package", "lint-package": "pnpm dlx publint", "watch": "tsc --build --watch", "start": "pnpm exec tsc --build --watch", "tsc": "pnpm exec tsc --build", - "typecheck": "pnpm exec tsc --noEmit" + "typecheck": "pnpm exec tsc --noEmit", + "prettier-check": "prettier --check ./src" }, "bin": { "create-template": "./bin/src/index.js" diff --git a/browser/data-browser/.npmrc b/browser/data-browser/.npmrc new file mode 100644 index 000000000..150cdf725 --- /dev/null +++ b/browser/data-browser/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=pdfjs-dist diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index 7c000bb42..1580476d1 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -16,12 +16,12 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/react": "^1.1.1", - "@emotion/is-prop-valid": "^1.3.1", + "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/dom": "^1.7.4", "@modelcontextprotocol/sdk": "^1.13.3", "@oddbird/css-anchor-positioning": "^0.6.1", "@openrouter/ai-sdk-provider": "^1.2.0", - "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-tabs": "^1.1.1", "@tanstack/react-router": "^1.95.1", @@ -65,9 +65,9 @@ "react-hotkeys-hook": "^3.4.7", "react-icons": "^4.12.0", "react-intersection-observer": "^9.13.1", - "react-is": "^19.0.0", + "react-is": "^19.2.0", "react-markdown": "^9.0.3", - "react-pdf": "^9.1.1", + "react-pdf": "^10.2.0", "react-virtualized-auto-sizer": "^1.0.24", "react-window": "^1.8.10", "reactflow": "^11.11.4", @@ -83,21 +83,21 @@ "devDependencies": { "@tanstack/router-devtools": "^1.95.1", "@types/prismjs": "^1.26.5", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", "@types/react-window": "^1.8.8", - "@vitejs/plugin-react": "^4.3.4", - "babel-plugin-react-compiler": "19.1.0-rc.2", + "@vitejs/plugin-react": "^5.0.4", + "babel-plugin-react-compiler": "1.0.0", "babel-plugin-styled-components": "^2.1.4", "csstype": "^3.1.3", "gh-pages": "^5.0.0", "lint-staged": "^10.5.4", "types-wm": "^1.1.0", "typescript": "^5.9.3", - "vite": "^5.4.10", + "vite": "^7.1.12", "vite-plugin-prismjs": "^0.0.11", - "vite-plugin-pwa": "^0.20.5", - "vite-plugin-webfont-dl": "^3.9.5" + "vite-plugin-pwa": "^1.1.0", + "vite-plugin-webfont-dl": "^3.11.1" }, "type": "module", "homepage": "https://atomicdata.dev/", @@ -110,12 +110,13 @@ "name": "@tomic/data-browser", "private": true, "repository": { - "url": "https://github.com/atomicdata-dev/atomic-data-browser/" + "url": "https://github.com/atomicdata-dev/atomic-server" }, "scripts": { "build": "vite build", - "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", + "lint": "eslint --quiet ./src --ext .js,.jsx,.ts,.tsx && pnpm prettier-check ./src", "lint-fix": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix", + "prettier-check": "prettier --check ./src", "preview": "vite preview", "start": "vite", "test": "vitest run", diff --git a/browser/data-browser/src/Providers.tsx b/browser/data-browser/src/Providers.tsx index 276e1052f..45a6fedf9 100644 --- a/browser/data-browser/src/Providers.tsx +++ b/browser/data-browser/src/Providers.tsx @@ -21,6 +21,7 @@ import { Toaster } from './components/Toaster'; import { McpServersProvider } from './components/AI/MCP/useMcpServers'; import { AISettingsContextProvider } from '@components/AI/AISettingsContext'; import { LocaleProvider } from '@components/LocaleContext'; +import { CustomContextItemsProvider } from './components/ResourceContextMenu'; // Setup bugsnag for error handling, but only if there's an API key const ErrBoundary = window.bugsnagApiKey @@ -66,10 +67,12 @@ export const Providers: React.FC = ({ children }) => { - - - {children} - + + + + {children} + + diff --git a/browser/data-browser/src/chunks/PDFViewer/index.tsx b/browser/data-browser/src/chunks/PDFViewer/index.tsx index 771e03316..83459d6e1 100644 --- a/browser/data-browser/src/chunks/PDFViewer/index.tsx +++ b/browser/data-browser/src/chunks/PDFViewer/index.tsx @@ -1,10 +1,14 @@ import { useCallback, useMemo, useState, type JSX } from 'react'; import { pdfjs, Document, Page } from 'react-pdf'; -import 'react-pdf/dist/esm/Page/TextLayer.css'; -import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; +import 'react-pdf/dist/Page/TextLayer.css'; +import 'react-pdf/dist/Page/AnnotationLayer.css'; import { styled } from 'styled-components'; -pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`; +pdfjs.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url, +).toString(); + interface PDFViewerProps { url: string; className?: string; diff --git a/browser/data-browser/src/chunks/RTE/AIChatInput/MentionList.tsx b/browser/data-browser/src/chunks/RTE/AIChatInput/MentionList.tsx index f99f93401..860df1d21 100644 --- a/browser/data-browser/src/chunks/RTE/AIChatInput/MentionList.tsx +++ b/browser/data-browser/src/chunks/RTE/AIChatInput/MentionList.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useEffect, useImperativeHandle } from 'react'; +import { forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; import { getIconForClass } from '../../../helpers/iconMap'; import { useSelectedIndex } from '../../../hooks/useSelectedIndex'; @@ -21,25 +21,22 @@ export interface MentionListRef { } export const MentionList = forwardRef( - ({ items, onSelect }, ref) => { - const { selectedIndex, onKeyDown, onMouseOver, onClick, resetIndex } = - useSelectedIndex( - items, - index => { - if (index === undefined) { - return; - } - - const item = items[index]; - - if (item) { - onSelect(item); - } - }, - 0, - ); - - useEffect(() => resetIndex(), [items]); + ({ items, onSelect, query }, ref) => { + const { selectedIndex, onKeyDown, onMouseOver, onClick } = useSelectedIndex( + items, + index => { + if (index === undefined) { + return; + } + + const item = items[index]; + + if (item) { + onSelect(item); + } + }, + { initialIndex: 0, key: query }, + ); useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => { diff --git a/browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx b/browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx index 23fe37dd0..05647ca01 100644 --- a/browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx +++ b/browser/data-browser/src/chunks/RTE/AsyncMarkdownEditor.tsx @@ -41,10 +41,14 @@ export default function AsyncMarkdownEditor({ }: AsyncMarkdownEditorProps): React.JSX.Element { const containerRef = usePopoverContainer(); + /* eslint-disable-next-line react-hooks/refs */ const container = containerRef.current ?? document.body; + /* eslint-disable-next-line react-hooks/refs */ const [extensions] = useState(() => [ - StarterKit, + StarterKit.configure({ + link: false, + }), Markdown, Typography, Link.configure({ diff --git a/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx b/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx index 24a410fc1..f7ea0912a 100644 --- a/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/BubbleMenu.tsx @@ -12,7 +12,7 @@ import * as RadixPopover from '@radix-ui/react-popover'; import { Column, Row } from '../../components/Row'; import { Popover } from '../../components/Popover'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { transparentize } from 'polished'; import { EditLinkForm } from './EditLinkForm'; import { useTipTapEditor } from './TiptapContext'; @@ -31,7 +31,6 @@ export function BubbleMenu({ extraItems, onShow, }: BubbleMenuProps): React.JSX.Element { - const bubbleMenuElement = useRef(null); const editor = useTipTapEditor(); const [linkMenuOpen, setLinkMenuOpen] = useState(false); @@ -48,16 +47,12 @@ export function BubbleMenu({ }), }); - if (!editor.view) { + if (!editor.isInitialized) { return <>; } return ( - + diff --git a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx index 59a3e6eef..27c270998 100644 --- a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx +++ b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx @@ -1,4 +1,4 @@ -import { EditorContent, useEditor } from '@tiptap/react'; +import { EditorContent, useEditor, type Editor } from '@tiptap/react'; import { FloatingMenu } from '@tiptap/react/menus'; import { StarterKit } from '@tiptap/starter-kit'; import { Link } from '@tiptap/extension-link'; @@ -22,22 +22,23 @@ import { buildResourceSuggestion, } from './ResourceExtension/ResourceExtention'; import { ExtendedImage } from './ImagePicker'; -import { usePopoverContainer } from '../../components/Popover'; import { FloatingMenuText } from './sharedEditorStyles'; import * as Y from 'yjs'; import { + dataBrowser, + useCanWrite, useDebouncedSave, useResource, useStore, type Core, type Resource, + type Server, } from '@tomic/react'; import { EditorEvents } from './EditorEvents'; import { useYSync } from './useYSync'; import { randomItem } from '@helpers/randomItem'; import { EditorWrapperBase } from './EditorWrapperBase'; import styled from 'styled-components'; -import { transition } from '@helpers/transition'; import { useSettings } from '@helpers/AppSettings'; import { FullBubbleMenu } from './FullBubbleMenu'; import { @@ -45,7 +46,13 @@ import { ResourceNodeInline, } from './ResourceExtension/ResourceNode'; import { IsInRTEContex } from '@hooks/useIsInRTE'; -import { FaGripVertical } from 'react-icons/fa6'; +import { FaGripVertical, FaLink, FaTable } from 'react-icons/fa6'; +import { useUpload } from '@hooks/useUpload'; +import FileHandler from '@tiptap/extension-file-handler'; +import { supportedImageTypes } from '@views/File/fileTypeUtils'; +import type { SuggestionItem } from './types'; +import { useNewResourceUI } from '@components/forms/NewForm/useNewResourceUI'; +import { addIf } from '@helpers/addIf'; export type CollaborativeEditorProps = { placeholder?: string; @@ -71,101 +78,197 @@ export default function CollaborativeEditor({ onBlur, }: CollaborativeEditorProps): React.JSX.Element { const store = useStore(); + const [color] = useState(randomItem(COLORS)); + const showNewResourceUI = useNewResourceUI(); const [save] = useDebouncedSave(resource, 2000); const { agent, drive } = useSettings(); const agentResource = useResource(agent?.subject); - const containerRef = usePopoverContainer(); - const color = randomItem(COLORS); - const container = containerRef.current ?? document.body; - + const { upload } = useUpload(resource); const awareness = useYSync(resource, property, doc); + const canWrite = useCanWrite(resource); - const [extensions] = useState(() => [ - StarterKit.configure({ - undoRedo: false, - link: false, - }), - Typography, - Link.configure({ - autolink: true, - openOnClick: true, - protocols: [ - 'http', - 'https', - 'mailto', - { - scheme: 'tel', - optionalSlashes: true, - }, - ], - HTMLAttributes: { - class: 'tiptap-link', - rel: 'noopener noreferrer', - target: '_blank', - }, - }), - ExtendedImage.configure({ - HTMLAttributes: { - class: 'tiptap-image', - }, - }), - Placeholder.configure({ - placeholder: placeholder ?? 'Start typing...', - }), - SlashCommands.configure({ - suggestion: buildSuggestion(container), - }), - ResourceCommands.configure({ - suggestion: buildResourceSuggestion(container, store, drive), - }), - ResourceNode, - ResourceNodeInline, - Collaboration.configure({ - document: doc, - field: 'content', - }), - CollaborationCaret.configure({ - provider: { - awareness, - }, - user: { - name: agentResource.title, - color, - }, - }), - TextAlign.configure({ - types: ['heading', 'paragraph'], - }), - TaskList, - TaskItem.configure({ - nested: true, - }), - TextStyle, - Color, - BackgroundColor, - ]); + const uploadAndInsertImage = async ( + currentEditor: Editor, + files: File[], + pos: number, + ) => { + const subjects = await upload(files); + + for (const imageSubject of subjects) { + const image = await store.getResource(imageSubject); + + currentEditor.commands.insertContentAt(pos, { + type: 'image', + attrs: { src: image.props.downloadUrl }, + }); + } + }; - const editor = useEditor({ - extensions, - onBlur, - autofocus: !!autoFocus, - editorProps: { - attributes: { - ...(id && { id }), - ...(labelId && { 'aria-labelledby': labelId }), - spellcheck: 'true', + const editor = useEditor( + { + extensions: [ + StarterKit.configure({ + undoRedo: false, + link: false, + }), + Typography, + Link.extend({ + parseHTML: () => [ + { + tag: 'a[href]', + getAttrs: node => { + // Links with a data-type are custom nodes that should be ignored by the link extension + if (node.getAttribute('data-type')) { + return false; + } + + // Default link parsing + return { + href: node.getAttribute('href'), + target: node.getAttribute('target'), + }; + }, + }, + ], + }).configure({ + autolink: true, + openOnClick: true, + protocols: [ + 'http', + 'https', + 'mailto', + { + scheme: 'tel', + optionalSlashes: true, + }, + ], + HTMLAttributes: { + class: 'tiptap-link', + rel: 'noopener noreferrer', + target: '_blank', + }, + }), + ExtendedImage.configure({ + uploadImage: upload, + HTMLAttributes: { + class: 'tiptap-image', + }, + }), + Placeholder.configure({ + placeholder: placeholder ?? 'Start typing...', + }), + SlashCommands.configure({ + suggestion: buildSuggestion(document.body, [ + { + title: 'Resource', + id: 'resource', + icon: FaLink, + command: ({ range }) => + editor + .chain() + .focus() + .deleteRange(range) + .insertContent('@') + .run(), + } as SuggestionItem, + { + title: 'Data Table', + id: 'data-table', + icon: FaTable, + command: ({ range }) => { + showNewResourceUI(dataBrowser.classes.table, resource.subject, { + skipNavigation: true, + onCreated: table => { + editor + .chain() + .focus() + .deleteRange(range) + .setResource({ subject: table.subject }) + .run(); + }, + }); + }, + }, + ]), + }), + ResourceCommands.configure({ + suggestion: buildResourceSuggestion(document.body, store, drive), + }), + ResourceNode.configure({ + store, + }), + ResourceNodeInline.configure({ + store, + }), + Collaboration.configure({ + document: doc, + field: 'content', + }), + ...addIf( + canWrite, + CollaborationCaret.configure({ + provider: { + awareness, + }, + user: { + name: agentResource.title, + color, + }, + }), + ), + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), + TaskList, + TaskItem.configure({ + nested: true, + }), + TextStyle, + Color, + BackgroundColor, + FileHandler.configure({ + allowedMimeTypes: Array.from(supportedImageTypes), + onDrop: (currentEditor, files, pos) => { + uploadAndInsertImage(currentEditor, files, pos); + }, + onPaste: (currentEditor, files, htmlContent) => { + if (htmlContent) { + // if there is htmlContent, stop manual insertion & let other extensions handle insertion via inputRule + // you could extract the pasted file from this url string and upload it to a server for example + + return false; + } + + uploadAndInsertImage( + currentEditor, + files, + currentEditor.state.selection.anchor, + ); + }, + }), + ], + editable: canWrite, + onBlur, + autofocus: !!autoFocus, + editorProps: { + attributes: { + ...(id && { id }), + ...(labelId && { 'aria-labelledby': labelId }), + spellcheck: 'true', + }, }, }, - }); + [canWrite], + ); useEffect(() => { if (agentResource) { - editor.commands.updateUser({ + editor.commands.updateUser?.({ name: agentResource.props.name ?? 'Untitled Agent', color, }); } - }, [agentResource]); + }, [agentResource, editor.commands, color, canWrite]); return ( @@ -183,25 +286,31 @@ export default function CollaborativeEditor({ + editor?.commands.focus('end')} /> ); } +const ClickUnderHandler = styled.div` + flex: 1; + width: 100%; + min-height: 10rem; +`; + export const StyledEditorWrapper = styled(EditorWrapperBase)` box-shadow: none; - min-height: 10rem; + min-height: 100%; border-radius: ${p => p.theme.radius}; min-height: 10rem; - padding: ${p => p.theme.size()}; width: 100%; - margin-bottom: 10rem; - ${transition('box-shadow')} + flex: 1; + display: flex; + flex-direction: column; & .tiptap { width: 100%; - min-height: 10rem; ::spelling-error { text-decoration: wavy red underline; } @@ -215,10 +324,5 @@ export const StyledEditorWrapper = styled(EditorWrapperBase)` justify-content: center; width: 1.5rem; color: ${p => p.theme.colors.textLight2}; - - /* svg { - width: 1.25rem; - height: 1.25rem; - } */ } `; diff --git a/browser/data-browser/src/chunks/RTE/EditorEvents.tsx b/browser/data-browser/src/chunks/RTE/EditorEvents.tsx index bdeaf83bd..eb49588f6 100644 --- a/browser/data-browser/src/chunks/RTE/EditorEvents.tsx +++ b/browser/data-browser/src/chunks/RTE/EditorEvents.tsx @@ -15,9 +15,7 @@ export function EditorEvents({ onChange }: EditorEventsProps): null { onChange?.(); }; - if (editor) { - editor.on('update', callback); - } + editor.on('update', callback); return () => { if (editor) { diff --git a/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx b/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx index a15cdfa2d..ec1c5fdb8 100644 --- a/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx @@ -47,6 +47,10 @@ export const FullBubbleMenu: React.FC = () => { }, ]; + if (!editor.view) { + return null; + } + return ( {colorMenuOpen && }} diff --git a/browser/data-browser/src/chunks/RTE/ImagePicker.tsx b/browser/data-browser/src/chunks/RTE/ImagePicker.tsx index 420178f95..263833faa 100644 --- a/browser/data-browser/src/chunks/RTE/ImagePicker.tsx +++ b/browser/data-browser/src/chunks/RTE/ImagePicker.tsx @@ -1,11 +1,11 @@ import { NodeViewWrapper, ReactNodeViewRenderer, - type Editor, + type ReactNodeViewProps, } from '@tiptap/react'; -import { Image } from '@tiptap/extension-image'; +import { Image, type ImageOptions } from '@tiptap/extension-image'; import { styled } from 'styled-components'; -import { forwardRef, useState } from 'react'; +import { useState } from 'react'; import { Button } from '../../components/Button'; import { InputStyled, InputWrapper } from '../../components/forms/InputStyles'; import { Column, Row } from '../../components/Row'; @@ -19,29 +19,31 @@ import { imageMimeTypes } from '../../helpers/filetypes'; import { useHTMLFormFieldValidation } from '../../helpers/useHTMLFormFieldValidation'; import { transition } from '../../helpers/transition'; -type PartialImageNodeProps = { - node: { - attrs: { - src?: string; - alt?: string; - }; - }; - updateAttributes: (attrs: { src: string; alt?: string }) => void; - selected: boolean; - editor: Editor; -}; +interface ExtendedImageProps extends ImageOptions { + uploadImage?: (file: File[]) => Promise; +} + +export const ExtendedImage = Image.extend({ + addOptions() { + return { + ...this.parent?.(), + onNewFilePicked: undefined, + } as ExtendedImageProps; + }, -export const ExtendedImage = Image.extend({ addNodeView() { - // @ts-ignore. Weird type issue probably due to incorrect tiptap types. return ReactNodeViewRenderer(MarkdownEditorImage); }, }); -const MarkdownEditorImage = forwardRef< - HTMLImageElement | HTMLDivElement, - PartialImageNodeProps ->(({ node, updateAttributes, selected, editor }, ref) => { +const MarkdownEditorImage = ({ + node, + updateAttributes, + selected, + editor, + extension, + ref, +}: ReactNodeViewProps) => { const store = useStore(); const [showPicker, setShowPicker] = useState(false); @@ -71,6 +73,13 @@ const MarkdownEditorImage = forwardRef< editor.chain().focus().run(); }; + const uploadAndSet = extension.options.uploadImage + ? async (file: File) => { + const subjects = await extension.options.uploadImage([file]); + setSelectedSubject(subjects[0]); + } + : undefined; + if (node.attrs.src) { return ( @@ -130,16 +139,15 @@ const MarkdownEditorImage = forwardRef< undefined} + onNewFilePicked={uploadAndSet} allowedMimes={imageMimeTypes} /> ); -}); +}; MarkdownEditorImage.displayName = 'MarkdownEditorImage'; diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/RTENodeViewWrapper.tsx b/browser/data-browser/src/chunks/RTE/ResourceExtension/RTENodeViewWrapper.tsx new file mode 100644 index 000000000..385af66a1 --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/RTENodeViewWrapper.tsx @@ -0,0 +1,29 @@ +import { NodeViewWrapper } from '@tiptap/react'; +import { styled } from 'styled-components'; +import styles from './ResourceNode.module.css'; + +const stopPropagation = (e: React.MouseEvent) => + e.stopPropagation(); + +interface RTENodeViewWrapperProps { + wide?: boolean; +} + +export const RTENodeViewWrapper: React.FC< + React.PropsWithChildren +> = ({ children, wide = false }) => { + return ( + + {children} + + ); +}; + +const StyledNodeViewWrapper = styled(NodeViewWrapper)` + margin-bottom: 1rem; +`; diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx index e29ee493d..5f39d0234 100644 --- a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx @@ -6,36 +6,30 @@ import { dataBrowser, useResource } from '@tomic/react'; import ResourceCard from '@views/Card/ResourceCard'; import { styled } from 'styled-components'; import { TableRTE } from '../TableRTE'; - -const stopPropagation = (e: React.MouseEvent) => - e.stopPropagation(); +import { RTENodeViewWrapper } from './RTENodeViewWrapper'; +import { ErrorBoundary } from '@views/ErrorPage'; export const ResourceComponent = ( props: ReactNodeViewProps, ) => { const resource = useResource(props.node.attrs.subject); - const Component = resource.matchClass( + const [Component, wide] = resource.matchClass( { - [dataBrowser.classes.table]: TableRTE, + [dataBrowser.classes.table]: [TableRTE, true], }, - ResourceCard, + [ResourceCard, false], ); return ( - - - + + + + + ); }; -const StyledNodeViewWrapper = styled(NodeViewWrapper)` - margin-bottom: 1rem; -`; - export const ResourceInlineComponent = ( props: ReactNodeViewProps, ) => { diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css new file mode 100644 index 000000000..70520261a --- /dev/null +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css @@ -0,0 +1,18 @@ +/** Can be added to a node view to make it wider than the container.**/ +.wideNode { + /* Add the wide-wrapper class to the node renderer */ +} + +.nodeRenderer { + width: 100%; + + &:has(.wideNode) { + width: 1100px; + margin-left: -150px; + + @container (max-width: 1100px) { + width: 100%; + margin-left: 0; + } + } +} diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts index 742b7d912..d678a4b14 100644 --- a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.ts @@ -1,15 +1,25 @@ -import { mergeAttributes, Node } from '@tiptap/core'; +import { Node } from '@tiptap/core'; import { ReactNodeViewRenderer } from '@tiptap/react'; -import { unknownSubject } from '@tomic/react'; +import { unknownSubject, type Store } from '@tomic/react'; import { ResourceComponent, ResourceInlineComponent, } from './ResourceComponent'; +import styles from './ResourceNode.module.css'; -export interface ResourceNodeOptions { +interface ResourceNodeOptions { + store?: Store; +} + +export interface SetResourceNodeOptions { subject: string; } +const TYPES = { + BLOCK: 'resource-block', + INLINE: 'resource-inline', +} as const; + declare module '@tiptap/core' { interface Commands { resource: { @@ -17,53 +27,59 @@ declare module '@tiptap/core' { * Add a resource view to the document. * @param options Object containing the subject. */ - setResource: (options: ResourceNodeOptions) => ReturnType; + setResource: (options: SetResourceNodeOptions) => ReturnType; }; resourceInline: { - setResourceInline: (options: ResourceNodeOptions) => ReturnType; + setResourceInline: (options: SetResourceNodeOptions) => ReturnType; }; } } -export const ResourceNode = Node.create({ +export const ResourceNode = Node.create({ name: 'atomic-data-resource', group: 'block', + atom: true, + + addOptions() { + return { + store: undefined, + }; + }, parseHTML() { return [ { - tag: 'a', + tag: `a[data-type="${TYPES.BLOCK}"]`, getAttrs: node => { const dataType = node.getAttribute('data-type'); - if (dataType !== 'resource-block') { + if (dataType !== TYPES.BLOCK) { return false; // Not a resource-block, ignore } return { - subject: node.getAttribute('data-subject'), // Extract the attribute + subject: node.getAttribute('href'), // Extract the attribute }; }, }, ]; }, - renderHTML({ HTMLAttributes, node }) { + renderHTML({ HTMLAttributes }) { + const title = + this.options.store?.getResourceLoading(HTMLAttributes['subject']).title ?? + ''; + return [ 'a', - mergeAttributes(HTMLAttributes, { - 'data-type': 'resource-block', - 'data-subject': node.attrs['subject'], - }), + { + 'data-type': TYPES.BLOCK, + href: HTMLAttributes['subject'], + }, + title, ]; }, - addOptions() { - return { - subject: unknownSubject, - }; - }, - addCommands() { return { setResource: @@ -81,16 +97,21 @@ export const ResourceNode = Node.create({ return { subject: { default: unknownSubject, - parseHTML: e => e.getAttribute('data-subject'), }, }; }, - addNodeView() { - if (this.options.inline) { - return ReactNodeViewRenderer(ResourceInlineComponent); - } - return ReactNodeViewRenderer(ResourceComponent); + addNodeView() { + return ReactNodeViewRenderer(ResourceComponent, { + className: styles.nodeRenderer, + contentDOMElementTag: 'div', + ignoreMutation: ({ mutation }) => { + return ( + mutation.type === 'attributes' && + mutation.attributeName === 'aria-hidden' + ); + }, + }); }, }); @@ -101,30 +122,35 @@ export const ResourceNodeInline = ResourceNode.extend({ parseHTML() { return [ { - tag: 'a', + tag: `a[data-type="${TYPES.INLINE}"]`, getAttrs: node => { const dataType = node.getAttribute('data-type'); - if (dataType !== 'resource-inline') { + if (dataType !== TYPES.INLINE) { return false; // Not a resource-block, ignore } return { - 'data-type': 'resource-inline', - subject: node.getAttribute('data-subject'), + 'data-type': TYPES.INLINE, + subject: node.getAttribute('href'), }; }, }, ]; }, - renderHTML({ HTMLAttributes, node }) { + renderHTML({ HTMLAttributes }) { + const title = + this.options.store?.getResourceLoading(HTMLAttributes['subject']).title ?? + ''; + return [ 'a', - mergeAttributes(HTMLAttributes, { - 'data-type': 'resource-inline', - 'data-subject': node.attrs['subject'], - }), + { + 'data-type': TYPES.INLINE, + href: HTMLAttributes['subject'], + }, + title, ]; }, diff --git a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx index 8b7166b1d..e568cc337 100644 --- a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx +++ b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx @@ -2,7 +2,6 @@ import { transparentize } from 'polished'; import { forwardRef, useState, - useEffect, useImperativeHandle, useId, useCallback, @@ -10,6 +9,7 @@ import { import { styled } from 'styled-components'; import { ScrollArea } from '../../../components/ScrollArea'; import type { SuggestionItem } from '../types'; +import { useOnValueChange } from '@helpers/useOnValueChange'; export type CommandListRefType = { onKeyDown: (event: KeyboardEvent) => boolean; @@ -45,7 +45,7 @@ export const CommandList = forwardRef( [command, items], ); - useEffect(() => setSelectedIndex(0), [items]); + useOnValueChange(() => setSelectedIndex(0), [items]); useImperativeHandle( ref, @@ -83,6 +83,7 @@ export const CommandList = forwardRef( return ( + {items.length === 0 &&
No results found
} {items.map((item, index) => { const Icon = item.icon; diff --git a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts index 5d307ad81..faba5a155 100644 --- a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts +++ b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandsExtension.ts @@ -17,7 +17,6 @@ import { FaCode, FaHeading, FaImage, - FaLink, FaListOl, FaListUl, FaParagraph, @@ -111,6 +110,7 @@ export const createRenderFunction = export const buildSuggestion = ( container: HTMLElement, + extraItems: SuggestionItem[] = [], ): Partial> => ({ items: async ({ query }: { query: string }): Promise => [ @@ -156,13 +156,7 @@ export const buildSuggestion = ( command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setImage({ src: '' }).run(), } as SuggestionItem, - { - title: 'Resource', - id: 'resource', - icon: FaLink, - command: ({ editor, range }) => - editor.chain().focus().deleteRange(range).insertContent('@').run(), - } as SuggestionItem, + ...extraItems, { title: 'Heading 1', id: 'heading-1', diff --git a/browser/data-browser/src/chunks/RTE/TableRTE.tsx b/browser/data-browser/src/chunks/RTE/TableRTE.tsx index c830970bc..2d1c146bb 100644 --- a/browser/data-browser/src/chunks/RTE/TableRTE.tsx +++ b/browser/data-browser/src/chunks/RTE/TableRTE.tsx @@ -1,4 +1,5 @@ import { AtomicLink } from '@components/AtomicLink'; +import { HideInPrint } from '@components/HideInPrint'; import { useResource, type DataBrowser } from '@tomic/react'; import { TableResource } from '@views/TablePage/TableResource'; import { FaArrowUpRightFromSquare } from 'react-icons/fa6'; @@ -12,20 +13,17 @@ export const TableRTE: React.FC = ({ subject }) => { const resource = useResource(subject); return ( - - - - {resource.title} - - + +
+ + + {resource.title} + +
+
); }; -const Wrapper = styled.div` - width: 1100px; - margin-left: -150px; -`; - const TableTitle = styled(AtomicLink)` display: flex; align-items: center; diff --git a/browser/data-browser/src/chunks/RTE/TiptapContext.tsx b/browser/data-browser/src/chunks/RTE/TiptapContext.tsx index a861d627d..aa48a9ed9 100644 --- a/browser/data-browser/src/chunks/RTE/TiptapContext.tsx +++ b/browser/data-browser/src/chunks/RTE/TiptapContext.tsx @@ -16,10 +16,6 @@ export const TiptapContextProvider = ({ editor, children, }: React.PropsWithChildren) => { - if (!editor) { - return null; - } - return ( {children} ); diff --git a/browser/data-browser/src/components/AllPropsSimple.tsx b/browser/data-browser/src/components/AllPropsSimple.tsx index be6d4afd9..c7907248a 100644 --- a/browser/data-browser/src/components/AllPropsSimple.tsx +++ b/browser/data-browser/src/components/AllPropsSimple.tsx @@ -6,6 +6,7 @@ import { useResource, useSubject, useTitle, + isYDoc, } from '@tomic/react'; import { useMemo, type JSX } from 'react'; import { styled } from 'styled-components'; @@ -19,9 +20,11 @@ export interface AllPropsSimpleProps { export function AllPropsSimple({ resource }: AllPropsSimpleProps): JSX.Element { return (
    - {[...resource.getPropVals()].map(([prop, val]) => ( - - ))} + {[...resource.getPropVals()] + .filter(([_, val]) => !isYDoc(val)) + .map(([prop, val]) => ( + + ))}
); } diff --git a/browser/data-browser/src/components/AtomicLink.tsx b/browser/data-browser/src/components/AtomicLink.tsx index 9714f8e98..d4d4e3089 100644 --- a/browser/data-browser/src/components/AtomicLink.tsx +++ b/browser/data-browser/src/components/AtomicLink.tsx @@ -1,4 +1,4 @@ -import { ReactNode, forwardRef, type JSX } from 'react'; +import { ReactNode, useCallback, useEffect, useRef } from 'react'; import { styled } from 'styled-components'; import { constructOpenURL, pathToURL } from '../helpers/navigation'; import { FaExternalLinkAlt } from 'react-icons/fa'; @@ -7,6 +7,7 @@ import { isRunningInTauri } from '../helpers/tauri'; import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; import clsx from 'clsx'; import { useIsInRTE } from '@hooks/useIsInRTE'; +import { useCombineRefs } from '@hooks/useCombineRefs'; export interface AtomicLinkProps extends React.AnchorHTMLAttributes { @@ -22,90 +23,130 @@ export interface AtomicLinkProps clean?: boolean; /** Used to extend with styled */ className?: string; + ref?: React.Ref; } /** * Renders a link. Either a subject or a href is required. You can wrap this * around other components and pass the `clean` prop to skip styling. */ -export const AtomicLink = forwardRef( - ( - { children, clean, subject, path, href, untabbable, className, ...props }, - ref, - ): JSX.Element => { - const navigate = useNavigateWithTransition(); - const isInRTE = useIsInRTE(); - - if (subject === undefined && href === undefined && path === undefined) { - return ( - - No `subject`, `path` or `href` passed to this AtomicLink. - - ); +export const AtomicLink: React.FC> = ({ + children, + clean, + subject, + path, + href, + untabbable, + className, + ref, + ...props +}) => { + const innerRef = useRef(null); + const combinedRef = useCombineRefs([ref, innerRef]); + const navigate = useNavigateWithTransition(); + const isInRTE = useIsInRTE(); + + let isOnCurrentPage: boolean; + + const handleClick = (e: React.MouseEvent) => { + if (href) { + // When there is a regular URL, let the browser handle it + return; } - let isOnCurrentPage: boolean; + e.preventDefault(); - try { - isOnCurrentPage = subject - ? window.location.toString() === constructOpenURL(subject) - : false; - } catch (e) { - return {subject}; + if (path) { + navigate(path); + + return; } - const handleClick = (e: React.MouseEvent) => { - if (href) { - // When there is a regular URL, let the browser handle it + if (subject) { + if (isOnCurrentPage) { return; } - e.preventDefault(); + navigate(constructOpenURL(subject)); + } + }; - if (path) { - navigate(path); + const constructHref = useCallback( + () => href || subject || pathToURL(path!), + [href, subject, path], + ); - return; - } + let hrefConstructed: string | undefined = constructHref(); - if (subject) { - if (isOnCurrentPage) { - return; - } + if (isInRTE) { + // HACK: The Tiptap editor has an event handler that always opens links in new tabs. We can't disable it so we have to remove the href from links when inside the editor. + hrefConstructed = undefined; + } - navigate(constructOpenURL(subject)); - } + useEffect(() => { + if (!innerRef.current) return; + + if (!isInRTE) return; + + // HACK: Because we remove the href from the links in the RTE we need to restore them when printing. + const handleBeforePrint = () => { + innerRef.current?.setAttribute('href', constructHref()); }; - let hrefConstructed: string | undefined = - href || subject || pathToURL(path!); + const handleAfterPrint = () => { + innerRef.current?.removeAttribute('href'); + }; - if (isInRTE) { - // HACK: The Tiptap editor has an event handler that always opens links in new tabs. We can't disable it so we have to remove the href from links when inside the editor. - hrefConstructed = undefined; - } + window.addEventListener('beforeprint', handleBeforePrint); + window.addEventListener('afterprint', handleAfterPrint); + + return () => { + window.removeEventListener('beforeprint', handleBeforePrint); + window.removeEventListener('afterprint', handleAfterPrint); + }; + }, [constructHref, isInRTE]); + if (subject === undefined && href === undefined && path === undefined) { return ( - - {children} - {href && !clean && } - + + No `subject`, `path` or `href` passed to this AtomicLink. + ); - }, -); + } + + try { + isOnCurrentPage = subject + ? window.location.toString() === constructOpenURL(subject) + : false; + } catch (e) { + return {subject}; + } + + return ( + + {children} + {href && !clean && ( + <> + {' '} + + + )} + + ); +}; AtomicLink.displayName = 'AtomicLink'; @@ -132,7 +173,6 @@ export const LinkView = styled.a` } &.atomic-link_external { - display: inline-flex; align-items: center; gap: 0.6ch; } diff --git a/browser/data-browser/src/components/CustomPopover.tsx b/browser/data-browser/src/components/CustomPopover.tsx new file mode 100644 index 000000000..b69f396d2 --- /dev/null +++ b/browser/data-browser/src/components/CustomPopover.tsx @@ -0,0 +1,149 @@ +import { + useEffectEvent, + useId, + useLayoutEffect, + useRef, + type ReactNode, +} from 'react'; +import { styled } from 'styled-components'; +import { transparentize } from 'polished'; +import { fadeIn } from '@helpers/commonAnimations'; +import { useControlLock } from '@hooks/useControlLock'; +import { useDialogTreeInfo } from './Dialog/dialogContext'; +import { useControllable } from '@hooks/useControlable'; + +export interface TriggerProps { + onClick: () => void; + 'data-popover-target': string; +} + +export interface PopoverProps { + Trigger: (props: TriggerProps) => ReactNode; + open?: boolean; + defaultOpen?: boolean; + onOpenChange: (open: boolean) => void; + className?: string; + noArrow?: boolean; + noLock?: boolean; + modal?: boolean; + side?: 'top' | 'bottom' | 'left' | 'right'; +} + +/** + * Popover component, consists of an outer dialog element and an inner content div. + * To style the content div use `${CustomPopover.Content}: { ... }` + */ +export function CustomPopover({ + Trigger, + open: parentOpen, + defaultOpen, + onOpenChange, + className, + noLock, + side = 'top', + modal, + children, +}: React.PropsWithChildren) { + const popoverRef = useRef(null); + const contentRef = useRef(null); + const id = useId(); + + const setElementState = (state: boolean) => { + if (state && !popoverRef.current?.hasAttribute('open')) { + if (modal) { + popoverRef.current?.showModal(); + } else { + popoverRef.current?.show(); + } + } else if (!state && popoverRef.current?.hasAttribute('open')) { + popoverRef.current?.close(); + } + }; + + const onStateChange = (state: boolean) => { + setElementState(state); + setHasOpenInnerPopup(state); + + onOpenChange?.(state); + }; + + const [open, setOpen] = useControllable({ + controlledValue: parentOpen, + defaultValue: defaultOpen, + onChange: onStateChange, + }); + + const { setHasOpenInnerPopup } = useDialogTreeInfo(); + + const handleOutsideClick = ( + e: React.MouseEvent, + ) => { + if ( + !contentRef.current?.contains(e.target as HTMLElement) && + contentRef.current !== e.target + ) { + setOpen(false); + } + }; + + const setElementStateEffect = useEffectEvent((state: boolean) => { + setElementState(state); + }); + + useLayoutEffect(() => { + setElementStateEffect(!!open); + }, [open]); + + useControlLock(!noLock && !!open); + + return ( + + setOpen(prev => !prev)} + data-popover-target={id} + /> + handleOutsideClick(e)} + className={className} + > + {open && children} + + + ); +} + +const PopoverContent = styled.div``; + +CustomPopover.Content = PopoverContent; + +const Wrapper = styled.div<{ anchorName: string }>` + display: contents; + + & button[data-popover-target='${p => p.anchorName}'] { + anchor-name: --${p => p.anchorName}; + } +`; + +const Popover = styled.dialog<{ anchorName: string; side: string }>` + border: none; + background-color: ${p => transparentize(0.2, p.theme.colors.bgBody)}; + backdrop-filter: blur(10px); + box-shadow: ${p => p.theme.boxShadowSoft}; + border-radius: ${p => p.theme.radius}; + animation: ${fadeIn} 0.1s ease-in-out; + margin: 0; + padding: 0; + inset: auto; + position-anchor: --${p => p.anchorName}; + position-area: ${p => p.side}; + position-try-fallbacks: flip-block; + max-height: unset; + &::backdrop { + background-color: transparent; + } +`; diff --git a/browser/data-browser/src/components/Dialog/index.tsx b/browser/data-browser/src/components/Dialog/index.tsx index 816275c51..e84568544 100644 --- a/browser/data-browser/src/components/Dialog/index.tsx +++ b/browser/data-browser/src/components/Dialog/index.tsx @@ -113,7 +113,7 @@ const InnerDialog: React.FC> = ({ cancelDialog(); } }, - [innerDialogRef.current, cancelDialog, isTopLevel], + [cancelDialog, isTopLevel], ); // Close the dialog when the escape key is pressed diff --git a/browser/data-browser/src/components/HideInPrint.tsx b/browser/data-browser/src/components/HideInPrint.tsx new file mode 100644 index 000000000..2e8e9ba49 --- /dev/null +++ b/browser/data-browser/src/components/HideInPrint.tsx @@ -0,0 +1,8 @@ +import { styled } from 'styled-components'; + +export const HideInPrint = styled.div` + display: contents; + @media print { + display: none; + } +`; diff --git a/browser/data-browser/src/components/IconButton/IconButton.tsx b/browser/data-browser/src/components/IconButton/IconButton.tsx index 55234857c..bda31d8e8 100644 --- a/browser/data-browser/src/components/IconButton/IconButton.tsx +++ b/browser/data-browser/src/components/IconButton/IconButton.tsx @@ -227,7 +227,8 @@ const MagicIconButton = styled(IconButtonBase)` opacity: 0; z-index: -2; will-change: filter; - background: radial-gradient(ellipse at top right, #365ccd, transparent), + background: + radial-gradient(ellipse at top right, #365ccd, transparent), radial-gradient( ellipse at bottom left, ${adjustHue(-45, '#365ccd')}, diff --git a/browser/data-browser/src/components/Main.tsx b/browser/data-browser/src/components/Main.tsx index bce635d09..afef4608b 100644 --- a/browser/data-browser/src/components/Main.tsx +++ b/browser/data-browser/src/components/Main.tsx @@ -50,4 +50,15 @@ const StyledMain = memo(styled.main` @media (prefers-reduced-motion: no-preference) { scroll-behavior: smooth; } + + @media print { + display: block; + position: static; + height: auto; + overflow-y: visible; + overflow: visible; + scroll-padding: 0; + page-break-after: auto; + page-break-inside: auto; + } `); diff --git a/browser/data-browser/src/components/Navigation.tsx b/browser/data-browser/src/components/Navigation.tsx index 19997fc06..31e6df82d 100644 --- a/browser/data-browser/src/components/Navigation.tsx +++ b/browser/data-browser/src/components/Navigation.tsx @@ -16,6 +16,7 @@ import { SearchbarFakeInput } from './Searchbar/SearchbarInput'; import { CalculatedPageHeight } from '../globalCssVars'; import { AISidebarContextProvider } from './AI/AISidebarContext'; import { AISidebarContainer } from './AI/AISidebarContainer'; +import { HideInPrint } from './HideInPrint'; export const NAVBAR_HEIGHT = '2.5rem'; @@ -43,8 +44,6 @@ const AISidebarMemo = React.memo(AISidebarContainer); /** Wraps the entire app and adds a navbar at the bottom or the top */ export function NavWrapper({ children }: NavWrapperProps): JSX.Element { const { navbarTop, navbarFloating } = useSettings(); - const contentRef = React.useRef(null); - const navbarPosition = getPosition(navbarTop, navbarFloating); return ( @@ -52,14 +51,12 @@ export function NavWrapper({ children }: NavWrapperProps): JSX.Element { {navbarTop && } - + {children} - + + + {!navbarTop && } @@ -74,7 +71,6 @@ interface ContentProps { const Content = styled.div` display: block; flex: 1; - overflow-y: auto; `; /** Persistently shown navigation bar */ @@ -158,6 +154,10 @@ const NavBarBase = styled.div` display: none; } } + + @media print { + display: none; + } `; /** Width of the floating navbar in rem */ @@ -231,4 +231,11 @@ const SideBarWrapper = styled.div<{ navbarPosition: NavBarPosition }>` @starting-style { opacity: 0; } + + @media print { + height: auto; + ${CalculatedPageHeight.define('auto')} + position: static; + display: block; + } `; diff --git a/browser/data-browser/src/components/Parent.tsx b/browser/data-browser/src/components/Parent.tsx index a44b5bc27..fd5e220c9 100644 --- a/browser/data-browser/src/components/Parent.tsx +++ b/browser/data-browser/src/components/Parent.tsx @@ -74,6 +74,10 @@ const ParentWrapper = styled.nav` justify-content: flex-start; view-transition-name: ${BREADCRUMB_BAR_TRANSITION_TAG}; + + @media print { + display: none; + } `; type NestedParentProps = { @@ -155,6 +159,7 @@ const BreadCrumbBase = css` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + max-width: 50ch; `; const BreadCrumbCurrent = styled.span` diff --git a/browser/data-browser/src/components/ResourceContextMenu/CustomContextItemsContext.tsx b/browser/data-browser/src/components/ResourceContextMenu/CustomContextItemsContext.tsx new file mode 100644 index 000000000..4d2c7498b --- /dev/null +++ b/browser/data-browser/src/components/ResourceContextMenu/CustomContextItemsContext.tsx @@ -0,0 +1,95 @@ +import { + createContext, + useContext, + useState, + useCallback, + type PropsWithChildren, + useEffect, +} from 'react'; +import type { DropdownItem } from '../Dropdown'; + +export interface CustomContextItemsContextValue { + items: DropdownItem[]; + registerItems: (items: DropdownItem[]) => () => void; +} + +const CustomContextItemsContext = createContext< + CustomContextItemsContextValue | undefined +>(undefined); + +export function CustomContextItemsProvider({ children }: PropsWithChildren) { + const [itemsMap, setItemsMap] = useState>( + new Map(), + ); + + const registerItems = useCallback((items: DropdownItem[]) => { + const id = Math.random().toString(36).substring(7); + + setItemsMap(prev => { + const next = new Map(prev); + next.set(id, items); + + return next; + }); + + // Return cleanup function + return () => { + setItemsMap(prev => { + const next = new Map(prev); + next.delete(id); + + return next; + }); + }; + }, []); + + const items = Array.from(itemsMap.values()).flat(); + + return ( + + {children} + + ); +} + +export function useCustomContextItemsContext() { + const context = useContext(CustomContextItemsContext); + + if (!context) { + throw new Error( + 'useCustomContextItemsContext must be used within CustomContextItemsProvider', + ); + } + + return context; +} + +/** + * Hook to register custom context menu items for the ResourceContextMenu. + * The items will be automatically cleaned up when the component unmounts. + * + * @param items - Array of DropdownItem to add to the context menu + * + * @example + * ```tsx + * useCustomContextItems([ + * { + * id: 'export-pdf', + * label: 'Export as PDF', + * helper: 'Export this document as a PDF file', + * icon: , + * onClick: () => handleExportPDF(), + * }, + * DIVIDER, + * ]); + * ``` + */ +export function useCustomContextItems(items: DropdownItem[]) { + const { registerItems } = useCustomContextItemsContext(); + + useEffect(() => { + const cleanup = registerItems(items); + + return cleanup; + }, [registerItems, items]); +} diff --git a/browser/data-browser/src/components/ResourceContextMenu/index.tsx b/browser/data-browser/src/components/ResourceContextMenu/index.tsx index 0254b216b..a79d46667 100644 --- a/browser/data-browser/src/components/ResourceContextMenu/index.tsx +++ b/browser/data-browser/src/components/ResourceContextMenu/index.tsx @@ -41,6 +41,14 @@ import { addIf } from '../../helpers/addIf'; import { useNavigateWithTransition } from '../../hooks/useNavigateWithTransition'; import { newContextItem, useAISidebar } from '../AI/AISidebarContext'; import { type AIAtomicResourceMessageContext } from '@chunks/AI/types'; +import { useCustomContextItemsContext } from './CustomContextItemsContext'; + +export { + CustomContextItemsProvider, + useCustomContextItems, +} from './CustomContextItemsContext'; + +export { DIVIDER, type DropdownItem } from '../Dropdown'; export const ContextMenuOptions = { View: 'view', @@ -98,6 +106,7 @@ export function ResourceContextMenu({ const canWrite = useCanWrite(resource); const { enableScope } = useQueryScopeHandler(subject); const { setContextItems, isOpen, setIsOpen } = useAISidebar(); + const { items: customItems } = useCustomContextItemsContext(); // Try to not have a useResource hook in here, as that will lead to many costly fetches when the user enters a new subject const addToChat = () => { @@ -130,7 +139,7 @@ export function ResourceContextMenu({ } catch (error) { toast.error(error.message); } - }, [resource, navigate, currentSubject, onAfterDelete]); + }, [resource, navigate, currentSubject, subject, onAfterDelete]); if (subject === undefined) { return null; @@ -242,13 +251,16 @@ export function ResourceContextMenu({ ), ]; + // Add custom items from context (if any) before filtering + const allItems = [...items, ...customItems]; + const filteredItems = showOnly - ? items.filter( + ? allItems.filter( item => !isItem(item) || showOnly.includes(item.id as ContextMenuOptionsUnion), ) - : items; + : allItems; const triggerComp = trigger ?? diff --git a/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx b/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx index 8e303027d..b2ab543bc 100644 --- a/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx +++ b/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx @@ -239,7 +239,7 @@ export const SearchbarInput: React.FC = ({ onClick: onTagClick, resetIndex, usingKeyboard, - } = useSelectedIndex(filteredTagList, onSelect); + } = useSelectedIndex(filteredTagList, onSelect, { key: tagQueryValue }); const handleKeyDown = (e: React.KeyboardEvent) => { if (tagRect) { diff --git a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx index 8b3fc42ee..46bf71a02 100644 --- a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx +++ b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx @@ -1,4 +1,5 @@ import { + core, dataBrowser, useArray, useCanWrite, @@ -58,7 +59,9 @@ export function SideBarDrive({ const navigate = useNavigateWithTransition(); const agentCanWrite = useCanWrite(driveResource); const [currentSubject] = useCurrentSubject(); - const currentResource = useResource(currentSubject); + const currentResource = useResource(currentSubject, { + track: [core.properties.parent], + }); const [ancestry, setAncestry] = useState([]); useEffect(() => { diff --git a/browser/data-browser/src/components/SideBar/index.tsx b/browser/data-browser/src/components/SideBar/index.tsx index 3ac8575a3..2b088d46c 100644 --- a/browser/data-browser/src/components/SideBar/index.tsx +++ b/browser/data-browser/src/components/SideBar/index.tsx @@ -54,7 +54,7 @@ export function SideBar(): JSX.Element { if (!isWideScreen) { setSideBarLocked(false); } - }, [isWideScreen]); + }, [isWideScreen, setSideBarLocked]); const sidebarVisible = sideBarLocked || (hoveringOverSideBar && isWideScreen); @@ -141,6 +141,10 @@ const StyledNav = styled.nav.attrs(p => ({ overflow-y: auto; overflow-x: hidden; padding-bottom: ${p => p.theme.size()}; + + @media print { + display: none; + } `; const MenuWrapper = styled.div` diff --git a/browser/data-browser/src/components/TableEditor/Cell.tsx b/browser/data-browser/src/components/TableEditor/Cell.tsx index 4052d390b..b8d293a52 100644 --- a/browser/data-browser/src/components/TableEditor/Cell.tsx +++ b/browser/data-browser/src/components/TableEditor/Cell.tsx @@ -15,6 +15,9 @@ import { import { FaExpandAlt } from 'react-icons/fa'; import { IconButton } from '../IconButton/IconButton'; import { KeyboardInteraction } from './helpers/keyboardHandlers'; +import { CSSVar } from '@helpers/CSSVar'; + +export const CELL_WIDTH = new CSSVar('table-cell-width'); export enum CellAlign { Start = 'flex-start', @@ -89,8 +92,9 @@ export function Cell({ const shouldEnterEditMode = useCallback( (e: React.MouseEvent) => { - // @ts-ignore - if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') { + const target = e.target as HTMLElement; + + if (target.tagName === 'INPUT' || target.tagName === 'BUTTON') { // If the user clicked on an input don't enter edit mode. (Necessary for normal checkbox behavior) return false; } @@ -103,6 +107,10 @@ export function Cell({ const handleMouseDown = useCallback( (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName === 'BUTTON') { + return; + } + if (disabledKeyboardInteractions.has(KeyboardInteraction.ExitEditMode)) { return; } @@ -226,6 +234,7 @@ export function Cell({ return ( ` +export const CellWrapper = styled.div.attrs(p => ({ + style: { + [CELL_WIDTH.raw]: `var(--cell-width-${p.index})`, + } as Record, +}))` background-color: ${p => p.disabled ? p.theme.colors.bg1 : p.theme.colors.bg}; cursor: ${p => (p.disabled ? 'not-allowed' : 'pointer')}; diff --git a/browser/data-browser/src/components/TableEditor/TableEditor.tsx b/browser/data-browser/src/components/TableEditor/TableEditor.tsx index 9d7e4692f..20f076616 100644 --- a/browser/data-browser/src/components/TableEditor/TableEditor.tsx +++ b/browser/data-browser/src/components/TableEditor/TableEditor.tsx @@ -58,6 +58,7 @@ interface FancyTableProps { onRowExpand?: (index: number) => void; HeadingComponent: TableHeadingComponent; NewColumnButtonComponent: React.ComponentType; + ref?: React.RefObject; } interface RowProps { @@ -101,7 +102,6 @@ function FancyTableInner({ const ariaUsageId = useId(); const scrollerRef = useRef(null); const headerRef = useRef(null); - const { listRef, tableRef, @@ -109,6 +109,7 @@ function FancyTableInner({ disabledKeyboardInteractions, readOnly, } = useTableEditorContext(); + const [onScroll, setOnScroll] = useState(() => undefined); const { templateColumns, contentRowWidth, resizeCell } = useCellSizes( @@ -210,6 +211,7 @@ function FancyTableInner({ tabIndex={0} onKeyDown={handleKeyDown} totalContentHeight={itemCount * rowHeight!} + columnSizes={columnSizes ?? []} ref={tableRef} > @@ -240,6 +242,7 @@ function FancyTableInner({ interface TableProps { gridTemplateColumns: string; + columnSizes: number[]; contentRowWidth: string; rowHeight: number; totalContentHeight: number; @@ -249,6 +252,13 @@ const Table = styled.div.attrs(p => ({ style: { '--table-template-columns': p.gridTemplateColumns, '--table-content-width': p.contentRowWidth, + ...p.columnSizes.reduce( + (acc, size, i) => ({ + ...acc, + [`--cell-width-${i + 1}`]: `${size}px`, + }), + {}, + ), } as Record, }))` --table-height: 80vh; diff --git a/browser/data-browser/src/components/Tag/TagSelectPopover.tsx b/browser/data-browser/src/components/Tag/TagSelectPopover.tsx index 83b57e4cf..da84af237 100644 --- a/browser/data-browser/src/components/Tag/TagSelectPopover.tsx +++ b/browser/data-browser/src/components/Tag/TagSelectPopover.tsx @@ -41,13 +41,25 @@ export const TagSelectPopover: React.FC = ({ .filter(tag => tag.title.includes(filterValue)) .map(t => t.subject); + const modifyTags = (add: boolean, tag: string) => { + if (add) { + setSelectedTags([...selectedTags, tag]); + } else if (selectedTags.includes(tag)) { + setSelectedTags(selectedTags.filter(t => t !== tag)); + } + }; + const { selectedIndex, onKeyDown, onMouseOver, resetIndex, usingKeyboard } = - useSelectedIndex(filteredTags, index => { - if (index !== undefined) { - const tag = filteredTags[index]; - modifyTags(!selectedTags.includes(tag), tag); - } - }); + useSelectedIndex( + filteredTags, + index => { + if (index !== undefined) { + const tag = filteredTags[index]; + modifyTags(!selectedTags.includes(tag), tag); + } + }, + { key: filterValue }, + ); const handleNewTag = async (tag: Resource) => { try { @@ -64,14 +76,6 @@ export const TagSelectPopover: React.FC = ({ setFilterValue(''); }; - const modifyTags = (add: boolean, tag: string) => { - if (add) { - setSelectedTags([...selectedTags, tag]); - } else if (selectedTags.includes(tag)) { - setSelectedTags(selectedTags.filter(t => t !== tag)); - } - }; - return ( {error.message}} - {isUploading ? 'Uploading...' : text ?? defaultText} + {isUploading ? 'Uploading...' : (text ?? defaultText)} diff --git a/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx b/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx index b73162173..aa119cb07 100644 --- a/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx +++ b/browser/data-browser/src/components/forms/FilePicker/FilePickerDialog.tsx @@ -15,13 +15,13 @@ import { Button } from '../../Button'; import { Row } from '../../Row'; import { useSettings } from '../../../helpers/AppSettings'; import { useMediaQuery } from '../../../hooks/useMediaQuery'; +import { useOnValueChange } from '@helpers/useOnValueChange'; interface FilePickerProps { show: boolean; onShowChange?: (show: boolean) => void; onResourcePicked: (subject: string) => void; - onNewFilePicked: (file: File) => void; - noUpload?: boolean; + onNewFilePicked?: (file: File) => void; allowedMimes?: Set; } @@ -31,7 +31,6 @@ export function FilePickerDialog({ onNewFilePicked, onResourcePicked, allowedMimes, - noUpload = false, }: FilePickerProps): React.JSX.Element { const { drive } = useSettings(); const [dialogProps, showDialog, closeDialog] = useDialog({ @@ -64,7 +63,7 @@ export function FilePickerDialog({ const handleFileInputChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (file) { + if (file && onNewFilePicked) { onNewFilePicked(file); closeDialog(true); } @@ -80,10 +79,11 @@ export function FilePickerDialog({ } }; + useOnValueChange(() => updateQuery(''), [show]); + useEffect(() => { if (show) { showDialog(); - updateQuery(''); } }, [show, showDialog]); @@ -102,7 +102,7 @@ export function FilePickerDialog({ onChange={e => updateQuery(e.target.value)} /> - {!noUpload && ( + {!!onNewFilePicked && ( + + )} +
+ {elements.map(subject => ( + + ))} +
+ ); } -type DocumentSubPageProps = { - resource: Resource; - setEditMode: (arg: boolean) => void; -}; - -function DocumentPageEdit({ - resource, - setEditMode, -}: DocumentSubPageProps): JSX.Element { - const [elements, setElements] = useArray( - resource, - dataBrowser.properties.elements, - { commit: false, validate: false, commitDebounce: 0 }, - ); - - const titleRef = useRef(null); - const store = useStore(); - const ref = useRef(null); - const [err, setErr] = useState(undefined); - const [current, setCurrent] = useState(0); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); - - const focusElement = (goto: number) => { - if (goto > elements.length - 1) { - goto = elements.length - 1; - } else if (goto < 0) { - goto = 0; - } - - setCurrent(goto); - let found: HTMLInputElement | undefined = ref?.current?.children[ - goto - ]?.getElementsByClassName('element')[0] as HTMLInputElement; - - if (!found) { - found = ref?.current?.children[goto] as HTMLInputElement; - } - - if (found) { - found.focus(); - } else { - ref.current?.focus(); - } - }; - - const moveElement = (from: number, to: number) => { - const element = elements[from]; - setElements(elements.toSpliced(from, 1).toSpliced(to, 0, element)); - focusElement(to); - resource.save(); - }; - - /** Creates a new Element at the given position, with the Document as its parent */ - const addElement = async (position: number) => { - // When an element is created, it should be a Resource that has this document as its parent. - // or maybe a nested resource? - const elementSubject = store.createSubject(resource.subject); - const newElements = [...elements]; - newElements.splice(position, 0, elementSubject); - - try { - const newElement = await store.newResource({ - subject: elementSubject, - isA: dataBrowser.classes.paragraph, - parent: resource.subject, - propVals: { - [core.properties.description]: '', - }, - }); - - await setElements(newElements); - focusElement(position); - await newElement.save(); - await resource.save(); - } catch (e) { - setErr(e); - } - }; - - // On init, focus on the last element - useEffect(() => { - setCurrent(elements.length - 1); - - if (elements === undefined) { - setElements([]); - } - }, []); - - // Always have one element - useEffect(() => { - if (elements.length === 0) { - addElement(0); - } - }, [JSON.stringify(elements)]); - - useHotkeys( - 'enter', - e => { - e.preventDefault(); - addElement(current + 1); - }, - { enableOnTags: ['TEXTAREA'] }, - [current], - ); - - /** Move from title to first element */ - useHotkeys( - 'enter', - e => { - e.preventDefault(); - addElement(0); - focusElement(0); - }, - { enableOnTags: ['INPUT'] }, - [addElement, focusElement], - ); - - useHotkeys( - 'up', - e => { - e.preventDefault(); - - if (!current || current === 0) { - titleRef.current?.focus(); - } else { - focusElement(current - 1); - } - }, - { enableOnTags: ['TEXTAREA'] }, - [current], - ); - - useHotkeys( - 'down', - e => { - e.preventDefault(); - - if (document.activeElement === titleRef.current) { - focusElement(0); - } else { - focusElement(current + 1); - } - }, - { enableOnTags: ['TEXTAREA', 'INPUT'] }, - [current], - ); - - // Move current element up - useHotkeys( - shortcuts.moveLineUp, - e => { - e.preventDefault(); - moveElement(current, current - 1); - }, - { enableOnTags: ['TEXTAREA'] }, - [current], - ); - - // Move element down - useHotkeys( - shortcuts.moveLineDown, - e => { - e.preventDefault(); - moveElement(current, current + 1); - }, - { enableOnTags: ['TEXTAREA'] }, - [current], - ); - - // Lose focus - useHotkeys( - 'esc', - e => { - e.preventDefault(); - setCurrent(-1); - }, - { enableOnTags: ['TEXTAREA'] }, - ); - - async function deleteElement(number: number) { - if (elements.length === 1) { - setElements([]); - focusElement(0); - resource.save(); - - return; - } - - setElements(elements.toSpliced(number, 1)); - focusElement(number - 1); - resource.save(); - } - - /** Sets the subject for a specific element and moves to the next element */ - async function setElement(index: number, subject: string) { - setElements(elements.with(index, subject)); - - if (index === elements.length - 1) { - addElement(index + 1); - } else { - focusElement(index + 1); - resource.save(); - } - } - - function handleSortEnd(event: DragEndEvent): void { - const { active, over } = event; - - if (active.id !== over?.id) { - const oldIndex = elements.indexOf(active.id.toString()); - - if (!over?.id) { - return; - } - - const newIndex = elements.indexOf(over.id.toString()); - moveElement(oldIndex, newIndex); - } - } - - /** Create elements for every new File resource */ - function handleUploadedFiles(fileSubjects: string[]) { - toast.success('Upload succeeded!'); - fileSubjects.map(subject => elements.push(subject)); - setElements([...elements]); - resource.save(); - } - - /** Add a new line, or move to the last line if it is empty */ - async function handleNewLineMaybe() { - const lastSubject = elements[elements.length - 1]; - - if (!lastSubject) { - addElement(elements.length); - - return; - } - - const lastElem = await store.getResource(lastSubject); - const description = lastElem.get(core.properties.description); - - if (description === undefined || description.length === 0) { - focusElement(elements.length - 1); - } else { - addElement(elements.length); - } - } - - return ( - - - - - - - {err?.message && {err.message}} - -
- - - {elements.map((elementSubject, index) => ( - - ))} - - - -
-
-
- ); -} - -function DocumentPageShow({ - resource, - setEditMode, -}: DocumentSubPageProps): JSX.Element { - const [elements] = useArray(resource, dataBrowser.properties.elements); - const canWrite = useCanWrite(resource); - - return ( - - -

{resource.title}

- {canWrite && ( - - )} -
- -
- {elements.map(subject => ( - - ))} -
-
- ); -} - -interface SortableElementProps extends ElementEditPropsBase { - subject: string; - index?: number; - active: boolean; -} - -function SortableElement(props: SortableElementProps) { - const { subject, active } = props; - - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id: subject }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - return ( - - - - - ); -} - const DocumentContainer = styled.div` width: min(100%, ${p => p.theme.containerWidth}rem); margin: auto; @@ -432,35 +67,10 @@ const DocumentContainer = styled.div` flex-direction: column; padding: 2rem; @media (max-width: ${props => props.theme.containerWidth}rem) { - padding: ${p => p.theme.margin}rem; + padding: ${p => p.theme.size()}; } `; -const NewLine = styled.div` - height: 20rem; - flex: 1; - cursor: text; -`; - -const SortableItemWrapper = styled.div` - display: flex; - flex-direction: row; - position: relative; -`; - -const GripItem = (props: GripItemProps) => { - return ( - - - - ); -}; - -interface GripItemProps { - /** The element is currently selected */ - active: boolean; -} - const FullPageWrapper = styled.div` background-color: ${p => p.theme.colors.bg}; display: flex; @@ -470,30 +80,12 @@ const FullPageWrapper = styled.div` box-sizing: border-box; `; -const SortHandleStyled = styled.div` - width: 1rem; - flex: 1; - display: flex; - align-items: center; - opacity: ${p => (p.active ? 0.3 : 0)}; - position: absolute; - left: -1rem; - bottom: 0; - height: 100%; - /* TODO fix cursor while dragging */ - cursor: grab; - border: solid 1px transparent; +const UpgradeMessage = styled(Column)` + background-color: ${p => p.theme.colors.mainSelectedBg}; + border: 1px solid ${p => p.theme.colors.mainSelectedFg}; + color: ${p => p.theme.colors.mainSelectedFg}; + padding: ${p => p.theme.size()}; border-radius: ${p => p.theme.radius}; - - &:drop(active), - &:focus, - &:active { - opacity: 0.5; - } - - &:hover { - opacity: 0.5; - } `; export default DocumentPage; diff --git a/browser/data-browser/src/views/Element.tsx b/browser/data-browser/src/views/Element.tsx index 7a1bda686..f580eada7 100644 --- a/browser/data-browser/src/views/Element.tsx +++ b/browser/data-browser/src/views/Element.tsx @@ -1,240 +1,28 @@ -import * as React from 'react'; -import { useState, type JSX } from 'react'; -import { - properties, - classes, - useArray, - useCanWrite, - useResource, - useServerSearch, - useString, -} from '@tomic/react'; +import { type JSX } from 'react'; +import { useResource, dataBrowser, core } from '@tomic/react'; import { styled, css } from 'styled-components'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { ResourceInline } from './ResourceInline'; import Markdown from '../components/datatypes/Markdown'; import ResourceCard from './Card/ResourceCard'; -import { shortcuts } from '../components/HotKeyWrapper'; -import { ErrorLook } from '../components/ErrorLook'; interface ElementShowProps { subject: string; } -/** Shared between all elements */ -export interface ElementEditPropsBase { - /** Removes element from the Array */ - deleteElement: (i: number) => void; - /** Position of the active Element */ - current?: number; - /** Sets the position of the active Element */ - setCurrent: (i: number) => void; - /** Changes the subject of a specific item in the array */ - setElementSubject: (i: number, subject: string) => void; - /** Show a drag icon */ - canDrag: boolean; -} - -interface ElementEditProps extends ElementEditPropsBase { - subject: string; - /** Position in the array of Elements */ - index?: number; - active: boolean; -} - -const searchChar = '/'; -const helpChar = '?'; -const linkChar = '['; -const headerChar = '#'; - -/** An element is a section inside document, such as a Paragraph, Header or Image */ -export function ElementEdit({ - subject, - deleteElement, - index, - setCurrent, - setElementSubject: setElement, - active, - canDrag, -}: ElementEditProps): JSX.Element { - const resource = useResource(subject, { - // Prevents a race condition, see https://github.com/atomicdata-dev/atomic-data-browser/issues/189 - newResource: true, - }); - const [err, setErr] = useState(undefined); - const [text, setText] = useString(resource, properties.description, { - commit: true, - handleValidationError: setErr, - validate: false, - }); - const [klass] = useArray(resource, properties.isA); - const ref = React.useRef(null); - const canWrite = useCanWrite(resource); - - /** If it is not a text element */ - const isAResource = - klass.length > 0 && !klass.includes(classes.elements.paragraph); - - function handleOnChange(e: React.ChangeEvent) { - handleResize(); - setErr(undefined); - setText(e.target.value); - } - - /** Let the textarea grow */ - function handleResize() { - if (ref.current?.style) { - ref.current.style.height = '0'; - ref.current.style.height = ref.current.scrollHeight + 'px'; - } - } - - /** Resize the text area when the text changes, or it is set to active */ - React.useEffect((): void => { - handleResize(); - }, [ref, text, active]); - - /** Auto focus on select, move cursor to end */ - React.useEffect(() => { - ref?.current?.focus(); - - if (text) { - ref?.current?.setSelectionRange(text?.length, text?.length); - } - }, [active]); - - /** Delete this element */ - useHotkeys( - 'backspace', - e => { - const isEmpty = text === '' || text === undefined; - - if ((active && isEmpty) || (active && isAResource)) { - e.preventDefault(); - deleteElement(index!); - } - }, - // no keybaord events captured by ContentEditable - { - enableOnTags: ['TEXTAREA'], - enabled: active, - }, - [index, text, active], - ); - - useHotkeys( - shortcuts.deleteLine, - e => { - if (active) { - e.preventDefault(); - deleteElement(index!); - } - }, - { - enableOnTags: ['TEXTAREA'], - enabled: active, - }, - [index, active], - ); - - function Err() { - if (err?.message) { - return {err.message}; - } else if (active && !canWrite) { - return Agent does not have edit rights; - } else { - return null; - } - } - - if (isAResource) { - return ( - setCurrent(index!)} - onBlur={() => setCurrent(-1)} - > - - - - ); - } +export function ElementShow({ subject }: ElementShowProps): JSX.Element { + const resource = useResource(subject); - if (!active) { + if (resource.hasClasses(dataBrowser.classes.paragraph)) { return ( - setCurrent(index!)} - onBlur={() => setCurrent(-1)} - > - - + + ); } - return ( - index && setCurrent(index)} - > - setCurrent(index!)} - onBlur={() => setCurrent(-1)} - placeholder={`type something (try ${helpChar} or ${searchChar})`} - // Not working, I think - autoFocus={active} - value={text ? text : ''} - /> - {text?.startsWith(searchChar) && ( - index && setElement(index, s)} - /> - )} - {text?.startsWith(helpChar) && ( - index && setElement(index, s)} - /> - )} - {text?.startsWith(linkChar) && ( - -

[link text](https://example.com)

-
- )} - {text?.startsWith(headerChar) && ( - -

# Big Header

-

## Header

-

### Smaller Header

-
- )} - -
- ); -} - -export function ElementShow({ subject }: ElementShowProps): JSX.Element { - const resource = useResource(subject); - const [text] = useString(resource, properties.description); - return ( - + ); } @@ -279,137 +67,3 @@ interface ElementViewProps { active?: boolean; canDrag?: boolean; } - -const ElementView = styled.textarea` - ${ElementTextStyle} - border: none; - width: 100%; - resize: none; - background-color: ${p => p.theme.colors.bg}; - color: ${p => p.theme.colors.text}; - padding: 0; - margin-bottom: 0.5rem; - &:focus { - outline: none; - ${ElementFocusStyle} - } -`; - -interface WidgetProps { - // Input without the matched string / character - query: string; - setElement: (subject: string) => void; -} - -/** Allows the user to search for Resources and include these as an Element. */ -function SearchWidget({ query, setElement }: WidgetProps) { - const { results } = useServerSearch(query); - // The currently selected result - const [index, setIndex] = useState(0); - - useHotkeys( - 'tab,enter', - e => { - e.preventDefault(); - - if (results[index]) { - setElement(results[index]); - } - }, - { enableOnTags: ['TEXTAREA'] }, - [], - ); - - useHotkeys( - 'left', - e => { - e.preventDefault(); - let next = index - 1; - - if (next < 0) { - next = results.length - 1; - } - - setIndex(index - 1); - }, - { enableOnTags: ['TEXTAREA'] }, - [index], - ); - - useHotkeys( - 'right', - e => { - e.preventDefault(); - let next = index + 1; - - if (next > results.length - 1) { - next = 0; - } - - setIndex(index + 1); - }, - { enableOnTags: ['TEXTAREA'] }, - [index], - ); - - if (query === '') { - return ( - -

Search something...

-
- ); - } - - if (results.length === 0) { - return ( - -

No results

-
- ); - } - - return ( - -

(press tab to select, left / right to browse)

-

- -

-
- ); -} - -const WidgetWrapper = styled.div` - position: absolute; - top: 100%; - right: 0; - left: -1rem; - border-radius: ${p => p.theme.radius}; - border: solid 1px ${p => p.theme.colors.bg2}; - padding: ${p => p.theme.margin}rem; - padding-bottom: 0; - background-color: ${p => p.theme.colors.bg1}; - backdrop-filter: blur(6px); - opacity: 0.9; - z-index: 1; -`; - -function HelperWidget({ query }: WidgetProps) { - return ( - - {query && } -

Try typing these:

-

- {'links: '} - [clickable link](https://example.com) -

-

- {'styling:'} - **bold** and _cursive_ -

-

- {'headings:'} - ## Header -

-
- ); -} diff --git a/browser/data-browser/src/views/FolderPage/FolderDisplayStyle.ts b/browser/data-browser/src/views/FolderPage/FolderDisplayStyle.ts index a6feeac3c..c786ce031 100644 --- a/browser/data-browser/src/views/FolderPage/FolderDisplayStyle.ts +++ b/browser/data-browser/src/views/FolderPage/FolderDisplayStyle.ts @@ -4,4 +4,5 @@ export interface ViewProps { subResources: Map; onNewClick: () => void; showNewButton: boolean; + basic?: boolean; } diff --git a/browser/data-browser/src/views/FolderPage/ListView.tsx b/browser/data-browser/src/views/FolderPage/ListView.tsx index 65569b7d9..e0d5907ec 100644 --- a/browser/data-browser/src/views/FolderPage/ListView.tsx +++ b/browser/data-browser/src/views/FolderPage/ListView.tsx @@ -1,5 +1,6 @@ import { - properties, + commits, + core, Resource, useResource, useString, @@ -20,6 +21,7 @@ export function ListView({ subResources, onNewClick, showNewButton, + basic, }: ViewProps): JSX.Element { return ( @@ -31,7 +33,7 @@ export function ListView({ Title Class - Last Modified + {!basic && Last Modified} @@ -43,9 +45,11 @@ export function ListView({ - - - + {!basic && ( + + + + )}
))} @@ -68,7 +72,7 @@ interface CellProps { function Title({ resource }: CellProps): JSX.Element { const [title] = useTitle(resource); - const [classType] = useString(resource, properties.isA); + const [classType] = useString(resource, core.properties.isA); const Icon = getIconForClass(classType ?? ''); return ( @@ -82,7 +86,7 @@ function Title({ resource }: CellProps): JSX.Element { } function LastCommit({ resource }: CellProps): JSX.Element { - const [commit] = useString(resource, properties.commit.lastCommit); + const [commit] = useString(resource, commits.properties.lastCommit); return ( @@ -92,7 +96,7 @@ function LastCommit({ resource }: CellProps): JSX.Element { } function ClassType({ resource }: CellProps): JSX.Element { - const [classType] = useString(resource, properties.isA); + const [classType] = useString(resource, core.properties.isA); const classTypeResource = useResource(classType); const [title] = useTitle(classTypeResource); @@ -111,8 +115,8 @@ const Wrapper = styled.div` --icon-width: 1rem; --icon-title-spacing: 1rem; --cell-padding: 0.4rem; - width: var(--container-width); - margin-inline: auto; + /* width: var(--container-width); */ + /* margin-inline: auto; */ `; const StyledTable = styled.table` diff --git a/browser/data-browser/wuchale.config.js b/browser/data-browser/wuchale.config.js index 64d9ea751..a4e4c7556 100644 --- a/browser/data-browser/wuchale.config.js +++ b/browser/data-browser/wuchale.config.js @@ -32,8 +32,13 @@ export default defineConfig({ otherLocales: ['es', 'fr', 'de'], adapters: { main: jsx({ - loaderPath: './src/locales/loader.ts', - heuristic: (msg, details) => { + runtime: { + useReactive: () => ({ init: false, use: false, }), + }, + loader: 'react', + heuristic: ({ msgStr, details }) => { + const [msg] = msgStr; + if (details.scope === 'script') { // Ignore certain functions if (details.call && IGNORED_FUNCTIONS.includes(details.call)) { diff --git a/browser/eslint.config.js b/browser/eslint.config.js index 7d2bb506a..e3c4bb5f8 100644 --- a/browser/eslint.config.js +++ b/browser/eslint.config.js @@ -1,5 +1,6 @@ import { defineConfig, + globalIgnores, } from 'eslint/config'; import tseslint from '@typescript-eslint/eslint-plugin'; import tsparser from '@typescript-eslint/parser'; @@ -11,6 +12,10 @@ import js from "@eslint/js"; import globals from 'globals'; export default defineConfig([ + globalIgnores([ + // These files are generated so we can't fix the linting errors in them. + 'data-browser/src/locales/**', + ]), { ...jsxA11Y.flatConfigs.recommended, rules: { @@ -99,5 +104,5 @@ export default defineConfig([ 'react-hooks/exhaustive-deps': 'warn', 'react-hooks/static-components': 'off', } - }, + } ]); diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index 1a6ed4f1d..49d7b65b1 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -132,6 +132,11 @@ export class Resource { return this._subject; } + /** Stable reference to the resource, even when the resource is proxied, for example when using @tomic/react or @tomic/svelte. */ + public get stable(): Resource { + return this.__internalObject; + } + /** A human readable title for the resource, returns first of either: name, shortname, filename or subject */ public get title(): string { return (this.get(core.properties.name) ?? @@ -364,13 +369,43 @@ export class Resource { return res as Resource; } - /** Merges a resource into this resource. If this resource has uncommited changes those changes will be applied on top of the new propvals. */ + /** Merges a resource into this resource. If this resource has uncommited changes those changes will be applied on top of the new propvals. + * Any unsaved changes on the incoming resource will not be merged. + */ public merge(resourceB: Resource): void { if (this.subject !== resourceB.subject) { throw new Error('Cannot merge resources with different subjects'); } - this.propvals = resourceB.getPropVals(); + const remoteProps = resourceB.getPropVals(); + + // Remove any propvals that are not present in the remote resource. + for (const [key] of this.propvals.entries()) { + if (!remoteProps.has(key)) { + this.propvals.delete(key); + } + } + + // Merge the remote propvals into this resource. + for (const [key, value] of remoteProps.entries()) { + // We handle YDoc instances separately because they need to be stable references. + if (YLoader.isLoaded() && isYDoc(value)) { + const Y = YLoader.Y; + const localDoc = this.propvals.get(key) as Y.Doc | undefined; + + if (!localDoc) { + this.setUnsafe(key, value); + } else { + const remoteState = Y.encodeStateAsUpdateV2(value); + Y.applyUpdateV2(localDoc, remoteState); + } + + continue; + } + + this.propvals.set(key, value); + } + this.new = resourceB.new; this.error = resourceB.error; this.commitError = resourceB.commitError; diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 75773c491..acab833c0 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -201,6 +201,9 @@ importers: '@tiptap/extension-placeholder': specifier: ^3.7.2 version: 3.7.2(@tiptap/extensions@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)) + '@tiptap/extension-table': + specifier: ^3.10.5 + version: 3.10.5(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2) '@tiptap/extension-text-align': specifier: ^3.7.2 version: 3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) @@ -238,11 +241,11 @@ importers: specifier: ^4.24.1 version: 4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@wuchale/jsx': - specifier: ^0.7.4 - version: 0.7.4(react@19.2.0) + specifier: ^0.9.4 + version: 0.9.4(react@19.2.0) '@wuchale/vite-plugin': - specifier: ^0.14.6 - version: 0.14.6 + specifier: ^0.15.3 + version: 0.15.3 ai: specifier: ^5.0.29 version: 5.0.29(zod@4.1.5) @@ -322,8 +325,8 @@ importers: specifier: ^0.8.10 version: 0.8.10(@tiptap/core@3.7.2(@tiptap/pm@3.7.2)) wuchale: - specifier: ^0.16.5 - version: 0.16.5 + specifier: ^0.18.3 + version: 0.18.3 y-protocols: specifier: ^1.0.6 version: 1.0.6(yjs@13.6.27) @@ -525,7 +528,7 @@ importers: version: 5.1.4 svelte-check: specifier: ^3.8.6 - version: 3.8.6(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4) + version: 3.8.6(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.5.6)(svelte@5.1.4) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -2175,9 +2178,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.2': - resolution: {integrity: sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -3386,8 +3386,8 @@ packages: '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} - '@sveltejs/acorn-typescript@1.0.5': - resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + '@sveltejs/acorn-typescript@1.0.6': + resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} peerDependencies: acorn: ^8.9.0 @@ -3714,6 +3714,12 @@ packages: peerDependencies: '@tiptap/core': ^3.7.2 + '@tiptap/extension-table@3.10.5': + resolution: {integrity: sha512-kuMgvrZBGsYtTcN8t8dN92xez99OY251Yig2B3/3aOIqapMkAFTJ2tZVPeTSiGSJpo1Tuw6ly2hs4neOOjpzvQ==} + peerDependencies: + '@tiptap/core': ^3.10.5 + '@tiptap/pm': ^3.10.5 + '@tiptap/extension-text-align@3.7.2': resolution: {integrity: sha512-tUdoatcxM8u16tFVfEURFZwmxvZQR33f9VLtkyR+1aXgy0Pi87cNoFC60pTjH7gNtktEuagNfPE00tGMvqIehg==} peerDependencies: @@ -4272,8 +4278,8 @@ packages: '@vitest/utils@2.1.3': resolution: {integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==} - '@wuchale/jsx@0.7.4': - resolution: {integrity: sha512-wyXFYTd8IPs9aQNqPf34mmaMpWAc7i2SKe0CwAMDlKA9ags2SLKdZmvxzzjJjCz5ahiBf9jYnIHWa4npGCpJMA==} + '@wuchale/jsx@0.9.4': + resolution: {integrity: sha512-IWDXB05jWuMbL5qusb+LWFwrq9yQOAOMEE6lXPPENM4ShpQjZnwBhkMg1vqDod0dprtUfZIzuUUnpxU6zMf6Og==} peerDependencies: react: ^19.1.1 solid-js: ^1.9.9 @@ -4283,8 +4289,8 @@ packages: solid-js: optional: true - '@wuchale/vite-plugin@0.14.6': - resolution: {integrity: sha512-/OPj/tK56xco16YscktzKbdc8hy0MtoEw4Bl5ifACxbTIFZ8CwFudiM37PrIwWqPHPlpzlxTjFdeauIu4X5qow==} + '@wuchale/vite-plugin@0.15.3': + resolution: {integrity: sha512-GtPT1gwAGJAb1oc7R5MuGyl1R8m50QdoqFiW/76g2e81d9iPUYNi/CTg8BEeg4WmQJ+91JRR//rwgEo+jG0fZQ==} '@xhmikosr/archive-type@6.0.1': resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==} @@ -6128,14 +6134,6 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -7516,6 +7514,9 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-cancellable-promise@2.0.0: resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==} @@ -8395,6 +8396,9 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8428,10 +8432,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -9043,8 +9043,8 @@ packages: remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-rehype@11.1.1: - resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} @@ -10699,8 +10699,8 @@ packages: utf-8-validate: optional: true - wuchale@0.16.5: - resolution: {integrity: sha512-g/6ZR+gBhzZq227y5MJvhrJkG/wYUfUvUa+zvAo1Q6K9dhiMWSz6YS2p3Aix2uM3sAx0KeCe9y8XAKprXnQiFg==} + wuchale@0.18.3: + resolution: {integrity: sha512-l4JxOjfGLiqY1u33I3TRlvrTGyMZfrXPei73Gw9wgdIGDicMI85eKN+fBNQxlunOTDavxY1M0qQNRBuv7gjmKQ==} hasBin: true xdg-basedir@5.1.0: @@ -12183,6 +12183,11 @@ snapshots: eslint: 9.13.0(jiti@2.3.3) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.13.0(jiti@2.3.3))': + dependencies: + eslint: 9.13.0(jiti@2.3.3) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.0(jiti@2.3.3))': dependencies: eslint: 9.39.0(jiti@2.3.3) @@ -12385,14 +12390,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.2 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.27': dependencies: @@ -12585,7 +12588,7 @@ snapshots: chalk: 5.3.0 clean-stack: 4.2.0 execa: 6.1.0 - fdir: 6.4.4(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) figures: 5.0.0 filter-obj: 5.1.0 got: 12.6.1 @@ -13699,7 +13702,7 @@ snapshots: magic-string: 0.25.9 string.prototype.matchall: 4.0.12 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': dependencies: acorn: 8.15.0 @@ -14003,6 +14006,11 @@ snapshots: dependencies: '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/extension-table@3.10.5(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))(@tiptap/pm@3.7.2)': + dependencies: + '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) + '@tiptap/pm': 3.7.2 + '@tiptap/extension-text-align@3.7.2(@tiptap/core@3.7.2(@tiptap/pm@3.7.2))': dependencies: '@tiptap/core': 3.7.2(@tiptap/pm@3.7.2) @@ -14575,7 +14583,7 @@ snapshots: '@typescript-eslint/utils@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0(jiti@2.3.3)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.13.0(jiti@2.3.3)) '@typescript-eslint/scope-manager': 8.11.0 '@typescript-eslint/types': 8.11.0 '@typescript-eslint/typescript-estree': 8.11.0(typescript@5.6.3) @@ -14694,7 +14702,7 @@ snapshots: dependencies: '@vitest/spy': 2.1.3 estree-walker: 3.0.3 - magic-string: 0.30.12 + magic-string: 0.30.19 optionalDependencies: vite: 5.4.10(@types/node@24.7.0)(terser@5.43.1) @@ -14710,7 +14718,7 @@ snapshots: '@vitest/snapshot@2.1.3': dependencies: '@vitest/pretty-format': 2.1.3 - magic-string: 0.30.12 + magic-string: 0.30.19 pathe: 1.1.2 '@vitest/spy@2.1.3': @@ -14723,17 +14731,17 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 - '@wuchale/jsx@0.7.4(react@19.2.0)': + '@wuchale/jsx@0.9.4(react@19.2.0)': dependencies: - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) acorn: 8.15.0 - wuchale: 0.16.5 + wuchale: 0.18.3 optionalDependencies: react: 19.2.0 - '@wuchale/vite-plugin@0.14.6': + '@wuchale/vite-plugin@0.15.3': dependencies: - wuchale: 0.16.5 + wuchale: 0.18.3 '@xhmikosr/archive-type@6.0.1': dependencies: @@ -16069,8 +16077,8 @@ snapshots: detective-postcss@6.1.3: dependencies: is-url: 1.2.4 - postcss: 8.4.49 - postcss-values-parser: 6.0.2(postcss@8.4.49) + postcss: 8.5.6 + postcss-values-parser: 6.0.2(postcss@8.5.6) detective-sass@5.0.3: dependencies: @@ -16669,15 +16677,15 @@ snapshots: eslint-plugin-svelte@2.46.0(eslint@9.13.0(jiti@2.3.3))(svelte@5.1.4)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0(jiti@2.3.3)) - '@jridgewell/sourcemap-codec': 1.5.2 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.13.0(jiti@2.3.3)) + '@jridgewell/sourcemap-codec': 1.5.5 eslint: 9.13.0(jiti@2.3.3) eslint-compat-utils: 0.5.1(eslint@9.13.0(jiti@2.3.3)) esutils: 2.0.3 known-css-properties: 0.35.0 - postcss: 8.4.47 - postcss-load-config: 3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) - postcss-safe-parser: 6.0.0(postcss@8.4.47) + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) + postcss-safe-parser: 6.0.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 semver: 7.7.2 svelte-eslint-parser: 0.43.0(svelte@5.1.4) @@ -16818,7 +16826,7 @@ snapshots: esrap@1.2.2: dependencies: - '@jridgewell/sourcemap-codec': 1.5.2 + '@jridgewell/sourcemap-codec': 1.5.5 '@types/estree': 1.0.8 esrecurse@4.3.0: @@ -17087,14 +17095,6 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.4(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - - fdir@6.4.4(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -18553,12 +18553,16 @@ snapshots: magic-string@0.30.12: dependencies: - '@jridgewell/sourcemap-codec': 1.5.2 + '@jridgewell/sourcemap-codec': 1.5.5 magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-cancellable-promise@2.0.0: {} make-dir@3.1.0: @@ -19740,6 +19744,8 @@ snapshots: path-to-regexp@8.2.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} path-type@5.0.0: {} @@ -19760,8 +19766,6 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} - picomatch@4.0.3: {} pify@2.3.0: {} @@ -19834,12 +19838,12 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)): + postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.4.47 + postcss: 8.5.6 ts-node: 10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3) postcss-load-config@6.0.1(jiti@2.3.3)(postcss@8.5.6)(yaml@2.6.0): @@ -19850,13 +19854,13 @@ snapshots: postcss: 8.5.6 yaml: 2.6.0 - postcss-safe-parser@6.0.0(postcss@8.4.47): + postcss-safe-parser@6.0.0(postcss@8.5.6): dependencies: - postcss: 8.4.47 + postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.4.47): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.4.47 + postcss: 8.5.6 postcss-selector-parser@6.1.2: dependencies: @@ -19865,11 +19869,11 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss-values-parser@6.0.2(postcss@8.4.49): + postcss-values-parser@6.0.2(postcss@8.5.6): dependencies: color-name: 1.1.4 is-url-superb: 4.0.0 - postcss: 8.4.49 + postcss: 8.5.6 quote-unquote: 1.0.0 postcss@8.4.47: @@ -20244,7 +20248,7 @@ snapshots: mdast-util-to-hast: 13.2.0 react: 19.2.0 remark-parse: 11.0.0 - remark-rehype: 11.1.1 + remark-rehype: 11.1.2 unified: 11.0.5 unist-util-visit: 5.0.0 vfile: 6.0.3 @@ -20468,7 +20472,7 @@ snapshots: transitivePeerDependencies: - supports-color - remark-rehype@11.1.1: + remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -21219,14 +21223,14 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@3.8.6(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4): + svelte-check@3.8.6(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.5.6)(svelte@5.1.4): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 picocolors: 1.1.1 sade: 1.8.1 svelte: 5.1.4 - svelte-preprocess: 5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4)(typescript@5.9.3) + svelte-preprocess: 5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.5.6)(svelte@5.1.4)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - '@babel/core' @@ -21244,23 +21248,23 @@ snapshots: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - postcss: 8.4.47 - postcss-scss: 4.0.9(postcss@8.4.47) + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) optionalDependencies: svelte: 5.1.4 - svelte-preprocess@5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.4.47)(svelte@5.1.4)(typescript@5.9.3): + svelte-preprocess@5.1.4(@babel/core@7.28.4)(postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)))(postcss@8.5.6)(svelte@5.1.4)(typescript@5.9.3): dependencies: '@types/pug': 2.0.10 detect-indent: 6.1.0 - magic-string: 0.30.12 + magic-string: 0.30.19 sorcery: 0.11.1 strip-indent: 3.0.0 svelte: 5.1.4 optionalDependencies: '@babel/core': 7.28.4 - postcss: 8.4.47 - postcss-load-config: 3.1.4(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.7.39)(@types/node@24.7.0)(typescript@5.6.3)) typescript: 5.9.3 svelte2tsx@0.7.22(svelte@5.1.4)(typescript@5.6.3): @@ -21449,8 +21453,8 @@ snapshots: tinyglobby@0.2.9: dependencies: - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 tinypool@1.0.1: {} @@ -22387,12 +22391,13 @@ snapshots: ws@8.17.1: {} - wuchale@0.16.5: + wuchale@0.18.3: dependencies: - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) acorn: 8.15.0 chokidar: 4.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 + path-to-regexp: 8.3.0 picomatch: 4.0.3 pofile: 1.1.4 tinyglobby: 0.2.15 diff --git a/browser/react/src/hooks.ts b/browser/react/src/hooks.ts index 23ce93e2b..528833c82 100644 --- a/browser/react/src/hooks.ts +++ b/browser/react/src/hooks.ts @@ -56,7 +56,7 @@ export function useResource( ); const unsubLoadingChangeRef = useRef( resource.on(ResourceEvents.LoadingChange, () => { - setResource(proxyResource(resource.__internalObject)); + setResource(proxyResource(resource.stable)); }), ); @@ -78,12 +78,12 @@ export function useResource( }, [store, subject, memoizedOpts]); useEffect(() => { - return resource.__internalObject.on(ResourceEvents.LocalChange, prop => { + return resource.stable.on(ResourceEvents.LocalChange, prop => { if (track === undefined || track.includes(prop)) { - setResource(proxyResource(resource.__internalObject)); + setResource(proxyResource(resource.stable)); } }); - }, [resource.__internalObject, track]); + }, [resource.stable, track]); // Update the proxy when the resource is done loading. useEffect(() => { @@ -91,10 +91,10 @@ export function useResource( unsubLoadingChangeRef.current(); } - return resource.__internalObject.on(ResourceEvents.LoadingChange, () => { - setResource(proxyResource(resource.__internalObject)); + return resource.stable.on(ResourceEvents.LoadingChange, () => { + setResource(proxyResource(resource.stable)); }); - }, [resource.__internalObject]); + }, [resource.stable]); return resource; } From fc96f17d61a023aaf46bada84e3ca8dff82d4e4d Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Fri, 14 Nov 2025 10:18:28 +0100 Subject: [PATCH 6/8] Fix linting errors --- .../create-template/src/createOutputFolder.ts | 2 +- browser/create-template/src/index.ts | 1 - browser/data-browser/.prettierignore | 1 + .../src/chunks/CodeEditor/AsyncJSONEditor.tsx | 5 +- .../chunks/CurrencyPicker/CurrencyPicker.tsx | 2 +- .../ResourceExtension/ResourceNode.module.css | 10 +- .../data-browser/src/components/Button.tsx | 3 - .../src/components/Dialog/index.tsx | 4 +- .../src/components/HotKeyWrapper.tsx | 10 +- .../src/components/Navigation.tsx | 2 +- .../src/components/NewInstanceButton/Base.tsx | 2 +- browser/data-browser/src/components/Row.tsx | 2 +- .../data-browser/src/components/Shortcut.tsx | 5 +- .../src/components/SideBar/AppMenu.tsx | 10 +- .../ResourceSideBar/SidebarItemTitle.tsx | 4 +- .../components/TableEditor/TableHeader.tsx | 3 - .../components/TableEditor/TableHeading.tsx | 12 +- .../forms/FilePicker/FilePickerItem.tsx | 8 +- .../src/components/forms/InputNumber.tsx | 2 +- .../src/components/forms/ResourceForm.tsx | 5 +- .../forms/ResourceSelector/DropdownInput.tsx | 14 +- .../components/forms/SearchBox/SearchBox.tsx | 2 +- .../data-browser/src/helpers/AppSettings.tsx | 10 +- browser/data-browser/src/helpers/debounce.ts | 4 +- .../src/helpers/focusOffsetElement.ts | 6 +- .../data-browser/src/helpers/navigation.tsx | 10 +- .../src/helpers/useCurrentSubject.tsx | 5 +- .../data-browser/src/hooks/useMediaQuery.ts | 12 +- .../src/routes/History/HistoryRoute.tsx | 5 +- .../data-browser/src/routes/SettingsAgent.tsx | 9 +- .../views/FolderPage/GridItem/components.tsx | 8 +- .../views/ResourceInline/ResourceInline.tsx | 2 +- browser/eslint.config.js | 5 + browser/pnpm-lock.yaml | 130 +++++++++--------- browser/react/src/components/Image.tsx | 6 +- browser/react/src/helpers/isDev.ts | 2 +- browser/react/src/helpers/useOnValueChange.ts | 10 ++ browser/react/src/hooks.ts | 91 +++++++----- browser/react/src/useCollection.ts | 21 +-- browser/react/src/useMarkdown.ts | 2 +- 40 files changed, 258 insertions(+), 189 deletions(-) create mode 100644 browser/data-browser/.prettierignore create mode 100644 browser/react/src/helpers/useOnValueChange.ts diff --git a/browser/create-template/src/createOutputFolder.ts b/browser/create-template/src/createOutputFolder.ts index 8dc9ee42f..de25f35f2 100644 --- a/browser/create-template/src/createOutputFolder.ts +++ b/browser/create-template/src/createOutputFolder.ts @@ -17,7 +17,7 @@ export async function createOutputFolder(outputDir: string): Promise { try { fs.mkdirSync(outputDir); - } catch (error) { + } catch (e) { console.error(`Failed to create directory: ${outputDir}`); process.exit(1); } diff --git a/browser/create-template/src/index.ts b/browser/create-template/src/index.ts index 2c36d34de..530895208 100644 --- a/browser/create-template/src/index.ts +++ b/browser/create-template/src/index.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* eslint-disable no-console */ import path from 'node:path'; import { parseArgs } from 'node:util'; import { copyTemplate } from './copyTemplate.js'; diff --git a/browser/data-browser/.prettierignore b/browser/data-browser/.prettierignore new file mode 100644 index 000000000..70372308a --- /dev/null +++ b/browser/data-browser/.prettierignore @@ -0,0 +1 @@ +/src/locales/** diff --git a/browser/data-browser/src/chunks/CodeEditor/AsyncJSONEditor.tsx b/browser/data-browser/src/chunks/CodeEditor/AsyncJSONEditor.tsx index 089e0ee05..b24a86200 100644 --- a/browser/data-browser/src/chunks/CodeEditor/AsyncJSONEditor.tsx +++ b/browser/data-browser/src/chunks/CodeEditor/AsyncJSONEditor.tsx @@ -56,7 +56,7 @@ const AsyncJSONEditor: React.FC = ({ ); // Wrap jsonParseLinter so we can tap into diagnostics - const validationLinter = useCallback(() => { + const validationLinter = useMemo(() => { const delegate = jsonParseLinter(); return (view: EditorView) => { @@ -85,8 +85,7 @@ const AsyncJSONEditor: React.FC = ({ }, [onValidationChange, required]); const extensions = useMemo( - // eslint-disable-next-line react-hooks/react-compiler - () => [json(), linter(validationLinter())], + () => [json(), linter(validationLinter)], [validationLinter], ); diff --git a/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx b/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx index 199cf1127..f181ae65e 100644 --- a/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx +++ b/browser/data-browser/src/chunks/CurrencyPicker/CurrencyPicker.tsx @@ -35,7 +35,7 @@ const CurrencyPicker: FC = ({ resource }) => { } // We only want to run this effect once. Maybe we should find a better way to do this. - // eslint-disable-next-line react-hooks/react-compiler, react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css index 70520261a..b46118bcd 100644 --- a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceNode.module.css @@ -8,11 +8,11 @@ &:has(.wideNode) { width: 1100px; - margin-left: -150px; + margin-left: -150px; - @container (max-width: 1100px) { - width: 100%; - margin-left: 0; - } + @container (max-width: 1100px) { + width: 100%; + margin-left: 0; + } } } diff --git a/browser/data-browser/src/components/Button.tsx b/browser/data-browser/src/components/Button.tsx index 3ca411d73..f0381cf7b 100644 --- a/browser/data-browser/src/components/Button.tsx +++ b/browser/data-browser/src/components/Button.tsx @@ -43,7 +43,6 @@ const getButtonComp = ({ clean, icon, subtle, alert }: ButtonProps) => { } if (clean) { - // @ts-ignore Comp = ButtonClean; } @@ -131,7 +130,6 @@ interface ButtonBarProps { } /** Button inside the navigation bar */ -// eslint-disable-next-line prettier/prettier export const ButtonBar = styled(ButtonClean)` padding-right: 0.5rem; padding-left: 0.5rem; @@ -157,7 +155,6 @@ export const ButtonBar = styled(ButtonClean)` `; /** Button with some optional margins around it */ -// eslint-disable-next-line prettier/prettier export const ButtonDefault = styled(ButtonBase)` --button-bg-color: ${p => p.theme.colors.main}; --button-bg-color-hover: ${p => p.theme.colors.mainLight}; diff --git a/browser/data-browser/src/components/Dialog/index.tsx b/browser/data-browser/src/components/Dialog/index.tsx index e84568544..5db470a39 100644 --- a/browser/data-browser/src/components/Dialog/index.tsx +++ b/browser/data-browser/src/components/Dialog/index.tsx @@ -139,15 +139,13 @@ const InnerDialog: React.FC> = ({ if (show) { if (!dialogRef.current.hasAttribute('open')) - // @ts-ignore dialogRef.current.showModal(); } if (dialogRef.current.hasAttribute('data-closing')) { // TODO: Use getAnimations() api to wait for the animations to complete instead of a timeout. return timeoutEffect(() => { - // @ts-ignore - dialogRef.current.close(); + dialogRef.current?.close(); dialogRef.current?.removeAttribute('data-closing'); onClosed(); }, ANIM_MS); diff --git a/browser/data-browser/src/components/HotKeyWrapper.tsx b/browser/data-browser/src/components/HotKeyWrapper.tsx index c6d0dd5d0..28a59475f 100644 --- a/browser/data-browser/src/components/HotKeyWrapper.tsx +++ b/browser/data-browser/src/components/HotKeyWrapper.tsx @@ -74,7 +74,10 @@ function HotKeysWrapper({ children }: Props): JSX.Element { shortcuts.edit, e => { e.preventDefault(); - Client.isValidSubject(subject) && navigate(editURL(subject!)); + + if (Client.isValidSubject(subject)) { + navigate(editURL(subject!)); + } }, {}, [subject], @@ -83,7 +86,10 @@ function HotKeysWrapper({ children }: Props): JSX.Element { shortcuts.data, e => { e.preventDefault(); - Client.isValidSubject(subject) && navigate(dataURL(subject!)); + + if (Client.isValidSubject(subject)) { + navigate(dataURL(subject!)); + } }, {}, [subject], diff --git a/browser/data-browser/src/components/Navigation.tsx b/browser/data-browser/src/components/Navigation.tsx index 31e6df82d..93aa09437 100644 --- a/browser/data-browser/src/components/Navigation.tsx +++ b/browser/data-browser/src/components/Navigation.tsx @@ -87,7 +87,7 @@ function NavBar(): JSX.Element { const isInStandaloneMode = React.useMemo( () => machesStandalone || - //@ts-ignore + // @ts-expect-error standalone is available on the navigator object. window.navigator.standalone || document.referrer.includes('android-app://') || isRunningInTauri(), diff --git a/browser/data-browser/src/components/NewInstanceButton/Base.tsx b/browser/data-browser/src/components/NewInstanceButton/Base.tsx index 918236295..19af55cc8 100644 --- a/browser/data-browser/src/components/NewInstanceButton/Base.tsx +++ b/browser/data-browser/src/components/NewInstanceButton/Base.tsx @@ -59,7 +59,7 @@ export function Base({ {label} ) : ( - label ?? title + (label ?? title) )} {children} diff --git a/browser/data-browser/src/components/Row.tsx b/browser/data-browser/src/components/Row.tsx index c36473c48..10c00de17 100644 --- a/browser/data-browser/src/components/Row.tsx +++ b/browser/data-browser/src/components/Row.tsx @@ -60,7 +60,7 @@ Column.displayName = 'Column'; * This component is only exported so it can be used in css selectors. */ export const Flex = styled.div` - align-items: ${p => (p.center ? 'center' : p.align ?? 'initial')}; + align-items: ${p => (p.center ? 'center' : (p.align ?? 'initial'))}; display: flex; gap: ${p => p.gap ?? `${p.theme.margin}rem`}; justify-content: ${p => p.justify ?? 'start'}; diff --git a/browser/data-browser/src/components/Shortcut.tsx b/browser/data-browser/src/components/Shortcut.tsx index 71f5fdf1e..738207868 100644 --- a/browser/data-browser/src/components/Shortcut.tsx +++ b/browser/data-browser/src/components/Shortcut.tsx @@ -31,7 +31,8 @@ const KBD = styled.kbd` background-color: ${p => p.theme.colors.bg1}; text-transform: capitalize; border-radius: 5px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI Adjusted', - 'Segoe UI', 'Liberation Sans', sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI Adjusted', 'Segoe UI', + 'Liberation Sans', sans-serif; padding: 0.3em; `; diff --git a/browser/data-browser/src/components/SideBar/AppMenu.tsx b/browser/data-browser/src/components/SideBar/AppMenu.tsx index 5c704e7f2..bbbac2f6a 100644 --- a/browser/data-browser/src/components/SideBar/AppMenu.tsx +++ b/browser/data-browser/src/components/SideBar/AppMenu.tsx @@ -44,19 +44,17 @@ export function AppMenu({ onItemClick }: AppMenuProps): JSX.Element { setShowInstallButton(false); } }); - }, [event.current]); + }, []); useEffect(() => { - const listener = (e: BeforeInstallPromptEvent) => { + const listener = (e: Event) => { e.preventDefault(); setShowInstallButton(true); - event.current = e; + event.current = e as unknown as BeforeInstallPromptEvent; }; - //@ts-ignore window.addEventListener('beforeinstallprompt', listener); - //@ts-ignore return () => window.removeEventListener('beforeinstallprompt', listener); }, []); @@ -66,7 +64,7 @@ export function AppMenu({ onItemClick }: AppMenuProps): JSX.Element { icon={} label={ agent - ? agentResource.get(core.properties.name) ?? 'User Settings' + ? (agentResource.get(core.properties.name) ?? 'User Settings') : 'Login' } helper='See and edit the current Agent / User (u)' diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx index 4feae1b4f..9dc63b919 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx @@ -123,8 +123,8 @@ const StyledIconButton = styled(IconButton)` const ActionWrapper = styled.div<{ isDragging?: boolean }>` --aw-box-shadow-start: 0 0 0 0px rgba(0, 0, 0, 0.1); - --aw-box-shadow-end: 0 0 0 1px ${p => p.theme.colors.main}, - ${p => p.theme.boxShadowSoft}; + --aw-box-shadow-end: + 0 0 0 1px ${p => p.theme.colors.main}, ${p => p.theme.boxShadowSoft}; display: flex; width: 100%; diff --git a/browser/data-browser/src/components/TableEditor/TableHeader.tsx b/browser/data-browser/src/components/TableEditor/TableHeader.tsx index aecac5774..c26239aef 100644 --- a/browser/data-browser/src/components/TableEditor/TableHeader.tsx +++ b/browser/data-browser/src/components/TableEditor/TableHeader.tsx @@ -54,9 +54,6 @@ export function TableHeader({ (event: DragStartEvent) => { const key = columns.map(columnToKey).indexOf(event.active.id as string); setActiveIndex(key); - - // Bug in react-compiler linter - // eslint-disable-next-line react-hooks/react-compiler document.body.style.cursor = 'grabbing'; }, [columns, columnToKey], diff --git a/browser/data-browser/src/components/TableEditor/TableHeading.tsx b/browser/data-browser/src/components/TableEditor/TableHeading.tsx index c625cdf85..b56e9ba60 100644 --- a/browser/data-browser/src/components/TableEditor/TableHeading.tsx +++ b/browser/data-browser/src/components/TableEditor/TableHeading.tsx @@ -51,11 +51,13 @@ export function TableHeading({ setIsDragging(isDragging); }, [isDragging]); - const setRef = useCallback((node: HTMLDivElement) => { - setNodeRef(node); - // @ts-ignore - targetRef.current = node; - }, []); + const setRef = useCallback( + (node: HTMLDivElement) => { + setNodeRef(node); + targetRef.current = node; + }, + [setNodeRef], + ); return ( p.theme.colors.main}; --card-banner-height: 0px; display: flex; diff --git a/browser/data-browser/src/components/forms/InputNumber.tsx b/browser/data-browser/src/components/forms/InputNumber.tsx index ba12932f4..cc840f168 100644 --- a/browser/data-browser/src/components/forms/InputNumber.tsx +++ b/browser/data-browser/src/components/forms/InputNumber.tsx @@ -39,7 +39,7 @@ export default function InputNumber({ const newVal = +e.target.value; validateDatatype(newVal, property.datatype); setValue(newVal); - } catch (er) { + } catch (_err) { setError('Invalid Number'); } } diff --git a/browser/data-browser/src/components/forms/ResourceForm.tsx b/browser/data-browser/src/components/forms/ResourceForm.tsx index ce53fa5cd..5916a7639 100644 --- a/browser/data-browser/src/components/forms/ResourceForm.tsx +++ b/browser/data-browser/src/components/forms/ResourceForm.tsx @@ -95,7 +95,10 @@ export function ResourceForm({ const onSaveSuccess = useCallback(() => { // We need to read the earlier .new state, because the resource is no // longer new after it was saved, during this callback - wasNew && store.notifyResourceManuallyCreated(resource); + if (wasNew) { + store.notifyResourceManuallyCreated(resource); + } + onSave?.(); navigate(constructOpenURL(resource.subject)); }, [resource, store, wasNew, onSave, navigate]); diff --git a/browser/data-browser/src/components/forms/ResourceSelector/DropdownInput.tsx b/browser/data-browser/src/components/forms/ResourceSelector/DropdownInput.tsx index f049017af..3d5e8b569 100644 --- a/browser/data-browser/src/components/forms/ResourceSelector/DropdownInput.tsx +++ b/browser/data-browser/src/components/forms/ResourceSelector/DropdownInput.tsx @@ -52,9 +52,8 @@ type CreateOption = { type: 'createOption'; }; -function isCreateOption(option: unknown): option is CreateOption { - // @ts-ignore - return option?.type === 'createOption'; +function isCreateOption(option: Hit | CreateOption): option is CreateOption { + return 'type' in option && option?.type === 'createOption'; } // TODO: Component is still used in collection page because we want to show a list of properties there even if the user is not searching anything. We should add predetermined options to Searchbox instead. @@ -134,7 +133,11 @@ export const DropdownInput: React.FC = ({ (e: React.ChangeEvent) => { const val = e.target.value; setInputValue(val); - onInputChange && onInputChange(val); + + if (onInputChange) { + onInputChange(val); + } + setUseKeys(true); setIsFocus(true); setIsOpen(true); @@ -241,6 +244,7 @@ export const DropdownInput: React.FC = ({ )} + {/* eslint-disable-next-line react-hooks/refs */}
setUseKeys(false)}> @@ -327,7 +331,7 @@ function DropDownItemsMenu({ const item = items[selectedIndex]; if (isCreateOption(item)) { - onCreateClick && onCreateClick(); + onCreateClick?.(); return; } diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx index b89b8d78d..2944f3ea0 100644 --- a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx +++ b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx @@ -116,7 +116,7 @@ export function SearchBox({ handleExit(false); removeCachedSearchResults(store); }, - [inputValue, onChange, handleExit, store], + [onChange, handleExit, store], ); const handleTriggerFocus = () => { diff --git a/browser/data-browser/src/helpers/AppSettings.tsx b/browser/data-browser/src/helpers/AppSettings.tsx index a1cfa107c..902285117 100644 --- a/browser/data-browser/src/helpers/AppSettings.tsx +++ b/browser/data-browser/src/helpers/AppSettings.tsx @@ -66,8 +66,14 @@ export const AppSettingsContextProvider = ( (newAgent: Agent | undefined) => { try { setAgent(newAgent); - newAgent?.subject && toast.success('Signed in!'); - newAgent === undefined && toast.success('Signed out.'); + + if (newAgent?.subject) { + toast.success('Signed in!'); + } + + if (newAgent === undefined) { + toast.success('Signed out.'); + } } catch (e) { errorHandler(new Error('Agent setting failed: ' + e.message)); } diff --git a/browser/data-browser/src/helpers/debounce.ts b/browser/data-browser/src/helpers/debounce.ts index ebec61fa3..ccb3a0f7f 100644 --- a/browser/data-browser/src/helpers/debounce.ts +++ b/browser/data-browser/src/helpers/debounce.ts @@ -1,4 +1,4 @@ -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const debounceMap = new Map(); /** @@ -9,7 +9,7 @@ const debounceMap = new Map(); * @param fn The function to debounce * @param delay The delay in milliseconds (defaults to 500) */ -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function debounce(fn: T, delay = 500): T { const debouncedFn = (...args: unknown[]) => { const debounceId = debounceMap.get(fn); diff --git a/browser/data-browser/src/helpers/focusOffsetElement.ts b/browser/data-browser/src/helpers/focusOffsetElement.ts index 2758b9102..0b099fefb 100644 --- a/browser/data-browser/src/helpers/focusOffsetElement.ts +++ b/browser/data-browser/src/helpers/focusOffsetElement.ts @@ -18,9 +18,9 @@ export function focusOffsetElement(offset: number, origin?: Element) { document.querySelectorAll(QUERY).forEach(element => { //check for visibility while always include the current activeElement if ( - // @ts-ignore + // @ts-expect-error All elements have this. element.offsetWidth > 0 || - // @ts-ignore + // @ts-expect-error All elements have this. element.offsetHeight > 0 || element === startElement ) { @@ -35,7 +35,7 @@ export function focusOffsetElement(offset: number, origin?: Element) { focussable[loopingIndex(index + offset, focussable.length)] || focussable[0]; - // @ts-ignore + // @ts-expect-error All elements have this. nextElement.focus(); } } diff --git a/browser/data-browser/src/helpers/navigation.tsx b/browser/data-browser/src/helpers/navigation.tsx index ba7e6ecea..a79798e67 100644 --- a/browser/data-browser/src/helpers/navigation.tsx +++ b/browser/data-browser/src/helpers/navigation.tsx @@ -58,8 +58,14 @@ export function newURL( const navTo = new URL(location.origin); navTo.pathname = paths.new; navTo.searchParams.append(newURLParams.classSubject, classUrl); - parentURL && navTo.searchParams.append(newURLParams.parent, parentURL); - subject && navTo.searchParams.append(newURLParams.newSubject, subject); + + if (parentURL) { + navTo.searchParams.append(newURLParams.parent, parentURL); + } + + if (subject) { + navTo.searchParams.append(newURLParams.newSubject, subject); + } return paths.new + navTo.search; } diff --git a/browser/data-browser/src/helpers/useCurrentSubject.tsx b/browser/data-browser/src/helpers/useCurrentSubject.tsx index 27157a04f..a1554aa3d 100644 --- a/browser/data-browser/src/helpers/useCurrentSubject.tsx +++ b/browser/data-browser/src/helpers/useCurrentSubject.tsx @@ -75,7 +75,10 @@ export function useSubjectParam( } const newUrl = new URL(subject); - newVal && newUrl.searchParams.set(key, newVal); + + if (newVal) { + newUrl.searchParams.set(key, newVal); + } if (newVal === undefined || newVal === '' || newVal === null) { newUrl.searchParams.delete(key); diff --git a/browser/data-browser/src/hooks/useMediaQuery.ts b/browser/data-browser/src/hooks/useMediaQuery.ts index 78341d327..353491b16 100644 --- a/browser/data-browser/src/hooks/useMediaQuery.ts +++ b/browser/data-browser/src/hooks/useMediaQuery.ts @@ -2,7 +2,13 @@ import { useEffect, useState } from 'react'; /** Watches a media query and returns a statefull result. */ export function useMediaQuery(query: string, initial = false): boolean { - const [matches, setMatches] = useState(initial); + const [matches, setMatches] = useState(() => { + if (!window.matchMedia) { + return initial; + } + + return window.matchMedia(query).matches; + }); useEffect(() => { if (!window.matchMedia) { @@ -14,12 +20,10 @@ export function useMediaQuery(query: string, initial = false): boolean { }; const queryList = window.matchMedia(query); - setMatches(queryList.matches); - queryList.addEventListener('change', listener); return () => queryList.removeEventListener('change', listener); - }, []); + }, [query]); return matches; } diff --git a/browser/data-browser/src/routes/History/HistoryRoute.tsx b/browser/data-browser/src/routes/History/HistoryRoute.tsx index bd5994065..5a375c93a 100644 --- a/browser/data-browser/src/routes/History/HistoryRoute.tsx +++ b/browser/data-browser/src/routes/History/HistoryRoute.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState, type JSX } from 'react'; +import { useCallback, useMemo, useState, type JSX } from 'react'; import { useResource, Version } from '@tomic/react'; import { ContainerNarrow } from '../../components/Containers'; @@ -19,6 +19,7 @@ import { Main } from '../../components/Main'; import { pathNames } from '../paths'; import { appRoute } from '../RootRoutes'; import { createRoute } from '@tanstack/react-router'; +import { useOnValueChange } from '@helpers/useOnValueChange'; export const HistoryRoute = createRoute({ path: pathNames.history, @@ -39,7 +40,7 @@ function History(): JSX.Element { [key: string]: Version[]; } = useMemo(() => groupVersionsByMonth(versions), [versions]); - useEffect(() => { + useOnValueChange(() => { if (versions.length > 0) { setSelectedVersion(versions[versions.length - 1]); } diff --git a/browser/data-browser/src/routes/SettingsAgent.tsx b/browser/data-browser/src/routes/SettingsAgent.tsx index 3191b9f66..6304010ba 100644 --- a/browser/data-browser/src/routes/SettingsAgent.tsx +++ b/browser/data-browser/src/routes/SettingsAgent.tsx @@ -23,6 +23,7 @@ import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; import { createRoute } from '@tanstack/react-router'; import { pathNames } from './paths'; import { appRoute } from './RootRoutes'; +import { useOnValueChange } from '@helpers/useOnValueChange'; export const AgentSettingsRoute = createRoute({ path: pathNames.agentSettings, @@ -42,7 +43,7 @@ const SettingsAgent: React.FunctionComponent = () => { // When there is an agent, set the advanced values // Otherwise, reset the secret value - React.useEffect(() => { + useOnValueChange(() => { if (agent !== undefined) { fillAdvanced(); } else { @@ -51,7 +52,7 @@ const SettingsAgent: React.FunctionComponent = () => { }, [agent]); // When the key or subject changes, update the secret - React.useEffect(() => { + useOnValueChange(() => { renewSecret(); }, [subject, privateKey]); @@ -113,7 +114,9 @@ const SettingsAgent: React.FunctionComponent = () => { } function handleCopy() { - secret && navigator.clipboard.writeText(secret); + if (secret) { + navigator.clipboard.writeText(secret); + } } /** When the Secret updates, parse it and try if the */ diff --git a/browser/data-browser/src/views/FolderPage/GridItem/components.tsx b/browser/data-browser/src/views/FolderPage/GridItem/components.tsx index a5115702e..9c1e8eceb 100644 --- a/browser/data-browser/src/views/FolderPage/GridItem/components.tsx +++ b/browser/data-browser/src/views/FolderPage/GridItem/components.tsx @@ -21,10 +21,10 @@ export const GridCard = styled.div.attrs(p => ({ `; export const GridItemWrapper = styled.a` - --shadow: 0px 0.7px 1.3px rgba(0, 0, 0, 0.06), - 0px 1.8px 3.2px rgba(0, 0, 0, 0.043), 0px 3.4px 6px rgba(0, 0, 0, 0.036), - 0px 6px 10.7px rgba(0, 0, 0, 0.03), 0px 11.3px 20.1px rgba(0, 0, 0, 0.024), - 0px 27px 48px rgba(0, 0, 0, 0.017); + --shadow: + 0px 0.7px 1.3px rgba(0, 0, 0, 0.06), 0px 1.8px 3.2px rgba(0, 0, 0, 0.043), + 0px 3.4px 6px rgba(0, 0, 0, 0.036), 0px 6px 10.7px rgba(0, 0, 0, 0.03), + 0px 11.3px 20.1px rgba(0, 0, 0, 0.024), 0px 27px 48px rgba(0, 0, 0, 0.017); --interaction-shadow: 0px 0px 0px 0px ${p => p.theme.colors.main}; --card-banner-padding: 1rem; --card-banner-height: calc(var(--card-banner-padding) * 2 + 1.5em); diff --git a/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx b/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx index a1e6774c7..c6c04218b 100644 --- a/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx +++ b/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx @@ -36,7 +36,7 @@ export function ResourceInline({ const resource = useResource(subject, { allowIncomplete: true }); const [isA] = useArray(resource, core.properties.isA); - const Comp = basic ? DefaultInline : classMap.get(isA[0]) ?? DefaultInline; + const Comp = basic ? DefaultInline : (classMap.get(isA[0]) ?? DefaultInline); if (!subject) { return No subject passed; diff --git a/browser/eslint.config.js b/browser/eslint.config.js index e3c4bb5f8..af9b5a127 100644 --- a/browser/eslint.config.js +++ b/browser/eslint.config.js @@ -44,7 +44,9 @@ export default defineConfig([ rules: { ...js.configs.recommended.rules, ...tseslint.configs.recommended.rules, + 'no-undef': 'off', 'no-unused-vars': 'off', + 'no-redeclare': 'off', '@typescript-eslint/no-unused-vars': ['error', { 'varsIgnorePattern': '^_', 'argsIgnorePattern': '^_', "caughtErrorsIgnorePattern": "^_|^e$" }], '@typescript-eslint/no-explicit-any': 'error', 'no-shadow': 'off', @@ -102,7 +104,10 @@ export default defineConfig([ ...reactHooks.configs.flat['recommended-latest'].rules, 'react-hooks/preserve-manual-memoization': 'warn', 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/set-state-in-effect': 'warn', 'react-hooks/static-components': 'off', + // This rule is way to aggressive and seems to be designed for people that don't understand refs. + 'react-hooks/refs': 'off', } } ]); diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index acab833c0..90b770d0a 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -16,10 +16,10 @@ importers: version: 24.7.0 '@typescript-eslint/eslint-plugin': specifier: ^8.46.2 - version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + version: 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^8.46.2 - version: 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + version: 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) eslint: specifier: ^9.39.0 version: 9.39.0(jiti@2.3.3) @@ -28,7 +28,7 @@ importers: version: 9.1.0(eslint@9.39.0(jiti@2.3.3)) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3)) + version: 2.31.0(@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.0(jiti@2.3.3)) @@ -4080,11 +4080,11 @@ packages: typescript: optional: true - '@typescript-eslint/eslint-plugin@8.46.2': - resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.2 + '@typescript-eslint/parser': ^8.46.4 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' @@ -4098,15 +4098,15 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.46.2': - resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==} + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.2': - resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -4115,12 +4115,12 @@ packages: resolution: {integrity: sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.46.2': - resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.2': - resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -4134,8 +4134,8 @@ packages: typescript: optional: true - '@typescript-eslint/type-utils@8.46.2': - resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==} + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4149,8 +4149,8 @@ packages: resolution: {integrity: sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.46.2': - resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@5.62.0': @@ -4171,8 +4171,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.46.2': - resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -4183,8 +4183,8 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/utils@8.46.2': - resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4198,8 +4198,8 @@ packages: resolution: {integrity: sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.46.2': - resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@uiw/codemirror-extensions-basic-setup@4.24.1': @@ -14425,7 +14425,7 @@ snapshots: '@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3)': dependencies: - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3) '@typescript-eslint/scope-manager': 8.11.0 '@typescript-eslint/type-utils': 8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3) @@ -14441,14 +14441,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 eslint: 9.39.0(jiti@2.3.3) graphemer: 1.4.0 ignore: 7.0.5 @@ -14471,22 +14471,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.1(supports-color@9.4.0) eslint: 9.39.0(jiti@2.3.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 debug: 4.4.1(supports-color@9.4.0) typescript: 5.9.3 transitivePeerDependencies: @@ -14497,12 +14497,12 @@ snapshots: '@typescript-eslint/types': 8.11.0 '@typescript-eslint/visitor-keys': 8.11.0 - '@typescript-eslint/scope-manager@8.46.2': + '@typescript-eslint/scope-manager@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 - '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -14518,11 +14518,11 @@ snapshots: - eslint - supports-color - '@typescript-eslint/type-utils@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) debug: 4.4.1(supports-color@9.4.0) eslint: 9.39.0(jiti@2.3.3) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -14534,7 +14534,7 @@ snapshots: '@typescript-eslint/types@8.11.0': {} - '@typescript-eslint/types@8.46.2': {} + '@typescript-eslint/types@8.46.4': {} '@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.9.3)': dependencies: @@ -14565,12 +14565,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.1(supports-color@9.4.0) fast-glob: 3.3.2 is-glob: 4.0.3 @@ -14592,12 +14592,12 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@2.3.3)) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) eslint: 9.39.0(jiti@2.3.3) typescript: 5.9.3 transitivePeerDependencies: @@ -14613,9 +14613,9 @@ snapshots: '@typescript-eslint/types': 8.11.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.46.2': + '@typescript-eslint/visitor-keys@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 '@uiw/codemirror-extensions-basic-setup@4.24.1(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1)': @@ -16574,17 +16574,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.3.3)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.3.3)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) eslint: 9.39.0(jiti@2.3.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint@9.39.0(jiti@2.3.3)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -16595,7 +16595,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.0(jiti@2.3.3) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.3.3)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.0(jiti@2.3.3)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -16607,7 +16607,7 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.0(jiti@2.3.3))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -16804,7 +16804,7 @@ snapshots: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.1.0 + eslint-visitor-keys: 4.2.1 espree@10.4.0: dependencies: diff --git a/browser/react/src/components/Image.tsx b/browser/react/src/components/Image.tsx index e77553038..f92c20e8c 100644 --- a/browser/react/src/components/Image.tsx +++ b/browser/react/src/components/Image.tsx @@ -208,9 +208,9 @@ const toUrl = ( ) => { const url = new URL(base); const queryParams = new URLSearchParams(); - format && queryParams.set('f', format); - width && queryParams.set('w', width.toString()); - quality && queryParams.set('q', quality.toString()); + if (format) queryParams.set('f', format); + if (width) queryParams.set('w', width.toString()); + if (quality) queryParams.set('q', quality.toString()); url.search = queryParams.toString(); return url.toString(); diff --git a/browser/react/src/helpers/isDev.ts b/browser/react/src/helpers/isDev.ts index d609ff0ee..ca6f0603f 100644 --- a/browser/react/src/helpers/isDev.ts +++ b/browser/react/src/helpers/isDev.ts @@ -1,5 +1,5 @@ /** Returns true if this is run in locally, in Development mode */ export function isDev(): boolean { - //@ts-ignore This key does exist + // @ts-expect-error This key does exist return import.meta.env['MODE'] === 'development'; } diff --git a/browser/react/src/helpers/useOnValueChange.ts b/browser/react/src/helpers/useOnValueChange.ts new file mode 100644 index 000000000..9d2094842 --- /dev/null +++ b/browser/react/src/helpers/useOnValueChange.ts @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +export function useOnValueChange(callback: () => void, dependants: unknown[]) { + const [deps, setDeps] = useState(dependants); + + if (deps.some((d, i) => d !== dependants[i])) { + setDeps(dependants); + callback(); + } +} diff --git a/browser/react/src/hooks.ts b/browser/react/src/hooks.ts index 528833c82..033ecd9bb 100644 --- a/browser/react/src/hooks.ts +++ b/browser/react/src/hooks.ts @@ -31,6 +31,7 @@ import { server, } from '@tomic/lib'; import type * as Y from 'yjs'; +import { useOnValueChange } from './helpers/useOnValueChange.js'; export type UseResourceOptions = FetchOpts & { /** @@ -110,10 +111,30 @@ export function useResources( opts: FetchOpts = {}, ): Map { const [resources, setResources] = useState(new Map()); + const [prevSubjects, setPrevSubjects] = useState([]); const store = useStore(); const memoizedOpts = useMemoizedOpts(opts); + if (subjects !== prevSubjects) { + setPrevSubjects(subjects); + setResources(prev => { + const newResources = new Map(); + + for (const subject of subjects) { + const resource = store.getResourceLoading(subject, memoizedOpts); + + if (!prev.has(subject)) { + newResources.set(subject, proxyResource(resource)); + } else { + newResources.set(subject, prev.get(subject)!); + } + } + + return newResources; + }); + } + useEffect(() => { // When a change happens, set the new Resource. function handleNotify(updated: Resource) { @@ -125,25 +146,33 @@ export function useResources( }); } - setResources(prev => { - for (const subject of subjects) { - const resource = store.getResourceLoading(subject, memoizedOpts); - prev.set(subject, proxyResource(resource)); + const unsubLoadingFuncs: (() => void)[] = []; - // Let the store know to call handleNotify when a resource is updated. - store.subscribe(subject, handleNotify); - } + for (const resource of resources.values()) { + store.subscribe(resource.subject, handleNotify); + unsubLoadingFuncs.push( + resource.on(ResourceEvents.LoadingChange, () => { + setResources(prev => { + prev.set(resource.subject, proxyResource(resource)); - return new Map(prev); - }); + // We need to create new Maps for react hooks to update - React only checks references, not content + return new Map(prev); + }); + }), + ); + } return () => { // When the component is unmounted, unsubscribe from the store. - for (const subject of subjects) { - store.unsubscribe(subject, handleNotify); + for (const resource of resources.values()) { + store.unsubscribe(resource.subject, handleNotify); + } + + for (const unsubLoadingFunc of unsubLoadingFuncs) { + unsubLoadingFunc(); } }; - }, [subjects, store, memoizedOpts]); + }, [resources, store, memoizedOpts]); return resources; } @@ -572,20 +601,14 @@ export function useYDoc( ); useEffect(() => { - if (resource.loading) { - return; - } - - setDoc(resource.getYDoc(propertyURL)); - - return resource.on(ResourceEvents.LocalChange, prop => { - if (prop !== propertyURL) { - return; - } - - setDoc(resource.getYDoc(propertyURL)); + return resource.stable.on(ResourceEvents.LoadingChange, () => { + setDoc( + resource.stable.loading + ? undefined + : resource.stable.getYDoc(propertyURL), + ); }); - }, [resource]); + }, [resource.stable, propertyURL]); return doc; } @@ -611,8 +634,7 @@ export function useCanWrite(resource: Resource): boolean { const [canWrite, setCanWrite] = useState(false); const agent = store.getAgent(); - // If the subject changes, make sure to change the resource! - useEffect(() => { + useOnValueChange(() => { if (agent?.subject === undefined) { setCanWrite(false); @@ -621,15 +643,18 @@ export function useCanWrite(resource: Resource): boolean { if (resource.new) { setCanWrite(true); - - return; } - - resource.canWrite(agent.subject).then(([result]) => { - setCanWrite(result); - }); }, [resource, agent?.subject]); + // If the subject changes, make sure to change the resource! + useEffect(() => { + if (agent && !resource.new) { + resource.canWrite(agent.subject).then(([result]) => { + setCanWrite(result); + }); + } + }, [resource, agent]); + return canWrite; } diff --git a/browser/react/src/useCollection.ts b/browser/react/src/useCollection.ts index f0db5658e..e50361ba3 100644 --- a/browser/react/src/useCollection.ts +++ b/browser/react/src/useCollection.ts @@ -55,11 +55,11 @@ const buildCollection = ( ) => { const builder = new CollectionBuilder(store, server); - property && builder.setProperty(property); - value && builder.setValue(value); - sort_by && builder.setSortBy(sort_by); - sort_desc !== undefined && builder.setSortDesc(sort_desc); - pageSize && builder.setPageSize(pageSize); + if (property) builder.setProperty(property); + if (value) builder.setValue(value); + if (sort_by) builder.setSortBy(sort_by); + if (sort_desc !== undefined) builder.setSortDesc(sort_desc); + if (pageSize) builder.setPageSize(pageSize); return builder.build(); }; @@ -102,12 +102,11 @@ export function useCollection( collection.waitForReady().then(() => { setCollection(proxyCollection(collection.__internalObject)); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (firstRun) { - setFirstRun(false); - return; } @@ -120,13 +119,14 @@ export function useCollection( newCollection.waitForReady().then(() => { setCollection(proxyCollection(newCollection.__internalObject)); + setFirstRun(false); }); - }, [queryFilterMemo, pageSize, store, server]); + }, [queryFilterMemo, pageSize, store, server, firstRun]); const invalidateCollection = useCallback(async () => { - await collection.refresh(); + await collection.__internalObject.refresh(); setCollection(proxyCollection(collection.__internalObject)); - }, [collection, store, server, queryFilter, pageSize]); + }, [collection.__internalObject]); return { collection, invalidateCollection, mapAll }; } @@ -134,6 +134,7 @@ export function useCollection( function useQueryFilterMemo(queryFilter: QueryFilter) { return useMemo( () => queryFilter, + // eslint-disable-next-line react-hooks/exhaustive-deps [ queryFilter.property, queryFilter.value, diff --git a/browser/react/src/useMarkdown.ts b/browser/react/src/useMarkdown.ts index bbe64529e..5463be0d8 100644 --- a/browser/react/src/useMarkdown.ts +++ b/browser/react/src/useMarkdown.ts @@ -57,7 +57,7 @@ export function useMarkdown(resource: Resource): string { } getPropValTexts(); - }, [resource]); + }, [resource, description, store, title]); if (resource.error) { return resource.error.message; From 6cd98b31482e08ab2dc01b4c87f50409115385c1 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Tue, 18 Nov 2025 15:57:19 +0100 Subject: [PATCH 7/8] Fix tests and fix new popover positioning --- browser/CHANGELOG.md | 1 + .../templates/nextjs-site/package.json | 4 +- .../src/chunks/RTE/CollaborativeEditor.tsx | 9 +- .../src/components/CustomPopover.tsx | 191 +++++++++++------- .../src/components/Searchbar/Searchbar.tsx | 9 +- .../data-browser/src/components/Tag/Tag.tsx | 5 +- .../components/forms/InputResourceArray.tsx | 4 +- .../src/helpers/useOnValueChange.ts | 10 +- .../data-browser/src/hooks/useControlable.ts | 50 ----- browser/data-browser/src/locales/de.po | 13 +- browser/data-browser/src/locales/en.po | 13 +- browser/data-browser/src/locales/es.po | 13 +- browser/data-browser/src/locales/fr.po | 13 +- .../src/routes/LinkOpenRouter.tsx | 15 +- .../data-browser/src/routes/SettingsAgent.tsx | 10 +- .../src/views/BookmarkPage/usePreview.ts | 14 +- .../src/views/Document/DocumentV2FullPage.tsx | 1 - .../data-browser/src/views/ImporterPage.tsx | 113 ++++++----- .../TablePage/EditorCells/AtomicURLCell.tsx | 91 ++++----- .../TablePage/EditorCells/CellComponents.tsx | 5 +- .../EditorCells/MultiRelationCell.tsx | 76 +++---- .../TablePage/EditorCells/SelectCell.tsx | 54 ++--- .../EditorCells/useResourceSearch.ts | 24 ++- browser/data-browser/vite.config.ts | 2 +- browser/e2e/tests/documents.spec.ts | 43 +++- browser/e2e/tests/e2e.spec.ts | 25 +-- browser/e2e/tests/search.spec.ts | 15 -- browser/e2e/tests/template.spec.ts | 23 +-- browser/e2e/tests/test-utils.ts | 10 +- browser/eslint.config.js | 3 +- browser/react/package.json | 4 +- browser/react/src/useServerSearch.tsx | 27 ++- 32 files changed, 446 insertions(+), 444 deletions(-) delete mode 100644 browser/data-browser/src/hooks/useControlable.ts diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 0fb6d1336..7923d76f6 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -41,6 +41,7 @@ This changelog covers all five packages, as they are (for now) updated as a whol - BREAKING CHANGE: `useCanWrite` now only returns a boolean. There is no longer a message returned. - BREAKING CHANGE: `useCanWrite` does not take an agent as argument any more and only checks the agent set in the store. If you need to explicitly check a different agent, use `await resource.canWrite(agent)`. - BREAKING CHANGE: `useDebounce` and `useDebouncedCallback` are no longer exported. +- BREAKING CHANGE: @tomic/react now requires React 19.2.0 or above. - Added `useDebouncedSave` hook. - Add a cjs build. diff --git a/browser/create-template/templates/nextjs-site/package.json b/browser/create-template/templates/nextjs-site/package.json index f24ba39f4..8d886dd58 100644 --- a/browser/create-template/templates/nextjs-site/package.json +++ b/browser/create-template/templates/nextjs-site/package.json @@ -17,8 +17,8 @@ "gray-matter": "^4.0.3", "modern-css-reset": "^1.4.0", "next": "15.0.4", - "react": "19.0.0", - "react-dom": "19.0.0", + "react": "19.2.0", + "react-dom": "19.2.0", "remark": "^15.0.1", "remark-html": "^16.0.1", "zod": "^3.23.8" diff --git a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx index 27c270998..104e2a693 100644 --- a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx +++ b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx @@ -57,11 +57,9 @@ import { addIf } from '@helpers/addIf'; export type CollaborativeEditorProps = { placeholder?: string; doc: Y.Doc; - autoFocus?: boolean; resource: Resource; property: string; id?: string; - labelId?: string; onBlur?: () => void; }; @@ -69,11 +67,9 @@ const COLORS = ['#70d6ff', '#ff70a6', '#ff9770', '#ffd670', '#e9ff70']; export default function CollaborativeEditor({ placeholder, - autoFocus, doc, property, id, - labelId, resource, onBlur, }: CollaborativeEditorProps): React.JSX.Element { @@ -249,11 +245,12 @@ export default function CollaborativeEditor({ ], editable: canWrite, onBlur, - autofocus: !!autoFocus, editorProps: { attributes: { ...(id && { id }), - ...(labelId && { 'aria-labelledby': labelId }), + 'aria-label': 'Rich Text Editor', + 'aria-multiline': 'true', + 'aria-readonly': canWrite ? 'true' : 'false', spellcheck: 'true', }, }, diff --git a/browser/data-browser/src/components/CustomPopover.tsx b/browser/data-browser/src/components/CustomPopover.tsx index b69f396d2..29143b890 100644 --- a/browser/data-browser/src/components/CustomPopover.tsx +++ b/browser/data-browser/src/components/CustomPopover.tsx @@ -1,117 +1,144 @@ import { - useEffectEvent, + useEffect, useId, - useLayoutEffect, useRef, + useState, type ReactNode, + type RefObject, } from 'react'; import { styled } from 'styled-components'; import { transparentize } from 'polished'; import { fadeIn } from '@helpers/commonAnimations'; import { useControlLock } from '@hooks/useControlLock'; import { useDialogTreeInfo } from './Dialog/dialogContext'; -import { useControllable } from '@hooks/useControlable'; +import { useOnValueChange } from '@helpers/useOnValueChange'; export interface TriggerProps { onClick: () => void; 'data-popover-target': string; } -export interface PopoverProps { - Trigger: (props: TriggerProps) => ReactNode; - open?: boolean; - defaultOpen?: boolean; - onOpenChange: (open: boolean) => void; +export interface PopoverPropsFromHook { + isOpen: boolean; + setIsOpen: React.Dispatch>; + anchorName: string; +} +export interface PopoverProps extends PopoverPropsFromHook { + Trigger: ReactNode; className?: string; - noArrow?: boolean; noLock?: boolean; - modal?: boolean; - side?: 'top' | 'bottom' | 'left' | 'right'; } +export interface UsePopoverProps { + defaultOpen?: boolean; + autoFocusElement?: RefObject; +} + +export interface UsePopoverReturn { + triggerProps: { + onClick: () => void; + 'data-popover-target': string; + }; + popoverProps: PopoverPropsFromHook; + openPopover: () => void; + closePopover: () => void; + isOpen: boolean; +} + +export const usePopover = ({ + defaultOpen = false, + autoFocusElement, +}: UsePopoverProps): UsePopoverReturn => { + const id = useId(); + const [isOpen, setIsOpen] = useState(defaultOpen); + const { setHasOpenInnerPopup } = useDialogTreeInfo(); + + const openPopover = () => { + setIsOpen(true); + }; + + const closePopover = () => { + setIsOpen(false); + }; + + const triggerProps = { + onClick: () => setIsOpen(prev => !prev), + 'data-popover-target': id, + }; + + const popoverProps = { + anchorName: id, + isOpen, + setIsOpen, + }; + + useOnValueChange(() => { + setHasOpenInnerPopup(isOpen); + }, [isOpen]); + + useEffect(() => { + if (isOpen && autoFocusElement && autoFocusElement.current) { + autoFocusElement.current.focus(); + } + }, [isOpen, autoFocusElement]); + + return { triggerProps, popoverProps, openPopover, closePopover, isOpen }; +}; + /** * Popover component, consists of an outer dialog element and an inner content div. * To style the content div use `${CustomPopover.Content}: { ... }` */ export function CustomPopover({ Trigger, - open: parentOpen, - defaultOpen, - onOpenChange, + anchorName, + isOpen, + setIsOpen, className, noLock, - side = 'top', - modal, children, }: React.PropsWithChildren) { - const popoverRef = useRef(null); + const popoverRef = useRef(null); const contentRef = useRef(null); - const id = useId(); - const setElementState = (state: boolean) => { - if (state && !popoverRef.current?.hasAttribute('open')) { - if (modal) { - popoverRef.current?.showModal(); - } else { - popoverRef.current?.show(); - } - } else if (!state && popoverRef.current?.hasAttribute('open')) { - popoverRef.current?.close(); + useEffect(() => { + if (isOpen && !popoverRef.current?.matches(':popover-open')) { + popoverRef.current?.showPopover(); + } else if (!isOpen && popoverRef.current?.matches(':popover-open')) { + popoverRef.current?.hidePopover(); } - }; - - const onStateChange = (state: boolean) => { - setElementState(state); - setHasOpenInnerPopup(state); - - onOpenChange?.(state); - }; - - const [open, setOpen] = useControllable({ - controlledValue: parentOpen, - defaultValue: defaultOpen, - onChange: onStateChange, - }); + }, [isOpen]); - const { setHasOpenInnerPopup } = useDialogTreeInfo(); + useEffect(() => { + const handleToggle = (e: ToggleEvent) => { + if (e.newState === 'closed') { + setIsOpen(false); + } + }; - const handleOutsideClick = ( - e: React.MouseEvent, - ) => { - if ( - !contentRef.current?.contains(e.target as HTMLElement) && - contentRef.current !== e.target - ) { - setOpen(false); - } - }; + if (!popoverRef.current) return; - const setElementStateEffect = useEffectEvent((state: boolean) => { - setElementState(state); - }); + const popover = popoverRef.current; + popover.addEventListener('toggle', handleToggle); - useLayoutEffect(() => { - setElementStateEffect(!!open); - }, [open]); + return () => { + popover.removeEventListener('toggle', handleToggle); + }; + }, [setIsOpen]); - useControlLock(!noLock && !!open); + useControlLock(!noLock && !!isOpen); return ( - - setOpen(prev => !prev)} - data-popover-target={id} - /> + + {Trigger} handleOutsideClick(e)} + id={anchorName} className={className} > - {open && children} + {isOpen && children} ); @@ -124,12 +151,25 @@ CustomPopover.Content = PopoverContent; const Wrapper = styled.div<{ anchorName: string }>` display: contents; - & button[data-popover-target='${p => p.anchorName}'] { + & *[data-popover-target='${p => p.anchorName}'] { anchor-name: --${p => p.anchorName}; } `; -const Popover = styled.dialog<{ anchorName: string; side: string }>` +const Popover = styled.div<{ anchorName: string }>` + @position-try --top-right { + position-area: top span-right; + } + @position-try --top-left { + position-area: top span-left; + } + @position-try --bottom-right { + position-area: bottom span-right; + } + @position-try --bottom-left { + position-area: bottom span-left; + } + border: none; background-color: ${p => transparentize(0.2, p.theme.colors.bgBody)}; backdrop-filter: blur(10px); @@ -139,11 +179,10 @@ const Popover = styled.dialog<{ anchorName: string; side: string }>` margin: 0; padding: 0; inset: auto; + position: fixed; position-anchor: --${p => p.anchorName}; - position-area: ${p => p.side}; - position-try-fallbacks: flip-block; + position-area: top center; + position-try: --top-right, --top-left, --bottom-right, --bottom-left; max-height: unset; - &::backdrop { - background-color: transparent; - } + min-width: max-content; `; diff --git a/browser/data-browser/src/components/Searchbar/Searchbar.tsx b/browser/data-browser/src/components/Searchbar/Searchbar.tsx index 3df6f2e2c..700cdd09a 100644 --- a/browser/data-browser/src/components/Searchbar/Searchbar.tsx +++ b/browser/data-browser/src/components/Searchbar/Searchbar.tsx @@ -17,6 +17,7 @@ import { base64StringToFilter, filterToBase64String, } from '../../routes/Search/searchUtils'; +import { addFieldsIf } from '@helpers/addIf'; function addTagsToFilter( base64Filter: string | undefined, @@ -52,10 +53,10 @@ export function Searchbar(): JSX.Element { to: paths.search, search: prev => ({ query: q, - ...(scope ? { queryscope: scope } : {}), - ...(tags.length > 0 - ? { filters: addTagsToFilter(prev.filters, tags) } - : {}), + ...addFieldsIf(!!scope, { queryscope: scope }), + ...addFieldsIf(tags.length > 0, { + filters: addTagsToFilter(prev.filters, tags), + }), }), replace: true, }); diff --git a/browser/data-browser/src/components/Tag/Tag.tsx b/browser/data-browser/src/components/Tag/Tag.tsx index 5dff6e11f..419fd1c61 100644 --- a/browser/data-browser/src/components/Tag/Tag.tsx +++ b/browser/data-browser/src/components/Tag/Tag.tsx @@ -104,7 +104,7 @@ export function TagButton({ e.stopPropagation(); onClick(subject); }, - [onClick], + [onClick, subject], ); const className = selected ? 'selected-tag' : ''; @@ -171,7 +171,7 @@ const TagWrapperButton = styled(TagWrapper)` cursor: pointer; user-select: none; - transition: ${transition('filter', 'box-shadow')}; + transition: ${transition('filter', 'box-shadow', 'transform')}; animation: ${fadeIn} 0.2s ease-in-out; &:hover, &:focus, @@ -179,6 +179,7 @@ const TagWrapperButton = styled(TagWrapper)` --shadow-color: ${({ theme }) => theme.darkMode ? 'var(--dark-color)' : 'var(--light-color)'}; filter: brightness(1.05); + transform: scale(1.1); box-shadow: 0 1px 20px 0px var(--shadow-color); } `; diff --git a/browser/data-browser/src/components/forms/InputResourceArray.tsx b/browser/data-browser/src/components/forms/InputResourceArray.tsx index 091f078dd..cc80a3b84 100644 --- a/browser/data-browser/src/components/forms/InputResourceArray.tsx +++ b/browser/data-browser/src/components/forms/InputResourceArray.tsx @@ -90,7 +90,7 @@ export default function InputResourceArray({ setError(newArray.length === 0 ? 'Required' : undefined); } }, - [property.datatype, setArray, setError, required, addingNewItem, array], + [property.datatype, setArray, setError, required, array], ); const handleSetSubjectMemos = useMemo(() => { @@ -198,7 +198,7 @@ export default function InputResourceArray({ > - {array.length > 1 && ( + {array.length > 0 && ( void, dependants: unknown[]) { - const [deps, setDeps] = useState(dependants); +const initialUnique = [Symbol('uniqueValue')]; + +export function useOnValueChange( + callback: () => void, + dependants: unknown[], + runOnMount: boolean = false, +) { + const [deps, setDeps] = useState(runOnMount ? initialUnique : dependants); if (deps.some((d, i) => d !== dependants[i])) { setDeps(dependants); diff --git a/browser/data-browser/src/hooks/useControlable.ts b/browser/data-browser/src/hooks/useControlable.ts deleted file mode 100644 index 2511691ad..000000000 --- a/browser/data-browser/src/hooks/useControlable.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback, useRef, useState, type SetStateAction } from 'react'; - -type UseControllableProps = { - controlledValue?: T | undefined; - defaultValue?: T | undefined; - onChange?: (state: T) => void; -}; - -type SetStateFn = (prevState: T) => T; - -export function useControllable({ - controlledValue, - defaultValue, - onChange, -}: UseControllableProps): [ - T | undefined, - (value: SetStateAction) => void, -] { - const isControlledRef = useRef(controlledValue !== undefined); - const [uncontrolledValue, setUncontrolledValue] = useState( - defaultValue, - ); - - // I'm not sure how to fix this linter error but this is intended behavior so we'll ignore the rule. - // eslint-disable-next-line react-hooks/refs - const value = isControlledRef.current ? controlledValue : uncontrolledValue; - - const setValue = useCallback( - (nextValue: SetStateAction) => { - let resolvedValue: T | undefined; - - if (typeof nextValue === 'function') { - resolvedValue = (nextValue as SetStateFn)(value); - } else { - resolvedValue = nextValue; - } - - if (!isControlledRef.current) { - setUncontrolledValue(resolvedValue); - } - - if (onChange) { - onChange(resolvedValue as T); - } - }, - [onChange, value], - ); - - return [value, setValue]; -} diff --git a/browser/data-browser/src/locales/de.po b/browser/data-browser/src/locales/de.po index 145bb8580..878be4da7 100644 --- a/browser/data-browser/src/locales/de.po +++ b/browser/data-browser/src/locales/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-29T10:47:38.272Z\n" -"PO-Revision-Date: 2025-11-12T12:53:58.910Z\n" +"PO-Revision-Date: 2025-11-18T09:35:24.196Z\n" "Last-Translator: \n" "Language: de\n" "Language-Team: \n" @@ -28,6 +28,7 @@ msgstr "Keine Klassen" #: src/components/ComboBox.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "No results" msgstr "Keine Ergebnisse" @@ -1099,10 +1100,6 @@ msgstr "Diese URL wird als Standard-Elternteil für importierte Ressourcen verwe msgid "Importing..." msgstr "Importiere..." -#: src/views/ImporterPage.tsx -msgid "Send JSON" -msgstr "JSON senden" - #: src/chunks/EmojiInput/EmojiInput.tsx msgid "Pick an emoji" msgstr "Wähle ein Emoji" @@ -1328,6 +1325,7 @@ msgid "Show the history of this resource" msgstr "Zeige den Verlauf dieser Ressource" #: src/components/ResourceContextMenu/index.tsx +#: src/views/ImporterPage.tsx msgid "Import" msgstr "Importieren" @@ -1569,7 +1567,6 @@ msgstr "Neuer Tag" #: src/components/Tag/CreateTagRow.tsx #: src/views/TablePage/EditorCells/SelectCell.tsx -#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "Add tag" msgstr "Tag hinzufügen" @@ -3081,3 +3078,7 @@ msgstr "" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Untitled Agent" msgstr "Unbenannter Agent" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Rich Text Editor" +msgstr "Rich-Text-Editor" diff --git a/browser/data-browser/src/locales/en.po b/browser/data-browser/src/locales/en.po index e71ebef52..e1a04c016 100644 --- a/browser/data-browser/src/locales/en.po +++ b/browser/data-browser/src/locales/en.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T12:18:27.636Z\n" -"PO-Revision-Date: 2025-11-12T12:53:58.901Z\n" +"PO-Revision-Date: 2025-11-18T09:35:24.168Z\n" "Last-Translator: \n" "Language: en\n" "Language-Team: \n" @@ -685,10 +685,6 @@ msgstr "This URL will be used as the default Parent for imported resources." msgid "Importing..." msgstr "Importing..." -#: src/views/ImporterPage.tsx -msgid "Send JSON" -msgstr "Send JSON" - #. placeholder {0}: name #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx msgid "Default ontology for the {0} drive" @@ -1644,6 +1640,7 @@ msgid "Show the history of this resource" msgstr "Show the history of this resource" #: src/components/ResourceContextMenu/index.tsx +#: src/views/ImporterPage.tsx msgid "Import" msgstr "Import" @@ -1833,6 +1830,7 @@ msgstr "Open menu ({0})" #: src/components/ComboBox.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "No results" msgstr "No results" @@ -2024,7 +2022,6 @@ msgstr "New tag" #: src/components/Tag/CreateTagRow.tsx #: src/views/TablePage/EditorCells/SelectCell.tsx -#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "Add tag" msgstr "Add tag" @@ -3087,3 +3084,7 @@ msgstr "" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Untitled Agent" msgstr "Untitled Agent" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Rich Text Editor" +msgstr "Rich Text Editor" diff --git a/browser/data-browser/src/locales/es.po b/browser/data-browser/src/locales/es.po index d0c44686a..aa2d25317 100644 --- a/browser/data-browser/src/locales/es.po +++ b/browser/data-browser/src/locales/es.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T09:59:41.856Z\n" -"PO-Revision-Date: 2025-11-12T12:53:58.904Z\n" +"PO-Revision-Date: 2025-11-18T09:35:24.182Z\n" "Last-Translator: \n" "Language: es\n" "Language-Team: \n" @@ -49,6 +49,7 @@ msgstr "Cancelar" #: src/components/ComboBox.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "No results" msgstr "Sin resultados" @@ -1084,10 +1085,6 @@ msgstr "Esta URL se utilizará como el padre predeterminado para los recursos im msgid "Importing..." msgstr "Importando..." -#: src/views/ImporterPage.tsx -msgid "Send JSON" -msgstr "Enviar JSON" - #: src/chunks/EmojiInput/EmojiInput.tsx msgid "Pick an emoji" msgstr "Elige un emoji" @@ -1404,6 +1401,7 @@ msgid "Show the history of this resource" msgstr "Mostrar el historial de este recurso" #: src/components/ResourceContextMenu/index.tsx +#: src/views/ImporterPage.tsx msgid "Import" msgstr "Importar" @@ -1632,7 +1630,6 @@ msgstr "Nueva etiqueta" #: src/components/Tag/CreateTagRow.tsx #: src/views/TablePage/EditorCells/SelectCell.tsx -#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "Add tag" msgstr "Añadir etiqueta" @@ -3059,3 +3056,7 @@ msgstr "" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Untitled Agent" msgstr "Agente sin título" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Rich Text Editor" +msgstr "Editor de texto enriquecido" diff --git a/browser/data-browser/src/locales/fr.po b/browser/data-browser/src/locales/fr.po index 249f51bb6..092cad8b0 100644 --- a/browser/data-browser/src/locales/fr.po +++ b/browser/data-browser/src/locales/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T10:06:11.465Z\n" -"PO-Revision-Date: 2025-11-12T12:53:58.907Z\n" +"PO-Revision-Date: 2025-11-18T09:35:24.191Z\n" "Last-Translator: \n" "Language: fr\n" "Language-Team: \n" @@ -49,6 +49,7 @@ msgstr "Annuler" #: src/components/ComboBox.tsx #: src/views/TablePage/EditorCells/AtomicURLCell.tsx #: src/views/TablePage/EditorCells/MultiRelationCell.tsx +#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "No results" msgstr "Aucun résultat" @@ -1101,10 +1102,6 @@ msgstr "Cette URL sera utilisée comme parent par défaut pour les ressources im msgid "Importing..." msgstr "Importation..." -#: src/views/ImporterPage.tsx -msgid "Send JSON" -msgstr "Envoyer JSON" - #: src/chunks/EmojiInput/EmojiInput.tsx msgid "Pick an emoji" msgstr "Choisissez un emoji" @@ -1417,6 +1414,7 @@ msgid "Show the history of this resource" msgstr "Afficher l'historique de cette ressource" #: src/components/ResourceContextMenu/index.tsx +#: src/views/ImporterPage.tsx msgid "Import" msgstr "Importer" @@ -1645,7 +1643,6 @@ msgstr "Nouvelle étiquette" #: src/components/Tag/CreateTagRow.tsx #: src/views/TablePage/EditorCells/SelectCell.tsx -#: src/views/TablePage/EditorCells/SelectCell.tsx msgid "Add tag" msgstr "Ajouter une étiquette" @@ -3076,3 +3073,7 @@ msgstr "" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Untitled Agent" msgstr "Agent sans titre" + +#: src/chunks/RTE/CollaborativeEditor.tsx +msgid "Rich Text Editor" +msgstr "Éditeur de texte enrichi" diff --git a/browser/data-browser/src/routes/LinkOpenRouter.tsx b/browser/data-browser/src/routes/LinkOpenRouter.tsx index 5dd1b477e..a6240d920 100644 --- a/browser/data-browser/src/routes/LinkOpenRouter.tsx +++ b/browser/data-browser/src/routes/LinkOpenRouter.tsx @@ -6,6 +6,7 @@ import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; import styled from 'styled-components'; import { effectFetch } from '../helpers/effectFetch'; import { useAISettings } from '@components/AI/AISettingsContext'; +import { Main } from '@components/Main'; export type LinkOpenRouterSearch = { code: string; @@ -77,12 +78,14 @@ function LinkOpenRouterPage() { } return ( -
-
-

Linking OpenRouter

-

Please wait while we link your OpenRouter account...

-
-
+
+
+
+

Linking OpenRouter

+

Please wait while we link your OpenRouter account...

+
+
+
); } diff --git a/browser/data-browser/src/routes/SettingsAgent.tsx b/browser/data-browser/src/routes/SettingsAgent.tsx index 6304010ba..fa812a706 100644 --- a/browser/data-browser/src/routes/SettingsAgent.tsx +++ b/browser/data-browser/src/routes/SettingsAgent.tsx @@ -52,9 +52,13 @@ const SettingsAgent: React.FunctionComponent = () => { }, [agent]); // When the key or subject changes, update the secret - useOnValueChange(() => { - renewSecret(); - }, [subject, privateKey]); + useOnValueChange( + () => { + renewSecret(); + }, + [subject, privateKey], + true, + ); function renewSecret() { if (agent) { diff --git a/browser/data-browser/src/views/BookmarkPage/usePreview.ts b/browser/data-browser/src/views/BookmarkPage/usePreview.ts index 02058ddf3..72d799e16 100644 --- a/browser/data-browser/src/views/BookmarkPage/usePreview.ts +++ b/browser/data-browser/src/views/BookmarkPage/usePreview.ts @@ -124,11 +124,15 @@ export function usePreview(resource: Resource): UsePreviewReturnType { ); }; - useOnValueChange(() => { - if (resource.isReady() && preview === undefined && url) { - update(url); - } - }, [preview, resource.isReady(), url]); + useOnValueChange( + () => { + if (resource.isReady() && preview === undefined && url) { + update(url); + } + }, + [preview, resource.isReady(), url], + true, + ); return { preview, error, update, loading }; } diff --git a/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx b/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx index 451186617..6f7bf8bd7 100644 --- a/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx +++ b/browser/data-browser/src/views/Document/DocumentV2FullPage.tsx @@ -46,7 +46,6 @@ export const DocumentV2FullPage: React.FC = ({ Loading...
}> !parent || !jsonAd, + [parent, jsonAd], + ); + return ( - - - <p> - Read more about how importing Atomic Data works{' '} - <a href='https://docs.atomicdata.dev/create-json-ad.html'> - in the docs - </a> - . - </p> - <Column> - <Field label='JSON-AD'> - <InputWrapper> - <TextAreaStyled - // disabled={!!url} - rows={15} - placeholder='Paste your JSON-AD...' - value={jsonAd} - onChange={e => setJsonAd(e.target.value)} - > - {jsonAd} - </TextAreaStyled> - </InputWrapper> - </Field> - <Header>Options</Header> - <Group> - <Label> - <input - type='checkbox' - checked={overwriteOutside} - onChange={e => setOverwriteOutside(e.target.checked)} - /> - {`Overwrite resources that are outside the scope of the parent. Do this only if you trust the imported data.`} - </Label> - <Field - label='Target parent' - helper='This URL will be used as the default Parent for imported resources.' - required - fieldId={parentFieldId} - > + <Main> + <ContainerNarrow> + <Title resource={resource} prefix='Import to' link /> + <p> + Read more about how importing Atomic Data works{' '} + <a href='https://docs.atomicdata.dev/create-json-ad.html'> + in the docs + </a> + . + </p> + <Column> + <Field label='JSON-AD'> <InputWrapper> - <InputStyled - id={parentFieldId} - required - placeholder='Enter subject' - value={parent} - onChange={e => setParent(e.target.value)} - /> + <TextAreaStyled + rows={15} + placeholder='Paste your JSON-AD...' + value={jsonAd} + onChange={e => setJsonAd(e.target.value)} + > + {jsonAd} + </TextAreaStyled> </InputWrapper> </Field> - </Group> - {jsonAd !== '' && ( + <Header>Options</Header> + <Group> + <Label> + <input + type='checkbox' + checked={overwriteOutside} + onChange={e => setOverwriteOutside(e.target.checked)} + /> + {`Overwrite resources that are outside the scope of the parent. Do this only if you trust the imported data.`} + </Label> + <Field + label='Target parent' + helper='This URL will be used as the default Parent for imported resources.' + required + fieldId={parentFieldId} + > + <InputWrapper> + <InputStyled + id={parentFieldId} + required + placeholder='Enter subject' + value={parent} + onChange={e => setParent(e.target.value)} + /> + </InputWrapper> + </Field> + </Group> <Button data-test='import-post' - disabled={!parent} + disabled={disableImportButton} onClick={handleImport} > - {isImporting ? 'Importing...' : 'Send JSON'} + {isImporting ? 'Importing...' : 'Import'} </Button> - )} - </Column> - </ContainerNarrow> + </Column> + </ContainerNarrow> + </Main> ); } diff --git a/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx index 97ba27a79..a3d191f3d 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx @@ -24,11 +24,7 @@ import { FileDropzoneInput } from '../../../components/forms/FileDropzone/FileDr import { InputStyled, InputWrapper, -} from '../../../components/forms/InputStyles'; -import { - CursorMode, - useTableEditorContext, -} from '../../../components/TableEditor/TableEditorContext'; +} from '../../../components/forms/InputStyles'; //// import { getIconForClass } from '../../../helpers/iconMap'; import { CellContainer, DisplayCellProps, EditCellProps } from './Type'; import { useResourceSearch } from './useResourceSearch'; @@ -45,7 +41,7 @@ import { SearchResultWrapper, } from './CellComponents'; import { FaXmark } from 'react-icons/fa6'; -import type { TriggerProps } from '@components/CustomPopover'; +import { usePopover } from '@components/CustomPopover'; const useClassType = (subject: string) => { const property = useResource<Core.Property>(subject); @@ -65,17 +61,20 @@ function AtomicURLCellEdit({ property, resource: row, }: EditCellProps<JSONValue>): JSX.Element { + const inputRef = useRef<HTMLInputElement>(null); const cell = useResource(value as string); const { classType, hasClassType } = useClassType(property); const [title] = useTitle(cell); - const [open, setOpen] = useState(true); - const { setCursorMode } = useTableEditorContext(); + const { triggerProps, popoverProps, isOpen, closePopover } = usePopover({ + defaultOpen: true, + autoFocusElement: inputRef, + }); const selectedElement = useRef<HTMLLIElement>(null); const [searchValue, setSearchValue] = useState(''); const cellOptions = useMemo(() => { - if (open) { + if (isOpen) { return { disabledKeyboardInteractions: new Set([ KeyboardInteraction.ExitEditMode, @@ -84,7 +83,7 @@ function AtomicURLCellEdit({ } else { return {}; } - }, [open]); + }, [isOpen]); useCellOptions(cellOptions); @@ -97,61 +96,52 @@ function AtomicURLCellEdit({ const handleResultClick = useCallback( (result: string) => { onChange(result); - setOpen(false); + closePopover(); }, - [onChange], + [onChange, closePopover], ); - const handleOpenChange = useCallback( - (state: boolean) => { - setOpen(state); - - if (!state) { - setCursorMode(CursorMode.Visual); - } - }, - [setCursorMode], + const { + results, + selectedIndex, + handleKeyDown, + onMouseOver, + onClick, + usingKeyboard, + } = useResourceSearch( + searchValue, + hasClassType ? classType.subject : undefined, + handleResultClick, ); - const { results, selectedIndex, handleKeyDown, onMouseOver, onClick } = - useResourceSearch( - searchValue, - hasClassType ? classType.subject : undefined, - setOpen, - handleResultClick, - ); - const handleFilesUploaded = useCallback( (files: string[]) => { const file = files[0]; if (file) { onChange(file); - setOpen(false); + closePopover(); } }, - [onChange, setOpen], + [onChange, closePopover], ); - const Trigger = useCallback( - (props: TriggerProps) => { - return ( - <PopoverTrigger {...props}> - <FaEdit />{' '} - {cell.subject === unknownSubject - ? `select ${hasClassType ? classType.title : 'resource'}` - : title} - </PopoverTrigger> - ); - }, - [title, cell, classType, hasClassType], - ); + const Trigger = useMemo(() => { + return ( + <PopoverTrigger {...triggerProps}> + <FaEdit />{' '} + {cell.subject === unknownSubject + ? `select ${hasClassType ? classType.title : 'resource'}` + : title} + </PopoverTrigger> + ); + }, [title, cell, classType, hasClassType, triggerProps]); useEffect(() => { - if (selectedElement.current) { + if (selectedElement.current && usingKeyboard) { selectedElement.current.scrollIntoView({ block: 'nearest' }); } - }, [selectedIndex]); + }, [selectedIndex, usingKeyboard]); const placehoder = hasClassType ? `Search ${classType.title}` : 'Search...'; @@ -161,13 +151,7 @@ function AtomicURLCellEdit({ results.length === 0 && classType.subject !== server.classes.file; return ( - <SearchPopover - modal - Trigger={Trigger} - open={open} - onOpenChange={handleOpenChange} - noLock - > + <SearchPopover Trigger={Trigger} noLock {...popoverProps}> <InputWrapper> <InputStyled type='search' @@ -175,6 +159,7 @@ function AtomicURLCellEdit({ placeholder={placehoder} onChange={handleChange} onKeyDown={handleKeyDown} + ref={inputRef} /> </InputWrapper> <SearchResultWrapper> diff --git a/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx b/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx index 429940995..61b9d8989 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx @@ -21,7 +21,6 @@ export const AbsoluteCell = styled.div` export const SearchPopover = styled(CustomPopover)` border: 1px solid ${p => p.theme.colors.bg2}; - background-color: ${p => p.theme.colors.bg}; ${CustomPopover.Content} { padding: 1rem; display: flex; @@ -37,7 +36,7 @@ export const SearchResultWrapper = styled.div` overflow-y: auto; ol { - padding: 0; + padding: 0 !important; margin: 0; } @@ -46,7 +45,7 @@ export const SearchResultWrapper = styled.div` &[data-selected='true'] button { background: ${p => p.theme.colors.mainSelectedBg}; color: ${p => p.theme.colors.mainSelectedFg}; - + box-shadow: 0 0 0 1px inset ${p => p.theme.colors.mainSelectedFg}; svg { color: ${p => p.theme.colors.mainSelectedFg}; } diff --git a/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx index 9fb022749..0cd7349b6 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx @@ -34,7 +34,7 @@ import { Row } from '../../../components/Row'; import { Checkbox } from '../../../components/forms/Checkbox'; import { ResourceCell } from './ResourceCells/ResourceCell'; import { AtomicLink } from '../../../components/AtomicLink'; -import type { TriggerProps } from '@components/CustomPopover'; +import { usePopover } from '@components/CustomPopover'; import { CELL_WIDTH } from '@components/TableEditor/Cell'; const useClassType = (subject: string) => { @@ -54,10 +54,13 @@ function MultiRelationCellEdit({ onChange, property, }: EditCellProps<JSONValue>): JSX.Element { + const inputRef = useRef<HTMLInputElement>(null); const val = Array.isArray(value) ? value : []; - const { classType, hasClassType } = useClassType(property); - const [open, setOpen] = useState(true); + const { isOpen, triggerProps, popoverProps } = usePopover({ + defaultOpen: true, + autoFocusElement: inputRef, + }); const { activeCellRef } = useTableEditorContext(); const selectedElement = useRef<HTMLLIElement>(null); @@ -67,7 +70,7 @@ function MultiRelationCellEdit({ KeyboardInteraction.EditNextRow, ]); - if (open) { + if (isOpen) { disabledKeyboardInteractions.add(KeyboardInteraction.ExitEditMode); } @@ -96,37 +99,31 @@ function MultiRelationCellEdit({ onChange(val.filter(v => v !== subject)); }; - const handleOpenChange = (state: boolean) => { - setOpen(state); - }; - - const { results, selectedIndex, handleKeyDown, onMouseOver, onClick } = - useResourceSearch( - searchValue, - hasClassType ? classType.subject : undefined, - setOpen, - handleResultClick, - ); - - const Trigger = (props: TriggerProps) => { - return ( - <IconButton title='Add resource' {...props}> - <FaPlus /> - </IconButton> - ); - }; + const { + results, + selectedIndex, + handleKeyDown, + onMouseOver, + onClick, + usingKeyboard, + } = useResourceSearch( + searchValue, + hasClassType ? classType.subject : undefined, + handleResultClick, + val as string[], + ); useEffect(() => { - if (!open) { + if (!isOpen) { activeCellRef.current?.focus(); } - }, [open, activeCellRef]); + }, [isOpen, activeCellRef]); useEffect(() => { - if (selectedElement.current) { + if (selectedElement.current && usingKeyboard) { selectedElement.current.scrollIntoView({ block: 'nearest' }); } - }, [selectedIndex]); + }, [selectedIndex, usingKeyboard]); const placehoder = hasClassType ? `Search ${classType.title}` : 'Search...'; @@ -136,19 +133,14 @@ function MultiRelationCellEdit({ return ( <AbsoluteCell> <Row wrapItems gap='1ch'> - {(val as string[])?.map(subject => ( - <ResourceItemButton - subject={subject} - key={subject} - onRemove={handleRemoveItem} - /> - ))} <SearchPopover - modal - Trigger={Trigger} - open={open} - onOpenChange={handleOpenChange} noLock + Trigger={ + <IconButton title='Add resource' {...triggerProps}> + <FaPlus /> + </IconButton> + } + {...popoverProps} > <InputWrapper> <InputStyled @@ -157,6 +149,7 @@ function MultiRelationCellEdit({ placeholder={placehoder} onChange={handleChange} onKeyDown={handleKeyDown} + ref={inputRef} /> </InputWrapper> <SearchResultWrapper> @@ -181,6 +174,13 @@ function MultiRelationCellEdit({ {showNoResults && 'No results'} </SearchResultWrapper> </SearchPopover> + {(val as string[])?.map(subject => ( + <ResourceItemButton + subject={subject} + key={subject} + onRemove={handleRemoveItem} + /> + ))} </Row> </AbsoluteCell> ); diff --git a/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx index 63da12c0e..16e5c6415 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx @@ -6,7 +6,7 @@ import { useResource, useStore, } from '@tomic/react'; -import { memo, useEffect, useState, type JSX } from 'react'; +import { useEffect, useRef, useState, type JSX } from 'react'; import { FaPlus } from 'react-icons/fa'; import { styled } from 'styled-components'; import { IconButton } from '../../../components/IconButton/IconButton'; @@ -27,7 +27,7 @@ import { import { useTableEditorContext } from '../../../components/TableEditor/TableEditorContext'; import { AbsoluteCell } from './CellComponents'; import { FaXmark } from 'react-icons/fa6'; -import { CustomPopover } from '@components/CustomPopover'; +import { CustomPopover, usePopover } from '@components/CustomPopover'; const TAG_SPACING = '0.5rem'; @@ -48,29 +48,12 @@ function buildListWithTitles( }); } -const Trigger: React.FC<{ popoverTarget: string }> = memo( - props => { - return ( - <IconButton - title='Add tag' - type='button' - onClick={e => e.stopPropagation()} - {...props} - > - <StyledIcon /> - </IconButton> - ); - }, - (prev, next) => prev.popoverTarget === next.popoverTarget, -); - -Trigger.displayName = 'Trigger'; - function SelectCellEdit({ value, property, onChange, }: EditCellProps<JSONValue>): JSX.Element { + const inputRef = useRef<HTMLInputElement>(null); const val = (value as string[]) ?? emptyArray; const store = useStore(); const propertyResource = useResource(property); @@ -81,7 +64,10 @@ function SelectCellEdit({ .filter(v => v.title.includes(query)) .map(ft => ft.subject); - const [open, setOpen] = useState(true); + const { isOpen, closePopover, triggerProps, popoverProps } = usePopover({ + defaultOpen: true, + autoFocusElement: inputRef, + }); const [selectedIndex, setSelectedIndex] = useState(0); const { activeCellRef } = useTableEditorContext(); @@ -90,7 +76,7 @@ function SelectCellEdit({ KeyboardInteraction.EditNextRow, ]); - if (open) { + if (isOpen) { disabledKeyboardInteractions.add(KeyboardInteraction.ExitEditMode); } @@ -119,10 +105,10 @@ function SelectCellEdit({ }; useEffect(() => { - if (!open) { + if (!isOpen) { activeCellRef.current?.focus(); } - }, [activeCellRef, open]); + }, [activeCellRef, isOpen]); const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { switch (e.key) { @@ -141,7 +127,7 @@ function SelectCellEdit({ case 'Escape': e.preventDefault(); - setOpen(false); + closePopover(); break; } }; @@ -160,22 +146,20 @@ function SelectCellEdit({ </Tag> ))} <CustomPopover - modal - open={open} noLock - onOpenChange={setOpen} - Trigger={props => ( - <IconButton title='Add tag' type='button' {...props}> + Trigger={ + <IconButton title='Add tag' type='button' {...triggerProps}> <StyledIcon /> </IconButton> - )} + } + {...popoverProps} > <Content onKeyDown={handleKeyDown}> <SearchInputWrapper> <InputStyled placeholder='Filter tags...' onChange={handleSearch} - autoFocus + ref={inputRef} /> </SearchInputWrapper> <ResultWrapper> @@ -188,6 +172,7 @@ function SelectCellEdit({ selected={i === selectedIndex} /> ))} + {filteredTags.length === 0 && 'No results'} </Row> </ResultWrapper> </Content> @@ -240,6 +225,11 @@ const Content = styled.div` const ResultWrapper = styled.div` padding: ${p => p.theme.margin}rem; + border: ${p => + p.theme.darkMode ? '1px solid ' + p.theme.colors.bg2 : 'none'}; + border-top: none; + border-bottom-left-radius: ${p => p.theme.radius}; + border-bottom-right-radius: ${p => p.theme.radius}; `; const SearchInputWrapper = styled(InputWrapper)` diff --git a/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts b/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts index 65e4ff5fb..1750463d2 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts +++ b/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts @@ -6,8 +6,8 @@ import { useSelectedIndex } from '@hooks/useSelectedIndex'; export function useResourceSearch( searchValue: string, classType: string | undefined, - setOpen: (state: boolean) => void, onResultPick: (result: string) => void, + valuesWhenEmpty: string[] = [], ) { const { drive } = useSettings(); @@ -21,15 +21,18 @@ export function useResourceSearch( ); const { results } = useServerSearch(searchValue, searchOpts); - const { selectedIndex, onKeyDown, onMouseOver, onClick } = useSelectedIndex( - results, - i => { - if (i === undefined) return; + const list = + !searchValue && valuesWhenEmpty !== undefined ? valuesWhenEmpty : results; + const { selectedIndex, onKeyDown, onMouseOver, onClick, usingKeyboard } = + useSelectedIndex( + list, + i => { + if (i === undefined) return; - onResultPick(results[i]); - }, - { initialIndex: 0, key: searchValue }, - ); + onResultPick(list[i]); + }, + { initialIndex: 0, key: searchValue }, + ); const handleKeyDown = useCallback( (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Tab') { @@ -47,10 +50,11 @@ export function useResourceSearch( ); return { - results, + results: list, selectedIndex, handleKeyDown, onMouseOver, onClick, + usingKeyboard, }; } diff --git a/browser/data-browser/vite.config.ts b/browser/data-browser/vite.config.ts index fdc654d13..84137064c 100644 --- a/browser/data-browser/vite.config.ts +++ b/browser/data-browser/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, type PluginOption } from 'vite'; +import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; import webfontDownload from 'vite-plugin-webfont-dl'; diff --git a/browser/e2e/tests/documents.spec.ts b/browser/e2e/tests/documents.spec.ts index 5168f28d6..b3dbca982 100644 --- a/browser/e2e/tests/documents.spec.ts +++ b/browser/e2e/tests/documents.spec.ts @@ -27,31 +27,54 @@ test.describe('documents', async () => { const teststring = `My test: ${timestamp()}`; - await page.locator('textarea').fill(teststring); + await expect(page.getByText('loading...')).not.toBeVisible(); - await expect(page.locator(`text=${teststring}`)).toBeVisible(); + const editor = page.getByLabel('Rich Text Editor'); + + await editor.fill('/heading'); + await expect(page.getByText('Heading 1')).toBeVisible(); + await page.keyboard.press('Enter'); + await page.keyboard.type(teststring); + + await expect( + page.getByRole('heading', { name: teststring, exact: true }), + ).toBeVisible(); // multi-user const currentSubject = await getCurrentSubject(page); - const page2 = await openNewSubjectWindow(browser, currentSubject!); + const page2 = await openNewSubjectWindow(browser, currentSubject!, true); + + await expect(page2.getByText('loading...')).not.toBeVisible(); await expect( - page2.locator(`text=${teststring}`), + page2.getByRole('heading', { name: teststring, exact: true }), 'First paragraph title not visible in second tab. Not a websocket issue', ).toBeVisible(); expect(await page2.title()).toEqual(title); + await page2.getByLabel('Rich Text Editor').focus(); + await page2.keyboard.press('ArrowDown'); // Add a new line on first page, check if it appears on the second - await page.keyboard.press('Enter'); const syncText = 'New paragraph'; - await page.keyboard.type(syncText); + await page2.keyboard.type(syncText); + await expect( - page2.locator(`text=${syncText}`), - 'New paragraph not found in second window. Websockets may not be working.', + page.locator(`text=${syncText}`), + 'New paragraph not found in first window. Websockets may not be working.', ).toBeVisible(); // Delete a row, cmd + backspace - await page.keyboard.down('Alt'); - await page.keyboard.press('Backspace'); + await page2.getByText(syncText).selectText(); + + // Test if page1 can see the cursor of page2 + await expect( + page.getByLabel('Rich Text Editor').getByText('Test user edited'), + ).toBeVisible(); + + // Delete the word paragraph. + await page2.keyboard.press('ArrowRight'); + await page2.keyboard.down('Alt'); + await page2.keyboard.press('Backspace'); + await expect( page.locator(`text=${syncText}`), 'Paragraph not deleted in first window.', diff --git a/browser/e2e/tests/e2e.spec.ts b/browser/e2e/tests/e2e.spec.ts index f16245a1f..e79b49fb0 100644 --- a/browser/e2e/tests/e2e.spec.ts +++ b/browser/e2e/tests/e2e.spec.ts @@ -31,8 +31,6 @@ import { waitForCommitOnCurrentResource, clickSidebarItem, inDialog, - PROPERTIES, - anyValue, } from './test-utils'; test.describe('data-browser', async () => { @@ -72,6 +70,11 @@ test.describe('data-browser', async () => { }); test('sign up and edit document atomicdata.dev', async ({ page }) => { + test.fixme( + true, + 'This test needs to be updated when atomicdata.dev has the new document editor.', + ); + await openAtomic(page); // Use invite await clickSidebarItem(DEMO_INVITE_NAME, page); @@ -469,11 +472,11 @@ test.describe('data-browser', async () => { 'https://atomicdata.dev/properties/localId': localID, 'https://atomicdata.dev/properties/name': name, }; - await page.fill( - '[placeholder="Paste your JSON-AD..."]', - JSON.stringify(importStr), - ); - await page.click('[data-test="import-post"]'); + await expect(page.getByRole('button', { name: 'Import' })).toBeDisabled(); + await page + .getByPlaceholder('Paste your JSON-AD...') + .pressSequentially(JSON.stringify(importStr)); + await page.getByRole('button', { name: 'Import' }).click(); await expect(page.locator('text=Imported!')).toBeVisible(); // get current url, append the localID @@ -542,17 +545,9 @@ test.describe('data-browser', async () => { // }, // }); - // commit for initializing the first element (paragraph) - const addParagraphCommit = waitForCommit(page, { - set: { - ['https://atomicdata.dev/properties/documents/elements']: anyValue, - }, - }); // Create new class from new resource menu await newResource('document', page); - await addParagraphCommit; - const firstTitleCommit = waitForCommit(page, { set: { ['https://atomicdata.dev/properties/name']: 'First Title', diff --git a/browser/e2e/tests/search.spec.ts b/browser/e2e/tests/search.spec.ts index ddae1bfed..eb92b4c26 100644 --- a/browser/e2e/tests/search.spec.ts +++ b/browser/e2e/tests/search.spec.ts @@ -2,7 +2,6 @@ import { test, expect } from '@playwright/test'; import { signIn, newDrive, - waitForCommit, before, REBUILD_INDEX_TIME, addressBar, @@ -13,7 +12,6 @@ import { contextMenuClick, timestamp, newResource, - anyValue, } from './test-utils'; test.describe('search', async () => { test.beforeEach(before); @@ -45,14 +43,8 @@ test.describe('search', async () => { await setTitle(page, 'Salad folder'); // Create document called 'Avocado Salad' - const addParagraphCommit = waitForCommit(page, { - set: { - ['https://atomicdata.dev/properties/documents/elements']: anyValue, - }, - }); await page.locator('button:has-text("New Resource")').click(); await page.locator('button:has-text("document")').click(); - await addParagraphCommit; await editTitle('Avocado Salad', page); @@ -63,15 +55,8 @@ test.describe('search', async () => { await setTitle(page, 'Cake Folder'); // Create document called 'Avocado Cake' - - const addParagraphCommit2 = waitForCommit(page, { - set: { - ['https://atomicdata.dev/properties/documents/elements']: anyValue, - }, - }); await page.locator('button:has-text("New Resource")').click(); await page.locator('button:has-text("document")').click(); - await addParagraphCommit2; await editTitle('Avocado Cake', page); diff --git a/browser/e2e/tests/template.spec.ts b/browser/e2e/tests/template.spec.ts index 54fb0a1dd..c4308f263 100644 --- a/browser/e2e/tests/template.spec.ts +++ b/browser/e2e/tests/template.spec.ts @@ -1,5 +1,4 @@ import { expect, test } from '@playwright/test'; -import AxeBuilder from '@axe-core/playwright'; import { exec } from 'child_process'; import { before, @@ -137,6 +136,10 @@ test.describe('Test create-template package', () => { test.beforeEach(before); test('apply next-js template', async ({ page }) => { + test.fixme( + true, + 'Template needs to be updated to Next.js 16 because we require React 19.2.0 or above.', + ); test.slow(); await signIn(page); const drive = await newDrive(page); @@ -171,21 +174,12 @@ test.describe('Test create-template package', () => { const response = await page.goto(url); expect(response?.status()).toBe(200); - // Check if home is following wcag AA standards - const homeScanResults = await new AxeBuilder({ page }).analyze(); - - expect(homeScanResults.violations).toEqual([]); - await expect(page.locator('body')).toContainText( 'This is a template site generated with @tomic/template.', ); await page.goto(`${url}/blog`); - // Check if blog is following wcag AA standards - const blogScanResults = await new AxeBuilder({ page }).analyze(); - expect(blogScanResults.violations).toEqual([]); - // Search for a blogpost const searchInput = page.getByRole('searchbox'); @@ -232,21 +226,12 @@ test.describe('Test create-template package', () => { const response = await page.goto(url); expect(response?.status()).toBe(200); - // Check if home is following wcag AA standards - const homeScanResults = await new AxeBuilder({ page }).analyze(); - - expect(homeScanResults.violations).toEqual([]); - await expect(page.locator('body')).toContainText( 'This is a template site generated with @tomic/template.', ); await page.goto(`${url}/blog`); - // Check if blog is following wcag AA standards - const blogScanResults = await new AxeBuilder({ page }).analyze(); - expect(blogScanResults.violations).toEqual([]); - // Search for a blogpost const searchInput = page.getByRole('searchbox'); await searchInput.fill('balloon'); diff --git a/browser/e2e/tests/test-utils.ts b/browser/e2e/tests/test-utils.ts index 0371fb6f9..9b0018969 100644 --- a/browser/e2e/tests/test-utils.ts +++ b/browser/e2e/tests/test-utils.ts @@ -278,11 +278,19 @@ export async function newResource(klass: string, page: Page) { } /** Opens a new browser page (for) */ -export async function openNewSubjectWindow(browser: Browser, url: string) { +export async function openNewSubjectWindow( + browser: Browser, + url: string, + doSignIn: boolean = false, +) { const context2 = await browser.newContext(); const page = await context2.newPage(); await page.goto(FRONTEND_URL); + if (doSignIn) { + await signIn(page); + } + // Only when we run on `localhost` we don't need to change drive during tests if (SERVER_URL !== FRONTEND_URL) { try { diff --git a/browser/eslint.config.js b/browser/eslint.config.js index af9b5a127..8dc11c8ed 100644 --- a/browser/eslint.config.js +++ b/browser/eslint.config.js @@ -107,7 +107,8 @@ export default defineConfig([ 'react-hooks/set-state-in-effect': 'warn', 'react-hooks/static-components': 'off', // This rule is way to aggressive and seems to be designed for people that don't understand refs. - 'react-hooks/refs': 'off', + // But it looks like sometimes it matters for react compiler so we'll set it to warn instead. + 'react-hooks/refs': 'warn', } } ]); diff --git a/browser/react/package.json b/browser/react/package.json index 950866439..e9976ce8e 100644 --- a/browser/react/package.json +++ b/browser/react/package.json @@ -26,8 +26,8 @@ "yjs": "^13.6.27" }, "peerDependencies": { - "react": ">18.3.0", - "react-dom": ">18.3.0" + "react": ">19.2.0", + "react-dom": ">19.2.0" }, "files": [ "dist", diff --git a/browser/react/src/useServerSearch.tsx b/browser/react/src/useServerSearch.tsx index 79f88a606..38e9ff510 100644 --- a/browser/react/src/useServerSearch.tsx +++ b/browser/react/src/useServerSearch.tsx @@ -1,7 +1,8 @@ import { removeCachedSearchResults, SearchOpts } from '@tomic/lib'; -import { useEffect, useState } from 'react'; +import { useEffect, useEffectEvent, useState } from 'react'; import { useStore } from './index.js'; import { useDebounce } from './useDebounce.js'; +import { useOnValueChange } from './helpers/useOnValueChange.js'; interface SearchResults { /** Subject URLs for resources that match the query */ @@ -34,16 +35,28 @@ export function useServerSearch( const [error, setError] = useState<Error | undefined>(undefined); const [loading, setLoading] = useState(false); const debouncedQuery = useDebounce(query, debounce) ?? ''; - const [prevDebounedQuery, setPrevDebounedQuery] = useState(debouncedQuery); - if (prevDebounedQuery !== debouncedQuery) { - setPrevDebounedQuery(debouncedQuery); - setLoading(true); + useOnValueChange(() => { + if (debouncedQuery) { + setLoading(true); + } if (!debouncedQuery && !allowEmptyQuery) { setResults([]); + setLoading(false); } - } + }, [debouncedQuery, allowEmptyQuery]); + + const updateResults = useEffectEvent( + (r: string[], relevantQuery: string, relevantOpts: SearchOpts) => { + // If the query became empty since the last fetch, don't update the results + if (relevantQuery !== debouncedQuery || relevantOpts !== searchOpts) { + return; + } + + setResults(r); + }, + ); useEffect(() => { if (!debouncedQuery && !allowEmptyQuery) { @@ -53,7 +66,7 @@ export function useServerSearch( store .search(debouncedQuery, searchOpts) .then(r => { - setResults(r); + updateResults(r, debouncedQuery, searchOpts); setError(undefined); }) .catch(e => { From 993bed27a416f06fdd3cba32b45651a8f18d769f Mon Sep 17 00:00:00 2001 From: Polle Pas <polleps@gmail.com> Date: Thu, 20 Nov 2025 15:56:55 +0100 Subject: [PATCH 8/8] Some document fixes and better document test --- .../src/chunks/RTE/CollaborativeEditor.tsx | 10 +-- .../data-browser/src/chunks/RTE/ColorMenu.tsx | 7 +- .../src/chunks/RTE/FullBubbleMenu.tsx | 9 +- .../ResourceExtension/ResourceExtention.ts | 19 ++--- .../src/chunks/RTE/SlashMenu/CommandList.tsx | 54 ++++++++---- .../data-browser/src/chunks/RTE/useYSync.ts | 43 ++++++---- browser/data-browser/src/components/Main.tsx | 4 +- .../src/components/Navigation.tsx | 2 + .../data-browser/src/components/Parent.tsx | 59 +++++++------ .../data-browser/src/components/Popover.tsx | 83 +++++-------------- .../src/components/ScrollArea.tsx | 6 +- browser/data-browser/src/locales/de.po | 6 +- browser/data-browser/src/locales/en.po | 6 +- browser/data-browser/src/locales/es.po | 6 +- browser/data-browser/src/locales/fr.po | 6 +- .../src/views/BookmarkPage/BookmarkPage.tsx | 1 + .../src/views/FolderPage/ListView.tsx | 2 - .../src/views/OntologyPage/OntologyPage.tsx | 2 +- browser/e2e/tests/documents.spec.ts | 34 ++++++-- browser/e2e/tests/filePicker.spec.ts | 4 +- browser/e2e/tests/search.spec.ts | 6 +- browser/e2e/tests/test-utils.ts | 4 + 22 files changed, 197 insertions(+), 176 deletions(-) diff --git a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx index 104e2a693..bc2d9d1e2 100644 --- a/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx +++ b/browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx @@ -159,8 +159,8 @@ export default function CollaborativeEditor({ title: 'Resource', id: 'resource', icon: FaLink, - command: ({ range }) => - editor + command: ({ range, editor: internalEditor }) => + internalEditor .chain() .focus() .deleteRange(range) @@ -171,11 +171,11 @@ export default function CollaborativeEditor({ title: 'Data Table', id: 'data-table', icon: FaTable, - command: ({ range }) => { + command: ({ range, editor: internalEditor }) => { showNewResourceUI(dataBrowser.classes.table, resource.subject, { skipNavigation: true, onCreated: table => { - editor + internalEditor .chain() .focus() .deleteRange(range) @@ -255,7 +255,7 @@ export default function CollaborativeEditor({ }, }, }, - [canWrite], + [canWrite, drive], ); useEffect(() => { diff --git a/browser/data-browser/src/chunks/RTE/ColorMenu.tsx b/browser/data-browser/src/chunks/RTE/ColorMenu.tsx index b3e2d3a37..b8ce0a95f 100644 --- a/browser/data-browser/src/chunks/RTE/ColorMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/ColorMenu.tsx @@ -4,7 +4,7 @@ import { MdFormatColorFill, MdFormatColorText } from 'react-icons/md'; import { useLocalStorage } from '@hooks/useLocalStorage'; import styled from 'styled-components'; import { transition } from '@helpers/transition'; -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { useEditorState } from '@tiptap/react'; import { FaPencil } from 'react-icons/fa6'; import { desaturate, readableColor, setLightness } from 'polished'; @@ -83,6 +83,11 @@ export const ColorMenu: React.FC = () => { event.preventDefault(); }; + useEffect(() => { + // The bubble menu might need to be repositioned if this component is shown. + editor.commands.setMeta('bubbleMenu', 'updatePosition'); + }, [editor]); + return ( <Column> <Row center> diff --git a/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx b/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx index ec1c5fdb8..90135c778 100644 --- a/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx +++ b/browser/data-browser/src/chunks/RTE/FullBubbleMenu.tsx @@ -12,7 +12,6 @@ import { useEditorState } from '@tiptap/react'; import { ToggleButton } from './ToggleButton'; import { useState } from 'react'; import { ColorMenu } from './ColorMenu'; -import { flushSync } from 'react-dom'; export const FullBubbleMenu: React.FC = () => { const editor = useTipTapEditor(); @@ -55,12 +54,8 @@ export const FullBubbleMenu: React.FC = () => { <BubbleMenu extraItems={<>{colorMenuOpen && <ColorMenu />}</>} onShow={() => { - flushSync(() => { - const style = editor.getAttributes('textStyle'); - setColorMenuOpen(!!style.color || !!style.backgroundColor); - - editor.commands.setMeta('bubbleMenu', 'updatePosition'); - }); + const style = editor.getAttributes('textStyle'); + setColorMenuOpen(!!style.color || !!style.backgroundColor); }} > <Separator /> diff --git a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts index 1dde04b2f..bf0045bdc 100644 --- a/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts +++ b/browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceExtention.ts @@ -38,7 +38,7 @@ export const buildResourceSuggestion = ( drive: string, ): Partial<SuggestionOptions> => ({ items: async ({ query }: { query: string }): Promise<SuggestionItem[]> => { - const results = await store.search(query, { + const results = await store.search(query.toLowerCase(), { limit: 10, // Including the results could lead to weird behavior when the document itself is returned from the server. include: false, @@ -53,11 +53,7 @@ export const buildResourceSuggestion = ( icon: getIconForClass(r.getClasses()[0]), command: ({ editor, range }) => { const subject = r.subject; - const textBeforeQuery = getTextBeforeQuery(editor, range); - - // If there is text before the query we are in not in a block context and the resource should be inserted inline. - const isBlockContext = textBeforeQuery.length === 0; - + const isBlockContext = getIsBlockContext(editor, range); const command = editor.chain().focus().deleteRange(range); if (isBlockContext) { @@ -72,17 +68,12 @@ export const buildResourceSuggestion = ( render: createRenderFunction<SuggestionItem>(container), }); -const getTextBeforeQuery = (editor: Editor, range: Range) => { +const getIsBlockContext = (editor: Editor, range: Range) => { const { from } = range; - const queryText = editor.state.doc.textBetween(range.from, range.to); - // Resolve the position and the parent node const $pos = editor.state.doc.resolve(from); - const parentNode = $pos.parent; - - // Calculate the offset within the parent node where the query starts - const startOfQueryOffset = $pos.parentOffset - queryText.length; - return parentNode.textContent.substring(0, startOfQueryOffset).trim(); + // Text offset tells us the distance to a previous node. This is 0 if there is no previous node meaning we are in a block context. + return $pos.textOffset === 0; }; diff --git a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx index e568cc337..6a71e3b30 100644 --- a/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx +++ b/browser/data-browser/src/chunks/RTE/SlashMenu/CommandList.tsx @@ -10,6 +10,7 @@ import { styled } from 'styled-components'; import { ScrollArea } from '../../../components/ScrollArea'; import type { SuggestionItem } from '../types'; import { useOnValueChange } from '@helpers/useOnValueChange'; +import { Column } from '@components/Row'; export type CommandListRefType = { onKeyDown: (event: KeyboardEvent) => boolean; @@ -82,24 +83,26 @@ export const CommandList = forwardRef<CommandListRefType, CommandListProps>( ); return ( - <ScrollingList type='hover'> - {items.length === 0 && <div>No results found</div>} - {items.map((item, index) => { - const Icon = item.icon; - - return ( - <ListItemButton - key={item.id} - id={buildItemId(compId, index)} - onClick={() => selectItem(index)} - onMouseEnter={() => setSelectedIndex(index)} - active={selectedIndex === index} - > - <Icon /> - {item.title} - </ListItemButton> - ); - })} + <ScrollingList type='hover' data-testid='rte-command-list'> + <ContainedColumn gap='0'> + {items.length === 0 && <div>No results found</div>} + {items.map((item, index) => { + const Icon = item.icon; + + return ( + <ListItemButton + key={item.id} + id={buildItemId(compId, index)} + onClick={() => selectItem(index)} + onMouseEnter={() => setSelectedIndex(index)} + active={selectedIndex === index} + > + <Icon /> + <span>{item.title}</span> + </ListItemButton> + ); + })} + </ContainedColumn> </ScrollingList> ); }, @@ -134,4 +137,19 @@ const ListItemButton = styled.button<{ active: boolean }>` gap: 1ch; padding: 0.5rem; border-radius: ${p => p.theme.radius}; + max-width: 60ch; + overflow: hidden; + + & > svg { + min-width: 1rem; + flex-basis: 1rem; + } + + & > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } `; + +const ContainedColumn = styled(Column)``; diff --git a/browser/data-browser/src/chunks/RTE/useYSync.ts b/browser/data-browser/src/chunks/RTE/useYSync.ts index 3b04dcd8d..ab71731a4 100644 --- a/browser/data-browser/src/chunks/RTE/useYSync.ts +++ b/browser/data-browser/src/chunks/RTE/useYSync.ts @@ -18,28 +18,30 @@ export function useYSync( const awareness = new awarenessProtocol.Awareness(doc); useEffect(() => { - awareness.on( - 'update', - ({ added, updated, removed }: AwarenessUpdate, origin: string) => { - if (origin !== 'local') { - // Only send local updates to the server. - return; - } + const handleAwarenessUpdate = ( + { added, updated, removed }: AwarenessUpdate, + origin: string, + ) => { + if (origin !== 'local') { + // Only send local updates to the server. + return; + } - const changedClients = [...updated, ...added, ...removed]; + const changedClients = [...updated, ...added, ...removed]; - const encodedUpdate = awarenessProtocol.encodeAwarenessUpdate( - awareness, - changedClients, - ); + const encodedUpdate = awarenessProtocol.encodeAwarenessUpdate( + awareness, + changedClients, + ); - store.broadcastYSyncUpdate(resource.subject, property, { - awarenessUpdate: encodedUpdate, - }); - }, - ); + store.broadcastYSyncUpdate(resource.subject, property, { + awarenessUpdate: encodedUpdate, + }); + }; + + awareness.on('update', handleAwarenessUpdate); - return store.subscribeYSync( + const unsubYSync = store.subscribeYSync( resource.subject, property, ({ awarenessUpdate, docUpdate }) => { @@ -56,6 +58,11 @@ export function useYSync( } }, ); + + return () => { + awareness.off('update', handleAwarenessUpdate); + unsubYSync(); + }; }, [awareness, resource.subject, property, store, doc]); useEffect(() => { diff --git a/browser/data-browser/src/components/Main.tsx b/browser/data-browser/src/components/Main.tsx index afef4608b..466d6d7d3 100644 --- a/browser/data-browser/src/components/Main.tsx +++ b/browser/data-browser/src/components/Main.tsx @@ -6,7 +6,6 @@ import { transitionName, } from '../helpers/transitionName'; import { ViewTransitionProps } from '../helpers/ViewTransitionProps'; -import { MAIN_CONTAINER } from '../helpers/containers'; import Parent from './Parent'; import { useResource } from '@tomic/react'; import { CalculatedPageHeight } from '../globalCssVars'; @@ -35,7 +34,6 @@ export function Main({ } const StyledMain = memo(styled.main<ViewTransitionProps>` - container: ${MAIN_CONTAINER} / inline-size; ${p => transitionName(RESOURCE_PAGE_TRANSITION_TAG, p.subject)}; height: calc( ${CalculatedPageHeight.var()} - ${p => p.theme.heights.breadCrumbBar} @@ -45,7 +43,7 @@ const StyledMain = memo(styled.main<ViewTransitionProps>` ${p => p.theme.heights.breadCrumbBar} + ${p => p.theme.size(2)} ); - width: 100%; + width: 100cqw; @media (prefers-reduced-motion: no-preference) { scroll-behavior: smooth; diff --git a/browser/data-browser/src/components/Navigation.tsx b/browser/data-browser/src/components/Navigation.tsx index 93aa09437..a20b06172 100644 --- a/browser/data-browser/src/components/Navigation.tsx +++ b/browser/data-browser/src/components/Navigation.tsx @@ -17,6 +17,7 @@ import { CalculatedPageHeight } from '../globalCssVars'; import { AISidebarContextProvider } from './AI/AISidebarContext'; import { AISidebarContainer } from './AI/AISidebarContainer'; import { HideInPrint } from './HideInPrint'; +import { MAIN_CONTAINER } from '@helpers/containers'; export const NAVBAR_HEIGHT = '2.5rem'; @@ -71,6 +72,7 @@ interface ContentProps { const Content = styled.div<ContentProps>` display: block; flex: 1; + container: ${MAIN_CONTAINER} / inline-size; `; /** Persistently shown navigation bar */ diff --git a/browser/data-browser/src/components/Parent.tsx b/browser/data-browser/src/components/Parent.tsx index fd5e220c9..b9d6626d1 100644 --- a/browser/data-browser/src/components/Parent.tsx +++ b/browser/data-browser/src/components/Parent.tsx @@ -34,31 +34,28 @@ function Parent({ resource }: ParentProps): JSX.Element { return ( <ParentWrapper aria-label='Breadcrumbs'> - <Row fullWidth center gap='initial'> - {parent ? ( - <NestedParent subject={parent} depth={0} /> - ) : ( - <DriveMismatch subject={resource.subject} /> - )} + {!parent && <DriveMismatch subject={resource.subject} />} + <BreadcrumbRow center gap='initial'> + {parent && <NestedParent subject={parent} depth={0} />} <BreadCrumbCurrent>{resource.title}</BreadCrumbCurrent> - <Spacer /> - <ButtonArea> - {enableAI && ( - <IconButton - title='Toggle AI panel' - variant={IconButtonVariant.Magic} - onClick={() => setIsOpen(prev => !prev)} - > - <AIIcon /> - </IconButton> - )} - <ResourceContextMenu - isMainMenu - subject={resource.subject} - trigger={MenuBarDropdownTrigger} - /> - </ButtonArea> - </Row> + </BreadcrumbRow> + <Spacer /> + <ButtonArea> + {enableAI && ( + <IconButton + title='Toggle AI panel' + variant={IconButtonVariant.Magic} + onClick={() => setIsOpen(prev => !prev)} + > + <AIIcon /> + </IconButton> + )} + <ResourceContextMenu + isMainMenu + subject={resource.subject} + trigger={MenuBarDropdownTrigger} + /> + </ButtonArea> </ParentWrapper> ); } @@ -159,7 +156,7 @@ const BreadCrumbBase = css` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 50ch; + min-width: 0; `; const BreadCrumbCurrent = styled.span` @@ -169,7 +166,7 @@ const BreadCrumbCurrent = styled.span` const Breadcrumb = styled.a` ${BreadCrumbBase} align-self: center; - cursor: 'pointer'; + cursor: pointer; text-decoration: none; border-radius: ${p => p.theme.radius}; @@ -183,6 +180,16 @@ const Breadcrumb = styled.a` } `; +const BreadcrumbRow = styled(Row)` + flex-shrink: 1; + min-width: 0; + overflow: hidden; + max-width: 80vw; + & > * { + min-width: 0; + } +`; + const Spacer = styled.span` flex: 1; `; diff --git a/browser/data-browser/src/components/Popover.tsx b/browser/data-browser/src/components/Popover.tsx index d29179abd..0ac006222 100644 --- a/browser/data-browser/src/components/Popover.tsx +++ b/browser/data-browser/src/components/Popover.tsx @@ -16,11 +16,6 @@ import { styled, keyframes } from 'styled-components'; import { transparentize } from 'polished'; import { useDialogTreeInfo } from './Dialog/dialogContext'; import { useControlLock } from '../hooks/useControlLock'; -import { EventManager } from '@helpers/EventManager'; - -type PopoverEvents = { - interactionOutside: () => void; -}; export interface PopoverProps { Trigger: ReactNode; @@ -46,10 +41,6 @@ export function Popover({ Trigger, side = 'bottom', }: PropsWithChildren<PopoverProps>): JSX.Element { - const eventManagerRef = useRef( - new EventManager<keyof PopoverEvents, PopoverEvents>(), - ); - const { setHasOpenInnerPopup } = useDialogTreeInfo(); const containerRef = useContext(PopoverContainerContext); @@ -70,30 +61,25 @@ export function Popover({ }, [open, setHasOpenInnerPopup]); return ( - <PopoverEventContext value={eventManagerRef.current}> - <RadixPopover.Root - modal={modal} - open={open} - onOpenChange={handleOpenChange} - defaultOpen={defaultOpen} - > - {Trigger} - <RadixPopover.Portal container={container}> - <Content - collisionPadding={10} - sticky='always' - className={className} - side={side} - onInteractOutside={() => - eventManagerRef.current.emit('interactionOutside') - } - > - {children} - {!noArrow && <Arrow />} - </Content> - </RadixPopover.Portal> - </RadixPopover.Root> - </PopoverEventContext> + <RadixPopover.Root + modal={modal} + open={open} + onOpenChange={handleOpenChange} + defaultOpen={defaultOpen} + > + {Trigger} + <RadixPopover.Portal container={container}> + <Content + collisionPadding={10} + sticky='always' + className={className} + side={side} + > + {children} + {!noArrow && <Arrow />} + </Content> + </RadixPopover.Portal> + </RadixPopover.Root> ); } @@ -153,34 +139,3 @@ export const PopoverContainer: FC<PropsWithChildren> = ({ children }) => { const ContainerDiv = styled.div` display: contents; `; - -const PopoverEventContext = createContext< - EventManager<keyof PopoverEvents, PopoverEvents> ->(new EventManager<keyof PopoverEvents, PopoverEvents>()); - -interface UsePopoverEventsProps { - onInteractionOutside: () => void; -} - -/** - * This hook allows children of a popover to listen to events emitted by the popover. - */ -export function usePopoverEvents({ - onInteractionOutside, -}: UsePopoverEventsProps) { - const eventManager = useContext(PopoverEventContext); - - useEffect(() => { - const unsubscribers: (() => void)[] = []; - - if (onInteractionOutside) { - unsubscribers.push( - eventManager.register('interactionOutside', onInteractionOutside), - ); - } - - return () => { - unsubscribers.forEach(unsubscribe => unsubscribe()); - }; - }, [eventManager, onInteractionOutside]); -} diff --git a/browser/data-browser/src/components/ScrollArea.tsx b/browser/data-browser/src/components/ScrollArea.tsx index 45f41d0d4..7498e3687 100644 --- a/browser/data-browser/src/components/ScrollArea.tsx +++ b/browser/data-browser/src/components/ScrollArea.tsx @@ -5,7 +5,7 @@ import { forwardRef, type JSX } from 'react'; const SIZE = '0.8rem'; -export interface ScrollAreaProps { +export interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> { className?: string; type?: 'hover' | 'scroll'; } @@ -13,9 +13,9 @@ export interface ScrollAreaProps { export const ScrollArea = forwardRef< HTMLDivElement, React.PropsWithChildren<ScrollAreaProps> ->(({ children, className, type = 'scroll' }, ref): JSX.Element => { +>(({ children, className, type = 'scroll', ...rest }, ref): JSX.Element => { return ( - <RadixScrollArea.Root type={type} className={className}> + <RadixScrollArea.Root type={type} className={className} {...rest} dir='ltr'> <ScrollViewPort ref={ref}>{children}</ScrollViewPort> <ScrollBar orientation='vertical'> <Thumb /> diff --git a/browser/data-browser/src/locales/de.po b/browser/data-browser/src/locales/de.po index 878be4da7..badddc49c 100644 --- a/browser/data-browser/src/locales/de.po +++ b/browser/data-browser/src/locales/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-29T10:47:38.272Z\n" -"PO-Revision-Date: 2025-11-18T09:35:24.196Z\n" +"PO-Revision-Date: 2025-11-20T14:47:31.763Z\n" "Last-Translator: \n" "Language: de\n" "Language-Team: \n" @@ -3082,3 +3082,7 @@ msgstr "Unbenannter Agent" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Rich Text Editor" msgstr "Rich-Text-Editor" + +#: src/views/BookmarkPage/BookmarkPage.tsx +msgid "Bookmark URL" +msgstr "Lesezeichen-URL" diff --git a/browser/data-browser/src/locales/en.po b/browser/data-browser/src/locales/en.po index e1a04c016..3fde3a458 100644 --- a/browser/data-browser/src/locales/en.po +++ b/browser/data-browser/src/locales/en.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T12:18:27.636Z\n" -"PO-Revision-Date: 2025-11-18T09:35:24.168Z\n" +"PO-Revision-Date: 2025-11-20T14:47:29.904Z\n" "Last-Translator: \n" "Language: en\n" "Language-Team: \n" @@ -3088,3 +3088,7 @@ msgstr "Untitled Agent" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Rich Text Editor" msgstr "Rich Text Editor" + +#: src/views/BookmarkPage/BookmarkPage.tsx +msgid "Bookmark URL" +msgstr "Bookmark URL" diff --git a/browser/data-browser/src/locales/es.po b/browser/data-browser/src/locales/es.po index aa2d25317..483bd0575 100644 --- a/browser/data-browser/src/locales/es.po +++ b/browser/data-browser/src/locales/es.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T09:59:41.856Z\n" -"PO-Revision-Date: 2025-11-18T09:35:24.182Z\n" +"PO-Revision-Date: 2025-11-20T14:47:30.591Z\n" "Last-Translator: \n" "Language: es\n" "Language-Team: \n" @@ -3060,3 +3060,7 @@ msgstr "Agente sin título" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Rich Text Editor" msgstr "Editor de texto enriquecido" + +#: src/views/BookmarkPage/BookmarkPage.tsx +msgid "Bookmark URL" +msgstr "URL del marcador" diff --git a/browser/data-browser/src/locales/fr.po b/browser/data-browser/src/locales/fr.po index 092cad8b0..5cc6292df 100644 --- a/browser/data-browser/src/locales/fr.po +++ b/browser/data-browser/src/locales/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-26T10:06:11.465Z\n" -"PO-Revision-Date: 2025-11-18T09:35:24.191Z\n" +"PO-Revision-Date: 2025-11-20T14:47:31.121Z\n" "Last-Translator: \n" "Language: fr\n" "Language-Team: \n" @@ -3077,3 +3077,7 @@ msgstr "Agent sans titre" #: src/chunks/RTE/CollaborativeEditor.tsx msgid "Rich Text Editor" msgstr "Éditeur de texte enrichi" + +#: src/views/BookmarkPage/BookmarkPage.tsx +msgid "Bookmark URL" +msgstr "URL du signet" diff --git a/browser/data-browser/src/views/BookmarkPage/BookmarkPage.tsx b/browser/data-browser/src/views/BookmarkPage/BookmarkPage.tsx index 06821cfc4..c9d624e7b 100644 --- a/browser/data-browser/src/views/BookmarkPage/BookmarkPage.tsx +++ b/browser/data-browser/src/views/BookmarkPage/BookmarkPage.tsx @@ -42,6 +42,7 @@ export function BookmarkPage({ resource }: ResourcePageProps): JSX.Element { <FieldWrapper> <InputWrapper> <InputStyled + aria-label='Bookmark URL' placeholder='https://example.com' value={url} onChange={handleUrlChange} diff --git a/browser/data-browser/src/views/FolderPage/ListView.tsx b/browser/data-browser/src/views/FolderPage/ListView.tsx index e0d5907ec..980bf894a 100644 --- a/browser/data-browser/src/views/FolderPage/ListView.tsx +++ b/browser/data-browser/src/views/FolderPage/ListView.tsx @@ -115,8 +115,6 @@ const Wrapper = styled.div` --icon-width: 1rem; --icon-title-spacing: 1rem; --cell-padding: 0.4rem; - /* width: var(--container-width); */ - /* margin-inline: auto; */ `; const StyledTable = styled.table` diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 411a9a3fa..35e9fc40f 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -177,7 +177,7 @@ const FullPageWrapper = styled.div<{ edit: boolean }>` @container (max-width: 600px) { grid-template-areas: ${p => p.edit ? `'title' 'list' 'list'` : `'title' 'graph' 'list'`}; - grid-template-columns: 100vw; + grid-template-columns: 100cqw; ${SidebarSlot} { display: none; diff --git a/browser/e2e/tests/documents.spec.ts b/browser/e2e/tests/documents.spec.ts index b3dbca982..1b7041000 100644 --- a/browser/e2e/tests/documents.spec.ts +++ b/browser/e2e/tests/documents.spec.ts @@ -9,6 +9,8 @@ import { openNewSubjectWindow, timestamp, before, + setTitle, + waitForSearchIndex, } from './test-utils'; test.describe('documents', async () => { test.beforeEach(before); @@ -17,10 +19,14 @@ test.describe('documents', async () => { page, browser, }) => { + const folderTitle = 'SomeFolder'; + await signIn(page); await newDrive(page); await makeDrivePublic(page); // Create a document + await newResource('folder', page); + await setTitle(page, folderTitle); await newResource('document', page); const title = `Document ${timestamp()}`; await editTitle(title, page); @@ -36,30 +42,30 @@ test.describe('documents', async () => { await page.keyboard.press('Enter'); await page.keyboard.type(teststring); - await expect( - page.getByRole('heading', { name: teststring, exact: true }), - ).toBeVisible(); + await expect(page.getByRole('heading', { name: teststring })).toBeVisible(); // multi-user const currentSubject = await getCurrentSubject(page); const page2 = await openNewSubjectWindow(browser, currentSubject!, true); + await page2.getByRole('button', { name: 'Set Drive' }).click(); await expect(page2.getByText('loading...')).not.toBeVisible(); await expect( - page2.getByRole('heading', { name: teststring, exact: true }), + page2.getByRole('heading', { name: teststring }), 'First paragraph title not visible in second tab. Not a websocket issue', ).toBeVisible(); expect(await page2.title()).toEqual(title); await page2.getByLabel('Rich Text Editor').focus(); await page2.keyboard.press('ArrowDown'); + await page2.keyboard.press('Enter'); // Add a new line on first page, check if it appears on the second const syncText = 'New paragraph'; await page2.keyboard.type(syncText); await expect( page.locator(`text=${syncText}`), - 'New paragraph not found in first window. Websockets may not be working.', + 'New paragraph not found in first window. Sync might not be working.', ).toBeVisible(); // Delete a row, cmd + backspace @@ -74,6 +80,7 @@ test.describe('documents', async () => { await page2.keyboard.press('ArrowRight'); await page2.keyboard.down('Alt'); await page2.keyboard.press('Backspace'); + await page2.keyboard.up('Alt'); await expect( page.locator(`text=${syncText}`), @@ -83,5 +90,22 @@ test.describe('documents', async () => { page2.locator(`text=${syncText}`), 'Paragraph not deleted in second window', ).not.toBeVisible(); + + // Wait for AtomicServer to index the folder + await waitForSearchIndex(page2); + // Add a link to a folder to the document + await page2.keyboard.press('Space'); + await page2.keyboard.type('@'); + await page2.waitForTimeout(500); + await page2.keyboard.type(folderTitle, { delay: 50 }); + await expect( + page2.getByTestId('rte-command-list').getByText(folderTitle), + ).toBeVisible(); + await page2.keyboard.press('Enter'); + + // Check if the link is visible in the document + await expect( + page.getByLabel('Rich Text Editor').locator('a:has-text("SomeFolder")'), + ).toBeVisible(); }); }); diff --git a/browser/e2e/tests/filePicker.spec.ts b/browser/e2e/tests/filePicker.spec.ts index 882fc99a1..73c7e61a7 100644 --- a/browser/e2e/tests/filePicker.spec.ts +++ b/browser/e2e/tests/filePicker.spec.ts @@ -3,7 +3,6 @@ import { test, expect, Page } from '@playwright/test'; import { DIALOG_CLOSE_BUTTON, FRONTEND_URL, - REBUILD_INDEX_TIME, before, fillSearchBox, inDialog, @@ -13,6 +12,7 @@ import { signIn, testFilePath, waitForCommit, + waitForSearchIndex, } from './test-utils'; const ONTOLOGY_NAME = 'filepicker-test'; @@ -102,7 +102,7 @@ test.describe('File Picker', () => { await createModel(page); // The new resource page relies on the search API to show ontology class buttons. If the prossess of creating the ontology took less than 5 seconds it will not appear on the new resource page. - await page.waitForTimeout(REBUILD_INDEX_TIME); + await waitForSearchIndex(page); { // Test selecting an existing file. diff --git a/browser/e2e/tests/search.spec.ts b/browser/e2e/tests/search.spec.ts index eb92b4c26..5ad9b131c 100644 --- a/browser/e2e/tests/search.spec.ts +++ b/browser/e2e/tests/search.spec.ts @@ -3,7 +3,6 @@ import { signIn, newDrive, before, - REBUILD_INDEX_TIME, addressBar, clickSidebarItem, editTitle, @@ -12,6 +11,7 @@ import { contextMenuClick, timestamp, newResource, + waitForSearchIndex, } from './test-utils'; test.describe('search', async () => { test.beforeEach(before); @@ -63,7 +63,7 @@ test.describe('search', async () => { await clickSidebarItem('Cake Folder', page); // Set search scope to 'Cake folder' - await page.waitForTimeout(REBUILD_INDEX_TIME); + await waitForSearchIndex(page); await page.reload(); await contextMenuClick('scope', page); // Search for 'Avocado' @@ -115,7 +115,7 @@ test.describe('search', async () => { await expect(page.getByRole('link', { name: secondTagName })).toBeVisible(); // Wait for the index to be rebuilt - await page.waitForTimeout(REBUILD_INDEX_TIME); + await waitForSearchIndex(page); // Search for the folder by the first tag await addressBar(page).fill('tag:first'); diff --git a/browser/e2e/tests/test-utils.ts b/browser/e2e/tests/test-utils.ts index 9b0018969..dac4657ba 100644 --- a/browser/e2e/tests/test-utils.ts +++ b/browser/e2e/tests/test-utils.ts @@ -196,6 +196,10 @@ export async function waitForCommitOnCurrentResource( }); } +export async function waitForSearchIndex(page: Page) { + return page.waitForTimeout(REBUILD_INDEX_TIME); +} + export async function openAgentPage(page: Page) { page.goto(`${FRONTEND_URL}/app/agent`); }