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 bd2b9357..1b765b78 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 e1e33a1c..d00efee7 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 83bfd65a..ea69df25 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 ee821b97..6771f0ba 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 ba251d8d..1fa0a9fc 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 27843dfb..0913939e 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 c7aaffa4..10bdee9d 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 351c9dae..373d2b34 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 972ece2a..b31abe56 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 05b73428..23fe37dd 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 ac3a8c07..0ff81192 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 00000000..06ccba78 --- /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 747c9bc2..bdeaf83b 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 cee32aac..d4bf417b 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 28071de5..2505f7aa 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 00000000..924389b4 --- /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 00000000..6d38bcd6 --- /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 00000000..4e0e05fc --- /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 1b8b8b51..be6d4afd 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 aafe0fa5..3137acb0 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 970a23f2..8fbcce5f 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 f07c999e..95163746 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 00000000..5826ea97 --- /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 3a6ef363..19ec87ec 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 00000000..35a929e9 --- /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 27200e23..c49b83de 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 4030b92f..5c675668 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 9f52b16a..e6a2c55f 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 7f52761a..dc917e5f 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 fd71a33a..685cee2d 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 06a28d7b..b8e29baa 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 05bee9cb..f61dab4d 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 cf9683de..b81b4935 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 00000000..309b46a1 --- /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 9ffeadfc..3b4ef2a0 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 85d91f6e..208496a7 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 30eaf90e..e22b789a 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 0e9a3313..af12060b 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 f3271337..5478d28b 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 e7060106..042fbceb 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 fb876987..7a0d661e 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 9db5252c..4b91e1ee 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 c37a69a9..70d0b752 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 14f3ef77..94682426 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 00000000..795fde0f --- /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 b85d1668..0afd65d6 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 6dcabba9..6f24bc0f 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 38c12a5d..6214f44d 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 cf670310..f72306bb 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 a51e3c9e..bbe64529 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 6aa81ddb..25832780 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 22694fac..fda14b6b 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 8584814b..072ec66e 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 11913ec2..c9858017 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 9b98388d..d084fa27 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 0377b2a2..cedf19db 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 c16147c6..0ac58770 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 9f4ad688..58f179ea 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 28bc191b..9e6c0602 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 1ef735cb..d2141d0b 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 9be33e24..4626c43d 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 fa84bbe0..0e60376e 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 62f363d6..88092ad9 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 cc821d73..8589ae11 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 8ce897cc..ce235547 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 47ff7298..9622c3a5 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 25795e7b..e203305b 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 e965c919..ab9882c9 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 8aab868e..a72cb265 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 9110712d..8b0acdfe 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 00000000..58a05eee --- /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 1b765b78..3d80476b 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 1fa0a9fc..7c000bb4 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 373d2b34..c9377788 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 b31abe56..f3ee37ae 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 0ff81192..24a410fc 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 06ccba78..59a3e6ee 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 00000000..b3e2d3a3 --- /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 d4bf417b..41210105 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 00000000..a15cdfa2 --- /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 5c2b60c7..61cb0132 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 00000000..e29ee493 --- /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 00000000..1dde04b2 --- /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 00000000..742b7d91 --- /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 3f9a3814..8b7166b1 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 2505f7aa..5d307ad8 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 00000000..c830970b --- /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 1ac48b7e..a861d627 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 00000000..54238eff --- /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 4e0e05fc..00000000 --- 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 00000000..3b04dcd8 --- /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 bc65d6f3..9714f8e9 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 29f2b25d..d29179ab 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 5826ea97..d2a9f88d 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 d4c24963..6244a2de 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 73267d7a..e9162ac7 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 00000000..976d424a --- /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 5c675668..30cfb4f3 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 e6a2c55f..47bdeb25 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 dc917e5f..9bc11da6 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 685cee2d..787af015 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 00000000..f5136f5d --- /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 c3a6e1cd..6ab00a0b 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 00000000..eaa329fb --- /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 02eb0557..16d6205a 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 cc3e73d5..ffb6d648 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 557ec564..e14d6186 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 4b91e1ee..048040b2 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 94682426..e4d936af 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 6f24bc0f..2bcd1978 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 d2141d0b..ba43c75e 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 8589ae11..2aa52c80 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 27ca9391..63262f27 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 9622c3a5..9264b6a3 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 e203305b..1c6b1b0d 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 ab9882c9..211bd301 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 a72cb265..e20d9216 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 8b0acdfe..ee80bf54 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 4a75677f..60bd08eb 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 58a05eee..00000000 --- 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 00000000..24af4771 --- /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 17448e0d..f868713d 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 9709c8ae..00000000 --- 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 264e8a7d..c0fdc9ec 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 3177c3a8..80ab8b54 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 d00efee7..28853049 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 6771f0ba..10eb7543 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 00000000..150cdf72 --- /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 7c000bb4..1580476d 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 276e1052..45a6fedf 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 771e0331..83459d6e 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 f99f9340..860df1d2 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 23fe37dd..05647ca0 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 24a410fc..f7ea0912 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 59a3e6ee..27c27099 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 bdeaf83b..eb49588f 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 a15cdfa2..ec1c5fdb 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 420178f9..263833fa 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 00000000..385af66a --- /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 e29ee493..5f39d023 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 00000000..70520261 --- /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 742b7d91..d678a4b1 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 8b7166b1..e568cc33 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 5d307ad8..faba5a15 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 c830970b..2d1c146b 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 a861d627..aa48a9ed 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 be6d4afd..c7907248 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 9714f8e9..d4d4e308 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 00000000..b69f396d --- /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 816275c5..e8456854 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 00000000..2e8e9ba4 --- /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 55234857..bda31d8e 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 bce635d0..afef4608 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 19997fc0..31e6df82 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 a44b5bc2..fd5e220c 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 00000000..4d2c7498 --- /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 0254b216..a79d4666 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 8e303027..b2ab543b 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 8b3fc42e..46bf71a0 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 3ac8575a..2b088d46 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 4052d390..b8d293a5 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 9d7e4692..20f07661 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 83b57e4c..da84af23 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 b7316217..aa119cb0 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 7a1bda68..f580eada 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 a6feeac3..c786ce03 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 65569b7d..e0d5907e 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 64d9ea75..a4e4c755 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 7d2bb506..e3c4bb5f 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 1a6ed4f1..49d7b65b 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 75773c49..acab833c 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 23ce93e2..528833c8 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 8dc9ee42..de25f35f 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 2c36d34d..53089520 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 00000000..70372308 --- /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 089e0ee0..b24a8620 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 199cf112..f181ae65 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 70520261..b46118bc 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 3ca411d7..f0381cf7 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 e8456854..5db470a3 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 c6d0dd5d..28a59475 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 31e6df82..93aa0943 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 91823629..19af55cc 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 c36473c4..10c00de1 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 71f5fdf1..73820786 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 5c704e7f..bbbac2f6 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 4feae1b4..9dc63b91 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 aecac577..c26239ae 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 c625cdf8..b56e9ba6 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 ba12932f..cc840f16 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 ce53fa5c..5916a763 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 f049017a..3d5e8b56 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 b89b8d78..2944f3ea 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 a1cfa107..90228511 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 ebec61fa..ccb3a0f7 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 2758b910..0b099fef 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 ba7e6ece..a79798e6 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 27157a04..a1554aa3 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 78341d32..353491b1 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 bd599406..5a375c93 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 3191b9f6..6304010b 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 a5115702..9c1e8ece 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 a1e6774c..c6c04218 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 e3c4bb5f..af9b5a12 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 acab833c..90b770d0 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 e7755303..f92c20e8 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 d609ff0e..ca6f0603 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 00000000..9d209484 --- /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 528833c8..033ecd9b 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 f0db5658..e50361ba 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 bbe64529..5463be0d 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 0fb6d133..7923d76f 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 f24ba39f..8d886dd5 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 27c27099..104e2a69 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 b69f396d..29143b89 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 3df6f2e2..700cdd09 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 5dff6e11..419fd1c6 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 091f078d..cc80a3b8 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 2511691a..00000000 --- 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 145bb858..878be4da 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 e71ebef5..e1a04c01 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 d0c44686..aa2d2531 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 249f51bb..092cad8b 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 5dd1b477..a6240d92 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 6304010b..fa812a70 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 02058ddf..72d799e1 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 45118661..6f7bf8bd 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 97ba27a7..a3d191f3 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 42994099..61b9d898 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 9fb02274..0cd7349b 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 63da12c0..16e5c641 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 65e4ff5f..1750463d 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 fdc654d1..84137064 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 5168f28d..b3dbca98 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 f16245a1..e79b49fb 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 ddae1bfe..eb92b4c2 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 54fb0a1d..c4308f26 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 0371fb6f..9b001896 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 af9b5a12..8dc11c8e 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 95086643..e9976ce8 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 79f88a60..38e9ff51 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 104e2a69..bc2d9d1e 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 b3e2d3a3..b8ce0a95 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 ec1c5fdb..90135c77 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 1dde04b2..bf0045bd 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 e568cc33..6a71e3b3 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 3b04dcd8..ab71731a 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 afef4608..466d6d7d 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 93aa0943..a20b0617 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 fd5e220c..b9d6626d 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 d29179ab..0ac00622 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 45f41d0d..7498e368 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 878be4da..badddc49 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 e1a04c01..3fde3a45 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 aa2d2531..483bd057 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 092cad8b..5cc6292d 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 06821cfc..c9d624e7 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 e0d5907e..980bf894 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 411a9a3f..35e9fc40 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 b3dbca98..1b704100 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 882fc99a..73c7e61a 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 eb92b4c2..5ad9b131 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 9b001896..dac4657b 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`); }