From 82ff16b4a7b9f5aa62f346d147c61f9e590d8e47 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 19 May 2026 20:57:30 +0200 Subject: [PATCH 01/23] feat(realtime-collab): land CollaborativeWrapper + codemirror / tiptap apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the realtime-collaboration PoC from opencloud-eu/web-extensions into the canonical web repo: - New `@opencloud-eu/web-pkg` component family at `src/components/Collaborative/`: `CollaborativeWrapper.vue` + `CollaborativeAdapter` type contract. Reads `useAuthStore` / `useConfigStore` via the existing composables barrel. - web-pkg deps gain `@hocuspocus/provider`, `yjs`, `y-protocols`, `semver` (+ `@types/semver` devDep). Editor-binding deps (`y-codemirror.next`, `@tiptap/y-tiptap`, `@tiptap/markdown`, `@tiptap/extension-collaboration`, `@codemirror/*`) stay with the consuming apps — web-pkg only ships the editor-agnostic plumbing. - Two new apps: `web-app-codemirror` + `web-app-tiptap`. Both are thin App.vue wrappers around `CollaborativeWrapper`, no own build config (web's central vite picks them up via the `web-app-*` convention). Adapter + editor component live alongside App.vue. - `realtimeUrl` prop is now three-state on the wrapper: `string` for an explicit URL, `null` for forced local-only mode, `undefined` (default) to derive from `configStore.serverUrl` plus the `/realtime` convention. Means collab is on out-of-the-box once the sidecar runs on the OC host. A first-class `options.realtimeUrl` field in OC's web config schema is a separate follow-up. - Tiptap StarterKit uses the v3 `undoRedo: false` option name (was `history: false` in v2) — clears the lingering `Partial` type error and keeps yUndoPlugin from `@tiptap/y-tiptap` as the collab-aware undo manager. - Registers codemirror + tiptap in `dev/docker/opencloud.web.config.json` `apps[]`. - Adds `REALTIME_COLLAB_MIGRATION.md` documenting the multi-phase plan (phases 0-1-2 done; 2.5, 3, 4, 4.5, 5 pending). --- REALTIME_COLLAB_MIGRATION.md | 267 +++++ dev/docker/opencloud.web.config.json | 4 +- packages/web-app-codemirror/extension.d.ts | 1 + .../web-app-codemirror/l10n/translations.json | 1 + packages/web-app-codemirror/package.json | 25 + packages/web-app-codemirror/src/App.vue | 36 + .../src/CodeMirrorEditor.vue | 55 + .../src/adapters/codemirrorMarkdown.ts | 31 + packages/web-app-codemirror/src/index.ts | 44 + packages/web-app-codemirror/tsconfig.json | 3 + packages/web-app-tiptap/extension.d.ts | 1 + .../web-app-tiptap/l10n/translations.json | 1 + packages/web-app-tiptap/package.json | 28 + packages/web-app-tiptap/src/App.vue | 36 + packages/web-app-tiptap/src/TiptapEditor.vue | 218 ++++ .../src/adapters/tiptapMarkdown.ts | 66 ++ packages/web-app-tiptap/src/index.ts | 44 + packages/web-app-tiptap/tsconfig.json | 3 + packages/web-pkg/package.json | 5 + .../Collaborative/CollaborativeWrapper.vue | 480 ++++++++ .../src/components/Collaborative/index.ts | 2 + .../src/components/Collaborative/types.ts | 43 + packages/web-pkg/src/components/index.ts | 1 + pnpm-lock.yaml | 1026 ++++++++++++----- 24 files changed, 2104 insertions(+), 317 deletions(-) create mode 100644 REALTIME_COLLAB_MIGRATION.md create mode 100644 packages/web-app-codemirror/extension.d.ts create mode 100644 packages/web-app-codemirror/l10n/translations.json create mode 100644 packages/web-app-codemirror/package.json create mode 100644 packages/web-app-codemirror/src/App.vue create mode 100644 packages/web-app-codemirror/src/CodeMirrorEditor.vue create mode 100644 packages/web-app-codemirror/src/adapters/codemirrorMarkdown.ts create mode 100644 packages/web-app-codemirror/src/index.ts create mode 100644 packages/web-app-codemirror/tsconfig.json create mode 100644 packages/web-app-tiptap/extension.d.ts create mode 100644 packages/web-app-tiptap/l10n/translations.json create mode 100644 packages/web-app-tiptap/package.json create mode 100644 packages/web-app-tiptap/src/App.vue create mode 100644 packages/web-app-tiptap/src/TiptapEditor.vue create mode 100644 packages/web-app-tiptap/src/adapters/tiptapMarkdown.ts create mode 100644 packages/web-app-tiptap/src/index.ts create mode 100644 packages/web-app-tiptap/tsconfig.json create mode 100644 packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue create mode 100644 packages/web-pkg/src/components/Collaborative/index.ts create mode 100644 packages/web-pkg/src/components/Collaborative/types.ts diff --git a/REALTIME_COLLAB_MIGRATION.md b/REALTIME_COLLAB_MIGRATION.md new file mode 100644 index 0000000000..b5de5bb391 --- /dev/null +++ b/REALTIME_COLLAB_MIGRATION.md @@ -0,0 +1,267 @@ +# Plan: Realtime-Collab Migration — web-extensions PoC → opencloud-eu/web + +## Context + +The realtime collaboration PoC in `opencloud-eu/web-extensions` (PR #447, branch `feat/realtime-collaboration-poc`) is now functionally complete: + +- `CollaborativeWrapper.vue` (in `web-app-codemirror`, reused by `web-app-tiptap`) provides Y.Doc + Awareness + optional Hocuspocus provider + stale-state recovery + app-version locking +- AppWrapper integration: wrapper emits `update:currentContent` so OC's Save action / Ctrl+S / unsaved-changes modal / auto-save loop all work transparently +- Local-only fallback when `applicationConfig.realtimeUrl` is unset: same wrapper, no provider, standalone Awareness, immediate hydration. The wrapper is the universal API for both modes. +- Tiptap on v3.20.4 aligned with web, custom Tiptap extension wires `@tiptap/y-tiptap`'s `yCursorPlugin` (upstream `@tiptap/extension-collaboration-cursor@3.0.0` still imports from `y-prosemirror` and is incompatible with v3 `@tiptap/extension-collaboration`) +- All 17/17 tests green (5 codemirror e2e + 4 tiptap e2e + 8 integration) + +User now wants: +- Move both apps and the wrapper into the canonical `opencloud-eu/web` repo +- Refactor the existing `web-app-text-editor` so it uses the realtime API (the wrapper) exclusively — every editor instance goes through Y.Doc, whether or not a sidecar is reachable (local-mode handles the no-server case) +- Bring the Hocuspocus sidecar into web's docker-compose so the whole dev loop lives in one repo + +User-clarified scope: +- **One canonical wrapper in web-pkg**, used by all three apps (codemirror, tiptap, refactored text-editor). The wrapper does NOT get forked. If integration churn is needed during the text-editor refactor, fork the `web-app-text-editor` package itself (not the wrapper) so we can iterate without fixing all call sites at once. Wrapper API stays single source of truth. +- **Y.Doc is always-on, no optional.** `useTextEditor` and friends always receive a Y.Doc; the wrapper's local-mode is the universal "no realtime backend" branch. Experimental but the wrapper just gained this capability and we want to see it carry the full text-editor surface. +- **App naming stays:** `web-app-codemirror` + `web-app-tiptap` as separate apps alongside the refactored `web-app-text-editor`. +- **E2E migration target:** Cucumber, reuse web's existing step helpers where possible. +- **Tiptap history under collab:** Y.Js Collaboration replaces Tiptap's StarterKit history extension with `yUndoPlugin` (from `@tiptap/y-tiptap` — Tiptap's fork of y-prosemirror). The feature isn't lost — undo/redo still work and become collab-aware (only undo your own edits). What changes is the wiring: `StarterKit.configure({ undoRedo: false })` (v3 name; was `history: false` in v2) and the `Collaboration` extension brings yUndoPlugin in automatically. Toolbar's undo/redo actions continue to call `editor.commands.undo()` / `redo()` and y-tiptap handles them. **One TS issue to clean up while we're there:** our current `StarterKit.configure({ history: false })` is the v2 name and emits a vue-tsc error (`'history' does not exist in type Partial`). Renaming to `undoRedo: false` clears it. + +--- + +## Phase 0 — web-extensions cleanup (prereq, must land before migration starts) + +While debugging the unit test for the etag-mirror behaviour I discovered a real bug in `CollaborativeWrapper.vue`: the `watchEffect((onCleanup) => { ... })` lifecycle re-runs whenever any tracked prop changes — including the `resource` prop update that AppWrapper fires after every save (via `resourcesStore.upsertResource`). Each AppWrapper save would tear down and rebuild the Y.Doc, losing peer state. The shared-file e2e doesn't catch it because the two peers' saves happen far apart in test time and the wrapper successfully re-hydrates from `currentContent`. + +**Fix (mid-edit in the current working tree, broken intermediate state):** +- Replace `watchEffect((onCleanup) => { ... })` with `watch(sessionKey, (key, _, onCleanup) => { ... }, { immediate: true })` +- `sessionKey = computed(() => name && `${name}::${realtimeUrl ?? 'local'}`)` — only rebuilds when the actual session identity changes +- Vue's computed equality check ensures `setProps({ resource: newResource })` with same `id` doesn't re-fire the watch +- Remove the debug `console.error` lines I added during diagnosis +- Close the `watch` callback + outer block properly (current state has dangling brace + leftover body) + +**Files:** +- `packages/web-app-codemirror/src/CollaborativeWrapper.vue` — finish refactor; remove debug logs +- `packages/web-app-codemirror/tests/unit/CollaborativeWrapper.spec.ts` — clean up the debug `console.log` I added in the etag-mirror test +- `packages/web-app-codemirror/tests/unit/CollaborativeWrapper.spec.ts` — **add a dedicated regression test** (separate from etag-mirror) that asserts: given a wrapper mounted with a resource, calling `setProps({ resource })` with a NEW resource object whose `id` is unchanged keeps the same `wrapper.vm.ydoc` instance reference (no rebuild). Tag the test name explicitly as "regression: does not rebuild Y.Doc when resource prop changes without identity change" so future readers see why it's there. + +**Verification:** +- `pnpm vitest run tests/unit/CollaborativeWrapper.spec.ts` — all unit tests green (12 existing + 1 regression) +- `pnpm playwright test --project=codemirror-chromium` — 5/5 green (regression check on real e2e) +- `pnpm playwright test --project=tiptap-chromium` — 4/4 green +- `pnpm --filter=codemirror exec vitest run tests/integration/realtime-sync.spec.ts` — 8/8 green + +**Commit:** one tight commit on `feat/realtime-collaboration-poc` summarising the bug fix + the new regression test. Push. + +Only AFTER Phase 0 is committed and pushed do we start Phase 1. + +--- + +## Phase 1 — Move CollaborativeWrapper into web-pkg + +**Target:** make `CollaborativeWrapper` importable as `import { CollaborativeWrapper } from '@opencloud-eu/web-pkg'`. + +**Files to create in `/home/domme/dev/sources/opencloud-eu/web/packages/web-pkg/`:** +- `src/components/Collaborative/CollaborativeWrapper.vue` — straight copy of the final web-extensions wrapper (post Phase 0). Reads `useAuthStore` from `../../composables/piniaStores/...` — verify the relative import path during the move. +- `src/components/Collaborative/types.ts` — the `CollaborativeAdapter` interface (currently at `packages/web-app-codemirror/src/types.ts`). +- `src/components/Collaborative/index.ts` — barrel export `{ default as CollaborativeWrapper } from ...` + `type CollaborativeAdapter`. +- Wire into `src/components/index.ts` so the named import works at the web-pkg root. + +**Dependency audit (the user explicitly asked for thoroughness here — many deps belong in the consuming apps, not in web-pkg):** + +Belongs in `web-pkg` (the wrapper uses them directly): +- `@hocuspocus/provider ^4.0.0` — runtime, the wrapper instantiates `HocuspocusProvider` +- `yjs ^13.6.0` — the wrapper imports `* as Y` for `new Y.Doc()` +- `y-protocols ^1.0.7` — the wrapper imports `Awareness` for the local-mode standalone +- `semver ^7.8.0` — the wrapper uses `semver/functions/{compare,valid}` for app-version handshake +- `@types/semver ^7.7.0` (devDep) + +Does NOT belong in web-pkg (consumer-specific): +- `y-codemirror.next` — only the codemirror app's adapter uses this +- `@tiptap/y-tiptap` — only the tiptap-using apps' adapters / editor components use this. Already pulled transitively via `@tiptap/extension-collaboration` (which web-pkg DOES have via its `useTextEditor`). When the tiptap app or text-editor needs to import from `@tiptap/y-tiptap` directly (custom cursor extension), it lists its own direct dep — same pattern as the codemirror app. +- `@tiptap/markdown` — already in web-pkg via the markdown strategy; stays as-is +- `@hocuspocus/server` — sidecar only, not a web-pkg concern + +This minimises web-pkg's surface — the wrapper is editor-agnostic, so adapter-bound deps stay with their consumers. + +**No web-pkg test added in this phase** — unit test moves with the wrapper in Phase 5. + +**Verification:** +- `pnpm install` in web root resolves cleanly with no peer warnings +- `pnpm --filter=@opencloud-eu/web-pkg build` succeeds (web-pkg's own build / type-check validates imports) + +--- + +## Phase 2 — Move web-app-codemirror + web-app-tiptap into web/packages + +**Per app (codemirror first as canonical, then tiptap):** + +1. `cp -R` the package directory: `web-extensions/packages/web-app-codemirror` → `web/packages/web-app-codemirror` +2. Rewrite `src/App.vue` import of the wrapper from local `./CollaborativeWrapper.vue` → `from '@opencloud-eu/web-pkg'` +3. Delete the now-unused `src/CollaborativeWrapper.vue` + `src/types.ts` from the moved app directory +4. Update `package.json` to match web's app conventions (study `web-app-text-editor/package.json` first as template): + - `@opencloud-eu/web-client` / `@opencloud-eu/web-pkg` become workspace siblings (peerDeps on `^7.0.0`, not devDeps — verify against the convention in other web apps) + - Drop `@hocuspocus/provider` / `y-protocols` (now indirect via web-pkg) + - Keep `y-codemirror.next` in codemirror app, keep `@tiptap/*` + `@tiptap/y-tiptap` + `@tiptap/markdown` in tiptap app — those are app-bound +5. Don't move tests yet — they go in Phase 5 + +**Files to update in web (not web-app-*):** +- `dev/docker/opencloud.apps.yaml` (or web's equivalent) — add `codemirror` / `tiptap` entries with `config.realtimeUrl: wss://host.docker.internal:9200/realtime` +- `docker-compose.yml` — add `./packages/web-app-codemirror/dist:/web/apps/codemirror` and `./packages/web-app-tiptap/dist:/web/apps/tiptap` volume mounts + +**Verification:** +- `pnpm --filter=codemirror build` + `pnpm --filter=tiptap build` succeed +- OC sees both apps via `/config.json` `external_apps` +- Manual smoke test: open .md file, both apps appear in "Open with...", both load + edit + save + +--- + +## Phase 3 — Hocuspocus sidecar in web's docker-compose + +1. Copy `web-extensions/dev/docker/hocuspocus/` → `web/dev/docker/hocuspocus/` verbatim (Dockerfile, patches/, package.json, server.js) +2. Append the `hocuspocus` service to `web/docker-compose.yml` matching the web-extensions definition (same env vars, same Traefik labels, same volume for SQLite state) +3. Reuse the existing `host.docker.internal:9200` Traefik route — `/realtime` path-prefix routes to hocuspocus +4. Verify the sidecar's `OPENCLOUD_URL` env points to the web compose's OC service name (likely identical to web-extensions) +5. **Switch the etag probe in `server.js` from WebDAV HEAD to Graph API.** The current code uses WebDAV HEAD `/remote.php/dav/spaces/{itemId}` with the comment "Graph's /items endpoint is share-jail-only and 400s on personal drives" — verify whether that's still true against current OC Graph API. Likely the right endpoint is Graph `/v1.0/drives/{driveId}/items/{itemId}` which returns a DriveItem with `eTag`. If 400 still happens on personal drives, dig into why (might be v1beta1 vs v1.0, or missing query param). Goal: one consistent Graph call for permissions + etag, drop the WebDAV path from the sidecar. + +**Verification:** +- `docker compose up -d hocuspocus` reachable at `wss://host.docker.internal:9200/realtime` +- Apps from Phase 2 connect successfully +- Integration spec (when ported in Phase 5) runs green against it + +--- + +## Phase 4 — Refactor web-app-text-editor onto the wrapper + +This is the biggest piece. `web-app-text-editor` currently uses `useTextEditor` from `web-pkg/editor`, which owns the Tiptap editor instance and handles content via strategies (markdown / html / plain-text / tiptap-json). AppWrapper handles load/save/dirty/etag. + +The user's directive: every text-editor instance goes through the realtime API (the wrapper). Y.Doc is always-on. Local-mode handles the no-sidecar case transparently. UX (toolbar, slash commands, strategies, multi-content-type support) is preserved. + +**Architectural shape:** +- `CollaborativeWrapper` is the outer shell. text-editor's `App.vue` sits inside it. +- `useTextEditor` is reworked to accept a **mandatory** Y.Doc parameter (NOT optional — the user explicitly wants this to always go through Y.Doc). When invoked, it includes the `Collaboration.configure({ document: ydoc, field: 'default' })` extension and disables StarterKit's built-in `undoRedo` (yUndoPlugin from y-tiptap takes over). +- The toolbar / slash commands keep operating on the Tiptap editor instance the composable returns — they don't know about Y.Doc, they just call `editor.commands.bold()` / `undo()` / `setLink()` / etc. +- Adapter per strategy: each existing strategy (markdown / html / plain-text / tiptap-json) gets a matching `CollaborativeAdapter` (`hydrate(ydoc, content)` deserializes via the strategy's own logic into the Tiptap editor bound to ydoc; `serialize(ydoc)` runs the strategy's serializer). +- text-editor's `App.vue` emits `update:currentContent` from the wrapper, AppWrapper drives the save (same pattern as our PoC apps). + +**`CollaborativeAdapter` contract extension (important for performance):** +The PoC adapters spawn a headless Tiptap editor inside `serialize(ydoc)` (see `web-app-tiptap/src/adapters/tiptapMarkdown.ts`). That's cheap when StarterKit + Markdown are the only extensions, but for text-editor's 4 strategies × 10+ extensions (link, image, table, task-list, etc.) we'd be re-instantiating Tiptap on every debounced serialize. The contract should be extended to optionally accept the LIVE editor for `serialize`, falling back to headless when no editor is bound (e.g., during stale-recovery on a peer that has the doc but no UI): +```ts +export interface CollaborativeAdapter { + hydrate(ydoc: Y.Doc, content: string): void | Promise + serialize(ydoc: Y.Doc, editor?: TiptapEditor): string | Promise + // ... rest unchanged +} +``` +Wrapper's `scheduleEmit` passes the live editor when one is bound (the editor component exposes it via `defineExpose` or a slot). This change is BACKWARDS compatible (existing adapters ignore the new arg) and lands as part of Phase 4 — no need to touch the wrapper in Phase 1. + +**What does NOT break going all-in collab:** +- Strategies, toolbar, slash commands, undo/redo, multi-extension setup — all preserved +- Single-user UX (no sidecar) — covered by the wrapper's local mode +- The existing 250ms debounce in `useTextEditor` becomes the wrapper's 300ms debounce — close enough, can be tuned via prop + +**What could break if uncareful:** +- Custom extensions that mutate editor state outside of commands (rare; none in current StarterKit / web-pkg extension set) +- Schema drift between peers running different bundles — already handled by `enableContentCheck` + `onContentError` (the wrapper's app-version lock plus Tiptap's content-check is belt-and-braces) + +**Forking discipline (per user clarification):** +- **DO NOT fork the wrapper.** It stays canonical in web-pkg. +- **CAN fork `web-app-text-editor`** if the refactor would otherwise need to touch every call-site of `useTextEditor` in web. The text-editor app may diverge during the migration, the rest of web stays on the old `useTextEditor` for now. Reconverge in a follow-up. + +**Files to touch:** +- `packages/web-app-text-editor/src/App.vue` — wrap content rendering inside `CollaborativeWrapper`; emit `update:currentContent`; drop the manual content-loading scaffolding +- `packages/web-pkg/src/editor/composables/useTextEditor.ts` — require `ydoc: Y.Doc` parameter; add `Collaboration` to the extension list; flip `undoRedo: false` (was `history: false` in v2, now lint-warns) on `StarterKit.configure(...)` +- `packages/web-pkg/src/editor/composables/strategies/markdown.ts` — expose a `CollaborativeAdapter` companion that uses the existing `editor.getMarkdown()` / `setContent(content, { contentType: 'markdown' })` logic +- Same shape for `html.ts`, `plainText.ts`, `tiptapJson.ts` — see Open Q #1 about whether all four ship in this PR or just markdown +- `packages/web-app-text-editor/src/index.ts` — pass `applicationConfig.realtimeUrl` from AppWrapperRoute config through to the wrapper + +**Local-mode safety net:** `CollaborativeWrapper` handles `realtimeUrl: null` → standalone Awareness, immediate hydrate, no provider. text-editor inherits this for free. Single-user mode works without a sidecar. + +**Verification:** +- Existing text-editor unit tests still pass (the editor's public behaviour is unchanged: it receives content, emits content) +- Manual: open .md → toolbar works, formatting buttons apply, slash commands work, content saves, isDirty flips, Ctrl+S works, undo/redo works (via yUndoPlugin now), route-leave modal — all unchanged from user POV +- Open same .md in second tab → realtime sync works (new capability) +- Local mode (realtimeUrl unset for text-editor in apps.yaml): single-user, no sidecar needed + +--- + +## Phase 5 — Tests in web + +**Unit:** +- Move `tests/unit/CollaborativeWrapper.spec.ts` to `web/packages/web-pkg/tests/unit/components/CollaborativeWrapper.spec.ts` +- Adapt to web-test-helpers' `shallowMount` + `defaultPlugins()` pattern (see `web/packages/design-system/src/components/OcButton/OcButton.spec.ts` as template) +- web-pkg's vitest config (`web/tests/unit/config/vitest.config.ts`) already has happy-dom + Vue SFC support — no config additions needed + +**Integration:** +- Move `realtime-sync.spec.ts` to `web/packages/web-pkg/tests/integration/realtime-sync.spec.ts` (new directory) +- Add an integration vitest config or extend the existing one to include this path +- Document `DEV_FAKE_TOKEN` setup in the integration test file for future maintainers + +**E2E (Cucumber, per user clarification):** +- Convert the 9 Playwright specs from `web-extensions/packages/web-app-{codemirror,tiptap}/tests/e2e/*.spec.ts` into Cucumber features under `web/tests/e2e/cucumber/features/` + step definitions under `web/tests/e2e/cucumber/steps/` +- Reuse existing helpers from `web/tests/e2e/support/` (login, file upload, navigation) as much as possible — the existing `editor.ts` page object is probably the right starting template +- New feature files to create (one per scenario from our existing specs): + - `collaboration/codemirror-open.feature` (load .md → connected → initial content visible) + - `collaboration/codemirror-switching.feature` (route between two .md files keeps the wrapper clean) + - `collaboration/codemirror-multi-user.feature` (admin + recipient on a shared space, cursor + CRDT propagation) + - `collaboration/codemirror-save-back.feature` (type + Ctrl+S → persists via WebDAV) + - `collaboration/codemirror-shared-file.feature` (owner + recipient share scenario) + - same five for tiptap (or fewer — empty-file + open + switching + multi-user) +- Step definitions reuse existing `app-files/utils/editor.ts` patterns; add collab-specific helpers where needed (`when admin opens file in CodeMirror`, `then realtime status reads "connected"`, etc.) + +--- + +## Resolved scope decisions + +1. **All 4 text-editor content-type strategies get a `CollaborativeAdapter`.** markdown + html + plain-text + tiptap-json all become collab-capable in this PR. Risk: only markdown is validated by the PoC e2e suites. The other three need new manual smoke tests (load a sample of each type → verify hydrate / serialize round-trip preserves content; type something → verify update:currentContent emits the right serialized form; save → verify file content on disk matches what was typed). Phase 4 grows by 3 adapters + 3 smoke tests. + +2. **Hocuspocus sidecar runs in web's CI woodpecker config.** Mirror the web-extensions woodpecker setup. The new `hocuspocus` service is started alongside the OC service in the e2e CI job; the Cucumber collab features (Phase 5) run against it. CI failure on collab regression becomes a hard signal instead of a silent local-only check. + +3. **Cross-peer `AppWrapper.currentETag` fix is in scope.** AppWrapper's private `currentETag` ref needs a way to be updated from outside so peer-saved etags don't 412 the local user's next save. Approach: extend AppWrapper to `provide()` a setter (or `defineExpose` a writable ref) — the CollaborativeWrapper `inject()`s it and writes whenever `_oc_meta.etag` is updated via CRDT (signal from a peer save). Cleaner than emit (other editors that don't care about etag pay nothing). Concretely: + - `web/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue`: `provide('appWrapperEtagSync', { setCurrentETag: (etag: string) => { currentETag.value = etag } })` (or a similar named injection key — exported as a typed symbol from AppTemplates/types.ts) + - `web/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue`: `const etagSync = inject(appWrapperEtagSync, null)` — when set and `_oc_meta.etag` changes via meta observer, call `etagSync.setCurrentETag(newEtag)` + - Inject is `null` when CollaborativeWrapper is used standalone (outside an AppWrapper context) — the sync is a no-op, no regression + - Adds one new test: peer A saves, peer B's AppWrapper `currentETag` updates without a refresh (Cucumber feature, Phase 5) + +--- + +## Critical files referenced + +- `web-extensions/packages/web-app-codemirror/src/CollaborativeWrapper.vue` — source (Phase 0 fix, then move in Phase 1) +- `web-extensions/packages/web-app-codemirror/src/types.ts` — adapter contract +- `web-extensions/packages/web-app-codemirror/tests/unit/CollaborativeWrapper.spec.ts` — unit suite (Phase 0 regression test, Phase 5 move) +- `web-extensions/dev/docker/hocuspocus/` — sidecar (Phase 3) +- `web/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue` — host wrapper, integration target +- `web/packages/web-pkg/src/editor/composables/useTextEditor.ts` — text-editor's editor factory (Phase 4) +- `web/packages/web-pkg/src/editor/composables/strategies/markdown.ts` — strategy (Phase 4, adapter source) +- `web/packages/web-app-text-editor/src/App.vue` — text-editor entry (Phase 4) +- `web/tests/unit/config/vitest.config.ts` — web's vitest baseline (Phase 5) +- `web/packages/design-system/src/components/OcButton/OcButton.spec.ts` — Vue spec template to match +- `web/tests/e2e/support/objects/app-files/utils/editor.ts` — existing editor page object (Phase 5, Cucumber port) + +--- + +## Future considerations / follow-ups (not in this PR) + +1. **Proper config field for `realtimeUrl`.** Phase 2 ships an auto-derive fallback (`wss://{configStore.serverUrl}/realtime` when no per-app config is set). Long-term we want a first-class config option — likely in `web/config.json` `options.realtimeUrl` — that requires extending `OptionsConfigSchema` in `web-pkg/composables/piniaStores/config/types.ts`. Until then, convention does the job. + +2. **Non-file collaborative rooms.** Right now the wrapper + sidecar assume every `documentName` is an OC file id (`$!`) and runs WebDAV/Graph permission checks against it. There are use cases for a room that has no file backing — ephemeral whiteboard-style collaboration, comment threads, etc. Idea: the sidecar treats `documentName` prefixes as a routing hint. `file_` → run the current ACL/etag probe path. `room_` → no permission checks, no etag, no save loop. Wrapper would need to know about this distinction too (skip save when in room mode). + +2. **File vs folder check in `onAuthenticate`.** Currently the sidecar's `probeFileAccess` runs WebDAV HEAD on `/remote.php/dav/spaces/{itemId}` and accepts any 200 response, including folders. A user could theoretically open a folder id in a collab editor and get an empty room (the editor would try to save back to a folder URL on every save — almost certainly errors, but ugly). Fix: check the Graph `/items/{itemId}` response's `folder` vs `file` discriminator, or `Resource-Type` PROPFIND, or `Content-Type` from a GET. Reject folders in `onAuthenticate`. + +3. **Cross-peer `AppWrapper.currentETag` fix.** Already scoped into Phase 4.5 of the main plan — extending AppWrapper with an inject contract for peer-saved etags. Listed here as the cleanup that ties the local-mode + collab-mode etag stories together. + +--- + +## Dev loop note (web vs web-extensions) + +In the web repo the dev workflow uses `pnpm vite` (dev server with HMR) instead of building dist/ + docker volume-mounting like web-extensions does. Much simpler iteration loop: no `--mode development` builds, no docker compose restart dance, no manifest path caching to worry about. Prefer `pnpm vite` (or web's equivalent `pnpm dev`) for the Phase 1–4 work; only fall back to compose mounts if a specific compose-only scenario needs them (the Hocuspocus sidecar from Phase 3 still needs compose since it's a sibling service). + +--- + +## Execution checklist + +- [x] **Phase 0**: finish sessionKey watch refactor, add regression test for Y.Doc-rebuild bug, clean up debug logs, all unit + e2e + integration green, commit + push to `feat/realtime-collaboration-poc` — `ac85423` on web-extensions, 13/13 unit + 5/5 codemirror e2e + 4/4 tiptap e2e + 8/8 integration green +- [x] **Phase 1**: copy wrapper + adapter types into web-pkg, add only the truly shared deps (hocuspocus/provider, yjs, y-protocols, semver), exports — files at `packages/web-pkg/src/components/Collaborative/`, vue-tsc type-check green +- [x] **Phase 2**: moved both apps to `web/packages/web-app-{codemirror,tiptap}`, rewired imports to `@opencloud-eu/web-pkg`, slimmed package.json (peer deps for web-pkg/web-client/design-system, app-bound deps for codemirror/tiptap-specific bits, yjs + y-protocols for type-only adapter imports). Registered both in `dev/docker/opencloud.web.config.json` `apps[]`. Wrapper's `realtimeUrl` prop now three-state (`string` / `null` = force-local / `undefined` = derive from `configStore.serverUrl` + `/realtime` convention). vue-tsc green on both apps + web-pkg. +- [ ] **Phase 2.5**: extend `CollaborativeAdapter.serialize` to optionally accept an opaque `context` object (typed `unknown`, each adapter casts as needed — a typed `editor: TiptapEditor` argument makes no sense for the CodeMirror adapter). Editor components expose context via `defineExpose({ getAdapterContext() })`; wrapper grabs the value via template ref and passes it down to `serialize(ydoc, context)`. Tiptap exposes `{ editor: tiptapEditor }` so its adapter reuses the live editor instance — no more headless-editor spawn per debounced serialize. CodeMirror returns `undefined` (its `Y.Text.toString()` is already cheap). Backwards-compatible signature change. Drops the dominant per-keystroke cost for rich Tiptap setups before Phase 4 brings 4 strategies × many extensions online. +- [ ] **Phase 3**: copy hocuspocus sidecar into web compose; switch the sidecar's etag probe from WebDAV HEAD to Graph `/items/{id}` (consistent with the existing Graph permissions call; investigate the "personal-drive 400" mentioned in the original WebDAV-fallback comment) +- [ ] **Phase 4**: refactor web-app-text-editor onto wrapper; require Y.Doc in `useTextEditor`; flip `StarterKit` `undoRedo: false`; ship all 4 content-type adapters (markdown, html, plain-text, tiptap-json) with manual round-trip smoke tests for the three not in the PoC e2e; toolbar / slash commands stay +- [ ] **Phase 4.5**: extend AppWrapper with the etag-sync inject contract; wire CollaborativeWrapper to call it when `_oc_meta.etag` updates via CRDT; one new Cucumber scenario for the cross-peer flow +- [ ] **Phase 5**: migrate unit + integration tests, Cucumber-port the e2e suites with existing helpers where possible; add hocuspocus to web's woodpecker e2e CI job +- [ ] Smoke test full dev loop in web; commit per phase; PR against `opencloud-eu/web` main diff --git a/dev/docker/opencloud.web.config.json b/dev/docker/opencloud.web.config.json index 3838f4896d..545cd70ec4 100644 --- a/dev/docker/opencloud.web.config.json +++ b/dev/docker/opencloud.web.config.json @@ -18,6 +18,8 @@ "activities", "preview", "mail", - "contacts" + "contacts", + "codemirror", + "tiptap" ] } diff --git a/packages/web-app-codemirror/extension.d.ts b/packages/web-app-codemirror/extension.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/web-app-codemirror/extension.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/web-app-codemirror/l10n/translations.json b/packages/web-app-codemirror/l10n/translations.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/web-app-codemirror/l10n/translations.json @@ -0,0 +1 @@ +{} diff --git a/packages/web-app-codemirror/package.json b/packages/web-app-codemirror/package.json new file mode 100644 index 0000000000..828c026148 --- /dev/null +++ b/packages/web-app-codemirror/package.json @@ -0,0 +1,25 @@ +{ + "name": "codemirror", + "version": "0.1.0", + "private": true, + "description": "OpenCloud Web collaborative CodeMirror editor", + "license": "AGPL-3.0", + "type": "module", + "dependencies": { + "@codemirror/lang-markdown": "^6.3.0", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.34.0", + "y-codemirror.next": "^0.3.5", + "y-protocols": "^1.0.7", + "yjs": "^13.6.0" + }, + "devDependencies": { + "@opencloud-eu/web-test-helpers": "workspace:*" + }, + "peerDependencies": { + "@opencloud-eu/design-system": "workspace:^", + "@opencloud-eu/web-client": "workspace:*", + "@opencloud-eu/web-pkg": "workspace:*", + "vue3-gettext": "^4.0.0-beta.1" + } +} diff --git a/packages/web-app-codemirror/src/App.vue b/packages/web-app-codemirror/src/App.vue new file mode 100644 index 0000000000..e3f34fde91 --- /dev/null +++ b/packages/web-app-codemirror/src/App.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/web-app-codemirror/src/CodeMirrorEditor.vue b/packages/web-app-codemirror/src/CodeMirrorEditor.vue new file mode 100644 index 0000000000..29eb29567e --- /dev/null +++ b/packages/web-app-codemirror/src/CodeMirrorEditor.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/web-app-codemirror/src/adapters/codemirrorMarkdown.ts b/packages/web-app-codemirror/src/adapters/codemirrorMarkdown.ts new file mode 100644 index 0000000000..8be08eeaa0 --- /dev/null +++ b/packages/web-app-codemirror/src/adapters/codemirrorMarkdown.ts @@ -0,0 +1,31 @@ +import type * as Y from 'yjs' +import type { CollaborativeAdapter } from '@opencloud-eu/web-pkg' + +const SHARED_TEXT_KEY = 'content' + +export const codemirrorMarkdownAdapter: CollaborativeAdapter = { + hydrate(ydoc: Y.Doc, content: string) { + const yText = ydoc.getText(SHARED_TEXT_KEY) + if (yText.length > 0) return + if (!content) return + ydoc.transact(() => { + yText.insert(0, content) + }, 'hydrate') + }, + + serialize(ydoc: Y.Doc): string { + return ydoc.getText(SHARED_TEXT_KEY).toString() + }, + + hasContent(ydoc: Y.Doc): boolean { + return ydoc.getText(SHARED_TEXT_KEY).length > 0 + }, + + reset(ydoc: Y.Doc) { + const yText = ydoc.getText(SHARED_TEXT_KEY) + if (yText.length === 0) return + ydoc.transact(() => { + yText.delete(0, yText.length) + }, 'reset') + } +} diff --git a/packages/web-app-codemirror/src/index.ts b/packages/web-app-codemirror/src/index.ts new file mode 100644 index 0000000000..f65998622e --- /dev/null +++ b/packages/web-app-codemirror/src/index.ts @@ -0,0 +1,44 @@ +import { AppWrapperRoute, defineWebApplication } from '@opencloud-eu/web-pkg' +import { useGettext } from 'vue3-gettext' +import App from './App.vue' +import translations from '../l10n/translations.json' + +const applicationId = 'codemirror' + +export default defineWebApplication({ + setup() { + const { $gettext } = useGettext() + + const routes = [ + { + name: applicationId, + path: '/:driveAliasAndItem(.*)?', + component: AppWrapperRoute(App, { applicationId }), + meta: { + authContext: 'hybrid', + title: $gettext('CodeMirror'), + patchCleanPath: true + } + } + ] + + const appInfo = { + name: $gettext('CodeMirror'), + id: applicationId, + icon: 'file-text', + defaultExtension: 'md', + extensions: [ + { + extension: 'md', + routeName: applicationId + } + ] + } + + return { + appInfo, + routes, + translations + } + } +}) diff --git a/packages/web-app-codemirror/tsconfig.json b/packages/web-app-codemirror/tsconfig.json new file mode 100644 index 0000000000..4082f16a5d --- /dev/null +++ b/packages/web-app-codemirror/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/web-app-tiptap/extension.d.ts b/packages/web-app-tiptap/extension.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/web-app-tiptap/extension.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/web-app-tiptap/l10n/translations.json b/packages/web-app-tiptap/l10n/translations.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/web-app-tiptap/l10n/translations.json @@ -0,0 +1 @@ +{} diff --git a/packages/web-app-tiptap/package.json b/packages/web-app-tiptap/package.json new file mode 100644 index 0000000000..1f70e64dae --- /dev/null +++ b/packages/web-app-tiptap/package.json @@ -0,0 +1,28 @@ +{ + "name": "tiptap", + "version": "0.1.0", + "private": true, + "description": "OpenCloud Web collaborative Tiptap rich-text editor", + "license": "AGPL-3.0", + "type": "module", + "dependencies": { + "@hocuspocus/provider": "^4.0.0", + "@tiptap/core": "^3.20.4", + "@tiptap/extension-collaboration": "^3.20.4", + "@tiptap/markdown": "^3.20.4", + "@tiptap/starter-kit": "^3.20.4", + "@tiptap/vue-3": "^3.20.4", + "@tiptap/y-tiptap": "^3.0.0", + "y-protocols": "^1.0.7", + "yjs": "^13.6.0" + }, + "devDependencies": { + "@opencloud-eu/web-test-helpers": "workspace:*" + }, + "peerDependencies": { + "@opencloud-eu/design-system": "workspace:^", + "@opencloud-eu/web-client": "workspace:*", + "@opencloud-eu/web-pkg": "workspace:*", + "vue3-gettext": "^4.0.0-beta.1" + } +} diff --git a/packages/web-app-tiptap/src/App.vue b/packages/web-app-tiptap/src/App.vue new file mode 100644 index 0000000000..e4d5744c12 --- /dev/null +++ b/packages/web-app-tiptap/src/App.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/web-app-tiptap/src/TiptapEditor.vue b/packages/web-app-tiptap/src/TiptapEditor.vue new file mode 100644 index 0000000000..675381f24f --- /dev/null +++ b/packages/web-app-tiptap/src/TiptapEditor.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/packages/web-app-tiptap/src/adapters/tiptapMarkdown.ts b/packages/web-app-tiptap/src/adapters/tiptapMarkdown.ts new file mode 100644 index 0000000000..66f277237c --- /dev/null +++ b/packages/web-app-tiptap/src/adapters/tiptapMarkdown.ts @@ -0,0 +1,66 @@ +import * as Y from 'yjs' +import { Editor } from '@tiptap/core' +import StarterKit from '@tiptap/starter-kit' +import { Collaboration } from '@tiptap/extension-collaboration' +import { Markdown } from '@tiptap/markdown' +import type { CollaborativeAdapter } from '@opencloud-eu/web-pkg' + +// Tiptap binds Collaboration to a named Y.XmlFragment on the doc. We use +// the extension's default field name; the editor component must match. +const FRAGMENT = 'default' + +// Spin up a Tiptap editor with no visible DOM and the editor bound to the +// caller's Y.Doc. Loading the schema + Markdown extension is enough to do +// MD ↔ ProseMirror conversions; the editor never paints anywhere on screen. +// We attach to a detached
because @tiptap/core requires an element. +function makeHeadlessEditor(ydoc: Y.Doc): Editor { + const detached = document.createElement('div') + return new Editor({ + element: detached, + extensions: [ + StarterKit.configure({ + // Yjs Collaboration replaces StarterKit's undo/redo with yUndoPlugin + // from y-tiptap. (Was `history: false` in tiptap v2, renamed in v3.) + undoRedo: false + }), + Markdown, + Collaboration.configure({ document: ydoc, field: FRAGMENT }) + ] + }) +} + +export const tiptapMarkdownAdapter: CollaborativeAdapter = { + hydrate(ydoc: Y.Doc, content: string) { + if (!content) return + const editor = makeHeadlessEditor(ydoc) + try { + // contentType: 'markdown' routes the input through @tiptap/markdown's + // parser. The Collaboration plugin propagates the resulting + // ProseMirror state into the bound Y.XmlFragment. + editor.commands.setContent(content, { contentType: 'markdown' }) + } finally { + editor.destroy() + } + }, + + serialize(ydoc: Y.Doc): string { + const editor = makeHeadlessEditor(ydoc) + try { + return editor.getMarkdown() + } finally { + editor.destroy() + } + }, + + hasContent(ydoc: Y.Doc): boolean { + return ydoc.getXmlFragment(FRAGMENT).length > 0 + }, + + reset(ydoc: Y.Doc) { + const frag = ydoc.getXmlFragment(FRAGMENT) + if (frag.length === 0) return + ydoc.transact(() => { + frag.delete(0, frag.length) + }, 'reset') + } +} diff --git a/packages/web-app-tiptap/src/index.ts b/packages/web-app-tiptap/src/index.ts new file mode 100644 index 0000000000..c75ba8b659 --- /dev/null +++ b/packages/web-app-tiptap/src/index.ts @@ -0,0 +1,44 @@ +import { AppWrapperRoute, defineWebApplication } from '@opencloud-eu/web-pkg' +import { useGettext } from 'vue3-gettext' +import App from './App.vue' +import translations from '../l10n/translations.json' + +const applicationId = 'tiptap' + +export default defineWebApplication({ + setup() { + const { $gettext } = useGettext() + + const routes = [ + { + name: applicationId, + path: '/:driveAliasAndItem(.*)?', + component: AppWrapperRoute(App, { applicationId }), + meta: { + authContext: 'hybrid', + title: $gettext('Tiptap'), + patchCleanPath: true + } + } + ] + + const appInfo = { + name: $gettext('Tiptap'), + id: applicationId, + icon: 'file-paper', + defaultExtension: 'md', + extensions: [ + { + extension: 'md', + routeName: applicationId + } + ] + } + + return { + appInfo, + routes, + translations + } + } +}) diff --git a/packages/web-app-tiptap/tsconfig.json b/packages/web-app-tiptap/tsconfig.json new file mode 100644 index 0000000000..4082f16a5d --- /dev/null +++ b/packages/web-app-tiptap/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/web-pkg/package.json b/packages/web-pkg/package.json index 2c4a51ce11..69d784f196 100644 --- a/packages/web-pkg/package.json +++ b/packages/web-pkg/package.json @@ -49,6 +49,7 @@ "dependencies": { "@casl/ability": "^6.8.0", "@casl/vue": "^2.2.6", + "@hocuspocus/provider": "^4.0.0", "@microsoft/fetch-event-source": "^2.0.1", "@opencloud-eu/design-system": "workspace:^", "@opencloud-eu/web-client": "workspace:^", @@ -91,16 +92,20 @@ "password-sheriff": "^2.0.0", "pinia": "^3.0.4", "qs": "^6.15.0", + "semver": "^7.8.0", "uuid": "^14.0.0", "vue-concurrency": "^5.0.3", "vue-router": "^5.0.4", "vue3-gettext": "4.0.0-beta.1", + "y-protocols": "^1.0.7", + "yjs": "^13.6.0", "zod": "^4.3.6" }, "devDependencies": { "@opencloud-eu/web-test-helpers": "workspace:^", "@types/lodash-es": "4.17.12", "@types/node": "^25.5.0", + "@types/semver": "^7.7.0", "@vitest/web-worker": "^4.1.2", "vite-plugin-node-polyfills": "0.28.0" } diff --git a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue new file mode 100644 index 0000000000..2b8e2aab2a --- /dev/null +++ b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue @@ -0,0 +1,480 @@ + + + diff --git a/packages/web-pkg/src/components/Collaborative/index.ts b/packages/web-pkg/src/components/Collaborative/index.ts new file mode 100644 index 0000000000..2d73d772d5 --- /dev/null +++ b/packages/web-pkg/src/components/Collaborative/index.ts @@ -0,0 +1,2 @@ +export { default as CollaborativeWrapper } from './CollaborativeWrapper.vue' +export type { CollaborativeAdapter } from './types' diff --git a/packages/web-pkg/src/components/Collaborative/types.ts b/packages/web-pkg/src/components/Collaborative/types.ts new file mode 100644 index 0000000000..0d3fdb33a5 --- /dev/null +++ b/packages/web-pkg/src/components/Collaborative/types.ts @@ -0,0 +1,43 @@ +import type * as Y from 'yjs' + +/** + * App-specific adapter between the native file format and the shared Y.Doc. + * The wrapper itself stays generic: it handles realtime sync, the etag loop, + * and lifecycle. Adapters describe how to move bytes in and out of the doc. + * + * Implementations must be deterministic — given the same Y.Doc state, + * `serialize` must always return the same content. + */ +export interface CollaborativeAdapter { + /** + * Populate an empty Y.Doc from the native file content. Called once per + * document by the elected hydrating client; other clients receive the + * resulting Y.Doc state through the realtime sync. + * + * Must be a no-op if the Y.Doc already has app data. + */ + hydrate(ydoc: Y.Doc, content: string): void | Promise + + /** + * Render the current Y.Doc state to the native file format for WebDAV PUT. + */ + serialize(ydoc: Y.Doc): string | Promise + + /** + * Returns true if the adapter has populated the doc with app data. + * Used to detect "doc is empty, needs hydration" without the wrapper + * knowing the adapter's shared-type layout. + */ + hasContent(ydoc: Y.Doc): boolean + + /** + * Wipe the adapter's shared content so `hasContent` returns false again. + * The wrapper calls this when the sidecar signals a stale persisted Y.Doc + * (external file write happened between sessions); the elected client + * then re-hydrates from the fresh native content. + * + * Optional — adapters that omit this won't recover from a stale-state + * signal in-place; the wrapper falls back to forcing a full reload. + */ + reset?(ydoc: Y.Doc): void +} diff --git a/packages/web-pkg/src/components/index.ts b/packages/web-pkg/src/components/index.ts index 2dfb1eeaf6..cb9f96501c 100644 --- a/packages/web-pkg/src/components/index.ts +++ b/packages/web-pkg/src/components/index.ts @@ -1,5 +1,6 @@ export * from './AppBar' export * from './AppTemplates' +export * from './Collaborative' export * from './ContextActions' export * from './FilesList' export * from './Filters' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90cc532d0d..8be5d279dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,7 +32,7 @@ importers: version: 2.4.0(node-fetch@3.3.2) '@module-federation/vite': specifier: 1.15.5 - version: 1.15.5(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3)) + version: 1.15.5(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3)) '@noble/hashes': specifier: 2.2.0 version: 2.2.0 @@ -50,7 +50,7 @@ importers: version: 1.60.0 '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -65,7 +65,7 @@ importers: version: 6.15.1 '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) '@vitest/coverage-v8': specifier: ^4.1.2 version: 4.1.6(vitest@4.1.6) @@ -116,7 +116,7 @@ importers: version: 4.3.0 ts-node: specifier: 10.9.2 - version: 10.9.2(@types/node@25.9.1)(typescript@6.0.3) + version: 10.9.2(@types/node@25.9.0)(typescript@6.0.3) tslib: specifier: 2.8.1 version: 2.8.1 @@ -125,16 +125,16 @@ importers: version: 6.0.3 vite: specifier: ^8.0.3 - version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + version: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) vite-plugin-node-polyfills: specifier: 0.28.0 - version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) vite-plugin-static-copy: specifier: ^4.0.0 - version: 4.1.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.1.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) vitest: specifier: ^4.1.2 - version: 4.1.6(@types/node@25.9.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.1.6(@types/node@25.9.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) vitest-mock-extended: specifier: 4.0.0 version: 4.0.0(typescript@6.0.3)(vitest@4.1.6) @@ -198,10 +198,10 @@ importers: version: link:../web-test-helpers '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) markdown-it-container: specifier: ^4.0.0 version: 4.0.0 @@ -222,13 +222,13 @@ importers: version: 0.11.4 vite: specifier: ^8.0.3 - version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + version: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) vite-plugin-node-polyfills: specifier: ^0.28.0 - version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) vitepress: specifier: ^1.6.4 - version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.1)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3) + version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.0)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3) packages/eslint-config: dependencies: @@ -258,26 +258,26 @@ importers: dependencies: '@module-federation/vite': specifier: 1.15.4 - version: 1.15.4(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3)) + version: 1.15.4(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3)) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@vitejs/plugin-basic-ssl': specifier: ^2.3.0 - version: 2.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 2.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) vite: specifier: ^7.2.0 || ^8.0.0 - version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + version: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.6(@types/node@25.9.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.1.6(@types/node@25.9.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) devDependencies: vite-plugin-static-copy: specifier: ^4.0.0 - version: 4.1.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 4.1.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) packages/prettier-config: dependencies: @@ -407,6 +407,43 @@ importers: specifier: workspace:* version: link:../web-test-helpers + packages/web-app-codemirror: + dependencies: + '@codemirror/lang-markdown': + specifier: ^6.3.0 + version: 6.5.0 + '@codemirror/state': + specifier: ^6.5.0 + version: 6.6.0 + '@codemirror/view': + specifier: ^6.34.0 + version: 6.43.0 + '@opencloud-eu/design-system': + specifier: workspace:^ + version: link:../design-system + '@opencloud-eu/web-client': + specifier: workspace:* + version: link:../web-client + '@opencloud-eu/web-pkg': + specifier: workspace:* + version: link:../web-pkg + vue3-gettext: + specifier: ^4.0.0-beta.1 + version: 4.0.0-beta.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@6.0.3)) + y-codemirror.next: + specifier: ^0.3.5 + version: 0.3.5(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(yjs@13.6.30) + y-protocols: + specifier: ^1.0.7 + version: 1.0.7(yjs@13.6.30) + yjs: + specifier: ^13.6.0 + version: 13.6.30 + devDependencies: + '@opencloud-eu/web-test-helpers': + specifier: workspace:* + version: link:../web-test-helpers + packages/web-app-contacts: dependencies: '@opencloud-eu/design-system': @@ -755,6 +792,52 @@ importers: specifier: workspace:* version: link:../web-test-helpers + packages/web-app-tiptap: + dependencies: + '@hocuspocus/provider': + specifier: ^4.0.0 + version: 4.0.0(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30) + '@opencloud-eu/design-system': + specifier: workspace:^ + version: link:../design-system + '@opencloud-eu/web-client': + specifier: workspace:* + version: link:../web-client + '@opencloud-eu/web-pkg': + specifier: workspace:* + version: link:../web-pkg + '@tiptap/core': + specifier: ^3.20.4 + version: 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/extension-collaboration': + specifier: ^3.20.4 + version: 3.23.5(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30) + '@tiptap/markdown': + specifier: ^3.20.4 + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/starter-kit': + specifier: ^3.20.4 + version: 3.23.4 + '@tiptap/vue-3': + specifier: ^3.20.4 + version: 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(vue@3.5.34(typescript@6.0.3)) + '@tiptap/y-tiptap': + specifier: ^3.0.0 + version: 3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30) + vue3-gettext: + specifier: ^4.0.0-beta.1 + version: 4.0.0-beta.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@6.0.3)) + y-protocols: + specifier: ^1.0.7 + version: 1.0.7(yjs@13.6.30) + yjs: + specifier: ^13.6.0 + version: 13.6.30 + devDependencies: + '@opencloud-eu/web-test-helpers': + specifier: workspace:* + version: link:../web-test-helpers + packages/web-app-webfinger: dependencies: '@opencloud-eu/design-system': @@ -809,13 +892,13 @@ importers: version: 3.7.1 '@types/node': specifier: ^25.5.0 - version: 25.9.1 + version: 25.9.0 vite: specifier: ^8.0.3 - version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + version: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) vite-plugin-node-polyfills: specifier: 0.28.0 - version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) packages/web-pkg: dependencies: @@ -825,6 +908,9 @@ importers: '@casl/vue': specifier: ^2.2.6 version: 2.2.6(@casl/ability@6.8.1)(vue@3.5.34(typescript@6.0.3)) + '@hocuspocus/provider': + specifier: ^4.0.0 + version: 4.0.0(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30) '@microsoft/fetch-event-source': specifier: ^2.0.1 version: 2.0.1 @@ -839,58 +925,58 @@ importers: version: 10.53.1(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)) '@tiptap/core': specifier: ^3.20.4 - version: 3.23.5(@tiptap/pm@3.23.5) + version: 3.23.4(@tiptap/pm@3.23.4) '@tiptap/extension-document': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/extension-hard-break': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/extension-image': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/extension-link': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) '@tiptap/extension-paragraph': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/extension-placeholder': specifier: ^3.20.4 - version: 3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) + version: 3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) '@tiptap/extension-table': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) '@tiptap/extension-task-item': specifier: ^3.20.4 - version: 3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) '@tiptap/extension-task-list': specifier: ^3.20.4 - version: 3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) '@tiptap/extension-text': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/extension-text-style': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/extension-underline': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) '@tiptap/markdown': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) '@tiptap/pm': specifier: ^3.20.4 - version: 3.23.5 + version: 3.23.4 '@tiptap/starter-kit': specifier: ^3.20.4 - version: 3.23.5 + version: 3.23.4 '@tiptap/suggestion': specifier: ^3.20.4 - version: 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) '@tiptap/vue-3': specifier: ^3.20.4 - version: 3.23.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)(vue@3.5.34(typescript@6.0.3)) + version: 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(vue@3.5.34(typescript@6.0.3)) '@uppy/core': specifier: ^5.2.0 version: 5.2.0 @@ -951,6 +1037,9 @@ importers: qs: specifier: ^6.15.0 version: 6.15.2 + semver: + specifier: ^7.8.0 + version: 7.8.0 uuid: specifier: ^14.0.0 version: 14.0.0 @@ -963,6 +1052,12 @@ importers: vue3-gettext: specifier: 4.0.0-beta.1 version: 4.0.0-beta.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@6.0.3)) + y-protocols: + specifier: ^1.0.7 + version: 1.0.7(yjs@13.6.30) + yjs: + specifier: ^13.6.0 + version: 13.6.30 zod: specifier: ^4.3.6 version: 4.4.3 @@ -975,13 +1070,16 @@ importers: version: 4.17.12 '@types/node': specifier: ^25.5.0 - version: 25.9.1 + version: 25.9.0 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.1 '@vitest/web-worker': specifier: ^4.1.2 version: 4.1.6(vitest@4.1.6) vite-plugin-node-polyfills: specifier: 0.28.0 - version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) packages/web-runtime: dependencies: @@ -1121,13 +1219,13 @@ importers: devDependencies: '@types/node': specifier: ^25.5.0 - version: 25.9.1 + version: 25.9.0 '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) vite: specifier: ^8.0.3 - version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + version: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) tests/e2e: devDependencies: @@ -1291,6 +1389,33 @@ packages: '@casl/ability': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0 vue: ^3.0.0 + '@codemirror/autocomplete@6.20.2': + resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + + '@codemirror/lint@6.9.6': + resolution: {integrity: sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==} + + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + + '@codemirror/view@6.43.0': + resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1607,6 +1732,15 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@hocuspocus/common@4.0.0': + resolution: {integrity: sha512-7BE8TsKBkdiOZO6tfm3ny6bIHPbxkIZb3hsYdVn/X5xbXI8n8w9pnE6pXgEMKQhJm6zsWsa9IDRJIp/c9u+DmA==} + + '@hocuspocus/provider@4.0.0': + resolution: {integrity: sha512-08gpeNZ6x2pmRD6m4XwRD52yQKnTl32a0HS9VSXZ5A1dIBVqxMz/x8Z06XbkKM2X8sp6vWEUCZCtzAGFSsofgg==} + peerDependencies: + y-protocols: ^1.0.6 + yjs: ^13.6.8 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1656,6 +1790,33 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/css@1.3.3': + resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@lezer/markdown@1.6.3': + resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + + '@lifeomic/attempt@3.1.0': + resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@microsoft/fetch-event-source@2.0.1': resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} @@ -2248,163 +2409,177 @@ packages: resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} engines: {node: '>=14'} - '@tiptap/core@3.23.5': - resolution: {integrity: sha512-657Xqcgf1IYWLkAmRDJKNSGdoS1AHJEgK6zHWHFJERQGIqHnwC7Fz7nvWs/NQhQVBkclQd0ERRdTCZ3XwRc1+g==} + '@tiptap/core@3.23.4': + resolution: {integrity: sha512-ni2LWE52bVeSt3L2HVBSmbBw+elc32ATej9C68EyKzN/8vR5ILxFn6RCdDTKm4asmwZyq2jys12dKmBdWMr9QA==} peerDependencies: - '@tiptap/pm': 3.23.5 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-blockquote@3.23.5': - resolution: {integrity: sha512-PBQRoGfSWfIY7HmGbS5PTHEBQl5nKbild5J5phPLFF+O3aOBQ0d49AC9cxbaou/6FRCtq6g4Uqse9rRTKJRM0w==} + '@tiptap/extension-blockquote@3.23.4': + resolution: {integrity: sha512-7YjSibNlPcy9eGK+tHt5G/Njr7nPxl+rZ3rCC6TwtLIRLSHPnoGDsfFOgTPkXxaQcE1a/VQwemnYfWc3kdIjDQ==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-bold@3.23.5': - resolution: {integrity: sha512-DZsDCCf53fA9HmsFzfUHl5jLOwDYf+XzfP+QJjJ4cK23SsxDirameTjgnwi4l1EgEPLWunMZQjU+wHmh7vvX6Q==} + '@tiptap/extension-bold@3.23.4': + resolution: {integrity: sha512-3L9tnZ12i+98u5df2nV2zGu/sc3rhI87E3ocn1YYAO8PJUAgZnMwdet8JawCrS1uut5sRKlxo3SXEmdNfRVm/w==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-bubble-menu@3.23.5': - resolution: {integrity: sha512-otcGwyVO6OfxdDPnbooZxYGrb+6q5WYmS+g2V+XGGNRn5oJgyY5pW0dqELIUJ66dosIIXXPyw2XqBDpMMY2kyQ==} + '@tiptap/extension-bubble-menu@3.23.4': + resolution: {integrity: sha512-EPTpL/IFp/aTGZErBq/Mc3dKznj6G/qNEkVYWjueOn1oKApyT0P6WVHGvu/vpMdErhzmoGDuFPPGVS6T8Upx2Q==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-bullet-list@3.23.5': - resolution: {integrity: sha512-o0bzZbFvOPhPX6+RAhIFPKMIN3jIenY6Ib3FJ6ZqxTdVcjuV2mIXUmJU0uV2BwKtz73GmKSRKRKia6KJ0ml8qA==} + '@tiptap/extension-bullet-list@3.23.4': + resolution: {integrity: sha512-mXB2KZOz1R+E6VNTZ3vzdAk7ZDGKjPmsJEZIQg1B5qRycTKg49/rCCkLA2QnqAwX6BzS3mLLH1RWE2W0oXD7vg==} peerDependencies: - '@tiptap/extension-list': 3.23.5 + '@tiptap/extension-list': 3.23.4 - '@tiptap/extension-code-block@3.23.5': - resolution: {integrity: sha512-P2XH8WPM4UahavcWoQgAwNAKQCbF/JWi6ZqgsQmVBfAqQ3mf8gMxB7HnciMq1DlyI9EfjXoJH11yUqldF/6AaQ==} + '@tiptap/extension-code-block@3.23.4': + resolution: {integrity: sha512-UEU1w/85CSNKktbhESnIRmtjKcH7DeschReZA8err1wAnYLTKzid5ucnJSJ25iRg2V5Fnuws5gnPT5CVgdfXCQ==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-code@3.23.5': - resolution: {integrity: sha512-NOJUD2Z0hrtBWnovXiiH1XtOjEQePOfIG3bNJgXSs1bWxPVhqp6KjVd8mUJNra974hxbml3tC97sL9QqjpAWFg==} + '@tiptap/extension-code@3.23.4': + resolution: {integrity: sha512-C0TeRipMycUEBnV+Mzx6eLp/yZb6Vi/waP3Tkb0lO5/ikg7LWLB7AlmMunjIXEUcR/pJHID/aEh5PfJFpysUDg==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-document@3.23.5': - resolution: {integrity: sha512-Y7uPjEM1xIK4Spcdk/kp/vZ/Az3cEaglTCk6uHrWvNFVglEoGehNb6IQbQFZW0wjE19YoMIiLBLtG6V9dqrpBw==} + '@tiptap/extension-collaboration@3.23.5': + resolution: {integrity: sha512-jtYuVAOAjehCpvm9Bk/Qy6uQQfml+DTVSFNJbLZ/nAyB2N1IfdGz4Mb1+v/K86OHBBqHQpq9ECs+VA4X8eK2wg==} peerDependencies: '@tiptap/core': 3.23.5 + '@tiptap/pm': 3.23.5 + '@tiptap/y-tiptap': ^3.0.2 + yjs: ^13 - '@tiptap/extension-dropcursor@3.23.5': - resolution: {integrity: sha512-l72R798Q69D6f89Vp9xreoRnPcpK0LHPKLZIc6pvqBC2iOjx5wLKtW0uP1uqVWdQtvF5AUYBRNIGAQ5Gel9XEg==} + '@tiptap/extension-document@3.23.4': + resolution: {integrity: sha512-YC4G6VkxT629rlqUTwD6XvOpxjvghn7fxrK4RbyKVJY2C6E1vgmX0won1Ast6v+qTE6iONOMS6f6VyPxSGjg4w==} peerDependencies: - '@tiptap/extensions': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-floating-menu@3.23.5': - resolution: {integrity: sha512-kP0bZKH/lxNogfvoIy/YJZ5gkty0OwqFVtQUwoc85vXYUfvy5Jh1VdO053tCE1iDzmvOITUpcb+MdWryP8dBxA==} + '@tiptap/extension-dropcursor@3.23.4': + resolution: {integrity: sha512-ujJQUIENk0RwVFCh5g/TOSEv1a7Pnam/cjHmSUqHWUNZkYS9aOqjm+JfURJPCinRS2oHvo3AARul5mkKgDJYcA==} + peerDependencies: + '@tiptap/extensions': 3.23.4 + + '@tiptap/extension-floating-menu@3.23.4': + resolution: {integrity: sha512-eAc72bKM26yIPx0jsU8qdjE71vFNVu5R9jGbdItBMFc0SPLS4qY8g+8RJ+iWoLwbcSEpgooLS9D9sLfdAU+Tvw==} peerDependencies: '@floating-ui/dom': ^1.0.0 - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-gapcursor@3.23.5': - resolution: {integrity: sha512-x9XlYG26TowX0Ly1w0ZV2D8qliyQy9fTmMY4suI6B/6o6m/sXHGTAJMmJqwP66sZKF6cMLU3HECumhtyQxPT2g==} + '@tiptap/extension-gapcursor@3.23.4': + resolution: {integrity: sha512-RuyvOlIGP6UpVOc0Lw0L63jKLtYM49CNhPV2OMSfwwwbBZ3pJGos2/SqpYg71d3sn+qpsAopS4Pfr8iPZog73A==} peerDependencies: - '@tiptap/extensions': 3.23.5 + '@tiptap/extensions': 3.23.4 - '@tiptap/extension-hard-break@3.23.5': - resolution: {integrity: sha512-j/BDBMOA1mA+RhCx622SRPBhpp2XWNFYz9asbg8T3yk8v9WI3Vjo6IDlfTp6fwsR2LGE7Pek3R0xDAjW6yVG3g==} + '@tiptap/extension-hard-break@3.23.4': + resolution: {integrity: sha512-ODlpZCi7n136BH9luM09EFL8Pg+bbRCd0tzCQM5BKMXRkLitYZA8Gl/f5DLmGJ50wzFsDPXK2Br2g9UvZK7COg==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-heading@3.23.5': - resolution: {integrity: sha512-tFI+iYk34geacVOGqYgyoC8siQjdGn605XaYSZcGRFF8NY+HrGlLkQi2QRRCeLaUhxoctONmWc8USn3H5U7wLQ==} + '@tiptap/extension-heading@3.23.4': + resolution: {integrity: sha512-8W9Hqi0J69Xbqg08nPf4xRMJXMccaKFAgUE1tvy5PAWJSQxOMwkKQXgZXxwe+80sOMUnV8qveBqUy/ODMPgAxQ==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-horizontal-rule@3.23.5': - resolution: {integrity: sha512-9XkRYc4XE0stERZB3y8bsJd32Jw9UZfMwZXo1GLNYRHFr7dmhSGUj0IzgofqOVmLDcOMW6XcCk54TBYw6BCrWA==} + '@tiptap/extension-horizontal-rule@3.23.4': + resolution: {integrity: sha512-EA4kK8ywZ4dQNOdxeZbplmDDs5T5LjMgHpqxRwukj9wwKiILOK5E3fcKm1fCKh9Q02w96jax6YVccHwmgJP3sQ==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-image@3.23.5': - resolution: {integrity: sha512-v6u9zbJSKLjml6DDn1/1WOOIzVxz3K5Idl1EgUl+IpJH7kR1HLRJ3TaSgF7z2RRQmqyHlmtdCzdaKoe0jCIyqQ==} + '@tiptap/extension-image@3.23.4': + resolution: {integrity: sha512-qandp5HLRl+n8D61+LCT67qtb1uSKffyEGD0fVTkg/RfbyFsJvCDFbjVEoiIG8JOx8O5DehgrDCvS35QOWgr2A==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-italic@3.23.5': - resolution: {integrity: sha512-XjRSPr6j4mz+8O5j5KNfxVb+1fGNt0wr+js6MLxxGdU7M+PoDPdVY6fARbmBazv4ERlZ5PNS9m35Vo5xDjDfrg==} + '@tiptap/extension-italic@3.23.4': + resolution: {integrity: sha512-jUAHi+HZlg47BzgVIy6y/UH5vev7vPQ95jddhB5K3hC122kvWFMXlken7UOnqzbxNcHs2+4Oi/ZJirYMpT4P5w==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-link@3.23.5': - resolution: {integrity: sha512-FEI58NAPnauBbs4nw1dkgRyEhcWnure0vIlStfQoQGXxj3xSRvxKH2lOkz54fGzuzRJAoudyLU65HW6D7kc+8Q==} + '@tiptap/extension-link@3.23.4': + resolution: {integrity: sha512-XjxltY7MomwfTs6jmN6Bw5bb/upb34lpyqv2RiXppFTK25Br7ipksRjUpWpB4/csZeg30qwrLGVKxCol38ffrw==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-list-item@3.23.5': - resolution: {integrity: sha512-l7Hb4rfNIkO6JrNJYkdXap6QYXCz4XeeFmI1bfQgEiwPGs+RAn/+0cOdg7q+6MmtZFac5uSXV0PftPk6A0GsEA==} + '@tiptap/extension-list-item@3.23.4': + resolution: {integrity: sha512-Q/JXosShD5oyDwukE6igdrZD2lb0ZgyoQTHYchk0pzU4frClFbn3RI1wKP+XeqKLhdO6KH2WZ9rERGH7PtDi7Q==} peerDependencies: - '@tiptap/extension-list': 3.23.5 + '@tiptap/extension-list': 3.23.4 - '@tiptap/extension-list-keymap@3.23.5': - resolution: {integrity: sha512-Hz8jRA51VSiHezEkwqwaMYbTEYcR/5Aq3UgCgDlNPlE6k1OZrvRtV/4s3AOO0RRgzyVLKv7yv7KuOJN/OLGErw==} + '@tiptap/extension-list-keymap@3.23.4': + resolution: {integrity: sha512-9FezifCfuoc0o+5K6l4QNOOfelqxnDGg/f9oL1D/LFZPC54bPxpWWft9QCWOqyqZgyLCLjbCjciAlbgkrFUmmw==} peerDependencies: - '@tiptap/extension-list': 3.23.5 + '@tiptap/extension-list': 3.23.4 - '@tiptap/extension-list@3.23.5': - resolution: {integrity: sha512-nzZXpVwnyKwTj4TVyPyu1bCUFjJCsaXnhAthmvJDnX3RBtemNG9Ka07xGR2NIspzumSbQSMFtDxjmxv3W5dEtg==} + '@tiptap/extension-list@3.23.4': + resolution: {integrity: sha512-yuauDm6qW/7q+ZO0YJBKQEGdnUm6DDTJM8AMp9bMZrT4jRf/zyUtNcZ91QEfFvBcyVuI+10PIOXtNPevhQ741Q==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-ordered-list@3.23.5': - resolution: {integrity: sha512-qQeU71ij0cAAD9bbGqot5T5bpR3dysgQ+W67quRs6VDyusU89EYaJHKn/qWU6a1XOEQ4sL+5GNw52FYQVHUxbA==} + '@tiptap/extension-ordered-list@3.23.4': + resolution: {integrity: sha512-+3ofyssYnOTa1+nFWEmCAY1ngn8nAV1xo25JnNNC87NMU9WkSgr93jB7/uUJP0uui1C2dBLlaup3XXm108yarw==} peerDependencies: - '@tiptap/extension-list': 3.23.5 + '@tiptap/extension-list': 3.23.4 - '@tiptap/extension-paragraph@3.23.5': - resolution: {integrity: sha512-LtgMcR1rvWnZDtphFJ/LBltlC0+6HGA07k7vhy+U7P/zIg/V3Fb4RD6YDuAo0cPfBsLm8p1WYJV92WpAsGgtlg==} + '@tiptap/extension-paragraph@3.23.4': + resolution: {integrity: sha512-KbhXjCFzWphvFn5VU7E4dtmYDm+bssI1i0+CnXPWCXkjdaaX88ck68Xp1fKz8/bbI/CqlgiNDO/3TvqgtZ6woQ==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 '@tiptap/extension-placeholder@3.23.5': resolution: {integrity: sha512-B2snUujc6fb/16p8jSQCN4+mto7RlHKLm8quBTUWXksY8D82u/cxjUdmRQ7ueq7vsbRsA+WoJTrKEjJ8RQOpjw==} peerDependencies: '@tiptap/extensions': 3.23.5 - '@tiptap/extension-strike@3.23.5': - resolution: {integrity: sha512-PMB9lpQGOJGuRTIS9rBw8UZtHQwmsiJbWKjcBr5z20MluaJQ3ZCHFhDYG6ncIDRz+0ny4ZvoJ7cKGpI+NTvXMA==} + '@tiptap/extension-strike@3.23.4': + resolution: {integrity: sha512-Vnq5vW801zPbu1LtKeA5k4R241jY+hRjXeijYwIPxy15KzIiipY12518HiCf6/8kkRbMxgOfdYg9X4BRV3HV3g==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-table@3.23.5': - resolution: {integrity: sha512-3uTaC+LsilQHaMGTW6vK4fXHsTYL/TPGM0mxoBz8UvMl+G/uzL149RcMC0d0qKvYPxInFQ2rFzxPTpnY3Rg3UA==} + '@tiptap/extension-table@3.23.4': + resolution: {integrity: sha512-TRh6JMTRYXCWpwavGt3aAHH2f51ZzkhurfW3XvrURG3It8MvfuuY1xB1xba1ss5c0QLWlrKx6GVaSXrUCdFLlg==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-task-item@3.23.5': - resolution: {integrity: sha512-PjnsH/0gFP92E7jb0dRrtN5GWj/kd8e/AOY6X31utXC6v5T16hI0dCAOSK7cs3wjyj6Ic/B0jpSvokOCfYRjOQ==} + '@tiptap/extension-task-item@3.23.4': + resolution: {integrity: sha512-b6lmmwCcF5/9WetpgnSa5gxq/dRpJNJNvl4om/XKVRsvC9MQ3GwJMnhjPmcIneop4M5n++644+PJRu/N03uM7w==} peerDependencies: - '@tiptap/extension-list': 3.23.5 + '@tiptap/extension-list': 3.23.4 - '@tiptap/extension-task-list@3.23.5': - resolution: {integrity: sha512-366CsI17fOsdOFps9/wTK9tLL0M5XZHxDC7z9MntdyxbKaWECczTygiyeGZtkR/rP4RqlAK42mrQAfpLTbjnGQ==} + '@tiptap/extension-task-list@3.23.4': + resolution: {integrity: sha512-WIz4MHvPZssS5pNTTn2BuriCNrHA6jeS1XiRIqJFrvRXj7Kc9scXtGpne3GmisXDDeDKcP1IjREwQO9nlD8mVw==} peerDependencies: - '@tiptap/extension-list': 3.23.5 + '@tiptap/extension-list': 3.23.4 - '@tiptap/extension-text-style@3.23.5': - resolution: {integrity: sha512-BLf/1gPxHGfLZbLT/nnnDzGdmcqZukOHlMfeufdgjakGY90CpZHSNq0VXgwO9xLda5Ijr5aUilU0/HLX0VreEg==} + '@tiptap/extension-text-style@3.23.4': + resolution: {integrity: sha512-jb9PBkvqqn14ju37VMYOmmva9t9Th7vutoio9iB7etcu4XhL/4Z8rYPZmO+9+HLros2TQ/1JeCNZzc8LzcuBiA==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-text@3.23.5': - resolution: {integrity: sha512-GLa+AaA2NC5XYRZad/Qq/oH5Pa95s+uA17J7+RCkF8j1RNREUBkYQ5CD5MT8kT+D3DHgU8MRyYdTd28I46HBDQ==} + '@tiptap/extension-text@3.23.4': + resolution: {integrity: sha512-q9kxver/MR18p66aWZHSPycnr9hcBFyVGeGj8gf+BQCzn5hpvtSYTfLvk1nq8GFhygdQ9/e3f7B5ovrm/jnpvw==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 - '@tiptap/extension-underline@3.23.5': - resolution: {integrity: sha512-fyxthzE6CNCi9a9OVAwXs1sSyJ7jlrzT3aP2KhYLQCsJABHaPJgJA7k52/CRuKqCW3WbxU1ULH9LGuGtBbhEyw==} + '@tiptap/extension-underline@3.23.4': + resolution: {integrity: sha512-F1ocPT10LV+seky25R1TMCRdc/Iof99jLcDSYDGr6mNEDY4ct2RvOeSM8aDdYq6CkH+vXt3i3JDeRwV23KzswQ==} peerDependencies: - '@tiptap/core': 3.23.5 + '@tiptap/core': 3.23.4 + + '@tiptap/extensions@3.23.4': + resolution: {integrity: sha512-SlGPXauW8iKWG7wwuwC/0y/smLImp0h6GBIGgNnTBgIP/ThXQnjLMSZH0mW/REO87dQxkku01V3ARRywi+juhg==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 '@tiptap/extensions@3.23.5': resolution: {integrity: sha512-ROcdNPV+buzldEFKvD3o29P7H7zpAf2lnLfndO2LHSToWyHw4hlzVPCeAU8uAvhl/jyfeUoFLrBwxphMX/KG6A==} @@ -2412,32 +2587,42 @@ packages: '@tiptap/core': 3.23.5 '@tiptap/pm': 3.23.5 - '@tiptap/markdown@3.23.5': - resolution: {integrity: sha512-R5snuHrg+lweGqiq2dkw4iwRGPmKXwJAnTxSoePNY3YY9rTNc8TMvH4XPi/664APPzBVnWTlx1hN09tcdHsIVw==} + '@tiptap/markdown@3.23.4': + resolution: {integrity: sha512-jRh/oa7WyhnXo+vaiaiZ42a5h/m1vvsrEWJHy12vD1qMivRKfNmRJN+lZmYpBV+6h+5vhQpg7EMMIH82xvVWRQ==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/pm@3.23.5': - resolution: {integrity: sha512-9tgLdpTvNN0/fLP4RcNzbyQ0qjg9J2ahaFbQzgV5uvd+QMy8Xkg2IqKKnOoJJUAV3FDjGq3Yx0WrV2BGro9pfw==} + '@tiptap/pm@3.23.4': + resolution: {integrity: sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ==} - '@tiptap/starter-kit@3.23.5': - resolution: {integrity: sha512-ac0edQ1a1nYkNAzOgdqIBKGdrOlNQpPP9wGAG3Q9EgTq4+C4/EftJZZJmUn3KzaSOUv4cLEDo0z0jurJvZPkaw==} + '@tiptap/starter-kit@3.23.4': + resolution: {integrity: sha512-3VhU+NO6/ec9DMj/5Ej0nzARSq42cXnqW+QHCmTL3FNXkXQz+tw1KlfruT5GGJ3M0RssjWjRC0a39N/4S3qxeA==} - '@tiptap/suggestion@3.23.5': - resolution: {integrity: sha512-m5QoCs4IZqxTnDTJkNYm14Y3UFpJPZzUjS2APXpx9+wxaoo1q0SZ6fXardUgQET1wCJeUrsu73mvEnlK8mmuog==} + '@tiptap/suggestion@3.23.4': + resolution: {integrity: sha512-KvrHKQcGpEKPPuetH2N4K21kA7hc31n5WDzw3FM+fNpMKdJOToYoNZzS9rmuBBHmNZ9wyK2sWmzi09enmv6wbg==} peerDependencies: - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 - '@tiptap/vue-3@3.23.5': - resolution: {integrity: sha512-1yGVtZdeAgRL1357g0YrhFgwDTI4fCPeKUtEInxsyRJkH/DQRmoaL3VmaPSWCn5R1Hxm1SY6f77djtNrNgNqtg==} + '@tiptap/vue-3@3.23.4': + resolution: {integrity: sha512-D8aUfiXSM1InPOe4jI4bBPSilz7bc42uVt5dMeto1cYYZrlzZEIe1vXvGm/0tvd/oVUtqQNk2Mjz+w0xoABT3Q==} peerDependencies: '@floating-ui/dom': ^1.0.0 - '@tiptap/core': 3.23.5 - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 vue: ^3.0.0 + '@tiptap/y-tiptap@3.0.3': + resolution: {integrity: sha512-8UvuV4lTisCE9cMTc/X8kRyTn9edUO7Kball0I6wb17VwZSjNDfh/YKtP4O5vcPawEzFHQIvZGq/k1h37kAf0w==} + 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 + '@transloadit/prettier-bytes@0.3.5': resolution: {integrity: sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==} @@ -2508,8 +2693,8 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@25.9.0': + resolution: {integrity: sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2520,6 +2705,9 @@ packages: '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -3235,6 +3423,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -3960,6 +4151,9 @@ packages: peerDependencies: ws: '*' + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4048,6 +4242,11 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + engines: {node: '>=16'} + hasBin: true + license-checker-rseidelsohn@4.4.2: resolution: {integrity: sha512-Sf8WaJhd2vELvCne+frS9AXqnY/vv591s2/nZcJDwTnoNgltG4mAmoenffVb8L2YPRYbxARLyrHJBC38AVfpuA==} engines: {node: '>=18', npm: '>=8'} @@ -4696,8 +4895,8 @@ packages: prosemirror-keymap@1.2.3: resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - prosemirror-model@1.25.7: - resolution: {integrity: sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==} + prosemirror-model@1.25.6: + resolution: {integrity: sha512-RIm+e9BiqAaJ1mRECv3vR3C+VG8ELoTTI+47tVudGi82yLnFOx3G/p/iSPK1HmHQdKhkkrJ68NJqxh7S+FBVmQ==} prosemirror-schema-list@1.5.1: resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} @@ -5057,6 +5256,9 @@ packages: strnum@2.3.0: resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} @@ -5628,11 +5830,28 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y-codemirror.next@0.3.5: + resolution: {integrity: sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw==} + peerDependencies: + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + yjs: ^13.5.6 + + y-protocols@1.0.7: + resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true + yjs@13.6.30: + resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -5831,6 +6050,79 @@ snapshots: '@casl/ability': 6.8.1 vue: 3.5.34(typescript@6.0.3) + '@codemirror/autocomplete@6.20.2': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.3 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.3 + '@lezer/html': 1.3.13 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/markdown': 1.6.3 + + '@codemirror/language@6.12.3': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.6': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + crelt: 1.0.6 + + '@codemirror/state@6.6.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.43.0': + dependencies: + '@codemirror/state': 6.6.0 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@colors/colors@1.5.0': optional: true @@ -6169,6 +6461,22 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@hocuspocus/common@4.0.0': + dependencies: + lib0: 0.2.117 + + '@hocuspocus/provider@4.0.0(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)': + dependencies: + '@hocuspocus/common': 4.0.0 + '@lifeomic/attempt': 3.1.0 + lib0: 0.2.117 + ws: 8.20.0 + y-protocols: 1.0.7(yjs@13.6.30) + yjs: 13.6.30 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -6221,6 +6529,43 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.2': {} + + '@lezer/css@1.3.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/markdown@1.6.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + + '@lifeomic/attempt@3.1.0': {} + + '@marijn/find-cluster-break@1.0.2': {} + '@microsoft/fetch-event-source@2.0.1': {} '@module-federation/dts-plugin@2.4.0(node-fetch@3.3.2)(typescript@6.0.3)(vue-tsc@3.3.1(typescript@6.0.3))': @@ -6276,7 +6621,7 @@ snapshots: find-pkg: 2.0.0 resolve: 1.22.8 - '@module-federation/vite@1.15.4(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3))': + '@module-federation/vite@1.15.4(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3))': dependencies: '@module-federation/dts-plugin': 2.4.0(node-fetch@3.3.2)(typescript@6.0.3)(vue-tsc@3.3.1(typescript@6.0.3)) '@module-federation/runtime': 2.4.0(node-fetch@3.3.2) @@ -6284,7 +6629,7 @@ snapshots: es-module-lexer: 2.1.0 estree-walker: 3.0.3 pathe: 2.0.3 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) transitivePeerDependencies: - bufferutil - node-fetch @@ -6292,7 +6637,7 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/vite@1.15.5(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3))': + '@module-federation/vite@1.15.5(node-fetch@3.3.2)(typescript@6.0.3)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue-tsc@3.3.1(typescript@6.0.3))': dependencies: '@module-federation/dts-plugin': 2.4.0(node-fetch@3.3.2)(typescript@6.0.3)(vue-tsc@3.3.1(typescript@6.0.3)) '@module-federation/runtime': 2.4.0(node-fetch@3.3.2) @@ -6300,7 +6645,7 @@ snapshots: es-module-lexer: 2.1.0 estree-walker: 3.0.3 pathe: 2.0.3 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) transitivePeerDependencies: - bufferutil - node-fetch @@ -6684,159 +7029,171 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) '@teppeis/multimaps@3.0.0': {} - '@tiptap/core@3.23.5(@tiptap/pm@3.23.5)': + '@tiptap/core@3.23.4(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/pm': 3.23.5 + '@tiptap/pm': 3.23.4 - '@tiptap/extension-blockquote@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-blockquote@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-bold@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-bold@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-bubble-menu@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-bubble-menu@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 optional: true - '@tiptap/extension-bullet-list@3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-bullet-list@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-code-block@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-code-block@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-code@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-code@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-collaboration@3.23.5(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + '@tiptap/y-tiptap': 3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30) + yjs: 13.6.30 - '@tiptap/extension-document@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-document@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-dropcursor@3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-dropcursor@3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extensions': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-floating-menu@3.23.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-floating-menu@3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 optional: true - '@tiptap/extension-gapcursor@3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-gapcursor@3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extensions': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-hard-break@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-hard-break@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-heading@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-heading@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-horizontal-rule@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-horizontal-rule@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 - '@tiptap/extension-image@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-image@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-italic@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-italic@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-link@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-link@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 linkifyjs: 4.3.3 - '@tiptap/extension-list-item@3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-list-item@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-list-keymap@3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-list-keymap@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 - '@tiptap/extension-ordered-list@3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-ordered-list@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-paragraph@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-paragraph@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-placeholder@3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-placeholder@3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extensions': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extensions': 3.23.5(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-strike@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-strike@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-table@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extension-table@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 - '@tiptap/extension-task-item@3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-task-item@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-task-list@3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5))': + '@tiptap/extension-task-list@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-text-style@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-text-style@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-text@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-text@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extension-underline@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))': + '@tiptap/extension-underline@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 - '@tiptap/markdown@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@tiptap/extensions@3.23.5(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/markdown@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 marked: 17.0.6 - '@tiptap/pm@3.23.5': + '@tiptap/pm@3.23.4': dependencies: prosemirror-changeset: 2.4.1 prosemirror-commands: 1.7.1 @@ -6844,54 +7201,63 @@ snapshots: prosemirror-gapcursor: 1.4.1 prosemirror-history: 1.5.0 prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 prosemirror-tables: 1.8.5 prosemirror-transform: 1.12.0 prosemirror-view: 1.41.8 - '@tiptap/starter-kit@3.23.5': - dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/extension-blockquote': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-bold': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-bullet-list': 3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) - '@tiptap/extension-code': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-code-block': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) - '@tiptap/extension-document': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-dropcursor': 3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) - '@tiptap/extension-gapcursor': 3.23.5(@tiptap/extensions@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) - '@tiptap/extension-hard-break': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-heading': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-horizontal-rule': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) - '@tiptap/extension-italic': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-link': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) - '@tiptap/extension-list': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) - '@tiptap/extension-list-item': 3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) - '@tiptap/extension-list-keymap': 3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) - '@tiptap/extension-ordered-list': 3.23.5(@tiptap/extension-list@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)) - '@tiptap/extension-paragraph': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-strike': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-text': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extension-underline': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5)) - '@tiptap/extensions': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 - - '@tiptap/suggestion@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': - dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 - - '@tiptap/vue-3@3.23.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)(vue@3.5.34(typescript@6.0.3))': + '@tiptap/starter-kit@3.23.4': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/extension-blockquote': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-bold': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-bullet-list': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-code': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-code-block': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-document': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-dropcursor': 3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-gapcursor': 3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-hard-break': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-heading': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-horizontal-rule': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-italic': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-link': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-list-item': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-list-keymap': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-ordered-list': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-paragraph': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-strike': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-text': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-underline': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/suggestion@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/vue-3@3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(vue@3.5.34(typescript@6.0.3))': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 vue: 3.5.34(typescript@6.0.3) optionalDependencies: - '@tiptap/extension-bubble-menu': 3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) - '@tiptap/extension-floating-menu': 3.23.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5) + '@tiptap/extension-bubble-menu': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-floating-menu': 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)': + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + y-protocols: 1.0.7(yjs@13.6.30) + yjs: 13.6.30 '@transloadit/prettier-bytes@0.3.5': {} @@ -6958,7 +7324,7 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/node@25.9.1': + '@types/node@25.9.0': dependencies: undici-types: 7.24.6 @@ -6968,6 +7334,8 @@ snapshots: '@types/retry@0.12.2': {} + '@types/semver@7.7.1': {} + '@types/trusted-types@2.0.7': optional: true @@ -6979,7 +7347,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.0 '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: @@ -7128,19 +7496,19 @@ snapshots: '@uppy/core': 5.2.0 '@uppy/utils': 7.2.0 - '@vitejs/plugin-basic-ssl@2.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': + '@vitejs/plugin-basic-ssl@2.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': dependencies: - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) - '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.9.1)(lightningcss@1.32.0)(sass@1.99.0))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0))(vue@3.5.34(typescript@6.0.3))': dependencies: - vite: 5.4.21(@types/node@25.9.1)(lightningcss@1.32.0)(sass@1.99.0) + vite: 5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0) vue: 3.5.34(typescript@6.0.3) - '@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) vue: 3.5.34(typescript@6.0.3) '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': @@ -7155,7 +7523,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@types/node@25.9.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + vitest: 4.1.6(@types/node@25.9.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@vitest/expect@4.1.6': dependencies: @@ -7166,13 +7534,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.6(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.6': dependencies: @@ -7201,7 +7569,7 @@ snapshots: '@vitest/web-worker@4.1.6(vitest@4.1.6)': dependencies: obug: 2.1.1 - vitest: 4.1.6(@types/node@25.9.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + vitest: 4.1.6(@types/node@25.9.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@volar/language-core@2.4.28': dependencies: @@ -7790,6 +8158,8 @@ snapshots: create-require@1.1.1: {} + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -8360,7 +8730,7 @@ snapshots: happy-dom@20.9.0: dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -8564,6 +8934,8 @@ snapshots: dependencies: ws: 8.18.0 + isomorphic.js@0.2.5: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -8643,6 +9015,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + license-checker-rseidelsohn@4.4.2: dependencies: chalk: 4.1.2 @@ -9273,7 +9649,7 @@ snapshots: prosemirror-commands@1.7.1: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 @@ -9286,7 +9662,7 @@ snapshots: prosemirror-gapcursor@1.4.1: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-state: 1.4.4 prosemirror-view: 1.41.8 @@ -9302,37 +9678,37 @@ snapshots: prosemirror-state: 1.4.4 w3c-keyname: 2.2.8 - prosemirror-model@1.25.7: + prosemirror-model@1.25.6: dependencies: orderedmap: 2.1.1 prosemirror-schema-list@1.5.1: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 prosemirror-state@1.4.4: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-transform: 1.12.0 prosemirror-view: 1.41.8 prosemirror-tables@1.8.5: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 prosemirror-view: 1.41.8 prosemirror-transform@1.12.0: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-view@1.41.8: dependencies: - prosemirror-model: 1.25.7 + prosemirror-model: 1.25.6 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 @@ -9750,6 +10126,8 @@ snapshots: strnum@2.3.0: {} + style-mod@4.1.3: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -9834,14 +10212,14 @@ snapshots: optionalDependencies: typescript: 6.0.3 - ts-node@10.9.2(@types/node@25.9.1)(typescript@6.0.3): + ts-node@10.9.2(@types/node@25.9.0)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 25.9.1 + '@types/node': 25.9.0 acorn: 8.16.0 acorn-walk: 8.3.5 arg: 4.1.3 @@ -10002,34 +10380,34 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-node-polyfills@0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)): + vite-plugin-node-polyfills@0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.60.0) node-stdlib-browser: 1.3.1 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) transitivePeerDependencies: - rollup - vite-plugin-static-copy@4.1.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)): + vite-plugin-static-copy@4.1.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 tinyglobby: 0.2.16 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) - vite@5.4.21(@types/node@25.9.1)(lightningcss@1.32.0)(sass@1.99.0): + vite@5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0): dependencies: esbuild: 0.21.5 postcss: 8.5.15 rollup: 4.60.0 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.0 fsevents: 2.3.3 lightningcss: 1.32.0 sass: 1.99.0 - vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0): + vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -10037,13 +10415,13 @@ snapshots: rolldown: 1.0.1 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.0 fsevents: 2.3.3 jiti: 2.7.0 sass: 1.99.0 yaml: 2.9.0 - vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.1)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3): + vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.0)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3): dependencies: '@docsearch/css': 3.8.2 '@docsearch/js': 3.8.2(@algolia/client-search@5.50.0)(search-insights@2.17.3) @@ -10052,7 +10430,7 @@ snapshots: '@shikijs/transformers': 2.5.0 '@shikijs/types': 2.5.0 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@25.9.1)(lightningcss@1.32.0)(sass@1.99.0))(vue@3.5.34(typescript@6.0.3)) + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0))(vue@3.5.34(typescript@6.0.3)) '@vue/devtools-api': 7.7.9 '@vue/shared': 3.5.34 '@vueuse/core': 12.8.2(typescript@6.0.3) @@ -10061,7 +10439,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.2.0 shiki: 2.5.0 - vite: 5.4.21(@types/node@25.9.1)(lightningcss@1.32.0)(sass@1.99.0) + vite: 5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0) vue: 3.5.34(typescript@6.0.3) optionalDependencies: postcss: 8.5.15 @@ -10096,12 +10474,12 @@ snapshots: dependencies: ts-essentials: 10.1.1(typescript@6.0.3) typescript: 6.0.3 - vitest: 4.1.6(@types/node@25.9.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + vitest: 4.1.6(@types/node@25.9.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) - vitest@4.1.6(@types/node@25.9.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)): + vitest@4.1.6(@types/node@25.9.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.6 '@vitest/runner': 4.1.6 '@vitest/snapshot': 4.1.6 @@ -10118,10 +10496,10 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.0 '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) happy-dom: 20.9.0 transitivePeerDependencies: @@ -10300,8 +10678,24 @@ snapshots: xtend@4.0.2: {} + y-codemirror.next@0.3.5(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(yjs@13.6.30): + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + lib0: 0.2.117 + yjs: 13.6.30 + + y-protocols@1.0.7(yjs@13.6.30): + dependencies: + lib0: 0.2.117 + yjs: 13.6.30 + yaml@2.9.0: {} + yjs@13.6.30: + dependencies: + lib0: 0.2.117 + yn@3.1.1: {} yocto-queue@0.1.0: {} From be0337a6f0303fe10be768f54c94f1bf870bc409 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 19 May 2026 21:06:58 +0200 Subject: [PATCH 02/23] perf(realtime-collab): adapter serialize() gets optional editor context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CollaborativeAdapter.serialize` now takes a second `context: unknown` arg. Editor components expose context via `defineExpose({ getAdapterContext() })` and the wrapper grabs it through a template ref. Adapters cast `context` to their expected shape and treat absence as "fall back to Y.Doc-only serialisation". TiptapEditor exposes `{ editor: editor.value }`. The markdown adapter now calls `live.getMarkdown()` on the bound editor on each debounced serialise instead of spawning a headless `new Editor({...})` (StarterKit + Markdown + Collaboration setup) per keystroke. Dominant cost during interactive editing, will matter a lot more once the text-editor refactor (Phase 4) brings 4 content-type strategies × many extensions online. CodeMirror returns no context. Its adapter's `Y.Text.toString()` is already cheap and ignores the new arg. Backwards-compatible signature change — `unknown` is the cleanest type because a typed `editor: TiptapEditor` argument would make no sense for the CodeMirror adapter or any future non-Tiptap adapter. Headless-editor fallback inside the Tiptap adapter stays for the case where no live editor is bound (e.g. stale-recovery on a peer that holds the doc but never mounted a UI). --- REALTIME_COLLAB_MIGRATION.md | 2 +- packages/web-app-tiptap/src/TiptapEditor.vue | 8 ++++++++ .../web-app-tiptap/src/adapters/tiptapMarkdown.ts | 10 +++++++++- .../Collaborative/CollaborativeWrapper.vue | 14 ++++++++++++-- .../web-pkg/src/components/Collaborative/types.ts | 15 ++++++++++++++- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/REALTIME_COLLAB_MIGRATION.md b/REALTIME_COLLAB_MIGRATION.md index b5de5bb391..18bccc941d 100644 --- a/REALTIME_COLLAB_MIGRATION.md +++ b/REALTIME_COLLAB_MIGRATION.md @@ -259,7 +259,7 @@ In the web repo the dev workflow uses `pnpm vite` (dev server with HMR) instead - [x] **Phase 0**: finish sessionKey watch refactor, add regression test for Y.Doc-rebuild bug, clean up debug logs, all unit + e2e + integration green, commit + push to `feat/realtime-collaboration-poc` — `ac85423` on web-extensions, 13/13 unit + 5/5 codemirror e2e + 4/4 tiptap e2e + 8/8 integration green - [x] **Phase 1**: copy wrapper + adapter types into web-pkg, add only the truly shared deps (hocuspocus/provider, yjs, y-protocols, semver), exports — files at `packages/web-pkg/src/components/Collaborative/`, vue-tsc type-check green - [x] **Phase 2**: moved both apps to `web/packages/web-app-{codemirror,tiptap}`, rewired imports to `@opencloud-eu/web-pkg`, slimmed package.json (peer deps for web-pkg/web-client/design-system, app-bound deps for codemirror/tiptap-specific bits, yjs + y-protocols for type-only adapter imports). Registered both in `dev/docker/opencloud.web.config.json` `apps[]`. Wrapper's `realtimeUrl` prop now three-state (`string` / `null` = force-local / `undefined` = derive from `configStore.serverUrl` + `/realtime` convention). vue-tsc green on both apps + web-pkg. -- [ ] **Phase 2.5**: extend `CollaborativeAdapter.serialize` to optionally accept an opaque `context` object (typed `unknown`, each adapter casts as needed — a typed `editor: TiptapEditor` argument makes no sense for the CodeMirror adapter). Editor components expose context via `defineExpose({ getAdapterContext() })`; wrapper grabs the value via template ref and passes it down to `serialize(ydoc, context)`. Tiptap exposes `{ editor: tiptapEditor }` so its adapter reuses the live editor instance — no more headless-editor spawn per debounced serialize. CodeMirror returns `undefined` (its `Y.Text.toString()` is already cheap). Backwards-compatible signature change. Drops the dominant per-keystroke cost for rich Tiptap setups before Phase 4 brings 4 strategies × many extensions online. +- [x] **Phase 2.5**: extended `CollaborativeAdapter.serialize(ydoc, context?: unknown)`. Wrapper grabs context via `editorRef.value?.getAdapterContext?.()` and passes it down. TiptapEditor exposes `{ editor: editor.value }` so its adapter reuses the live editor instance via `live.getMarkdown()` — no more per-keystroke headless-editor spawn. CodeMirror doesn't expose anything; its adapter ignores the new arg and keeps `Y.Text.toString()`. Backwards-compatible (existing adapter signatures still match the wider type). vue-tsc green on all three packages. - [ ] **Phase 3**: copy hocuspocus sidecar into web compose; switch the sidecar's etag probe from WebDAV HEAD to Graph `/items/{id}` (consistent with the existing Graph permissions call; investigate the "personal-drive 400" mentioned in the original WebDAV-fallback comment) - [ ] **Phase 4**: refactor web-app-text-editor onto wrapper; require Y.Doc in `useTextEditor`; flip `StarterKit` `undoRedo: false`; ship all 4 content-type adapters (markdown, html, plain-text, tiptap-json) with manual round-trip smoke tests for the three not in the PoC e2e; toolbar / slash commands stay - [ ] **Phase 4.5**: extend AppWrapper with the etag-sync inject contract; wire CollaborativeWrapper to call it when `_oc_meta.etag` updates via CRDT; one new Cucumber scenario for the cross-peer flow diff --git a/packages/web-app-tiptap/src/TiptapEditor.vue b/packages/web-app-tiptap/src/TiptapEditor.vue index 675381f24f..b53cd0b9a8 100644 --- a/packages/web-app-tiptap/src/TiptapEditor.vue +++ b/packages/web-app-tiptap/src/TiptapEditor.vue @@ -86,6 +86,14 @@ const editor = useEditor({ onBeforeUnmount(() => { editor.value?.destroy() }) + +// CollaborativeWrapper grabs this via template ref + `getAdapterContext()` +// and forwards it as the second arg of `adapter.serialize(ydoc, ctx)`. The +// tiptap markdown adapter reaches into `ctx.editor` to call `getMarkdown()` +// on the live instance, skipping a per-keystroke headless-editor spawn. +defineExpose({ + getAdapterContext: () => ({ editor: editor.value }) +}) @@ -33,5 +35,7 @@ defineEmits<{ document-prefix="codemirror" :realtime-url="(applicationConfig?.realtimeUrl as string | null | undefined) ?? undefined" @update:current-content="$emit('update:currentContent', $event)" + @update:server-content="$emit('update:serverContent', $event)" + @update:etag="$emit('update:etag', $event)" /> diff --git a/packages/web-app-text-editor/src/App.vue b/packages/web-app-text-editor/src/App.vue index ecfca2b5a2..38dca23dc4 100644 --- a/packages/web-app-text-editor/src/App.vue +++ b/packages/web-app-text-editor/src/App.vue @@ -9,6 +9,8 @@ document-prefix="text-editor" :realtime-url="(applicationConfig?.realtimeUrl as string | null | undefined) ?? undefined" @update:current-content="$emit('update:currentContent', $event)" + @update:server-content="$emit('update:serverContent', $event)" + @update:etag="$emit('update:etag', $event)" /> @@ -32,6 +34,8 @@ const props = defineProps<{ defineEmits<{ (e: 'update:currentContent', value: string): void + (e: 'update:serverContent', value: string): void + (e: 'update:etag', value: string): void }>() const { $gettext } = useGettext() diff --git a/packages/web-app-tiptap/src/App.vue b/packages/web-app-tiptap/src/App.vue index 562fe9a6b8..4473846f15 100644 --- a/packages/web-app-tiptap/src/App.vue +++ b/packages/web-app-tiptap/src/App.vue @@ -19,6 +19,8 @@ defineProps({ // auto-save loop arms (or stays off if `disableAutoSave` is passed). defineEmits<{ (e: 'update:currentContent', value: string): void + (e: 'update:serverContent', value: string): void + (e: 'update:etag', value: string): void }>() @@ -33,5 +35,7 @@ defineEmits<{ document-prefix="tiptap" :realtime-url="(applicationConfig?.realtimeUrl as string | null | undefined) ?? undefined" @update:current-content="$emit('update:currentContent', $event)" + @update:server-content="$emit('update:serverContent', $event)" + @update:etag="$emit('update:etag', $event)" /> diff --git a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue index efb43e3202..ff60523faa 100644 --- a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue +++ b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue @@ -434,6 +434,11 @@ const saveFileTask = useTask(function* () { serverContent.value = newContent currentETag.value = putFileContentsResponse.etag resourcesStore.upsertResource(putFileContentsResponse) + // Keep our local `resource` ref in sync with the fresh etag so any + // downstream watcher on `props.resource.etag` (CollaborativeWrapper's + // meta-mirror, for one) actually fires. `upsertResource` only touches + // the store; the local ref is the one passed down via slotAttrs. + resource.value = { ...unref(resource), etag: putFileContentsResponse.etag } } catch (e) { // 409 / 412 — `previousEntityTag` didn't match what the server has. // Usually means another peer in a collaborative session saved this @@ -456,6 +461,7 @@ const saveFileTask = useTask(function* () { currentETag.value = freshEtag if (unref(resource)) { resourcesStore.upsertResource({ ...unref(resource), etag: freshEtag }) + resource.value = { ...unref(resource), etag: freshEtag } } return } @@ -474,6 +480,7 @@ const saveFileTask = useTask(function* () { serverContent.value = newContent currentETag.value = retry.etag resourcesStore.upsertResource(retry) + resource.value = { ...unref(resource), etag: retry.etag } return } catch (retryErr) { // Refetch or retry blew up — drop through to the user-facing @@ -763,6 +770,20 @@ const slotAttrs = computed(() => ({ 'onUpdate:currentContent': (value: unknown) => { currentContent.value = value }, + // Optional companion to update:currentContent — collab-aware wrappers + // emit this when a peer save just landed, so we can sync `serverContent` + // to the freshly-on-disk state without a refetch. Non-collab editors + // never emit it and the binding is a no-op. + 'onUpdate:serverContent': (value: unknown) => { + serverContent.value = value + }, + // Companion to update:serverContent — collab wrappers also publish the + // peer-saved etag so our next PUT's `If-Match` is current and we skip the + // 412 → refetch → retry recovery path entirely. + 'onUpdate:etag': (value: unknown) => { + if (typeof value !== 'string' || currentETag.value === value) return + currentETag.value = value + }, 'onRegister:onDeleteResourceCallback': (value: () => void) => { appOnDeleteResourceCallback = value diff --git a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue index 65a6261fff..7cf6f40ead 100644 --- a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue +++ b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue @@ -61,6 +61,16 @@ const props = defineProps({ // AppWrapper's save path then PUTs that string with its own etag tracking. const emit = defineEmits<{ (e: 'update:currentContent', value: string): void + // Emitted after a peer save: the Y.Doc state at that moment is exactly + // what's now on disk, so AppWrapper can flip its `serverContent` to the + // same string we'd push as `currentContent` and `isDirty` falls to false + // without anyone having to round-trip through WebDAV. + (e: 'update:serverContent', value: string): void + // Emitted alongside `update:serverContent` when a peer save propagates a + // fresh etag through `_oc_meta`. AppWrapper writes it into its + // `currentETag`, so the next save's `If-Match` is correct and we skip the + // 412 → refetch → retry recovery loop entirely. + (e: 'update:etag', value: string): void }>() const META_KEY = '_oc_meta' @@ -267,7 +277,45 @@ watch( // types (e.g. Y.Text 'content' for CodeMirror). In local mode no one ever // sets isStale / bumps appVersion, so the observer is dormant but harmless. const meta = doc.getMap(META_KEY) - const metaObserver = (event: Y.YMapEvent) => { + const metaObserver = (event: Y.YMapEvent, transaction: Y.Transaction) => { + // Peer-save fan-out. Another client just saved (its etag-mirror watch + // fired LOCAL_SAVE_ORIGIN on its side, then Yjs synced the meta-map + // change to us with `transaction.origin === undefined` — remote ops have + // no string origin). Our Y.Doc already reflects every edit that save + // covered, so serialize it now and tell AppWrapper "this is what's on + // disk" — its isDirty (currentContent vs serverContent) flips false and + // the unsaved-changes modal stops firing on navigate. + if ( + event.keysChanged.has('etag') && + transaction.origin !== LOCAL_SAVE_ORIGIN + ) { + const newEtag = meta.get('etag') as string | undefined + if (newEtag) emit('update:etag', newEtag) + } + + if ( + event.keysChanged.has('lastSavedAt') && + transaction.origin !== LOCAL_SAVE_ORIGIN && + props.adapter.hasContent(doc) + ) { + try { + const editorCtx = + (editorRef.value as { getAdapterContext?: () => unknown } | null)?.getAdapterContext?.() + const serialized = props.adapter.serialize(doc, editorCtx) + if (typeof serialized === 'string') { + emit('update:serverContent', serialized) + } else { + void Promise.resolve(serialized).then((value) => { + if (doc.isDestroyed) return + emit('update:serverContent', value) + }) + } + } catch (e) { + console.error('[collab] serialize for peer-save sync failed:', e) + } + } + + // App version mismatch surfaced after-the-fact (e.g. a newer peer joined // and bumped `appVersion`). Any non-zero diff at this point means the // room moved past or ahead of us mid-session — lock and prompt reload. @@ -324,6 +372,13 @@ watch( // any future peer-aware logic see the current authoritative tag. In local // mode no sidecar reads `_oc_meta`, but the mirror is cheap and keeps the // two modes symmetrical. +// Tag we put on our own meta-write so the meta observer can tell a local +// save (this watch firing) apart from a peer save (CRDT update from another +// client). Peer saves get the `update:serverContent` fan-out below; local +// saves don't need it because AppWrapper already sets `serverContent` itself +// in its save success path. +const LOCAL_SAVE_ORIGIN = 'local-save' + watch( () => props.resource.etag, (newEtag) => { @@ -334,7 +389,7 @@ watch( doc.transact(() => { meta.set('etag', newEtag) meta.set('lastSavedAt', Date.now()) - }) + }, LOCAL_SAVE_ORIGIN) } ) From a5fb009d34890e60ab280ff1f567b64ef346888e Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 20 May 2026 01:26:58 +0200 Subject: [PATCH 17/23] test(realtime-collab): port CollaborativeWrapper unit spec to web-pkg 13 specs covering local + collab mode, hydration gating, debounced emit, internal-origin filtering, etag mirror + the Y.Doc-rebuild regression, and lifecycle teardown. HocuspocusProvider is mocked so the suite stays hermetic; useAuthStore / useConfigStore are mocked so we don't need pinia. A tiny inline adapter mimics codemirror's Y.Text-on-'content' shape -- the wrapper is adapter-agnostic, so any concrete implementation exercises the same contract. Also: update the migration plan to mark Phase 4.5 done with a note on the two actual mechanisms (refetch+retry on 412/409 + the new _oc_meta fan-out of `update:serverContent` / `update:etag`) that replaced the original etag-sync inject design. --- REALTIME_COLLAB_MIGRATION.md | 2 +- .../CollaborativeWrapper.spec.ts | 347 ++++++++++++++++++ 2 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 packages/web-pkg/tests/unit/components/Collaborative/CollaborativeWrapper.spec.ts diff --git a/REALTIME_COLLAB_MIGRATION.md b/REALTIME_COLLAB_MIGRATION.md index 26f78ced28..3a09d7ca09 100644 --- a/REALTIME_COLLAB_MIGRATION.md +++ b/REALTIME_COLLAB_MIGRATION.md @@ -274,6 +274,6 @@ In the web repo the dev workflow uses `pnpm vite` (dev server with HMR) instead - [x] **Phase 2.5**: extended `CollaborativeAdapter.serialize(ydoc, context?: unknown)`. Wrapper grabs context via `editorRef.value?.getAdapterContext?.()` and passes it down. TiptapEditor exposes `{ editor: editor.value }` so its adapter reuses the live editor instance via `live.getMarkdown()` — no more per-keystroke headless-editor spawn. CodeMirror doesn't expose anything; its adapter ignores the new arg and keeps `Y.Text.toString()`. Backwards-compatible (existing adapter signatures still match the wider type). vue-tsc green on all three packages. - [x] **Phase 3**: copied sidecar to `dev/docker/hocuspocus/`, appended `hocuspocus` service to `docker-compose.yml`. Web stack up + reachable: OC https://host.docker.internal:9200 HTTP 200, sidecar /realtime HTTP 200, `pnpm build:w` running, WEB_APPS_MAP now includes `web-app-codemirror` + `web-app-tiptap`. Etag-probe switch to Graph + folder-vs-file check moved to follow-ups (see Future considerations). - [x] **Phase 4**: text-editor refactored onto CollaborativeWrapper. `useTextEditor` accepts optional `ydoc` → adds `Collaboration` extension, skips initial-content assignment, suppresses `modelValue` round-trip. All 3 Tiptap strategies flip `StarterKit.configure({ undoRedo: false })`; plain-text strategy needs no change (no StarterKit). `makeTextEditorAdapter(strategy)` bridges to `CollaborativeAdapter`; App.vue is now a thin shell around the wrapper with `TextEditorBinding.vue` as the editor component. Codemirror + tiptap e2e 9/9 still green; text-editor unit test stubs the wrapper. Manual smoke tests for the 3 non-markdown content types: pending (separate todo). -- [ ] **Phase 4.5**: extend AppWrapper with the etag-sync inject contract; wire CollaborativeWrapper to call it when `_oc_meta.etag` updates via CRDT; one new Cucumber scenario for the cross-peer flow +- [x] **Phase 4.5**: peer-save coordination — replaces the originally planned inject contract. Two simpler mechanisms instead: (1) `AppWrapper.saveFileTask` catches 412/409 with refetch+retry (`d26d836ef0`) so a stale local etag self-heals on the next save. (2) `CollaborativeWrapper` tags its own `_oc_meta` writes with a `LOCAL_SAVE_ORIGIN` transaction origin and emits `update:serverContent` + `update:etag` from the meta observer when the change came in via CRDT (`e4939c3168`). AppWrapper listens to both new emits — `serverContent` flips `isDirty` false on peers without a refetch, `etag` updates `currentETag` so peers skip the 412→refetch→retry round-trip on their next save. Same origin-tag pattern as the existing `update:currentContent` emit; no inject machinery. The same commit also fixes `saveFileTask` to update the local `resource` ref on save (previously only `serverContent` / `currentETag` / `resourcesStore` were touched, so the etag-mirror watch keyed on `props.resource.etag` never fired). Cucumber scenario for the cross-peer dirty + etag flow: still pending, moved into Phase 5. - [ ] **Phase 5**: migrate unit + integration tests, Cucumber-port the e2e suites with existing helpers where possible; add hocuspocus to web's woodpecker e2e CI job - [ ] Smoke test full dev loop in web; commit per phase; PR against `opencloud-eu/web` main diff --git a/packages/web-pkg/tests/unit/components/Collaborative/CollaborativeWrapper.spec.ts b/packages/web-pkg/tests/unit/components/Collaborative/CollaborativeWrapper.spec.ts new file mode 100644 index 0000000000..6267c29ab1 --- /dev/null +++ b/packages/web-pkg/tests/unit/components/Collaborative/CollaborativeWrapper.spec.ts @@ -0,0 +1,347 @@ +// Unit coverage for the wrapper that lives in web-pkg and is shared by +// the codemirror / tiptap / text-editor apps. The wrapper carries the +// non-trivial branching (collab vs local) and a handful of side effects +// (debounced emit, etag mirror, lifecycle teardown) that aren't exercised +// by the cucumber e2e suites unless we run them through the whole OC + +// sidecar stack. +// +// We mock HocuspocusProvider so the tests stay hermetic (no network), +// and useAuthStore / useConfigStore so we don't have to bring in pinia. +// A tiny inline adapter mimics the Y.Text-on-'content' shape that +// web-app-codemirror's real adapter uses; the wrapper only sees the +// CollaborativeAdapter interface and doesn't care which app produced it. + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { defineComponent, h, nextTick } from 'vue' +import * as Y from 'yjs' +import { Awareness } from 'y-protocols/awareness' +import type { Resource } from '@opencloud-eu/web-client' + +import CollaborativeWrapper from '../../../../src/components/Collaborative/CollaborativeWrapper.vue' +import type { CollaborativeAdapter } from '../../../../src/components/Collaborative/types' + +// vi.hoisted is required so providerInstances is reachable from the +// hoisted vi.mock factory; defining the class outside the factory hits +// "Cannot access before initialization". +interface MockProvider { + url: string + name: string + document: Y.Doc + awareness: Awareness + destroy: ReturnType + disconnect: ReturnType + setAwarenessField: ReturnType + triggerSynced(): void + triggerAuthFailed(reason: string): void +} + +const { providerInstances } = vi.hoisted(() => { + return { providerInstances: [] as MockProvider[] } +}) + +vi.mock('@hocuspocus/provider', async () => { + const { Awareness: AwarenessImpl } = await import('y-protocols/awareness') + class MockHocuspocusProvider { + url: string + name: string + document: Y.Doc + awareness: Awareness + destroy = vi.fn() + disconnect = vi.fn() + setAwarenessField = vi.fn() + private _opts: any + constructor(opts: any) { + this.url = opts.url + this.name = opts.name + this.document = opts.document + this.awareness = new AwarenessImpl(opts.document) + this._opts = opts + providerInstances.push(this as MockProvider & MockHocuspocusProvider) + } + triggerSynced() { + this._opts.onSynced?.({ state: true }) + } + triggerAuthFailed(reason: string) { + this._opts.onAuthenticationFailed?.({ reason }) + } + } + return { HocuspocusProvider: MockHocuspocusProvider } +}) + +vi.mock('../../../../src/composables', () => ({ + useAuthStore: () => ({ accessToken: 'test-token' }), + useConfigStore: () => ({ serverUrl: 'https://oc.test' }) +})) + +// Match the codemirror adapter's shape (Y.Text on 'content'). The wrapper +// is adapter-agnostic, so any concrete adapter exercising hydrate / +// serialize / hasContent / reset works for these tests. +const SHARED_TEXT_KEY = 'content' +const testAdapter: CollaborativeAdapter = { + hydrate(ydoc: Y.Doc, content: string) { + const yText = ydoc.getText(SHARED_TEXT_KEY) + if (yText.length > 0) return + if (!content) return + ydoc.transact(() => { + yText.insert(0, content) + }, 'hydrate') + }, + serialize(ydoc: Y.Doc): string { + return ydoc.getText(SHARED_TEXT_KEY).toString() + }, + hasContent(ydoc: Y.Doc): boolean { + return ydoc.getText(SHARED_TEXT_KEY).length > 0 + }, + reset(ydoc: Y.Doc) { + const yText = ydoc.getText(SHARED_TEXT_KEY) + if (yText.length === 0) return + ydoc.transact(() => { + yText.delete(0, yText.length) + }, 'reset') + } +} + +const DummyEditor = defineComponent({ + name: 'DummyEditor', + props: ['ydoc', 'awareness', 'provider', 'isReadOnly'], + setup() { + return () => h('div', { class: 'dummy-editor' }) + } +}) + +function makeResource(overrides: Partial = {}): Resource { + return { + id: 'storage$space!item-1', + etag: 'etag-initial', + ...overrides + } as Resource +} + +function mountWrapper(overrides: Record = {}) { + return mount(CollaborativeWrapper, { + props: { + resource: makeResource(), + currentContent: '', + adapter: testAdapter, + editor: DummyEditor, + appVersion: '1.2.3', + realtimeUrl: null, + ...overrides + } + }) +} + +beforeEach(() => { + providerInstances.length = 0 +}) + +afterEach(() => { + vi.useRealTimers() +}) + +describe('CollaborativeWrapper — local mode (no realtimeUrl)', () => { + it('reports status "local" and does not construct a HocuspocusProvider', async () => { + const wrapper = mountWrapper({ currentContent: 'hello' }) + await flushPromises() + expect(wrapper.text()).toContain('local') + expect(providerInstances).toHaveLength(0) + }) + + it('hydrates the Y.Doc from currentContent (election degenerates to "we win")', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + const wrapper = mountWrapper({ currentContent: 'hello local' }) + await flushPromises() + // Hydration is gated by a 150ms awareness-settle wait. + vi.advanceTimersByTime(200) + await flushPromises() + + const ydocAny = (wrapper.vm as unknown as { ydoc: Y.Doc | null }).ydoc + expect(ydocAny).toBeTruthy() + expect(ydocAny!.getText('content').toString()).toBe('hello local') + }) + + it('mounts the editor component with a real Awareness instance', async () => { + const wrapper = mountWrapper({ currentContent: 'x' }) + await flushPromises() + const editor = wrapper.findComponent(DummyEditor) + expect(editor.exists()).toBe(true) + expect(editor.props('awareness')).toBeInstanceOf(Awareness) + expect(editor.props('provider')).toBeNull() + }) +}) + +describe('CollaborativeWrapper — collab mode (realtimeUrl set)', () => { + it('constructs a HocuspocusProvider with the appVersion query param appended', async () => { + mountWrapper({ + realtimeUrl: 'wss://example.test/realtime', + appVersion: '2.3.4' + }) + await flushPromises() + expect(providerInstances).toHaveLength(1) + expect(providerInstances[0].url).toBe('wss://example.test/realtime?appVersion=2.3.4') + expect(providerInstances[0].setAwarenessField).toHaveBeenCalledWith('user', {}) + }) + + it('does not hydrate until onSynced fires (collab waits for the server)', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + const wrapper = mountWrapper({ + realtimeUrl: 'wss://example.test/realtime', + currentContent: 'should-only-land-after-sync' + }) + await flushPromises() + vi.advanceTimersByTime(500) + await flushPromises() + + const ydocAny = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + expect(ydocAny.getText('content').toString()).toBe('') + + providerInstances[0].triggerSynced() + vi.advanceTimersByTime(200) + await flushPromises() + expect(ydocAny.getText('content').toString()).toBe('should-only-land-after-sync') + }) + + it('surfaces an auth failure as a lifecycle error and locks the editor read-only', async () => { + const wrapper = mountWrapper({ realtimeUrl: 'wss://example.test/realtime' }) + await flushPromises() + providerInstances[0].triggerAuthFailed('token expired') + await nextTick() + expect(wrapper.text()).toContain('token expired') + const editor = wrapper.findComponent(DummyEditor) + expect(editor.props('isReadOnly')).toBe(true) + }) +}) + +describe('CollaborativeWrapper — update:currentContent emission', () => { + it('emits debounced after a user-origin Y.Doc update', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + const wrapper = mountWrapper({ currentContent: 'seed' }) + await flushPromises() + vi.advanceTimersByTime(200) + await flushPromises() + + const ydoc = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + // Clear any emit produced by hydration (hydration uses an internal + // origin so this should be a no-op, but we're isolating the + // post-hydrate signal explicitly). + ;(wrapper.emitted()['update:currentContent'] ?? []).length = 0 + + ydoc.getText('content').insert(4, ' edit') // no origin = user-typed + + // Nothing emitted within the debounce window yet. + vi.advanceTimersByTime(100) + await flushPromises() + expect(wrapper.emitted('update:currentContent') ?? []).toHaveLength(0) + + // 300ms after the last edit, the debounced serialize fires. + vi.advanceTimersByTime(300) + await flushPromises() + const emits = wrapper.emitted('update:currentContent') ?? [] + expect(emits.length).toBeGreaterThanOrEqual(1) + expect(emits[emits.length - 1][0]).toBe('seed edit') + }) + + it('does NOT emit for internal-origin transactions (hydrate / reset / recovery)', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + const wrapper = mountWrapper({ currentContent: 'seed' }) + await flushPromises() + vi.advanceTimersByTime(200) // let hydration run + await flushPromises() + vi.advanceTimersByTime(400) // past the debounce window + await flushPromises() + + // After hydration the wrapper may have emitted once with the + // post-hydrate serialization — that's a debounce artefact, not the + // internal-origin transaction itself. The contract we're verifying: + // a fresh internal-origin transact() does NOT schedule a NEW emit. + const before = (wrapper.emitted('update:currentContent') ?? []).length + + const ydoc = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + ydoc.transact(() => { + ydoc.getText('content').insert(0, 'internal-') + }, 'hydrate') + + vi.advanceTimersByTime(400) + await flushPromises() + const after = (wrapper.emitted('update:currentContent') ?? []).length + expect(after).toBe(before) + }) +}) + +describe('CollaborativeWrapper — etag mirror', () => { + it('writes a new resource.etag into _oc_meta.etag', async () => { + const wrapper = mountWrapper({ currentContent: 'x', resource: makeResource({ etag: 'a' }) }) + await flushPromises() + const ydoc = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + const meta = ydoc.getMap('_oc_meta') + + await wrapper.setProps({ resource: makeResource({ etag: 'b' }) }) + await flushPromises() + expect(meta.get('etag')).toBe('b') + expect(meta.get('lastSavedAt')).toBeTypeOf('number') + }) + + // Regression: `setProps({ resource })` with a new resource OBJECT whose + // `id` is unchanged must NOT tear down and rebuild the Y.Doc. The earlier + // implementation used `watchEffect((onCleanup) => { unref(documentName); ... })`, + // which Vue re-ran on every tracked prop access — including `props.resource` + // mutations from AppWrapper's post-save `resourcesStore.upsertResource`. + // Every save would have rebuilt the Y.Doc, losing in-flight peer edits. + // The current implementation gates rebuilds on a `sessionKey` computed + // (documentName + realtimeUrl), so an identity-preserving resource update + // is a no-op for the watch. + it('regression: does not rebuild Y.Doc when resource prop changes without identity change', async () => { + const wrapper = mountWrapper({ currentContent: 'x', resource: makeResource({ etag: 'a' }) }) + await flushPromises() + const ydocBefore = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + expect(ydocBefore).toBeTruthy() + expect(ydocBefore.isDestroyed).toBe(false) + + // Same id, different etag — simulates AppWrapper bouncing `resource` after + // a successful save. + await wrapper.setProps({ resource: makeResource({ etag: 'b' }) }) + await flushPromises() + const ydocAfter = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + expect(ydocAfter).toBe(ydocBefore) + expect(ydocBefore.isDestroyed).toBe(false) + }) + + it('does nothing when the etag is unchanged', async () => { + const wrapper = mountWrapper({ currentContent: 'x', resource: makeResource({ etag: 'a' }) }) + await flushPromises() + const ydoc = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + const meta = ydoc.getMap('_oc_meta') + // Initial etag may have been seeded by onProviderSynced. + const initialMeta = meta.get('etag') + + await wrapper.setProps({ resource: makeResource({ etag: 'a' }) }) + await flushPromises() + expect(meta.get('etag')).toBe(initialMeta) + }) +}) + +describe('CollaborativeWrapper — cleanup', () => { + it('destroys provider, awareness, and doc on unmount (collab mode)', async () => { + const wrapper = mountWrapper({ realtimeUrl: 'wss://example.test/realtime' }) + await flushPromises() + const prov = providerInstances[0] + const ydoc = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + expect(ydoc.isDestroyed).toBe(false) + + wrapper.unmount() + expect(prov.destroy).toHaveBeenCalledOnce() + expect(ydoc.isDestroyed).toBe(true) + }) + + it('destroys awareness and doc on unmount (local mode)', async () => { + const wrapper = mountWrapper({ currentContent: 'x' }) + await flushPromises() + const ydoc = (wrapper.vm as unknown as { ydoc: Y.Doc }).ydoc + expect(ydoc.isDestroyed).toBe(false) + + wrapper.unmount() + expect(ydoc.isDestroyed).toBe(true) + expect(providerInstances).toHaveLength(0) + }) +}) From f41aabf715614fe4c439db3ec8d01ae3c9ffcdcf Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 20 May 2026 02:16:46 +0200 Subject: [PATCH 18/23] test(realtime-collab): cucumber-port collaboration e2e suite Ports the 5 Playwright specs from the web-extensions PoC to web's cucumber suite plus a new cross-peer fan-out scenario covering the Phase 4.5 mechanism. 7 scenarios, 87 steps, ~21s run. Features: - codemirror-open: open + status + content, multi-file navigation - codemirror-save-back: type + Ctrl+S persists via WebDAV - codemirror-multi-user: shared file, peer caret on line N labelled by the server-stamped name, CRDT typing propagation - tiptap-empty-file: empty .md opens cleanly and accepts input - tiptap-open: Markdown rendered as rich text (headings, marks) - cross-peer-fan-out: A saves, B's next save uses the fresh etag (covers the new update:serverContent + update:etag emits) Shared infrastructure (small, only what the suite needs): - `support/objects/app-files/utils/collab.ts`: status-strip + per- editor content selectors, codemirror line + remote-caret helpers. - `cucumber/steps/ui/collaboration.ts`: realtime status check, content assertions, typing, Ctrl+S save, remote-caret-with-label assertion, WebDAV content assertion. - `support/api/davSpaces/getFileContentInPersonalSpace`: new helper for the post-save WebDAV round-trip assertion. Plumbing: - `cucumber/steps/ui/resources.ts`: extend the existing `opens file X via Y using the context menu` step's allowed viewers with `code-mirror` and `tiptap` (kebab-case of `appInfo.name`). - `cucumber.mjs`: scope the cucumber import glob to `tests/e2e/cucumber/**` and `tests/e2e/support/**` so a leftover playwright-mf scratch dir outside those paths doesn't break the runner. --- cucumber.mjs | 3 +- .../codemirror-multi-user.feature | 33 +++++ .../collaboration/codemirror-open.feature | 37 +++++ .../codemirror-save-back.feature | 24 ++++ .../collaboration/cross-peer-fan-out.feature | 42 ++++++ .../collaboration/tiptap-empty-file.feature | 21 +++ .../collaboration/tiptap-open.feature | 24 ++++ tests/e2e/cucumber/steps/ui/collaboration.ts | 136 ++++++++++++++++++ tests/e2e/cucumber/steps/ui/resources.ts | 2 +- tests/e2e/support/api/davSpaces/index.ts | 1 + tests/e2e/support/api/davSpaces/spaces.ts | 17 +++ .../support/objects/app-files/utils/collab.ts | 43 ++++++ 12 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/cucumber/features/collaboration/codemirror-multi-user.feature create mode 100644 tests/e2e/cucumber/features/collaboration/codemirror-open.feature create mode 100644 tests/e2e/cucumber/features/collaboration/codemirror-save-back.feature create mode 100644 tests/e2e/cucumber/features/collaboration/cross-peer-fan-out.feature create mode 100644 tests/e2e/cucumber/features/collaboration/tiptap-empty-file.feature create mode 100644 tests/e2e/cucumber/features/collaboration/tiptap-open.feature create mode 100644 tests/e2e/cucumber/steps/ui/collaboration.ts create mode 100644 tests/e2e/support/objects/app-files/utils/collab.ts diff --git a/cucumber.mjs b/cucumber.mjs index 45b79bf32a..7edd48b012 100644 --- a/cucumber.mjs +++ b/cucumber.mjs @@ -8,7 +8,8 @@ if (!fs.existsSync(config.reportDir)) { const e2e = ` --loader ts-node/esm - --import ./tests/e2e/**/*.ts + --import ./tests/e2e/cucumber/**/*.ts + --import ./tests/e2e/support/**/*.ts --retry ${config.retry} --format @cucumber/pretty-formatter --format pretty diff --git a/tests/e2e/cucumber/features/collaboration/codemirror-multi-user.feature b/tests/e2e/cucumber/features/collaboration/codemirror-multi-user.feature new file mode 100644 index 0000000000..ed200b0dc7 --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/codemirror-multi-user.feature @@ -0,0 +1,33 @@ +Feature: Multi-user collaboration in CodeMirror + As two users editing the same file + I want to see each other's caret and typed text live + So that we can collaborate without stepping on each other + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + | Brian | + + + Scenario: Brian sees Alice's caret and typed content on a shared file + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | shared-note.md | # Shared Note\n\nLINE-A\nLINE-B\nLINE-C\nLINE-D\nLINE-E\n | + And "Alice" shares the following resource using API + | resource | recipient | type | role | + | shared-note.md | Brian | user | Can edit | + When "Alice" logs in + And "Alice" opens file "shared-note.md" via "code-mirror" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Brian" logs in + And "Brian" navigates to the shared with me page + And "Brian" opens file "shared-note.md" via "code-mirror" using the context menu + Then "Brian" should see the realtime collab status "connected" + And "Brian" should see content "LINE-C" in the "codemirror" editor + When "Alice" places the caret on line 4 in the codemirror editor + Then "Brian" should see a remote caret on line 4 labelled "Alice" + When "Alice" types "ALICE-WROTE" at the end of the "codemirror" editor + Then "Brian" should see content "ALICE-WROTE" in the "codemirror" editor + And "Alice" logs out + And "Brian" logs out diff --git a/tests/e2e/cucumber/features/collaboration/codemirror-open.feature b/tests/e2e/cucumber/features/collaboration/codemirror-open.feature new file mode 100644 index 0000000000..c02384261f --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/codemirror-open.feature @@ -0,0 +1,37 @@ +Feature: Open a Markdown file in CodeMirror + As a user with the collaborative editor available + I want to open .md files in CodeMirror + So that I can edit them as raw markdown with realtime collaboration + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + + + Scenario: open .md in CodeMirror, see content, realtime connects + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | alpha.md | # Alpha\n\nALPHA-CONTENT | + When "Alice" logs in + And "Alice" opens file "alpha.md" via "code-mirror" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see content "ALPHA-CONTENT" in the "codemirror" editor + And "Alice" logs out + + + Scenario: navigate between two .md files rebuilds the collab session without leaks + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | alpha.md | # Alpha\n\nALPHA-CONTENT | + | beta.md | # Beta\n\nBETA-CONTENT | + When "Alice" logs in + And "Alice" opens file "alpha.md" via "code-mirror" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see content "ALPHA-CONTENT" in the "codemirror" editor + When "Alice" closes the file viewer + And "Alice" opens file "beta.md" via "code-mirror" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see content "BETA-CONTENT" in the "codemirror" editor + And "Alice" should not see content "ALPHA-CONTENT" in the "codemirror" editor + And "Alice" logs out diff --git a/tests/e2e/cucumber/features/collaboration/codemirror-save-back.feature b/tests/e2e/cucumber/features/collaboration/codemirror-save-back.feature new file mode 100644 index 0000000000..6be95f1601 --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/codemirror-save-back.feature @@ -0,0 +1,24 @@ +Feature: Save-back from CodeMirror to native file + As a user with the collaborative CodeMirror editor + I want my edits to persist to the OC backend on save + So that the file on disk reflects what I typed + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + + + Scenario: typing then Ctrl+S persists the marker to OC over WebDAV + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | save-back.md | initial content | + When "Alice" logs in + And "Alice" opens file "save-back.md" via "code-mirror" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see content "initial content" in the "codemirror" editor + When "Alice" types "MARKER-CUC" at the end of the "codemirror" editor + And "Alice" saves the current file with Ctrl+S + Then the file "save-back.md" in "Alice"'s personal space should contain "MARKER-CUC" + And the file "save-back.md" in "Alice"'s personal space should contain "initial content" + And "Alice" logs out diff --git a/tests/e2e/cucumber/features/collaboration/cross-peer-fan-out.feature b/tests/e2e/cucumber/features/collaboration/cross-peer-fan-out.feature new file mode 100644 index 0000000000..e303b62f7b --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/cross-peer-fan-out.feature @@ -0,0 +1,42 @@ +Feature: Peer-save fan-out via _oc_meta + As two users editing the same file + I want a save in one tab to mark the other tab clean + So that I don't see a stale "save changes?" prompt and don't 412 on my next save + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + | Brian | + + + Scenario: Alice saves, Brian sees the new state and his next save uses the fresh etag + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | fanout.md | initial fan-out content | + And "Alice" shares the following resource using API + | resource | recipient | type | role | + | fanout.md | Brian | user | Can edit | + When "Alice" logs in + And "Alice" opens file "fanout.md" via "code-mirror" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Brian" logs in + And "Brian" navigates to the shared with me page + And "Brian" opens file "fanout.md" via "code-mirror" using the context menu + Then "Brian" should see the realtime collab status "connected" + And "Brian" should see content "initial fan-out content" in the "codemirror" editor + When "Alice" types "ALICE-SAVED" at the end of the "codemirror" editor + Then "Brian" should see content "ALICE-SAVED" in the "codemirror" editor + When "Alice" saves the current file with Ctrl+S + Then the file "fanout.md" in "Alice"'s personal space should contain "ALICE-SAVED" + # _oc_meta.lastSavedAt + .etag propagate via CRDT to Brian's wrapper, which + # emits update:serverContent + update:etag. Brian's local content now + # matches serverContent (isDirty == false) and currentETag is current, so + # when Brian types more and saves, the PUT goes straight through without + # the 412 -> refetch -> retry recovery loop. + When "Brian" types "BRIAN-ADDED" at the end of the "codemirror" editor + And "Brian" saves the current file with Ctrl+S + Then the file "fanout.md" in "Alice"'s personal space should contain "BRIAN-ADDED" + And the file "fanout.md" in "Alice"'s personal space should contain "ALICE-SAVED" + And "Alice" logs out + And "Brian" logs out diff --git a/tests/e2e/cucumber/features/collaboration/tiptap-empty-file.feature b/tests/e2e/cucumber/features/collaboration/tiptap-empty-file.feature new file mode 100644 index 0000000000..6fdcd550d0 --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/tiptap-empty-file.feature @@ -0,0 +1,21 @@ +Feature: Open an empty Markdown file in Tiptap + As a user + I want to open an empty .md file in Tiptap + So that I can start writing rich content from scratch + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + + + Scenario: empty .md opens cleanly and accepts input + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | empty-note.md | | + When "Alice" logs in + And "Alice" opens file "empty-note.md" via "tiptap" using the context menu + Then "Alice" should see the realtime collab status "connected" + When "Alice" types "hello from empty" at the end of the "tiptap" editor + Then "Alice" should see content "hello from empty" in the "tiptap" editor + And "Alice" logs out diff --git a/tests/e2e/cucumber/features/collaboration/tiptap-open.feature b/tests/e2e/cucumber/features/collaboration/tiptap-open.feature new file mode 100644 index 0000000000..ffb17ea0d8 --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/tiptap-open.feature @@ -0,0 +1,24 @@ +Feature: Open a Markdown file in Tiptap + As a user + I want to open .md files in Tiptap + So that I can edit them as rich text with realtime collaboration + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + + + Scenario: Markdown content is rendered as rich text + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | rich-note.md | # Rich Note\n\n## Section Two\n\nThis is **bold** and this is *italic* and `inline code`.\n\n- one\n- two\n- three\n\n1. first\n2. second | + When "Alice" logs in + And "Alice" opens file "rich-note.md" via "tiptap" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see content "Rich Note" in the "tiptap" editor + And "Alice" should see content "Section Two" in the "tiptap" editor + And "Alice" should see content "bold" in the "tiptap" editor + And "Alice" should see content "italic" in the "tiptap" editor + And "Alice" should see content "inline code" in the "tiptap" editor + And "Alice" logs out diff --git a/tests/e2e/cucumber/steps/ui/collaboration.ts b/tests/e2e/cucumber/steps/ui/collaboration.ts new file mode 100644 index 0000000000..acaa8a4048 --- /dev/null +++ b/tests/e2e/cucumber/steps/ui/collaboration.ts @@ -0,0 +1,136 @@ +import { When, Then } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import { World } from '../../environment' +import { + awaitCollabStatus, + collabContent, + codemirrorLine, + remoteCaretCount, + remoteCaretLabelText, + type CollabEditor +} from '../../../support/objects/app-files/utils/collab' +import { api } from '../../../support' + +Then( + /^"([^"]+)" should see the realtime collab status "(connected|disconnected|connecting|local)"$/, + async function ( + this: World, + stepUser: string, + status: 'connected' | 'disconnected' | 'connecting' | 'local' + ): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + await awaitCollabStatus(page, status) + } +) + +Then( + /^"([^"]+)" should see content "([^"]+)" in the "(codemirror|tiptap|text-editor)" editor$/, + async function ( + this: World, + stepUser: string, + text: string, + editor: CollabEditor + ): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + await expect(collabContent(page, editor)).toContainText(text, { timeout: 10_000 }) + } +) + +Then( + /^"([^"]+)" should not see content "([^"]+)" in the "(codemirror|tiptap|text-editor)" editor$/, + async function ( + this: World, + stepUser: string, + text: string, + editor: CollabEditor + ): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + await expect(collabContent(page, editor)).not.toContainText(text) + } +) + +When( + /^"([^"]+)" types "([^"]+)" at the end of the "(codemirror|tiptap|text-editor)" editor$/, + async function ( + this: World, + stepUser: string, + text: string, + editor: CollabEditor + ): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + await collabContent(page, editor).click() + await page.keyboard.press('End') + await page.keyboard.type(text) + } +) + +When( + /^"([^"]+)" places the caret on line (\d+) in the codemirror editor$/, + async function (this: World, stepUser: string, lineNumber: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + // Feature uses 1-based line numbers, locator is 0-based. + await codemirrorLine(page, parseInt(lineNumber, 10) - 1).click() + await page.keyboard.press('End') + } +) + +When( + /^"([^"]+)" saves the current file with Ctrl\+S$/, + async function (this: World, stepUser: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + // Give the wrapper's debounced serialize a chance to publish the dirty + // content into AppWrapper before we trigger its save. + await page.waitForTimeout(500) + await Promise.all([ + page.waitForResponse( + (resp) => resp.request().method() === 'PUT' && [204, 201].includes(resp.status()) + ), + page.keyboard.press('Control+s') + ]) + } +) + +Then( + /^"([^"]+)" should see a remote caret on line (\d+) labelled "([^"]+)"$/, + async function ( + this: World, + stepUser: string, + lineNumber: string, + label: string + ): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + const lineIdx = parseInt(lineNumber, 10) - 1 + await expect(codemirrorLine(page, lineIdx).locator('.cm-ySelectionCaret')).toHaveCount(1, { + timeout: 10_000 + }) + // y-codemirror's label appears the first time the peer's awareness + // entry includes a `user.name`; the server stamps that during + // beforeHandleAwareness. + await expect.poll(async () => await remoteCaretLabelText(page)).toContain(label) + } +) + +Then( + /^"([^"]+)" should not see any remote caret$/, + async function (this: World, stepUser: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + await expect.poll(async () => await remoteCaretCount(page)).toBe(0) + } +) + +Then( + /^the file "([^"]+)" in "([^"]+)"'s personal space should contain "([^"]+)"$/, + async function ( + this: World, + pathToFile: string, + stepUser: string, + expected: string + ): Promise { + const user = this.usersEnvironment.getCreatedUser({ key: stepUser }) + await expect + .poll(async () => await api.dav.getFileContentInPersonalSpace({ user, pathToFile }), { + timeout: 10_000 + }) + .toContain(expected) + } +) diff --git a/tests/e2e/cucumber/steps/ui/resources.ts b/tests/e2e/cucumber/steps/ui/resources.ts index 5754c8308f..776c4a0dfc 100644 --- a/tests/e2e/cucumber/steps/ui/resources.ts +++ b/tests/e2e/cucumber/steps/ui/resources.ts @@ -1103,7 +1103,7 @@ Then( When( '{string} opens file {string} via {string} using the context menu', async function (this: World, stepUser: string, file: string, fileViewer: string): Promise { - const allowedViewers = ['collabora', 'text-editor', 'preview'] as const + const allowedViewers = ['collabora', 'text-editor', 'preview', 'code-mirror', 'tiptap'] as const if (!allowedViewers.includes(fileViewer as any)) { throw new Error(`Unsupported file viewer: ${fileViewer}`) diff --git a/tests/e2e/support/api/davSpaces/index.ts b/tests/e2e/support/api/davSpaces/index.ts index 5ae6342eea..2f2263ce61 100644 --- a/tests/e2e/support/api/davSpaces/index.ts +++ b/tests/e2e/support/api/davSpaces/index.ts @@ -1,5 +1,6 @@ export { uploadFileInPersonalSpace, + getFileContentInPersonalSpace, createFolderInsideSpaceBySpaceName, createFolderInsidePersonalSpace, getIdOfFileInsideSpace, diff --git a/tests/e2e/support/api/davSpaces/spaces.ts b/tests/e2e/support/api/davSpaces/spaces.ts index c3d6d9aadc..94ee1c855d 100644 --- a/tests/e2e/support/api/davSpaces/spaces.ts +++ b/tests/e2e/support/api/davSpaces/spaces.ts @@ -95,6 +95,23 @@ const deleteFile = async ({ checkResponseStatus(response, `Failed deleting file '${pathToFile}'`) } +export const getFileContentInPersonalSpace = async ({ + user, + pathToFile +}: { + user: User + pathToFile: string +}): Promise => { + const spaceId = await getSpaceIdBySpaceName({ user, spaceType: 'personal' }) + const response = await request({ + method: 'GET', + path: urlJoin('remote.php', 'dav', 'spaces', spaceId, pathToFile), + user + }) + checkResponseStatus(response, `Failed while reading file ${pathToFile}`) + return await response.text() +} + export const uploadFileInPersonalSpace = async ({ user, pathToFile, diff --git a/tests/e2e/support/objects/app-files/utils/collab.ts b/tests/e2e/support/objects/app-files/utils/collab.ts new file mode 100644 index 0000000000..04463a4e82 --- /dev/null +++ b/tests/e2e/support/objects/app-files/utils/collab.ts @@ -0,0 +1,43 @@ +import { Page, expect } from '@playwright/test' + +// Realtime status strip lives at the top of CollaborativeWrapper.vue. The +// connect handshake is async (hocuspocus auth + initial sync), so callers +// generally wait on this before touching the editor. +const statusStrip = (page: Page, status: string) => + page.locator('.oc-text-meta', { hasText: status }).first() + +export const awaitCollabStatus = async ( + page: Page, + status: 'connected' | 'disconnected' | 'connecting' | 'local' +): Promise => { + await expect(statusStrip(page, status)).toBeVisible({ timeout: 10_000 }) +} + +// Per-editor content selectors. The wrapper itself is editor-agnostic; the +// bound editor component renders its own DOM. We keep this in one place so +// scenarios stay readable. +const editorContent = { + codemirror: '.cm-content', + tiptap: '.ProseMirror', + // text-editor uses the tiptap editor under the hood since the Phase 4 + // refactor, so the selector is the same. + 'text-editor': '.ProseMirror' +} as const + +export type CollabEditor = keyof typeof editorContent + +export const collabContent = (page: Page, editor: CollabEditor) => + page.locator(editorContent[editor]) + +// Codemirror exposes one line per `.cm-line`. Tiptap renders paragraphs as +// `

` etc. inside `.ProseMirror`. For multi-user cursor assertions we lean +// on codemirror's `.cm-ySelectionCaret` / `.cm-ySelectionInfo`, which the +// y-codemirror.next integration paints. +export const codemirrorLine = (page: Page, lineIndex: number) => + page.locator('.cm-line').nth(lineIndex) + +export const remoteCaretCount = (page: Page) => + page.locator('.cm-ySelectionCaret').count() + +export const remoteCaretLabelText = (page: Page) => + page.locator('.cm-ySelectionInfo').first().textContent() From b335de9444669c85feff8089eb1dc4a264a9f960 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 20 May 2026 02:43:05 +0200 Subject: [PATCH 19/23] build(realtime-collab): route /realtime through OC's reverse proxy Unifies dev and CI behind a single routing model: OC's reverse proxy WebSocket-upgrades incoming /realtime requests and forwards them to the hocuspocus sidecar via an `additional_policies` entry, the same pattern opencloud-music uses. Drops the Traefik labels on the dev hocuspocus service; CI now has parity without needing a Traefik sidecar of its own. Dev: - `dev/docker/opencloud/proxy.yaml`: add the /realtime route (alongside the existing radicale routes). - `docker-compose.yml`: remove the hocuspocus Traefik labels + depends_on, keep the service on the traefik network so OC can reach it by name. CI: - `tests/woodpecker/proxy.yaml`: new file, /realtime route only. - `.woodpecker.star`: cp proxy.yaml into OC's config dir during init, define `hocuspocusService()` (plain HTTP node service, no TLS), wire it into a new `collab` e2e suite that runs alongside OC. - `tests/woodpecker/config-opencloud.json`: register codemirror + tiptap so OC loads them. - Doc the unified routing in REALTIME_COLLAB_MIGRATION.md Phase 5. Verified locally: full 7/7 cucumber collab suite still green after removing the Traefik labels; OC's proxy handles the WS upgrade transparently. --- .woodpecker.star | 42 ++++++++++++++++++++++++++ REALTIME_COLLAB_MIGRATION.md | 2 +- dev/docker/opencloud/proxy.yaml | 12 ++++++++ docker-compose.yml | 16 +++------- tests/woodpecker/config-opencloud.json | 2 ++ tests/woodpecker/proxy.yaml | 15 +++++++++ 6 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 tests/woodpecker/proxy.yaml diff --git a/.woodpecker.star b/.woodpecker.star index b6776eab54..cb5ce8833e 100644 --- a/.woodpecker.star +++ b/.woodpecker.star @@ -175,6 +175,13 @@ config = { "OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE": "%s" % dir["ocmProviders"], }, }, + "collab": { + "earlyFail": True, + "skip": False, + "suites": [ + "collaboration", + ], + }, "mobile-view": { "skip": False, "suites": [ @@ -627,6 +634,12 @@ def e2eTests(ctx): elif "ocm" in suite: steps += openCloudService(params["extraServerEnvironment"]) + \ (openCloudService(params["extraServerEnvironment"], "federation") if params["federationServer"] else []) + elif "collab" in suite: + # Realtime collab: start hocuspocus alongside OC. The OC + # reverse proxy forwards /realtime to it via the proxy.yaml + # mounted in openCloudService. + steps += hocuspocusService() + \ + openCloudService(params["extraServerEnvironment"]) else: # OpenCloud specific steps steps += (tikaService() if params["tikaNeeded"] else []) + \ @@ -946,6 +959,7 @@ def openCloudService(extra_env_config = {}, deploy_type = "opencloud"): "mkdir -p /srv/app/tmp/opencloud/storage/users/", "./opencloud init", "cp %s/tests/woodpecker/app-registry.yaml /root/.opencloud/config/app-registry.yaml" % dir["web"], + "cp %s/tests/woodpecker/proxy.yaml /root/.opencloud/config/proxy.yaml" % dir["web"], "./opencloud server", ], }, @@ -1409,6 +1423,34 @@ def tikaService(): "detach": True, }] + waitForService("tika", "9998") +def hocuspocusService(): + # Build + start the hocuspocus realtime collab sidecar in-place from the + # checked-out web tree. Plain HTTP on :1234; OC's reverse proxy + # WebSocket-upgrades and forwards via the `additional_policies` entry in + # tests/woodpecker/proxy.yaml. Mirrors the dev/docker/hocuspocus image + # setup but skips Docker — we install + patch the same way the Dockerfile + # does, then run server.js with node. + sidecar_dir = "%s/dev/docker/hocuspocus" % dir["web"] + return [{ + "name": "hocuspocus", + "image": OC_CI_NODEJS, + "detach": True, + "environment": { + "PORT": "1234", + "DB_PATH": "/tmp/hocuspocus-state.db", + "OPENCLOUD_URL": "https://opencloud:9200", + "NODE_TLS_REJECT_UNAUTHORIZED": "0", + }, + "commands": [ + "cd %s" % sidecar_dir, + "npm install --omit=dev --no-audit --no-fund --loglevel=error", + # The Dockerfile applies a pre-built patch on top of + # @hocuspocus/server@4.0.0. `patch` is in nodejs-ci:24. + "cd node_modules/@hocuspocus/server && patch -p1 < %s/patches/hocuspocus-server-4.0.0.patch && cd -" % sidecar_dir, + "node server.js", + ], + }] + waitForService("hocuspocus", "1234") + def collaboraService(): return [ { diff --git a/REALTIME_COLLAB_MIGRATION.md b/REALTIME_COLLAB_MIGRATION.md index 3a09d7ca09..5a182424e3 100644 --- a/REALTIME_COLLAB_MIGRATION.md +++ b/REALTIME_COLLAB_MIGRATION.md @@ -275,5 +275,5 @@ In the web repo the dev workflow uses `pnpm vite` (dev server with HMR) instead - [x] **Phase 3**: copied sidecar to `dev/docker/hocuspocus/`, appended `hocuspocus` service to `docker-compose.yml`. Web stack up + reachable: OC https://host.docker.internal:9200 HTTP 200, sidecar /realtime HTTP 200, `pnpm build:w` running, WEB_APPS_MAP now includes `web-app-codemirror` + `web-app-tiptap`. Etag-probe switch to Graph + folder-vs-file check moved to follow-ups (see Future considerations). - [x] **Phase 4**: text-editor refactored onto CollaborativeWrapper. `useTextEditor` accepts optional `ydoc` → adds `Collaboration` extension, skips initial-content assignment, suppresses `modelValue` round-trip. All 3 Tiptap strategies flip `StarterKit.configure({ undoRedo: false })`; plain-text strategy needs no change (no StarterKit). `makeTextEditorAdapter(strategy)` bridges to `CollaborativeAdapter`; App.vue is now a thin shell around the wrapper with `TextEditorBinding.vue` as the editor component. Codemirror + tiptap e2e 9/9 still green; text-editor unit test stubs the wrapper. Manual smoke tests for the 3 non-markdown content types: pending (separate todo). - [x] **Phase 4.5**: peer-save coordination — replaces the originally planned inject contract. Two simpler mechanisms instead: (1) `AppWrapper.saveFileTask` catches 412/409 with refetch+retry (`d26d836ef0`) so a stale local etag self-heals on the next save. (2) `CollaborativeWrapper` tags its own `_oc_meta` writes with a `LOCAL_SAVE_ORIGIN` transaction origin and emits `update:serverContent` + `update:etag` from the meta observer when the change came in via CRDT (`e4939c3168`). AppWrapper listens to both new emits — `serverContent` flips `isDirty` false on peers without a refetch, `etag` updates `currentETag` so peers skip the 412→refetch→retry round-trip on their next save. Same origin-tag pattern as the existing `update:currentContent` emit; no inject machinery. The same commit also fixes `saveFileTask` to update the local `resource` ref on save (previously only `serverContent` / `currentETag` / `resourcesStore` were touched, so the etag-mirror watch keyed on `props.resource.etag` never fired). Cucumber scenario for the cross-peer dirty + etag flow: still pending, moved into Phase 5. -- [ ] **Phase 5**: migrate unit + integration tests, Cucumber-port the e2e suites with existing helpers where possible; add hocuspocus to web's woodpecker e2e CI job +- [x] **Phase 5**: 13/13 unit specs ported into `packages/web-pkg/tests/unit/components/Collaborative/` (`458fc4d8ec`). 7 cucumber scenarios under `tests/e2e/cucumber/features/collaboration/` covering open + navigate + save-back + multi-user + tiptap rich-render + the cross-peer fan-out (`cfbf5a8ad8`). Integration suite skipped — its DEV_FAKE_TOKEN-based coverage is subsumed by the cucumber suite running against a real OIDC flow. Woodpecker `collab` matrix wires a `hocuspocusService` plus a `proxy.yaml` policy that routes `/realtime` through OC's reverse proxy to the sidecar; same routing model is mirrored locally (dev `docker-compose.yml` drops the Traefik labels on hocuspocus and lets the OC proxy do the WS-upgrade) so dev and CI stay symmetric. - [ ] Smoke test full dev loop in web; commit per phase; PR against `opencloud-eu/web` main diff --git a/dev/docker/opencloud/proxy.yaml b/dev/docker/opencloud/proxy.yaml index 543e108350..b4a99792cd 100644 --- a/dev/docker/opencloud/proxy.yaml +++ b/dev/docker/opencloud/proxy.yaml @@ -4,6 +4,18 @@ additional_policies: - name: default routes: + # Realtime collab sidecar. The hocuspocus container terminates plain + # HTTP on :1234; OC's reverse proxy upgrades the incoming WebSocket + # request and forwards it on. Pattern is borrowed from the + # opencloud-music sidecar setup. `unprotected: true` because + # hocuspocus does its own bearer-token validation against OC's + # Graph API (see dev/docker/hocuspocus/server.js + # `validateTokenAgainstOpenCloud`) and must see the Authorization + # header the client sent verbatim, not whatever OC's proxy would + # substitute. + - endpoint: /realtime + backend: http://hocuspocus:1234 + unprotected: true - endpoint: /caldav/ backend: http://host.docker.internal:5232 remote_user_header: X-Remote-User diff --git a/docker-compose.yml b/docker-compose.yml index 4da34e53f4..de4ac3d911 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -367,20 +367,14 @@ services: - hocuspocus-data:/var/lib/hocuspocus # Dev override: edit server.js without rebuild; remove for prod - ./dev/docker/hocuspocus/server.js:/app/server.js:ro - labels: - traefik.enable: true - traefik.docker.network: traefik - traefik.http.routers.hocuspocus.tls: true - traefik.http.routers.hocuspocus.rule: Host(`host.docker.internal`) && PathPrefix(`/realtime`) - traefik.http.routers.hocuspocus.entrypoints: opencloud - traefik.http.services.hocuspocus.loadbalancer.server.port: 1234 - traefik.http.middlewares.hocuspocus-strip.stripprefix.prefixes: /realtime - traefik.http.routers.hocuspocus.middlewares: hocuspocus-strip,cors networks: + # On the traefik network so OC (which is also on it) can reach + # hocuspocus by service name. Browser-facing routing is no longer + # done at the Traefik layer — OC's reverse proxy forwards /realtime + # via `additional_policies` in opencloud/proxy.yaml. This keeps the + # routing model identical to what CI uses (CI has no Traefik). - traefik restart: unless-stopped - depends_on: - - traefik volumes: uploads: diff --git a/tests/woodpecker/config-opencloud.json b/tests/woodpecker/config-opencloud.json index 43f95e05d5..dbd123b5c2 100644 --- a/tests/woodpecker/config-opencloud.json +++ b/tests/woodpecker/config-opencloud.json @@ -4,6 +4,8 @@ "apps": [ "files", "text-editor", + "codemirror", + "tiptap", "preview", "pdf-viewer", "search", diff --git a/tests/woodpecker/proxy.yaml b/tests/woodpecker/proxy.yaml new file mode 100644 index 0000000000..9492bc1f9a --- /dev/null +++ b/tests/woodpecker/proxy.yaml @@ -0,0 +1,15 @@ +# OC reverse-proxy policies used during CI e2e runs. Mirrors what the +# dev compose mounts at /etc/opencloud/proxy.yaml, but keeps only the +# routes relevant to the test suites. Added by the openCloudService +# step via `cp ... /root/.opencloud/config/proxy.yaml`. +additional_policies: + - name: default + routes: + # Realtime collab sidecar. Same pattern as in dev: hocuspocus runs + # plain HTTP on :1234, OC's reverse proxy WebSocket-upgrades the + # incoming request and forwards it. `unprotected: true` because + # hocuspocus does its own bearer-token validation against OC's + # Graph API and must see the Authorization header verbatim. + - endpoint: /realtime + backend: http://hocuspocus:1234 + unprotected: true From 6e0703a96217eb2baa75b47dc3c98104c94c91ae Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 20 May 2026 13:32:13 +0200 Subject: [PATCH 20/23] chore(realtime-collab): satisfy prettier + vue-tsc CI surfaced two gaps from the migration: - Prettier formatting drift across 9 files (touched during the PoC port but not run through the project's prettier config). Reformat in place; no behavioural change. - vue-tsc TS7011 on the text-editor app stub: the inline strategy mock returned `() => []` for `extensions` / `editorActionGroups`, which TS infers as `() => any[]` and rejects under the strict config. Add explicit `: unknown[]` return annotations -- the shape doesn't matter to this test (it only asserts the wrapper mounts), it just needs to satisfy the interface. Local: prettier clean, `pnpm check:types` green, the affected unit specs (CollaborativeWrapper.spec.ts + app.spec.ts) still pass. --- REALTIME_COLLAB_MIGRATION.md | 35 +- dev/docker/hocuspocus/server.js | 9 +- .../tests/unit/app.spec.ts | 14 +- packages/web-app-tiptap/src/TiptapEditor.vue | 6 +- .../components/AppTemplates/AppWrapper.vue | 4 +- .../Collaborative/CollaborativeWrapper.vue | 352 +++++++++--------- .../src/editor/composables/useTextEditor.ts | 4 +- tests/e2e/cucumber/steps/ui/collaboration.ts | 7 +- .../support/objects/app-files/utils/collab.ts | 3 +- 9 files changed, 226 insertions(+), 208 deletions(-) diff --git a/REALTIME_COLLAB_MIGRATION.md b/REALTIME_COLLAB_MIGRATION.md index 5a182424e3..023d52f081 100644 --- a/REALTIME_COLLAB_MIGRATION.md +++ b/REALTIME_COLLAB_MIGRATION.md @@ -11,11 +11,13 @@ The realtime collaboration PoC in `opencloud-eu/web-extensions` (PR #447, branch - All 17/17 tests green (5 codemirror e2e + 4 tiptap e2e + 8 integration) User now wants: + - Move both apps and the wrapper into the canonical `opencloud-eu/web` repo - Refactor the existing `web-app-text-editor` so it uses the realtime API (the wrapper) exclusively — every editor instance goes through Y.Doc, whether or not a sidecar is reachable (local-mode handles the no-server case) - Bring the Hocuspocus sidecar into web's docker-compose so the whole dev loop lives in one repo User-clarified scope: + - **One canonical wrapper in web-pkg**, used by all three apps (codemirror, tiptap, refactored text-editor). The wrapper does NOT get forked. If integration churn is needed during the text-editor refactor, fork the `web-app-text-editor` package itself (not the wrapper) so we can iterate without fixing all call sites at once. Wrapper API stays single source of truth. - **Y.Doc is always-on, no optional.** `useTextEditor` and friends always receive a Y.Doc; the wrapper's local-mode is the universal "no realtime backend" branch. Experimental but the wrapper just gained this capability and we want to see it carry the full text-editor surface. - **App naming stays:** `web-app-codemirror` + `web-app-tiptap` as separate apps alongside the refactored `web-app-text-editor`. @@ -29,6 +31,7 @@ User-clarified scope: While debugging the unit test for the etag-mirror behaviour I discovered a real bug in `CollaborativeWrapper.vue`: the `watchEffect((onCleanup) => { ... })` lifecycle re-runs whenever any tracked prop changes — including the `resource` prop update that AppWrapper fires after every save (via `resourcesStore.upsertResource`). Each AppWrapper save would tear down and rebuild the Y.Doc, losing peer state. The shared-file e2e doesn't catch it because the two peers' saves happen far apart in test time and the wrapper successfully re-hydrates from `currentContent`. **Fix (mid-edit in the current working tree, broken intermediate state):** + - Replace `watchEffect((onCleanup) => { ... })` with `watch(sessionKey, (key, _, onCleanup) => { ... }, { immediate: true })` - `sessionKey = computed(() => name && `${name}::${realtimeUrl ?? 'local'}`)` — only rebuilds when the actual session identity changes - Vue's computed equality check ensures `setProps({ resource: newResource })` with same `id` doesn't re-fire the watch @@ -36,11 +39,13 @@ While debugging the unit test for the etag-mirror behaviour I discovered a real - Close the `watch` callback + outer block properly (current state has dangling brace + leftover body) **Files:** + - `packages/web-app-codemirror/src/CollaborativeWrapper.vue` — finish refactor; remove debug logs - `packages/web-app-codemirror/tests/unit/CollaborativeWrapper.spec.ts` — clean up the debug `console.log` I added in the etag-mirror test - `packages/web-app-codemirror/tests/unit/CollaborativeWrapper.spec.ts` — **add a dedicated regression test** (separate from etag-mirror) that asserts: given a wrapper mounted with a resource, calling `setProps({ resource })` with a NEW resource object whose `id` is unchanged keeps the same `wrapper.vm.ydoc` instance reference (no rebuild). Tag the test name explicitly as "regression: does not rebuild Y.Doc when resource prop changes without identity change" so future readers see why it's there. **Verification:** + - `pnpm vitest run tests/unit/CollaborativeWrapper.spec.ts` — all unit tests green (12 existing + 1 regression) - `pnpm playwright test --project=codemirror-chromium` — 5/5 green (regression check on real e2e) - `pnpm playwright test --project=tiptap-chromium` — 4/4 green @@ -57,6 +62,7 @@ Only AFTER Phase 0 is committed and pushed do we start Phase 1. **Target:** make `CollaborativeWrapper` importable as `import { CollaborativeWrapper } from '@opencloud-eu/web-pkg'`. **Files to create in `/home/domme/dev/sources/opencloud-eu/web/packages/web-pkg/`:** + - `src/components/Collaborative/CollaborativeWrapper.vue` — straight copy of the final web-extensions wrapper (post Phase 0). Reads `useAuthStore` from `../../composables/piniaStores/...` — verify the relative import path during the move. - `src/components/Collaborative/types.ts` — the `CollaborativeAdapter` interface (currently at `packages/web-app-codemirror/src/types.ts`). - `src/components/Collaborative/index.ts` — barrel export `{ default as CollaborativeWrapper } from ...` + `type CollaborativeAdapter`. @@ -65,6 +71,7 @@ Only AFTER Phase 0 is committed and pushed do we start Phase 1. **Dependency audit (the user explicitly asked for thoroughness here — many deps belong in the consuming apps, not in web-pkg):** Belongs in `web-pkg` (the wrapper uses them directly): + - `@hocuspocus/provider ^4.0.0` — runtime, the wrapper instantiates `HocuspocusProvider` - `yjs ^13.6.0` — the wrapper imports `* as Y` for `new Y.Doc()` - `y-protocols ^1.0.7` — the wrapper imports `Awareness` for the local-mode standalone @@ -72,6 +79,7 @@ Belongs in `web-pkg` (the wrapper uses them directly): - `@types/semver ^7.7.0` (devDep) Does NOT belong in web-pkg (consumer-specific): + - `y-codemirror.next` — only the codemirror app's adapter uses this - `@tiptap/y-tiptap` — only the tiptap-using apps' adapters / editor components use this. Already pulled transitively via `@tiptap/extension-collaboration` (which web-pkg DOES have via its `useTextEditor`). When the tiptap app or text-editor needs to import from `@tiptap/y-tiptap` directly (custom cursor extension), it lists its own direct dep — same pattern as the codemirror app. - `@tiptap/markdown` — already in web-pkg via the markdown strategy; stays as-is @@ -82,6 +90,7 @@ This minimises web-pkg's surface — the wrapper is editor-agnostic, so adapter- **No web-pkg test added in this phase** — unit test moves with the wrapper in Phase 5. **Verification:** + - `pnpm install` in web root resolves cleanly with no peer warnings - `pnpm --filter=@opencloud-eu/web-pkg build` succeeds (web-pkg's own build / type-check validates imports) @@ -100,11 +109,13 @@ This minimises web-pkg's surface — the wrapper is editor-agnostic, so adapter- - Keep `y-codemirror.next` in codemirror app, keep `@tiptap/*` + `@tiptap/y-tiptap` + `@tiptap/markdown` in tiptap app — those are app-bound 5. Don't move tests yet — they go in Phase 5 -**Files to update in web (not web-app-*):** +**Files to update in web (not web-app-\*):** + - `dev/docker/opencloud.apps.yaml` (or web's equivalent) — add `codemirror` / `tiptap` entries with `config.realtimeUrl: wss://host.docker.internal:9200/realtime` - `docker-compose.yml` — add `./packages/web-app-codemirror/dist:/web/apps/codemirror` and `./packages/web-app-tiptap/dist:/web/apps/tiptap` volume mounts **Verification:** + - `pnpm --filter=codemirror build` + `pnpm --filter=tiptap build` succeed - OC sees both apps via `/config.json` `external_apps` - Manual smoke test: open .md file, both apps appear in "Open with...", both load + edit + save @@ -120,6 +131,7 @@ This minimises web-pkg's surface — the wrapper is editor-agnostic, so adapter- 5. **Switch the etag probe in `server.js` from WebDAV HEAD to Graph API.** The current code uses WebDAV HEAD `/remote.php/dav/spaces/{itemId}` with the comment "Graph's /items endpoint is share-jail-only and 400s on personal drives" — verify whether that's still true against current OC Graph API. Likely the right endpoint is Graph `/v1.0/drives/{driveId}/items/{itemId}` which returns a DriveItem with `eTag`. If 400 still happens on personal drives, dig into why (might be v1beta1 vs v1.0, or missing query param). Goal: one consistent Graph call for permissions + etag, drop the WebDAV path from the sidecar. **Verification:** + - `docker compose up -d hocuspocus` reachable at `wss://host.docker.internal:9200/realtime` - Apps from Phase 2 connect successfully - Integration spec (when ported in Phase 5) runs green against it @@ -133,6 +145,7 @@ This is the biggest piece. `web-app-text-editor` currently uses `useTextEditor` The user's directive: every text-editor instance goes through the realtime API (the wrapper). Y.Doc is always-on. Local-mode handles the no-sidecar case transparently. UX (toolbar, slash commands, strategies, multi-content-type support) is preserved. **Architectural shape:** + - `CollaborativeWrapper` is the outer shell. text-editor's `App.vue` sits inside it. - `useTextEditor` is reworked to accept a **mandatory** Y.Doc parameter (NOT optional — the user explicitly wants this to always go through Y.Doc). When invoked, it includes the `Collaboration.configure({ document: ydoc, field: 'default' })` extension and disables StarterKit's built-in `undoRedo` (yUndoPlugin from y-tiptap takes over). - The toolbar / slash commands keep operating on the Tiptap editor instance the composable returns — they don't know about Y.Doc, they just call `editor.commands.bold()` / `undo()` / `setLink()` / etc. @@ -141,6 +154,7 @@ The user's directive: every text-editor instance goes through the realtime API ( **`CollaborativeAdapter` contract extension (important for performance):** The PoC adapters spawn a headless Tiptap editor inside `serialize(ydoc)` (see `web-app-tiptap/src/adapters/tiptapMarkdown.ts`). That's cheap when StarterKit + Markdown are the only extensions, but for text-editor's 4 strategies × 10+ extensions (link, image, table, task-list, etc.) we'd be re-instantiating Tiptap on every debounced serialize. The contract should be extended to optionally accept the LIVE editor for `serialize`, falling back to headless when no editor is bound (e.g., during stale-recovery on a peer that has the doc but no UI): + ```ts export interface CollaborativeAdapter { hydrate(ydoc: Y.Doc, content: string): void | Promise @@ -148,22 +162,27 @@ export interface CollaborativeAdapter { // ... rest unchanged } ``` + Wrapper's `scheduleEmit` passes the live editor when one is bound (the editor component exposes it via `defineExpose` or a slot). This change is BACKWARDS compatible (existing adapters ignore the new arg) and lands as part of Phase 4 — no need to touch the wrapper in Phase 1. **What does NOT break going all-in collab:** + - Strategies, toolbar, slash commands, undo/redo, multi-extension setup — all preserved - Single-user UX (no sidecar) — covered by the wrapper's local mode - The existing 250ms debounce in `useTextEditor` becomes the wrapper's 300ms debounce — close enough, can be tuned via prop **What could break if uncareful:** + - Custom extensions that mutate editor state outside of commands (rare; none in current StarterKit / web-pkg extension set) - Schema drift between peers running different bundles — already handled by `enableContentCheck` + `onContentError` (the wrapper's app-version lock plus Tiptap's content-check is belt-and-braces) **Forking discipline (per user clarification):** + - **DO NOT fork the wrapper.** It stays canonical in web-pkg. - **CAN fork `web-app-text-editor`** if the refactor would otherwise need to touch every call-site of `useTextEditor` in web. The text-editor app may diverge during the migration, the rest of web stays on the old `useTextEditor` for now. Reconverge in a follow-up. **Files to touch:** + - `packages/web-app-text-editor/src/App.vue` — wrap content rendering inside `CollaborativeWrapper`; emit `update:currentContent`; drop the manual content-loading scaffolding - `packages/web-pkg/src/editor/composables/useTextEditor.ts` — require `ydoc: Y.Doc` parameter; add `Collaboration` to the extension list; flip `undoRedo: false` (was `history: false` in v2, now lint-warns) on `StarterKit.configure(...)` - `packages/web-pkg/src/editor/composables/strategies/markdown.ts` — expose a `CollaborativeAdapter` companion that uses the existing `editor.getMarkdown()` / `setContent(content, { contentType: 'markdown' })` logic @@ -173,6 +192,7 @@ Wrapper's `scheduleEmit` passes the live editor when one is bound (the editor co **Local-mode safety net:** `CollaborativeWrapper` handles `realtimeUrl: null` → standalone Awareness, immediate hydrate, no provider. text-editor inherits this for free. Single-user mode works without a sidecar. **Verification:** + - Existing text-editor unit tests still pass (the editor's public behaviour is unchanged: it receives content, emits content) - Manual: open .md → toolbar works, formatting buttons apply, slash commands work, content saves, isDirty flips, Ctrl+S works, undo/redo works (via yUndoPlugin now), route-leave modal — all unchanged from user POV - Open same .md in second tab → realtime sync works (new capability) @@ -183,16 +203,19 @@ Wrapper's `scheduleEmit` passes the live editor when one is bound (the editor co ## Phase 5 — Tests in web **Unit:** + - Move `tests/unit/CollaborativeWrapper.spec.ts` to `web/packages/web-pkg/tests/unit/components/CollaborativeWrapper.spec.ts` - Adapt to web-test-helpers' `shallowMount` + `defaultPlugins()` pattern (see `web/packages/design-system/src/components/OcButton/OcButton.spec.ts` as template) - web-pkg's vitest config (`web/tests/unit/config/vitest.config.ts`) already has happy-dom + Vue SFC support — no config additions needed **Integration:** + - Move `realtime-sync.spec.ts` to `web/packages/web-pkg/tests/integration/realtime-sync.spec.ts` (new directory) - Add an integration vitest config or extend the existing one to include this path - Document `DEV_FAKE_TOKEN` setup in the integration test file for future maintainers **E2E (Cucumber, per user clarification):** + - Convert the 9 Playwright specs from `web-extensions/packages/web-app-{codemirror,tiptap}/tests/e2e/*.spec.ts` into Cucumber features under `web/tests/e2e/cucumber/features/` + step definitions under `web/tests/e2e/cucumber/steps/` - Reuse existing helpers from `web/tests/e2e/support/` (login, file upload, navigation) as much as possible — the existing `editor.ts` page object is probably the right starting template - New feature files to create (one per scenario from our existing specs): @@ -248,15 +271,15 @@ Wrapper's `scheduleEmit` passes the live editor when one is bound (the editor co 5. **Cross-app collab room design (revisit).** Phase 4 ships per-app room keys: `documentName = `${appId}::${fileId}``. Different editors opening the same file → different Y.Doc rooms → clean schema/awareness isolation. The alternative we discussed but parked: one Y.Doc per file, per-app Y.XmlFragment field, shared awareness. Storage-cheaper (one SQLite row per file) but awareness leaks across apps (you'd see cursors of users editing the file in a different app, pointing at offsets that don't match your schema). User's read at the time: editing the same file from multiple apps simultaneously is itself a bad idea (race on WebDAV saves regardless of room shape), so per-app room wins. Worth revisiting if a real cross-app collab use case shows up. -4. **Sidecar SQLite persistence — necessary?** The Hocuspocus server currently persists every Y.Doc to SQLite via `@hocuspocus/extension-sqlite`. Worth questioning whether we need it at all: when a room loads after eviction and the persisted etag doesn't match the live file's etag, we rehydrate from `props.currentContent` anyway (the stale-state recovery path), so the persisted CRDT history is throwaway most of the time. Trade-off: persistence buys us late-join performance (a new client joining a "warm" room with a long history gets the diff instead of a full re-hydrate) and a partial offline buffer (writes accepted while the room is in memory but the native file is unavailable). For the rooms-without-files use case in (4) below, persistence would matter more. For file-backed rooms with our hydrate-from-currentContent fallback, persistence is mostly ceremony. Consider running stateless and only enabling SQLite for non-file rooms. +6. **Sidecar SQLite persistence — necessary?** The Hocuspocus server currently persists every Y.Doc to SQLite via `@hocuspocus/extension-sqlite`. Worth questioning whether we need it at all: when a room loads after eviction and the persisted etag doesn't match the live file's etag, we rehydrate from `props.currentContent` anyway (the stale-state recovery path), so the persisted CRDT history is throwaway most of the time. Trade-off: persistence buys us late-join performance (a new client joining a "warm" room with a long history gets the diff instead of a full re-hydrate) and a partial offline buffer (writes accepted while the room is in memory but the native file is unavailable). For the rooms-without-files use case in (4) below, persistence would matter more. For file-backed rooms with our hydrate-from-currentContent fallback, persistence is mostly ceremony. Consider running stateless and only enabling SQLite for non-file rooms. -4. **Rename "realtime".** The current names (`realtimeUrl` prop, `/realtime` Traefik path, `realtimeBaseUrl` derivation) are vague — "realtime" describes a transport property, not the user-facing capability. Better candidates: `/collab` (short, semantic), `/coediting` (descriptive), `/sync` (generic, possibly too generic). Rename touches: wrapper prop name + docs, sidecar Traefik label, env var, apps' applicationConfig key, downstream documentation. Defer to one focused rename PR after the migration settles so we don't bikeshed mid-flight. +7. **Rename "realtime".** The current names (`realtimeUrl` prop, `/realtime` Traefik path, `realtimeBaseUrl` derivation) are vague — "realtime" describes a transport property, not the user-facing capability. Better candidates: `/collab` (short, semantic), `/coediting` (descriptive), `/sync` (generic, possibly too generic). Rename touches: wrapper prop name + docs, sidecar Traefik label, env var, apps' applicationConfig key, downstream documentation. Defer to one focused rename PR after the migration settles so we don't bikeshed mid-flight. -2. **Non-file collaborative rooms.** Right now the wrapper + sidecar assume every `documentName` is an OC file id (`$!`) and runs WebDAV/Graph permission checks against it. There are use cases for a room that has no file backing — ephemeral whiteboard-style collaboration, comment threads, etc. Idea: the sidecar treats `documentName` prefixes as a routing hint. `file_` → run the current ACL/etag probe path. `room_` → no permission checks, no etag, no save loop. Wrapper would need to know about this distinction too (skip save when in room mode). +8. **Non-file collaborative rooms.** Right now the wrapper + sidecar assume every `documentName` is an OC file id (`$!`) and runs WebDAV/Graph permission checks against it. There are use cases for a room that has no file backing — ephemeral whiteboard-style collaboration, comment threads, etc. Idea: the sidecar treats `documentName` prefixes as a routing hint. `file_` → run the current ACL/etag probe path. `room_` → no permission checks, no etag, no save loop. Wrapper would need to know about this distinction too (skip save when in room mode). -2. **File vs folder check in `onAuthenticate`.** Currently the sidecar's `probeFileAccess` runs WebDAV HEAD on `/remote.php/dav/spaces/{itemId}` and accepts any 200 response, including folders. A user could theoretically open a folder id in a collab editor and get an empty room (the editor would try to save back to a folder URL on every save — almost certainly errors, but ugly). Fix: check the Graph `/items/{itemId}` response's `folder` vs `file` discriminator, or `Resource-Type` PROPFIND, or `Content-Type` from a GET. Reject folders in `onAuthenticate`. +9. **File vs folder check in `onAuthenticate`.** Currently the sidecar's `probeFileAccess` runs WebDAV HEAD on `/remote.php/dav/spaces/{itemId}` and accepts any 200 response, including folders. A user could theoretically open a folder id in a collab editor and get an empty room (the editor would try to save back to a folder URL on every save — almost certainly errors, but ugly). Fix: check the Graph `/items/{itemId}` response's `folder` vs `file` discriminator, or `Resource-Type` PROPFIND, or `Content-Type` from a GET. Reject folders in `onAuthenticate`. -3. **Cross-peer `AppWrapper.currentETag` fix.** Already scoped into Phase 4.5 of the main plan — extending AppWrapper with an inject contract for peer-saved etags. Listed here as the cleanup that ties the local-mode + collab-mode etag stories together. +10. **Cross-peer `AppWrapper.currentETag` fix.** Already scoped into Phase 4.5 of the main plan — extending AppWrapper with an inject contract for peer-saved etags. Listed here as the cleanup that ties the local-mode + collab-mode etag stories together. --- diff --git a/dev/docker/hocuspocus/server.js b/dev/docker/hocuspocus/server.js index 92a058453c..816c512d3f 100644 --- a/dev/docker/hocuspocus/server.js +++ b/dev/docker/hocuspocus/server.js @@ -3,7 +3,10 @@ import { SQLite } from '@hocuspocus/extension-sqlite' const port = parseInt(process.env.PORT ?? '1234', 10) const dbPath = process.env.DB_PATH ?? '/var/lib/hocuspocus/state.db' -const opencloudUrl = (process.env.OPENCLOUD_URL ?? 'https://host.docker.internal:9200').replace(/\/$/, '') +const opencloudUrl = (process.env.OPENCLOUD_URL ?? 'https://host.docker.internal:9200').replace( + /\/$/, + '' +) const devFakeToken = process.env.DEV_FAKE_TOKEN ?? '' // Per-document first-seen app version. Acts as the authoritative gate for @@ -140,9 +143,7 @@ const server = new Server({ if (devFakeToken && token === devFakeToken) { const id = 'dev-fake-user' const nativeEtag = requestParameters.get('devEtag') ?? '' - console.log( - `[onAuthenticate] dev-fake document="${documentName}" nativeEtag="${nativeEtag}"` - ) + console.log(`[onAuthenticate] dev-fake document="${documentName}" nativeEtag="${nativeEtag}"`) return { nativeEtag, user: { diff --git a/packages/web-app-text-editor/tests/unit/app.spec.ts b/packages/web-app-text-editor/tests/unit/app.spec.ts index 04e2565c40..cecab92be2 100644 --- a/packages/web-app-text-editor/tests/unit/app.spec.ts +++ b/packages/web-app-text-editor/tests/unit/app.spec.ts @@ -13,7 +13,15 @@ vi.mock('@opencloud-eu/web-pkg', async () => { return { CollaborativeWrapper: defineComponent({ name: 'CollaborativeWrapperStub', - props: ['resource', 'currentContent', 'isReadOnly', 'adapter', 'editor', 'appVersion', 'realtimeUrl'], + props: [ + 'resource', + 'currentContent', + 'isReadOnly', + 'adapter', + 'editor', + 'appVersion', + 'realtimeUrl' + ], setup() { return () => h('div', { class: 'oc-text-editor' }) } @@ -26,8 +34,8 @@ vi.mock('@opencloud-eu/web-pkg/editor', () => { useContentStrategy: () => ({ resolveStrategy: () => ({ editorContentType: () => 'markdown', - extensions: () => [], - editorActionGroups: () => [], + extensions: (): unknown[] => [], + editorActionGroups: (): unknown[] => [], serialize: () => '', deserialize: (s: string) => s }) diff --git a/packages/web-app-tiptap/src/TiptapEditor.vue b/packages/web-app-tiptap/src/TiptapEditor.vue index b53cd0b9a8..ac0b6c3826 100644 --- a/packages/web-app-tiptap/src/TiptapEditor.vue +++ b/packages/web-app-tiptap/src/TiptapEditor.vue @@ -127,7 +127,11 @@ defineExpose({ .tiptap-content :deep(.ProseMirror) { outline: none; min-height: 100%; - font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + sans-serif; font-size: 1rem; line-height: 1.6; color: var(--oc-role-on-surface, #1d1f23); diff --git a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue index ff60523faa..8b57bc7169 100644 --- a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue +++ b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue @@ -449,9 +449,7 @@ const saveFileTask = useTask(function* () { // when the refetch / retry path itself fails. if (e.statusCode === 412 || e.statusCode === 409) { try { - const fresh = yield* call( - getFileContents(currentFileContext, { ...fileContentOptions }) - ) + const fresh = yield* call(getFileContents(currentFileContext, { ...fileContentOptions })) const freshEtag = fresh.headers['OC-ETag'] if (fresh.body === newContent) { diff --git a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue index 7cf6f40ead..b65fb97d37 100644 --- a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue +++ b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue @@ -1,13 +1,5 @@ diff --git a/packages/web-app-excalidraw/src/ExcalidrawEditor.vue b/packages/web-app-excalidraw/src/ExcalidrawEditor.vue new file mode 100644 index 0000000000..2eb2c4ad77 --- /dev/null +++ b/packages/web-app-excalidraw/src/ExcalidrawEditor.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/packages/web-app-excalidraw/src/adapters/excalidrawAdapter.ts b/packages/web-app-excalidraw/src/adapters/excalidrawAdapter.ts new file mode 100644 index 0000000000..93fb803051 --- /dev/null +++ b/packages/web-app-excalidraw/src/adapters/excalidrawAdapter.ts @@ -0,0 +1,108 @@ +import * as Y from 'yjs' +import { yjsToExcalidraw } from 'y-excalidraw' +import { generateKeyBetween } from 'fractional-indexing' +import type { CollaborativeAdapter } from '@opencloud-eu/web-pkg' +import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types' +import type { BinaryFiles } from '@excalidraw/excalidraw/types' + +// Y.Array/Y.Map shapes that match what `y-excalidraw`'s ExcalidrawBinding +// reads and writes. Each Y.Map in the elements array carries: +// { el: ExcalidrawElement, pos: string (fractional-indexing key) } +// Assets is a flat Y.Map keyed by file id with BinaryFileData values. +const ELEMENTS_KEY = 'elements' +const ASSETS_KEY = 'assets' + +interface ExcalidrawWireFormat { + type?: string + version?: number + source?: string + elements?: ExcalidrawElement[] + appState?: Record + files?: BinaryFiles +} + +const APP_NAME = 'opencloud-excalidraw' +const APP_STATE_DEFAULTS: Record = { + viewBackgroundColor: '#ffffff', + gridSize: null +} + +/** + * Seed the Y.Array with elements from a parsed .excalidraw payload. + * We assign each element a fractional-indexing position so later inserts + * between two existing elements can land at a stable spot without + * renumbering. y-excalidraw's helpers expect this exact shape. + */ +function seedElements(yElements: Y.Array>, elements: ExcalidrawElement[]) { + let prevKey: string | null = null + for (const el of elements) { + const pos = generateKeyBetween(prevKey, null) + const yEl = new Y.Map() + yEl.set('el', el) + yEl.set('pos', pos) + yElements.push([yEl]) + prevKey = pos + } +} + +export const excalidrawAdapter: CollaborativeAdapter = { + hydrate(ydoc, content) { + if (!content) return + let parsed: ExcalidrawWireFormat + try { + parsed = JSON.parse(content) as ExcalidrawWireFormat + } catch { + // Empty / unparseable file: nothing to seed, the editor will open + // on a blank canvas. + return + } + const yElements = ydoc.getArray>(ELEMENTS_KEY) + const yAssets = ydoc.getMap(ASSETS_KEY) + // The wrapper's election only lets one client hydrate, so this re-entry + // guard is more belt-and-braces than load-bearing. + if (yElements.length > 0 || yAssets.size > 0) return + ydoc.transact(() => { + if (parsed.elements?.length) { + seedElements(yElements, parsed.elements) + } + if (parsed.files) { + for (const [id, file] of Object.entries(parsed.files)) { + yAssets.set(id, file) + } + } + }, 'hydrate') + }, + + serialize(ydoc) { + const yElements = ydoc.getArray>(ELEMENTS_KEY) + const yAssets = ydoc.getMap(ASSETS_KEY) + const elements = yjsToExcalidraw(yElements) + const files: BinaryFiles = {} + for (const key of yAssets.keys()) { + files[key] = yAssets.get(key) as BinaryFiles[string] + } + const payload: ExcalidrawWireFormat = { + type: 'excalidraw', + version: 2, + source: APP_NAME, + elements, + appState: APP_STATE_DEFAULTS, + files + } + return JSON.stringify(payload) + }, + + hasContent(ydoc) { + return ydoc.getArray(ELEMENTS_KEY).length > 0 + }, + + reset(ydoc) { + const yElements = ydoc.getArray>(ELEMENTS_KEY) + const yAssets = ydoc.getMap(ASSETS_KEY) + if (yElements.length === 0 && yAssets.size === 0) return + ydoc.transact(() => { + yElements.delete(0, yElements.length) + for (const key of Array.from(yAssets.keys())) yAssets.delete(key) + }, 'reset') + } +} diff --git a/packages/web-app-excalidraw/src/index.ts b/packages/web-app-excalidraw/src/index.ts new file mode 100644 index 0000000000..993dfd2427 --- /dev/null +++ b/packages/web-app-excalidraw/src/index.ts @@ -0,0 +1,47 @@ +import { AppWrapperRoute, defineWebApplication } from '@opencloud-eu/web-pkg' +import { useGettext } from 'vue3-gettext' +import App from './App.vue' +import translations from '../l10n/translations.json' + +const applicationId = 'excalidraw' + +export default defineWebApplication({ + setup() { + const { $gettext } = useGettext() + + const routes = [ + { + name: applicationId, + path: '/:driveAliasAndItem(.*)?', + component: AppWrapperRoute(App, { applicationId }), + meta: { + authContext: 'hybrid', + title: $gettext('Excalidraw'), + patchCleanPath: true + } + } + ] + + const appInfo = { + name: $gettext('Excalidraw'), + id: applicationId, + icon: 'pencil-ruler', + defaultExtension: 'excalidraw', + extensions: [ + { + extension: 'excalidraw', + routeName: applicationId, + newFileMenu: { + menuTitle: () => $gettext('Excalidraw whiteboard') + } + } + ] + } + + return { + appInfo, + routes, + translations + } + } +}) diff --git a/packages/web-app-excalidraw/src/react_app/ExcalidrawCanvas.tsx b/packages/web-app-excalidraw/src/react_app/ExcalidrawCanvas.tsx new file mode 100644 index 0000000000..cd1862cc30 --- /dev/null +++ b/packages/web-app-excalidraw/src/react_app/ExcalidrawCanvas.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef, useState } from 'react' +import { Excalidraw } from '@excalidraw/excalidraw' +import { ExcalidrawBinding } from 'y-excalidraw' +import '@excalidraw/excalidraw/index.css' +import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types' +import * as Y from 'yjs' +import type { Awareness } from 'y-protocols/awareness' + +// Excalidraw lazy-loads fonts / locales / lib data at runtime. Without an +// override, it falls back to `https://esm.sh/@excalidraw/excalidraw@…/dist/prod/` +// — which would mean whitelisting esm.sh in OC's CSP. We mirror the +// upstream prod/{fonts,locales,data} tree into our own dist via +// viteStaticCopy (see root vite.config.ts) and point Excalidraw at it. +// +// `new URL(..., import.meta.url)` is the only path that survives an OC +// subpath deployment: import.meta.url at runtime is the actual served +// URL of this chunk (`/js/web-app-excalidraw-XXXX.mjs`), so the +// resolved asset URL becomes `/excalidraw-assets/` regardless of +// where OC is mounted. +const EXCALIDRAW_ASSET_PATH = new URL('../excalidraw-assets/', import.meta.url).href +if (typeof window !== 'undefined') { + const w = window as unknown as { EXCALIDRAW_ASSET_PATH?: string | string[] } + if (!w.EXCALIDRAW_ASSET_PATH) { + w.EXCALIDRAW_ASSET_PATH = EXCALIDRAW_ASSET_PATH + } +} + +interface ExcalidrawCanvasProps { + ydoc: Y.Doc + awareness: Awareness + isReadOnly?: boolean +} + +// Test hook: exposes the live ExcalidrawImperativeAPI on `window` so the +// cucumber suite can read `getSceneElements()` etc. without trying to query +// the canvas DOM (Excalidraw paints to a single `` element, no +// per-element DOM is available for selectors). Cleared on unmount. +declare global { + interface Window { + __excalidrawAPI?: ExcalidrawImperativeAPI + } +} + +export default function ExcalidrawCanvas({ + ydoc, + awareness, + isReadOnly = false +}: ExcalidrawCanvasProps) { + const [api, setApi] = useState(null) + const containerRef = useRef(null) + + useEffect(() => { + if (!api) return + + const yElements = ydoc.getArray>('elements') + const yAssets = ydoc.getMap('assets') + + // y-excalidraw needs the DOM node for its undo/redo button hijacking. + // We pass it only when we also pass an undoManager; without one, the + // binding skips that whole block and the DOM node isn't read. + const undoManager = new Y.UndoManager(yElements, { + // Skip transactions coming from the wrapper (hydrate / reset / + // stale-recovery) — those aren't user actions and shouldn't land in + // the undo stack. + trackedOrigins: new Set([null, undefined]) + }) + + const binding = new ExcalidrawBinding( + yElements, + yAssets, + api, + awareness, + containerRef.current ? { excalidrawDom: containerRef.current, undoManager } : undefined + ) + + window.__excalidrawAPI = api + + return () => { + binding.destroy() + undoManager.destroy() + if (window.__excalidrawAPI === api) delete window.__excalidrawAPI + } + }, [api, ydoc, awareness]) + + // We do NOT pass `initialData` to Excalidraw — the wrapper hydrates the + // Y.Doc first, and the binding's constructor calls + // `api.updateScene({ elements: yjsToExcalidraw(yElements) })` synchronously + // after we register it. Skipping initialData avoids a flash of + // un-collab-synced state and double-rendering. + + return ( +

+ setApi(instance)} + viewModeEnabled={isReadOnly} + onPointerUpdate={(payload: { + pointer: { x: number; y: number; tool: 'pointer' | 'laser' } + button: 'down' | 'up' + }) => { + // The ExcalidrawBinding exposes `onPointerUpdate`, but it can only + // be wired once the `api` ref is set — and the prop is captured + // at render time. Easiest: forward straight to awareness here and + // let the binding's awareness observer pick remote ones up. + awareness.setLocalStateField('pointer', payload.pointer) + awareness.setLocalStateField('button', payload.button) + }} + UIOptions={{ + canvasActions: { + loadScene: false, + saveToActiveFile: false, + export: false, + saveAsImage: !isReadOnly + } + }} + /> +
+ ) +} diff --git a/packages/web-app-excalidraw/tsconfig.json b/packages/web-app-excalidraw/tsconfig.json new file mode 100644 index 0000000000..2431b86fe4 --- /dev/null +++ b/packages/web-app-excalidraw/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c1c5bdfc5..549fbd0c9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@types/qs': specifier: ^6.15.0 version: 6.15.1 + '@vitejs/plugin-react': + specifier: ^6.0.2 + version: 6.0.2(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) '@vitejs/plugin-vue': specifier: 6.0.7 version: 6.0.7(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) @@ -111,6 +114,12 @@ importers: qs: specifier: ^6.15.0 version: 6.15.2 + react: + specifier: ^19.2.6 + version: 19.2.6 + react-dom: + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) tailwindcss: specifier: ^4.2.2 version: 4.3.0 @@ -228,7 +237,7 @@ importers: version: 0.28.0(rollup@4.60.0)(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0)) vitepress: specifier: ^1.6.4 - version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.0)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3) + version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.0)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3) packages/eslint-config: dependencies: @@ -506,6 +515,55 @@ importers: specifier: workspace:* version: link:../web-test-helpers + packages/web-app-excalidraw: + dependencies: + '@excalidraw/excalidraw': + specifier: ^0.18.0 + version: 0.18.1(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@opencloud-eu/design-system': + specifier: workspace:^ + version: link:../design-system + '@opencloud-eu/web-client': + specifier: workspace:* + version: link:../web-client + '@opencloud-eu/web-pkg': + specifier: workspace:* + version: link:../web-pkg + fractional-indexing: + specifier: ^3.2.0 + version: 3.2.0 + react: + specifier: ^19.0.0 + version: 19.2.6 + react-dom: + specifier: ^19.0.0 + version: 19.2.6(react@19.2.6) + veaury: + specifier: ^2.6.0 + version: 2.6.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + vue3-gettext: + specifier: ^4.0.0-beta.1 + version: 4.0.0-beta.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@6.0.3)) + y-excalidraw: + specifier: ^2.0.12 + version: 2.0.12(@excalidraw/excalidraw@0.18.1(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(yjs@13.6.30) + y-protocols: + specifier: ^1.0.7 + version: 1.0.7(yjs@13.6.30) + yjs: + specifier: ^13.6.0 + version: 13.6.30 + devDependencies: + '@opencloud-eu/web-test-helpers': + specifier: workspace:* + version: link:../web-test-helpers + '@types/react': + specifier: ^19.0.0 + version: 19.2.15 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.15) + packages/web-app-external: dependencies: '@opencloud-eu/web-client': @@ -1350,6 +1408,9 @@ packages: resolution: {integrity: sha512-xpwefe4fCOWnZgXCbkGpqQY6jgBSCf2hmgnySbyzZIccrv3SoashHKGPE4x6vVG+gdHrGciMTAcDo9HOZwH22Q==} engines: {node: '>= 14.0.0'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@axe-core/playwright@4.11.3': resolution: {integrity: sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==} peerDependencies: @@ -1389,6 +1450,10 @@ packages: engines: {node: ^22.18.0 || >=24.11.0} hasBin: true + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -1401,6 +1466,12 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@braintree/sanitize-url@6.0.2': + resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@buttercup/fetch@0.2.1': resolution: {integrity: sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==} @@ -1413,6 +1484,24 @@ packages: '@casl/ability': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0 vue: ^3.0.0 + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@codemirror/autocomplete@6.20.2': resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} @@ -1747,12 +1836,37 @@ packages: resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@excalidraw/excalidraw@0.18.1': + resolution: {integrity: sha512-6i5Gt7IDTOH//qa0Z315Ly5iVRhjWpu2whrlQFqkuwrkKUWgRsMk0P5qdE7bpyDpai7jeLeWYkyj1eVAfni1lw==} + peerDependencies: + react: ^17.0.2 || ^18.2.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.2.0 || ^19.0.0 + + '@excalidraw/laser-pointer@1.3.1': + resolution: {integrity: sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==} + + '@excalidraw/markdown-to-text@0.1.2': + resolution: {integrity: sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==} + + '@excalidraw/mermaid-to-excalidraw@2.2.2': + resolution: {integrity: sha512-5VKQq5CdRocC82vOIUpQ5ufJOVV9FpBTdHGA+ULqazeIVV+cr299877omQCibsdS3Bpitz2fsnTwnIXEmLVDSg==} + + '@excalidraw/random-username@1.1.0': + resolution: {integrity: sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==} + engines: {node: '>=10'} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} '@floating-ui/dom@1.7.6': resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} @@ -1787,6 +1901,9 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1841,6 +1958,12 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + '@microsoft/fetch-event-source@2.0.1': resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} @@ -2017,6 +2140,288 @@ packages: engines: {node: '>=18'} hasBin: true + '@radix-ui/primitive@1.0.0': + resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} + + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/react-arrow@1.1.2': + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.0.1': + resolution: {integrity: sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-compose-refs@1.0.0': + resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.0.0': + resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.0.0': + resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.2': + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.0.0': + resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.6': + resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.2': + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.4': + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.0.0': + resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@1.0.1': + resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-primitive@2.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.0.2': + resolution: {integrity: sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-slot@1.0.1': + resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.0.2': + resolution: {integrity: sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-callback-ref@1.0.0': + resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.0.0': + resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.0.0': + resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@rolldown/binding-android-arm64@1.0.1': resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2668,6 +3073,99 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2677,6 +3175,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -2726,6 +3227,14 @@ packages: '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -2846,12 +3355,28 @@ packages: peerDependencies: '@uppy/core': ^5.2.0 + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vitejs/plugin-basic-ssl@2.3.0': resolution: {integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3132,6 +3657,10 @@ packages: resolution: {integrity: sha512-dgojXfc4SiqmNwe38PnbT3zJasrz7g62dLAPD+VFT5RJb8W7LGRqw2IFd2ES+plnhsp4HYNJmFqMU1tCThdCww==} engines: {node: '>=14.0.0'} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + array-back@6.2.3: resolution: {integrity: sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==} engines: {node: '>=12.17'} @@ -3230,6 +3759,9 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + browser-fs-access@0.29.1: + resolution: {integrity: sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==} + browser-resolve@2.0.0: resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} @@ -3305,6 +3837,9 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + canvas-roundrect-polyfill@0.0.1: + resolution: {integrity: sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -3332,6 +3867,14 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -3359,6 +3902,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@1.1.1: + resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==} + engines: {node: '>=6'} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -3407,6 +3954,14 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -3435,6 +3990,16 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + crc-32@0.3.0: + resolution: {integrity: sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==} + engines: {node: '>=0.8'} + create-ecdh@4.0.4: resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} @@ -3457,6 +4022,11 @@ packages: cropperjs@2.1.1: resolution: {integrity: sha512-FDJMarkY+/SepYarPZsvkG2LmI2PElecciMFnvBiBIoKnFYua/scprC5qejCLLyuX2jEqJRS2njbAsHxfjtIXA==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} @@ -3482,18 +4052,177 @@ packages: custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 - dateformat@4.6.3: - resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} + engines: {node: '>=0.10'} - debug@4.4.3: + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: @@ -3523,6 +4252,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -3538,6 +4270,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3632,6 +4367,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + es5-ext@0.10.64: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} @@ -3639,6 +4377,10 @@ packages: es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + es6-promise-pool@2.5.0: + resolution: {integrity: sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==} + engines: {node: '>=0.10.0'} + es6-symbol@3.1.4: resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} engines: {node: '>=0.12'} @@ -3885,6 +4627,10 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + fractional-indexing@3.2.0: + resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==} + engines: {node: ^14.13.1 || >=16.0.0} + franc-min@6.2.0: resolution: {integrity: sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==} @@ -3905,6 +4651,10 @@ packages: resolution: {integrity: sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==} engines: {node: '>=10'} + fuzzy@0.1.3: + resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==} + engines: {node: '>= 0.6.0'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -3913,6 +4663,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -3952,6 +4706,9 @@ packages: resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} engines: {node: '>=0.10.0'} + glur@1.1.2: + resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3959,6 +4716,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + happy-dom@20.9.0: resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} @@ -4043,6 +4803,10 @@ packages: ical.js@2.2.1: resolution: {integrity: sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4054,12 +4818,21 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + image-blob-reduce@3.0.1: + resolution: {integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==} + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immutable@4.3.8: + resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==} + immutable@5.1.5: resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -4082,6 +4855,13 @@ packages: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -4201,6 +4981,24 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jotai-scope@0.7.2: + resolution: {integrity: sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==} + peerDependencies: + jotai: '>=2.9.2' + react: '>=17.0.0' + + jotai@2.11.0: + resolution: {integrity: sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -4253,15 +5051,32 @@ packages: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + knuth-shuffle-seeded@1.0.6: resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==} + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + layerr@3.0.0: resolution: {integrity: sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==} + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -4374,6 +5189,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} @@ -4401,6 +5219,9 @@ packages: lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -4463,6 +5284,11 @@ packages: markdown-it-container@4.0.0: resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==} + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + marked@17.0.6: resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} engines: {node: '>= 20'} @@ -4488,6 +5314,9 @@ packages: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -4569,6 +5398,9 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multimath@2.0.0: + resolution: {integrity: sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -4583,6 +5415,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + nanoid@5.1.7: resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} engines: {node: ^18 || >=20} @@ -4693,6 +5535,9 @@ packages: oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + open-color@1.9.1: + resolution: {integrity: sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4730,6 +5575,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pad-right@0.2.2: resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==} engines: {node: '>=0.10.0'} @@ -4737,6 +5585,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.0.3: + resolution: {integrity: sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==} + parse-asn1@5.1.9: resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} engines: {node: '>= 0.10'} @@ -4755,6 +5606,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4797,6 +5651,12 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + perfect-freehand@1.2.0: + resolution: {integrity: sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==} + + pica@7.1.1: + resolution: {integrity: sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4851,9 +5711,27 @@ packages: engines: {node: '>=18'} hasBin: true + png-chunk-text@1.0.0: + resolution: {integrity: sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==} + + png-chunks-encode@1.0.0: + resolution: {integrity: sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==} + + png-chunks-extract@1.0.0: + resolution: {integrity: sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==} + pofile@1.1.4: resolution: {integrity: sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g==} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-curve@1.0.1: + resolution: {integrity: sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4957,6 +5835,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pwacompat@2.0.17: + resolution: {integrity: sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==} + qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -4980,6 +5861,45 @@ packages: randomfill@1.0.4: resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + read-installed-packages@2.0.1: resolution: {integrity: sha512-t+fJOFOYaZIjBpTVxiV8Mkt7yQyy4E6MSrrnt5FmPd4enYvpU/9DYGirDmN1XQwkfeuWIhM/iu0t2rm6iSr0CA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -5074,6 +5994,9 @@ packages: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown@1.0.1: resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5087,6 +6010,15 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + roughjs@4.6.4: + resolution: {integrity: sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==} + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -5101,11 +6033,22 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.51.0: + resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==} + engines: {node: '>=12.0.0'} + hasBin: true + sass@1.99.0: resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} engines: {node: '>=14.0.0'} hasBin: true + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -5177,6 +6120,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sliced@1.0.1: + resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==} + deprecated: Unsupported + slide@1.1.6: resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} @@ -5283,6 +6230,9 @@ packages: style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} @@ -5408,6 +6358,9 @@ packages: tty-browserify@0.0.1: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + tus-js-client@4.3.1: resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} engines: {node: '>=18'} @@ -5515,6 +6468,31 @@ packages: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-arity@1.1.0: resolution: {integrity: sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==} @@ -5534,6 +6512,12 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + veaury@2.6.3: + resolution: {integrity: sha512-hb4R1iAjaN0wlNdPJefF2E7z4JCXKzipaFIuWUarFism6OSnHdk74fMsj7uRe38/iSNGPiHPy0+x6H25uLUR4g==} + peerDependencies: + react: '>= 16.4.0' + react-dom: '>= 16.4.0' + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -5687,6 +6671,26 @@ packages: vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -5771,6 +6775,9 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webworkify@1.5.0: + resolution: {integrity: sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -5861,6 +6868,12 @@ packages: '@codemirror/view': ^6.0.0 yjs: ^13.5.6 + y-excalidraw@2.0.12: + resolution: {integrity: sha512-/dp0MUSD7WC4TFXsv9DyXxeg+CQoSM4iwh9UpLx8+VFwAg47F3O9KW62C/Jkuq7Rt3y9MAmKC2rE8beZOHCGaw==} + peerDependencies: + '@excalidraw/excalidraw': ^0.17.6 + yjs: ^13.6.19 + y-protocols@1.0.7: resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -5890,6 +6903,21 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -6013,6 +7041,11 @@ snapshots: dependencies: '@algolia/client-common': 5.50.0 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.1.2 + '@axe-core/playwright@4.11.3(playwright-core@1.60.0)': dependencies: axe-core: 4.11.4 @@ -6049,6 +7082,8 @@ snapshots: dependencies: '@babel/types': 8.0.0-rc.5 + '@babel/runtime@7.29.2': {} + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -6061,6 +7096,10 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@braintree/sanitize-url@6.0.2': {} + + '@braintree/sanitize-url@7.1.2': {} + '@buttercup/fetch@0.2.1': optionalDependencies: node-fetch: 3.3.2 @@ -6074,6 +7113,25 @@ snapshots: '@casl/ability': 6.8.1 vue: 3.5.34(typescript@6.0.3) + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/types@11.1.2': {} + + '@chevrotain/utils@11.0.3': {} + '@codemirror/autocomplete@6.20.2': dependencies: '@codemirror/language': 6.12.3 @@ -6335,9 +7393,9 @@ snapshots: '@docsearch/css@3.8.2': {} - '@docsearch/js@3.8.2(@algolia/client-search@5.50.0)(search-insights@2.17.3)': + '@docsearch/js@3.8.2(@algolia/client-search@5.50.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(search-insights@2.17.3)': dependencies: - '@docsearch/react': 3.8.2(@algolia/client-search@5.50.0)(search-insights@2.17.3) + '@docsearch/react': 3.8.2(@algolia/client-search@5.50.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(search-insights@2.17.3) preact: 10.29.0 transitivePeerDependencies: - '@algolia/client-search' @@ -6346,13 +7404,15 @@ snapshots: - react-dom - search-insights - '@docsearch/react@3.8.2(@algolia/client-search@5.50.0)(search-insights@2.17.3)': + '@docsearch/react@3.8.2(@algolia/client-search@5.50.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.50.0)(algoliasearch@5.50.0)(search-insights@2.17.3) '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.50.0)(algoliasearch@5.50.0) '@docsearch/css': 3.8.2 algoliasearch: 5.50.0 optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' @@ -6474,6 +7534,59 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@excalidraw/excalidraw@0.18.1(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@braintree/sanitize-url': 6.0.2 + '@excalidraw/laser-pointer': 1.3.1 + '@excalidraw/mermaid-to-excalidraw': 2.2.2 + '@excalidraw/random-username': 1.1.0 + '@radix-ui/react-popover': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-tabs': 1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + browser-fs-access: 0.29.1 + canvas-roundrect-polyfill: 0.0.1 + clsx: 1.1.1 + cross-env: 7.0.3 + es6-promise-pool: 2.5.0 + fractional-indexing: 3.2.0 + fuzzy: 0.1.3 + image-blob-reduce: 3.0.1 + jotai: 2.11.0(@types/react@19.2.15)(react@19.2.6) + jotai-scope: 0.7.2(jotai@2.11.0(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + lodash.debounce: 4.0.8 + lodash.throttle: 4.1.1 + nanoid: 3.3.3 + open-color: 1.9.1 + pako: 2.0.3 + perfect-freehand: 1.2.0 + pica: 7.1.1 + png-chunk-text: 1.0.0 + png-chunks-encode: 1.0.0 + png-chunks-extract: 1.0.0 + points-on-curve: 1.0.1 + pwacompat: 2.0.17 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + roughjs: 4.6.4 + sass: 1.51.0 + tunnel-rat: 0.1.2(@types/react@19.2.15)(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - immer + + '@excalidraw/laser-pointer@1.3.1': {} + + '@excalidraw/markdown-to-text@0.1.2': {} + + '@excalidraw/mermaid-to-excalidraw@2.2.2': + dependencies: + '@excalidraw/markdown-to-text': 0.1.2 + '@mermaid-js/parser': 0.6.3 + mermaid: 11.15.0 + nanoid: 4.0.2 + + '@excalidraw/random-username@1.1.0': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -6483,6 +7596,12 @@ snapshots: '@floating-ui/core': 1.7.5 '@floating-ui/utils': 0.2.11 + '@floating-ui/react-dom@2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + '@floating-ui/utils@0.2.11': {} '@hocuspocus/common@4.0.0': @@ -6518,6 +7637,12 @@ snapshots: '@iconify/types@2.0.0': {} + '@iconify/utils@3.1.3': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -6590,6 +7715,14 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + + '@mermaid-js/parser@1.1.1': + dependencies: + '@chevrotain/types': 11.1.2 + '@microsoft/fetch-event-source@2.0.1': {} '@module-federation/dts-plugin@2.4.0(node-fetch@3.3.2)(typescript@6.0.3)(vue-tsc@3.3.1(typescript@6.0.3))': @@ -6772,6 +7905,286 @@ snapshots: dependencies: playwright: 1.60.0 + '@radix-ui/primitive@1.0.0': + dependencies: + '@babel/runtime': 7.29.2 + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-collection@1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-compose-refs': 1.0.0(react@19.2.6) + '@radix-ui/react-context': 1.0.0(react@19.2.6) + '@radix-ui/react-primitive': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.0.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-compose-refs@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.6 + + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-context@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.6 + + '@radix-ui/react-context@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-direction@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.6 + + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-id@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-use-layout-effect': 1.0.0(react@19.2.6) + react: 19.2.6 + + '@radix-ui/react-id@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-popover@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.2.15)(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-popper@1.2.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-arrow': 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/rect': 1.1.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-portal@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-presence@1.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-compose-refs': 1.0.0(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.0.0(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-primitive@1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-slot': 1.0.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-roving-focus@1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-collection': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.0.0(react@19.2.6) + '@radix-ui/react-context': 1.0.0(react@19.2.6) + '@radix-ui/react-direction': 1.0.0(react@19.2.6) + '@radix-ui/react-id': 1.0.0(react@19.2.6) + '@radix-ui/react-primitive': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.0.0(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.0.0(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-slot@1.0.1(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-compose-refs': 1.0.0(react@19.2.6) + react: 19.2.6 + + '@radix-ui/react-slot@1.1.2(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-tabs@1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-context': 1.0.0(react@19.2.6) + '@radix-ui/react-direction': 1.0.0(react@19.2.6) + '@radix-ui/react-id': 1.0.0(react@19.2.6) + '@radix-ui/react-presence': 1.0.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 1.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.0.0(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-use-callback-ref@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.6 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-use-controllable-state@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-use-callback-ref': 1.0.0(react@19.2.6) + react: 19.2.6 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-use-layout-effect@1.0.0(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.6 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-use-rect@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/react-use-size@1.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 + + '@radix-ui/rect@1.1.0': {} + '@rolldown/binding-android-arm64@1.0.1': optional: true @@ -7259,49 +8672,166 @@ snapshots: '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) '@tiptap/pm': 3.23.4 - '@tiptap/suggestion@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + '@tiptap/suggestion@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/vue-3@3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(vue@3.5.34(typescript@6.0.3))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + vue: 3.5.34(typescript@6.0.3) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-floating-menu': 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)': + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + y-protocols: 1.0.7(yjs@13.6.30) + yjs: 13.6.30 + + '@transloadit/prettier-bytes@0.3.5': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': dependencies: - '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/pm': 3.23.4 + '@types/geojson': 7946.0.16 - '@tiptap/vue-3@3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(vue@3.5.34(typescript@6.0.3))': + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) - '@tiptap/pm': 3.23.4 - vue: 3.5.34(typescript@6.0.3) - optionalDependencies: - '@tiptap/extension-bubble-menu': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) - '@tiptap/extension-floating-menu': 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@types/d3-color': 3.1.3 - '@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.6)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)': + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': dependencies: - lib0: 0.2.117 - prosemirror-model: 1.25.6 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 - y-protocols: 1.0.7(yjs@13.6.30) - yjs: 13.6.30 + '@types/d3-time': 3.0.4 - '@transloadit/prettier-bytes@0.3.5': {} + '@types/d3-selection@3.0.11': {} - '@tsconfig/node10@1.0.12': {} + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 - '@tsconfig/node12@1.0.11': {} + '@types/d3-time-format@4.0.3': {} - '@tsconfig/node14@1.0.3': {} + '@types/d3-time@3.0.4': {} - '@tsconfig/node16@1.0.4': {} + '@types/d3-timer@3.0.2': {} - '@tybys/wasm-util@0.10.2': + '@types/d3-transition@3.0.9': dependencies: - tslib: 2.8.1 - optional: true + '@types/d3-selection': 3.0.11 - '@types/chai@5.2.3': + '@types/d3-zoom@3.0.8': dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 '@types/deep-eql@4.0.2': {} @@ -7309,6 +8839,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -7356,6 +8888,14 @@ snapshots: '@types/qs@6.15.1': {} + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + '@types/retry@0.12.2': {} '@types/semver@7.7.1': {} @@ -7520,10 +9060,20 @@ snapshots: '@uppy/core': 5.2.0 '@uppy/utils': 7.2.0 + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + '@vitejs/plugin-basic-ssl@2.3.0(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': dependencies: vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.13(@types/node@25.9.0)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0))(vue@3.5.34(typescript@6.0.3))': dependencies: vite: 5.4.21(@types/node@25.9.0)(lightningcss@1.32.0)(sass@1.99.0) @@ -7839,6 +9389,10 @@ snapshots: argue-cli@2.1.0: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + array-back@6.2.3: {} array-find-index@1.0.2: {} @@ -7935,6 +9489,8 @@ snapshots: brorand@1.1.0: {} + browser-fs-access@0.29.1: {} + browser-resolve@2.0.0: dependencies: resolve: 1.22.11 @@ -8039,6 +9595,8 @@ snapshots: caniuse-lite@1.0.30001793: {} + canvas-roundrect-polyfill@0.0.1: {} + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -8062,6 +9620,20 @@ snapshots: charenc@0.0.2: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.18.1 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -8099,6 +9671,8 @@ snapshots: clone@1.0.4: optional: true + clsx@1.1.1: {} + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -8135,6 +9709,10 @@ snapshots: commander@14.0.3: {} + commander@7.2.0: {} + + commander@8.3.0: {} + confbox@0.1.8: {} confbox@0.2.4: {} @@ -8158,6 +9736,16 @@ snapshots: core-util-is@1.0.3: {} + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + crc-32@0.3.0: {} + create-ecdh@4.0.4: dependencies: bn.js: 4.12.3 @@ -8193,6 +9781,10 @@ snapshots: '@cropper/elements': 2.1.1 '@cropper/utils': 2.1.1 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 @@ -8228,15 +9820,201 @@ snapshots: custom-error-instance@2.1.1: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.4 + + cytoscape-fcose@2.2.0(cytoscape@3.33.4): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.4 + + cytoscape@3.33.4: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + d@1.0.2: dependencies: es5-ext: 0.10.64 type: 2.7.3 + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + data-uri-to-buffer@4.0.1: {} dateformat@4.6.3: {} + dayjs@1.11.20: {} + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -8266,6 +10044,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} dequal@2.0.3: {} @@ -8277,6 +10059,8 @@ snapshots: detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -8384,6 +10168,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.3 + es-toolkit@1.46.1: {} + es5-ext@0.10.64: dependencies: es6-iterator: 2.0.3 @@ -8397,6 +10183,8 @@ snapshots: es5-ext: 0.10.64 es6-symbol: 3.1.4 + es6-promise-pool@2.5.0: {} + es6-symbol@3.1.4: dependencies: d: 1.0.2 @@ -8664,6 +10452,8 @@ snapshots: dependencies: fetch-blob: 3.2.0 + fractional-indexing@3.2.0: {} + franc-min@6.2.0: dependencies: trigram-utils: 2.0.1 @@ -8678,6 +10468,8 @@ snapshots: fuse.js@7.3.0: {} + fuzzy@0.1.3: {} + generator-function@2.0.1: {} get-intrinsic@1.3.0: @@ -8693,6 +10485,8 @@ snapshots: hasown: 2.0.3 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -8748,10 +10542,14 @@ snapshots: is-windows: 1.0.2 which: 1.3.1 + glur@1.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} + hachure-fill@0.5.2: {} + happy-dom@20.9.0: dependencies: '@types/node': 25.9.0 @@ -8858,16 +10656,28 @@ snapshots: ical.js@2.2.1: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} ignore@7.0.5: {} + image-blob-reduce@3.0.1: + dependencies: + pica: 7.1.1 + immediate@3.0.6: {} + immutable@4.3.8: {} + immutable@5.1.5: {} + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -8880,6 +10690,10 @@ snapshots: ini@2.0.0: {} + internmap@1.0.1: {} + + internmap@2.0.3: {} + is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -8985,6 +10799,16 @@ snapshots: jiti@2.7.0: {} + jotai-scope@0.7.2(jotai@2.11.0(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): + dependencies: + jotai: 2.11.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + + jotai@2.11.0(@types/react@19.2.15)(react@19.2.6): + optionalDependencies: + '@types/react': 19.2.15 + react: 19.2.6 + joycon@3.1.1: {} js-base64@3.7.8: {} @@ -9024,16 +10848,34 @@ snapshots: jwt-decode@4.0.0: {} + katex@0.16.47: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + knuth-shuffle-seeded@1.0.6: dependencies: seed-random: 2.2.0 + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + layerr@3.0.0: {} + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -9134,6 +10976,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash-es@4.18.1: {} lodash._baseiteratee@4.7.0: @@ -9159,6 +11003,8 @@ snapshots: lodash.clonedeep@4.5.0: {} + lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -9212,6 +11058,8 @@ snapshots: markdown-it-container@4.0.0: {} + marked@16.4.2: {} + marked@17.0.6: {} marks-pane@1.0.9: {} @@ -9244,6 +11092,30 @@ snapshots: meow@13.2.0: {} + mermaid@11.15.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.3 + '@mermaid-js/parser': 1.1.1 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + cytoscape-fcose: 2.2.0(cytoscape@3.33.4) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.4.5 + es-toolkit: 1.46.1 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.2.0 + uuid: 14.0.0 + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 @@ -9313,6 +11185,11 @@ snapshots: muggle-string@0.4.1: {} + multimath@2.0.0: + dependencies: + glur: 1.1.2 + object-assign: 4.1.1 + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -9325,6 +11202,10 @@ snapshots: nanoid@3.3.12: {} + nanoid@3.3.3: {} + + nanoid@4.0.2: {} + nanoid@5.1.7: {} natural-compare@1.4.0: {} @@ -9454,6 +11335,8 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + open-color@1.9.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9492,12 +11375,16 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.6.0: {} + pad-right@0.2.2: dependencies: repeat-string: 1.6.1 pako@1.0.11: {} + pako@2.0.3: {} + parse-asn1@5.1.9: dependencies: asn1.js: 4.10.1 @@ -9518,6 +11405,8 @@ snapshots: path-browserify@1.0.1: {} + path-data-parser@0.1.0: {} + path-exists@4.0.0: {} path-expression-matcher@1.5.0: {} @@ -9555,6 +11444,16 @@ snapshots: perfect-debounce@2.1.0: {} + perfect-freehand@1.2.0: {} + + pica@7.1.1: + dependencies: + glur: 1.1.2 + inherits: 2.0.4 + multimath: 2.0.0 + object-assign: 4.1.1 + webworkify: 1.5.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -9628,8 +11527,28 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + png-chunk-text@1.0.0: {} + + png-chunks-encode@1.0.0: + dependencies: + crc-32: 0.3.0 + sliced: 1.0.1 + + png-chunks-extract@1.0.0: + dependencies: + crc-32: 0.3.0 + pofile@1.1.4: {} + points-on-curve@0.2.0: {} + + points-on-curve@1.0.1: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + possible-typed-array-names@1.1.0: {} postcss-selector-parser@7.1.1: @@ -9758,6 +11677,8 @@ snapshots: punycode@2.3.1: {} + pwacompat@2.0.17: {} + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -9779,6 +11700,40 @@ snapshots: randombytes: 2.1.0 safe-buffer: 5.2.1 + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.15)(react@19.2.6): + dependencies: + react: 19.2.6 + react-style-singleton: 2.2.3(@types/react@19.2.15)(react@19.2.6) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + react-remove-scroll@2.7.2(@types/react@19.2.15)(react@19.2.6): + dependencies: + react: 19.2.6 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.15)(react@19.2.6) + react-style-singleton: 2.2.3(@types/react@19.2.15)(react@19.2.6) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.15)(react@19.2.6) + use-sidecar: 1.1.3(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + + react-style-singleton@2.2.3(@types/react@19.2.15)(react@19.2.6): + dependencies: + get-nonce: 1.0.1 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + react@19.2.6: {} + read-installed-packages@2.0.1: dependencies: '@npmcli/fs': 3.1.1 @@ -9888,6 +11843,8 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 + robust-predicates@3.0.3: {} + rolldown@1.0.1: dependencies: '@oxc-project/types': 0.130.0 @@ -9942,6 +11899,22 @@ snapshots: rope-sequence@1.3.4: {} + roughjs@4.6.4: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -9954,6 +11927,14 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + sass@1.51.0: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.8 + source-map-js: 1.2.1 + sass@1.99.0: dependencies: chokidar: 4.0.3 @@ -9962,6 +11943,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.6 + scheduler@0.27.0: {} + scule@1.3.0: {} search-insights@2.17.3: {} @@ -10042,6 +12025,8 @@ snapshots: signal-exit@4.1.0: {} + sliced@1.0.1: {} + slide@1.1.6: {} sonic-boom@4.2.1: @@ -10152,6 +12137,8 @@ snapshots: style-mod@4.1.3: {} + stylis@4.4.0: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -10258,6 +12245,14 @@ snapshots: tty-browserify@0.0.1: {} + tunnel-rat@0.1.2(@types/react@19.2.15)(react@19.2.6): + dependencies: + zustand: 4.5.7(@types/react@19.2.15)(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - immer + - react + tus-js-client@4.3.1: dependencies: buffer-from: 1.1.2 @@ -10373,6 +12368,25 @@ snapshots: punycode: 1.4.1 qs: 6.15.2 + use-callback-ref@1.3.3(@types/react@19.2.15)(react@19.2.6): + dependencies: + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + use-sidecar@1.1.3(@types/react@19.2.15)(react@19.2.6): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + util-arity@1.1.0: {} util-deprecate@1.0.2: {} @@ -10394,6 +12408,11 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + veaury@2.6.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -10445,10 +12464,10 @@ snapshots: sass: 1.99.0 yaml: 2.9.0 - vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.0)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3): + vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@25.9.0)(axios@1.16.1)(fuse.js@7.3.0)(jwt-decode@4.0.0)(lightningcss@1.32.0)(postcss@8.5.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.99.0)(search-insights@2.17.3)(typescript@6.0.3): dependencies: '@docsearch/css': 3.8.2 - '@docsearch/js': 3.8.2(@algolia/client-search@5.50.0)(search-insights@2.17.3) + '@docsearch/js': 3.8.2(@algolia/client-search@5.50.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(search-insights@2.17.3) '@iconify-json/simple-icons': 1.2.75 '@shikijs/core': 2.5.0 '@shikijs/transformers': 2.5.0 @@ -10531,6 +12550,23 @@ snapshots: vm-browserify@1.1.2: {} + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + vscode-uri@3.1.0: {} vue-component-type-helpers@3.2.7: {} @@ -10642,6 +12678,8 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webworkify@1.5.0: {} + whatwg-mimetype@3.0.0: {} whatwg-url@5.0.0: @@ -10709,6 +12747,12 @@ snapshots: lib0: 0.2.117 yjs: 13.6.30 + y-excalidraw@2.0.12(@excalidraw/excalidraw@0.18.1(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(yjs@13.6.30): + dependencies: + '@excalidraw/excalidraw': 0.18.1(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + fractional-indexing: 3.2.0 + yjs: 13.6.30 + y-protocols@1.0.7(yjs@13.6.30): dependencies: lib0: 0.2.117 @@ -10733,4 +12777,11 @@ snapshots: zod@4.4.3: {} + zustand@4.5.7(@types/react@19.2.15)(react@19.2.6): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + react: 19.2.6 + zwitch@2.0.4: {} diff --git a/tests/e2e/cucumber/features/collaboration/excalidraw-multi-user.feature b/tests/e2e/cucumber/features/collaboration/excalidraw-multi-user.feature new file mode 100644 index 0000000000..593f17aa69 --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/excalidraw-multi-user.feature @@ -0,0 +1,39 @@ +Feature: Multi-user collaboration on an Excalidraw whiteboard + As two users editing the same whiteboard + I want the scene to stay in sync across tabs via the y-excalidraw binding + So that we can collaborate without bumping into each other's edits + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + | Brian | + + + Scenario: a shape that Alice creates appears in Brian's scene + # Pre-seeded with one rectangle so we can assert the file content+ + # binding wiring on initial load, then watch for the count to grow + # once Alice draws another shape. Alice's drawing simulated via the + # imperative API (see step) rather than actually clicking and dragging + # on the canvas — canvas pointer interaction in Playwright is brittle. + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | shared.excalidraw | {"type":"excalidraw","version":2,"source":"test","elements":[{"id":"r1","type":"rectangle","x":100,"y":100,"width":200,"height":100,"angle":0,"strokeColor":"#000","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":1,"version":1,"versionNonce":1,"isDeleted":false,"boundElements":null,"updated":1,"link":null,"locked":false}],"appState":{},"files":{}} | + And "Alice" shares the following resource using API + | resource | recipient | type | role | + | shared.excalidraw | Brian | user | Can edit | + When "Alice" logs in + And "Alice" opens file "shared.excalidraw" via "excalidraw" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see the excalidraw canvas mounted + And "Alice" should see at least 1 element in the excalidraw scene + And "Brian" logs in + And "Brian" navigates to the shared with me page + And "Brian" opens file "shared.excalidraw" via "excalidraw" using the context menu + Then "Brian" should see the realtime collab status "connected" + And "Brian" should see the excalidraw canvas mounted + And "Brian" should see at least 1 element in the excalidraw scene + When "Alice" adds a rectangle to the excalidraw scene via the API + Then "Brian" should see at least 2 elements in the excalidraw scene + And "Alice" logs out + And "Brian" logs out diff --git a/tests/e2e/cucumber/features/collaboration/excalidraw-open.feature b/tests/e2e/cucumber/features/collaboration/excalidraw-open.feature new file mode 100644 index 0000000000..67d6b5e0c2 --- /dev/null +++ b/tests/e2e/cucumber/features/collaboration/excalidraw-open.feature @@ -0,0 +1,33 @@ +Feature: Open an Excalidraw whiteboard + As a user with the collaborative Excalidraw editor available + I want to open .excalidraw files + So that I can draw and collaborate on a shared canvas + + Background: + Given "Admin" creates following user using API + | id | + | Alice | + + + Scenario: open an empty .excalidraw, canvas mounts, realtime connects + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | blank.excalidraw | {"type":"excalidraw","version":2,"source":"test","elements":[],"appState":{},"files":{}} | + When "Alice" logs in + And "Alice" opens file "blank.excalidraw" via "excalidraw" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see the excalidraw canvas mounted + And "Alice" should see 0 elements in the excalidraw scene + And "Alice" logs out + + + Scenario: open an .excalidraw with pre-seeded shapes, the scene hydrates from the file + And "Alice" creates the following files into personal space using API + | pathToFile | content | + | preseeded.excalidraw | {"type":"excalidraw","version":2,"source":"test","elements":[{"id":"r1","type":"rectangle","x":100,"y":100,"width":200,"height":100,"angle":0,"strokeColor":"#000","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":1,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"roundness":null,"seed":1,"version":1,"versionNonce":1,"isDeleted":false,"boundElements":null,"updated":1,"link":null,"locked":false}],"appState":{},"files":{}} | + When "Alice" logs in + And "Alice" opens file "preseeded.excalidraw" via "excalidraw" using the context menu + Then "Alice" should see the realtime collab status "connected" + And "Alice" should see the excalidraw canvas mounted + And "Alice" should see at least 1 element in the excalidraw scene + And "Alice" logs out diff --git a/tests/e2e/cucumber/steps/ui/collaboration.ts b/tests/e2e/cucumber/steps/ui/collaboration.ts index 1555e023a5..74cdb04611 100644 --- a/tests/e2e/cucumber/steps/ui/collaboration.ts +++ b/tests/e2e/cucumber/steps/ui/collaboration.ts @@ -7,6 +7,8 @@ import { codemirrorLine, remoteCaretCount, remoteCaretLabelText, + excalidrawSceneElementCount, + awaitExcalidrawMounted, type CollabEditor } from '../../../support/objects/app-files/utils/collab' import { api } from '../../../support' @@ -113,6 +115,99 @@ Then( } ) +Then( + /^"([^"]+)" should see the excalidraw canvas mounted$/, + async function (this: World, stepUser: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + await awaitExcalidrawMounted(page) + } +) + +Then( + /^"([^"]+)" should see (\d+) elements? in the excalidraw scene$/, + async function (this: World, stepUser: string, count: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + const expected = parseInt(count, 10) + // Excalidraw hydrates from Y.Doc shortly after the canvas mounts — + // poll for the count rather than read once. -1 means the test hook + // hasn't been registered yet (component not mounted), which we treat + // as "not yet". + await expect + .poll(async () => await excalidrawSceneElementCount(page), { + timeout: 10_000 + }) + .toBe(expected) + } +) + +Then( + /^"([^"]+)" should see at least (\d+) elements? in the excalidraw scene$/, + async function (this: World, stepUser: string, count: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + const minimum = parseInt(count, 10) + await expect + .poll(async () => await excalidrawSceneElementCount(page), { + timeout: 10_000 + }) + .toBeGreaterThanOrEqual(minimum) + } +) + +When( + /^"([^"]+)" adds a rectangle to the excalidraw scene via the API$/, + async function (this: World, stepUser: string): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + // Drawing on the canvas via Playwright pointer events is fragile — + // Excalidraw's renderer uses requestAnimationFrame + pointer-capture + // semantics that don't always match Playwright's synthetic events. + // The test-only `window.__excalidrawAPI` hook lets us inject elements + // directly through Excalidraw's own imperative API, which is exactly + // what user drawing would land at. The y-excalidraw binding's + // onChange picks the new element up the same way and propagates it + // to the peer. + await page.evaluate(() => { + const w = window as unknown as { + __excalidrawAPI?: { + getSceneElements: () => readonly Record[] + updateScene: (args: { elements: Record[] }) => void + } + } + const api = w.__excalidrawAPI + if (!api) throw new Error('__excalidrawAPI not available on window') + const existing = api.getSceneElements() + const id = `r-test-${Date.now()}` + const rectangle: Record = { + id, + type: 'rectangle', + x: 400, + y: 400, + width: 120, + height: 80, + angle: 0, + strokeColor: '#1971c2', + backgroundColor: 'transparent', + fillStyle: 'hachure', + strokeWidth: 1, + strokeStyle: 'solid', + roughness: 1, + opacity: 100, + groupIds: [], + frameId: null, + roundness: null, + seed: Math.floor(Math.random() * 1_000_000), + version: 1, + versionNonce: Math.floor(Math.random() * 1_000_000), + isDeleted: false, + boundElements: null, + updated: Date.now(), + link: null, + locked: false + } + api.updateScene({ elements: [...existing, rectangle] }) + }) + } +) + Then( /^the file "([^"]+)" in "([^"]+)"'s personal space should contain "([^"]+)"$/, async function ( diff --git a/tests/e2e/cucumber/steps/ui/resources.ts b/tests/e2e/cucumber/steps/ui/resources.ts index 776c4a0dfc..15555291d7 100644 --- a/tests/e2e/cucumber/steps/ui/resources.ts +++ b/tests/e2e/cucumber/steps/ui/resources.ts @@ -1103,7 +1103,14 @@ Then( When( '{string} opens file {string} via {string} using the context menu', async function (this: World, stepUser: string, file: string, fileViewer: string): Promise { - const allowedViewers = ['collabora', 'text-editor', 'preview', 'code-mirror', 'tiptap'] as const + const allowedViewers = [ + 'collabora', + 'text-editor', + 'preview', + 'code-mirror', + 'tiptap', + 'excalidraw' + ] as const if (!allowedViewers.includes(fileViewer as any)) { throw new Error(`Unsupported file viewer: ${fileViewer}`) diff --git a/tests/e2e/support/objects/app-files/utils/collab.ts b/tests/e2e/support/objects/app-files/utils/collab.ts index 417bc059f8..4d693443e2 100644 --- a/tests/e2e/support/objects/app-files/utils/collab.ts +++ b/tests/e2e/support/objects/app-files/utils/collab.ts @@ -40,3 +40,24 @@ export const remoteCaretCount = (page: Page) => page.locator('.cm-ySelectionCare export const remoteCaretLabelText = (page: Page) => page.locator('.cm-ySelectionInfo').first().textContent() + +// Excalidraw paints its scene to a single — no per-element DOM, +// no useful selectors for individual shapes. ExcalidrawCanvas.tsx exposes +// the imperative API on `window.__excalidrawAPI` for exactly this case +// (test-only hook); we read scene element count through it. Cleared on +// unmount, so a stale read after navigation returns -1 (sentinel). +export const excalidrawSceneElementCount = (page: Page) => + page.evaluate(() => { + const w = window as unknown as { + __excalidrawAPI?: { getSceneElements: () => unknown[] } + } + return w.__excalidrawAPI?.getSceneElements().length ?? -1 + }) + +// Wait for Excalidraw's root to mount inside our wrapper div. Confirms the +// React island rendered, not just that the Vue shell is up. +export const awaitExcalidrawMounted = async (page: Page): Promise => { + await expect(page.locator('.excalidraw-host .excalidraw').first()).toBeVisible({ + timeout: 15_000 + }) +} diff --git a/tests/woodpecker/config-opencloud.json b/tests/woodpecker/config-opencloud.json index dbd123b5c2..cf3c9ecd80 100644 --- a/tests/woodpecker/config-opencloud.json +++ b/tests/woodpecker/config-opencloud.json @@ -6,6 +6,7 @@ "text-editor", "codemirror", "tiptap", + "excalidraw", "preview", "pdf-viewer", "search", diff --git a/tsconfig.json b/tsconfig.json index 264b86cd8f..efa4dd3ef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "@opencloud-eu/tsconfig" + "extends": "@opencloud-eu/tsconfig", + "compilerOptions": { + "jsx": "react-jsx" + } } diff --git a/vite.config.ts b/vite.config.ts index 68660c3327..57c77faee6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,7 @@ import { ViteDevServer } from 'vite' import vue from '@vitejs/plugin-vue' +import react from '@vitejs/plugin-react' import { Target, viteStaticCopy } from 'vite-plugin-static-copy' import { nodePolyfills } from 'vite-plugin-node-polyfills' import tailwindcss from '@tailwindcss/vite' @@ -191,9 +192,20 @@ export default defineConfig(({ mode, command }) => { 'process.env.PACKAGE_VERSION': JSON.stringify(version) }, resolve: { - dedupe: ['vue3-gettext'], + // `react`/`react-dom` are dedup'd because Excalidraw (React-only) is + // mounted into Vue via veaury, which itself pulls tunnel-rat/zustand — + // both expect React as a peer. + dedupe: ['vue3-gettext', 'react', 'react-dom'], alias: { - crypto: join(projectRootDir, 'polyfills/crypto.js') + crypto: join(projectRootDir, 'polyfills/crypto.js'), + // tunnel-rat and zustand (pulled in by veaury) declare react as a + // peer but pnpm doesn't symlink it into their own node_modules, + // and rolldown can't follow pnpm's peer-resolution suffix on its + // own. Aliasing react / react-dom to web-app-excalidraw's + // installed copy points every importer at the same instance and + // sidesteps the resolution failure. + react: join(projectRootDir, 'packages/web-app-excalidraw/node_modules/react'), + 'react-dom': join(projectRootDir, 'packages/web-app-excalidraw/node_modules/react-dom') } }, plugins: [ @@ -210,6 +222,12 @@ export default defineConfig(({ mode, command }) => { compilerOptions } }), + // Only matches files outside .vue templates: web-app-excalidraw is the + // sole React user today (Excalidraw is React-only). vue-plugin and + // react-plugin coexist cleanly because the file extensions don't + // overlap (.vue vs .tsx). The `include` filter keeps Babel out of + // every other package's hot path. + react({ include: /packages\/web-app-excalidraw\/.*\.(tsx|ts)$/ }), viteStaticCopy({ targets: (() => { return [ @@ -228,9 +246,49 @@ export default defineConfig(({ mode, command }) => { dest: '.', rename: { stripBase: 0 } } + // Excalidraw fetches its fonts + locales + lib data at runtime + // via `window.EXCALIDRAW_ASSET_PATH`. Without an override it + // falls back to esm.sh, which would require whitelisting that + // host in OC's CSP — a non-starter for an official app. We + // mirror the upstream `dist/prod/{fonts,locales,data}` tree + // into our own dist and the React canvas points + // EXCALIDRAW_ASSET_PATH at it via `import.meta.url` so the + // URL resolves correctly under any OC subpath deployment. ] })() }), + // Excalidraw fetches fonts / locales / lib data at runtime via + // `window.EXCALIDRAW_ASSET_PATH`. Without an override it falls back + // to `https://esm.sh/...` — would force whitelisting esm.sh in OC's + // CSP, non-starter for an official app. We mirror the upstream + // `dist/prod/{fonts,locales,data}` tree into our own dist; + // ExcalidrawCanvas.tsx points EXCALIDRAW_ASSET_PATH at it via + // `new URL('../excalidraw-assets/', import.meta.url)` so it resolves + // correctly under any OC subpath deployment. + // + // vite-plugin-static-copy flattens nested dirs when used with `**/*` + // (even with custom rename), so we just do the recursive copy + // ourselves in `writeBundle`. Cheap, ~17MB, one-shot per build. + { + name: '@opencloud-eu/vite-plugin-copy-excalidraw-assets', + async writeBundle() { + const { promises: fsp } = await import('fs') + const srcRoot = join( + projectRootDir, + 'packages/web-app-excalidraw/node_modules/@excalidraw/excalidraw/dist/prod' + ) + const destRoot = join(projectRootDir, dist, 'excalidraw-assets') + for (const sub of ['fonts', 'locales', 'data']) { + const src = join(srcRoot, sub) + const dest = join(destRoot, sub) + try { + await fsp.cp(src, dest, { recursive: true }) + } catch (e) { + console.warn(`[excalidraw-assets] skipped ${sub}:`, (e as Error).message) + } + } + } + }, registrationHost, { name: '@opencloud-eu/vite-plugin-runtime-config', From ade0d119ff96caf6979f75723965b3a819a4aa6a Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 21 May 2026 00:48:06 +0200 Subject: [PATCH 22/23] feat(collab): client-side _oc_meta.isStale detection for relay backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stale-state detection used to be hocuspocus-only: its onLoadDocument hook compared the persisted Y.Doc's etag against a fresh PROPFIND and set _oc_meta.isStale = true when they drifted. Relay-only backends (opencloud-yjs) cannot do this because they hold no persisted state and never PROPFIND. Moves the comparison into the wrapper's onProviderSynced: after sync, if _oc_meta.etag (synced via CRDT from whichever peer joined first) is non-empty and differs from props.resource.etag (just refetched by AppWrapper), set isStale + nativeEtag exactly as the hocuspocus hook would have. The existing metaObserver then fires recoverFromStaleState with the existing election + rehydrate flow unchanged. Fully drop-in compatible with hocuspocus: the server-side check still fires earlier and an existing guard (if meta.get('isStale')) skips the client-side branch. Verified by running the full cucumber suite against both backends — 10/10 scenarios pass on each. The two paths use byte-identical fields and comparison logic, so the client check never produces a spurious trigger in hocuspocus mode. --- .../Collaborative/CollaborativeWrapper.vue | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue index b65fb97d37..8fb9a513d1 100644 --- a/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue +++ b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue @@ -442,10 +442,33 @@ async function onProviderSynced( } } - // Seed the etag immediately so future stale-state probes have a baseline. - if (!meta.get('etag') && props.resource.etag) { + // Etag drift check — the client-side equivalent of hocuspocus's + // `onLoadDocument` stale probe. Relay-only backends (opencloud-yjs) do + // not persist Y.Docs, so the server cannot compare a persisted etag + // against the native file. Instead, after sync we look at what the + // synced room thinks the etag is (`_oc_meta.etag`, seeded by whichever + // peer entered first) and compare against the etag the AppWrapper + // just refetched as `props.resource.etag`: + // - no doc etag yet → we are the first peer, seed our baseline + // - doc == native → no-op + // - doc != native → the room's view is older than the file on + // disk; flag isStale so the meta observer + // fires `recoverFromStaleState` (election + // inside that fn picks one peer to rehydrate) + // Stamping native etag into a sidecar field lets the recovery path + // settle the final value into `_oc_meta.etag` without an extra fetch. + const docEtag = meta.get('etag') as string | undefined + const nativeEtag = props.resource.etag + if (docEtag && nativeEtag && docEtag !== nativeEtag) { doc.transact(() => { - if (!meta.get('etag')) meta.set('etag', props.resource.etag) + meta.set('nativeEtag', nativeEtag) + meta.set('isStale', true) + }) + return + } + if (!docEtag && nativeEtag) { + doc.transact(() => { + if (!meta.get('etag')) meta.set('etag', nativeEtag) }) } From c821fb8a5ff12d391fb850f70942cfcee187bcc1 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 21 May 2026 03:32:44 +0200 Subject: [PATCH 23/23] build(realtime-collab): bump hocuspocus to 4.1.0, drop the patch + sqlite @hocuspocus/server 4.1.0 ships the `beforeHandleAwareness` hook natively (the very hook our patch added against 4.0.0), so: - bump the dependency to ^4.1.0 - drop dev/docker/hocuspocus/patches/ entirely - drop the Dockerfile's `apk add patch` + patch-apply step - update server.js's hook signature from the old `{ states, connection }` destructured form to 4.1.0's positional `(document, states, origin)` shape, with `connection` recovered from `origin.source === 'connection'` Same commit also drops the SQLite persistence layer. The relay-only spike validated that file-backed collab (hydrate from `currentContent`, no server-side Y.Doc snapshot) is sufficient for our workloads. Carrying SQLite forward on hocuspocus added a mount, a volume, an extension dep, and a stale-state code path that we no longer rely on (the equivalent check moved to CollaborativeWrapper.onProviderSynced). - drop `@hocuspocus/extension-sqlite` dep - drop the SQLite extension wiring in server.js - drop DB_PATH env + the hocuspocus-data volume in docker-compose - keep `onLoadDocument` commented out as scaffolding: if a future persistence layer (extension-sqlite, redis, ...) is reintroduced the cold-load etag-drift probe slots straight back in Full cucumber collab suite (10/10) passes on the unpatched 4.1.0 build with the client-side stale-state path doing the work. --- dev/docker/hocuspocus/Dockerfile | 9 - dev/docker/hocuspocus/package.json | 3 +- .../patches/hocuspocus-server-4.0.0.patch | 234 ------------------ dev/docker/hocuspocus/server.js | 95 +++---- docker-compose.yml | 2 - 5 files changed, 53 insertions(+), 290 deletions(-) delete mode 100644 dev/docker/hocuspocus/patches/hocuspocus-server-4.0.0.patch diff --git a/dev/docker/hocuspocus/Dockerfile b/dev/docker/hocuspocus/Dockerfile index be91074090..fa79dea68d 100644 --- a/dev/docker/hocuspocus/Dockerfile +++ b/dev/docker/hocuspocus/Dockerfile @@ -2,18 +2,9 @@ FROM node:22-alpine WORKDIR /app -# `patch` isn't in node:alpine by default; needed for the hocuspocus patch. -RUN apk add --no-cache patch - COPY package.json ./ RUN npm install --omit=dev --no-audit --no-fund --loglevel=error -# Apply the pre-built hocuspocus PR #1096 (beforeHandleAwareness) on top of -# the npm @hocuspocus/server@4.0.0 release. Removable once that hook ships in -# a release. See patches/hocuspocus-server-4.0.0.patch. -COPY patches/ ./patches/ -RUN cd node_modules/@hocuspocus/server && patch -p1 < /app/patches/hocuspocus-server-4.0.0.patch - COPY server.js ./ CMD ["node", "server.js"] diff --git a/dev/docker/hocuspocus/package.json b/dev/docker/hocuspocus/package.json index 5515ad0f49..9685d8e1fc 100644 --- a/dev/docker/hocuspocus/package.json +++ b/dev/docker/hocuspocus/package.json @@ -7,7 +7,6 @@ "start": "node server.js" }, "dependencies": { - "@hocuspocus/server": "^4.0.0", - "@hocuspocus/extension-sqlite": "^4.0.0" + "@hocuspocus/server": "^4.1.0" } } diff --git a/dev/docker/hocuspocus/patches/hocuspocus-server-4.0.0.patch b/dev/docker/hocuspocus/patches/hocuspocus-server-4.0.0.patch deleted file mode 100644 index acb238e1c7..0000000000 --- a/dev/docker/hocuspocus/patches/hocuspocus-server-4.0.0.patch +++ /dev/null @@ -1,234 +0,0 @@ -diff -urN package/dist/hocuspocus-server.esm.js server-new/package/dist/hocuspocus-server.esm.js ---- package/dist/hocuspocus-server.esm.js 1985-10-26 09:15:00.000000000 +0100 -+++ server-new/package/dist/hocuspocus-server.esm.js 2026-05-18 13:57:00.808448548 +0200 -@@ -193,9 +193,19 @@ - else if (connection) connection.send(message.toUint8Array()); - } - break; -- case MessageType.Awareness: -- applyAwarenessUpdate(document.awareness, message.readVarUint8Array(), connection ?? null); -+ case MessageType.Awareness: { -+ let update = message.readVarUint8Array(); -+ const origin = connection ? { -+ source: "connection", -+ connection -+ } : this.defaultTransactionOrigin ?? { source: "local" }; -+ const scratch = new Awareness(new Y.Doc()); -+ applyAwarenessUpdate(scratch, update, null); -+ await document.callbacks.beforeHandleAwareness(document, scratch.getStates(), origin); -+ update = encodeAwarenessUpdate(scratch, [...scratch.getStates().keys()]); -+ applyAwarenessUpdate(document.awareness, update, origin); - break; -+ } - case MessageType.QueryAwareness: - this.applyQueryAwarenessMessage(document, connection, reply); - break; -@@ -455,7 +465,8 @@ - super(yDocOptions); - this.callbacks = { - onUpdate: (document, origin, update) => {}, -- beforeBroadcastStateless: (document, stateless) => {} -+ beforeBroadcastStateless: (document, stateless) => {}, -+ beforeHandleAwareness: (document, states, transactionOrigin) => Promise.resolve() - }; - this.connections = /* @__PURE__ */ new Map(); - this.directConnectionsCount = 0; -@@ -499,6 +510,19 @@ - return this; - } - /** -+ * Set a callback that will be triggered before an inbound awareness update -+ * is applied to this document's awareness state. The callback receives the -+ * document, the decoded per-client states as a mutable `Map`, and the -+ * `TransactionOrigin` that will be forwarded to `applyAwarenessUpdate`. -+ * Use `isTransactionOrigin(origin)` to discriminate sources. Mutate the -+ * map in place (set/delete/field changes) to rewrite the update, or throw -+ * to reject it entirely. -+ */ -+ beforeHandleAwareness(callback) { -+ this.callbacks.beforeHandleAwareness = callback; -+ return this; -+ } -+ /** - * Register a connection and a set of clients on this document keyed by the - * underlying websocket connection - */ -@@ -558,15 +582,19 @@ - * Apply the given awareness update - */ - applyAwarenessUpdate(connection, update) { -- applyAwarenessUpdate(this.awareness, update, connection); -+ applyAwarenessUpdate(this.awareness, update, { -+ source: "connection", -+ connection -+ }); - return this; - } - /** - * Handle an awareness update and sync changes to clients - * @private - */ -- handleAwarenessUpdate({ added, updated, removed }, originConnection) { -+ handleAwarenessUpdate({ added, updated, removed }, origin) { - const changedClients = added.concat(updated, removed); -+ const originConnection = isTransactionOrigin(origin) && origin.source === "connection" ? origin.connection : null; - if (originConnection !== null) { - const entry = this.connections.get(originConnection); - if (entry) { -@@ -1003,6 +1031,7 @@ - onConnect: () => new Promise((r) => r(null)), - connected: () => new Promise((r) => r(null)), - beforeHandleMessage: () => new Promise((r) => r(null)), -+ beforeHandleAwareness: () => new Promise((r) => r()), - beforeSync: () => new Promise((r) => r(null)), - beforeBroadcastStateless: () => new Promise((r) => r(null)), - onStateless: () => new Promise((r) => r(null)), -@@ -1048,6 +1077,7 @@ - onLoadDocument: this.configuration.onLoadDocument, - afterLoadDocument: this.configuration.afterLoadDocument, - beforeHandleMessage: this.configuration.beforeHandleMessage, -+ beforeHandleAwareness: this.configuration.beforeHandleAwareness, - beforeBroadcastStateless: this.configuration.beforeBroadcastStateless, - beforeSync: this.configuration.beforeSync, - onStateless: this.configuration.onStateless, -@@ -1236,6 +1266,24 @@ - }; - this.hooks("beforeBroadcastStateless", hookPayload); - }); -+ document.beforeHandleAwareness((document, states, transactionOrigin) => { -+ const connection = isTransactionOrigin(transactionOrigin) && transactionOrigin.source === "connection" ? transactionOrigin.connection : void 0; -+ const request = connection?.request; -+ return this.hooks("beforeHandleAwareness", { -+ awareness: document.awareness, -+ clientsCount: document.getConnectionsCount(), -+ context: connection?.context, -+ document, -+ documentName: document.name, -+ instance: this, -+ requestHeaders: request?.headers ?? new Headers(), -+ requestParameters: request ? getParameters(request) : new URLSearchParams(), -+ socketId: connection?.socketId ?? "", -+ transactionOrigin, -+ connection, -+ states -+ }); -+ }); - document.awareness.on("update", (update, origin) => { - this.hooks("onAwarenessUpdate", { - document, -diff -urN package/dist/index.d.ts server-new/package/dist/index.d.ts ---- package/dist/index.d.ts 1985-10-26 09:15:00.000000000 +0100 -+++ server-new/package/dist/index.d.ts 2026-05-18 13:57:00.810243726 +0200 -@@ -13,6 +13,7 @@ - callbacks: { - onUpdate: (document: Document, origin: unknown, update: Uint8Array) => void; - beforeBroadcastStateless: (document: Document, stateless: string) => void; -+ beforeHandleAwareness: (document: Document, states: Map>, transactionOrigin: unknown) => Promise; - }; - connections: Map; -@@ -44,6 +45,16 @@ - */ - beforeBroadcastStateless(callback: (document: Document, stateless: string) => void): Document; - /** -+ * Set a callback that will be triggered before an inbound awareness update -+ * is applied to this document's awareness state. The callback receives the -+ * document, the decoded per-client states as a mutable `Map`, and the -+ * `TransactionOrigin` that will be forwarded to `applyAwarenessUpdate`. -+ * Use `isTransactionOrigin(origin)` to discriminate sources. Mutate the -+ * map in place (set/delete/field changes) to rewrite the update, or throw -+ * to reject it entirely. -+ */ -+ beforeHandleAwareness(callback: (document: Document, states: Map>, transactionOrigin: unknown) => Promise): Document; -+ /** - * Register a connection and a set of clients on this document keyed by the - * underlying websocket connection - */ -@@ -352,6 +363,18 @@ - onLoadDocument?(data: onLoadDocumentPayload): Promise; - afterLoadDocument?(data: afterLoadDocumentPayload): Promise; - beforeHandleMessage?(data: beforeHandleMessagePayload): Promise; -+ /** -+ * Fired before an inbound awareness update is applied to the document's -+ * awareness state. The hook receives the decoded per-client `states` as a -+ * mutable `Map` keyed by Yjs clientId. Mutate the map and the contained -+ * state objects in place to rewrite fields, drop peers (`states.delete`), -+ * or add synthetic ones (`states.set`); mutations are reflected in the -+ * broadcast. Throw to reject the update without applying anything. -+ * -+ * Multiple extensions chain naturally: each extension sees the map as -+ * mutated by previous extensions and can mutate it further. -+ */ -+ beforeHandleAwareness?(data: beforeHandleAwarenessPayload): Promise; - beforeSync?(data: beforeSyncPayload): Promise; - beforeBroadcastStateless?(data: beforeBroadcastStatelessPayload): Promise; - onStateless?(payload: onStatelessPayload): Promise; -@@ -365,7 +388,7 @@ - afterUnloadDocument?(data: afterUnloadDocumentPayload): Promise; - onDestroy?(data: onDestroyPayload): Promise; - } --type HookName = "onConfigure" | "onListen" | "onUpgrade" | "onConnect" | "connected" | "onAuthenticate" | "onTokenSync" | "onCreateDocument" | "onLoadDocument" | "afterLoadDocument" | "beforeHandleMessage" | "beforeBroadcastStateless" | "beforeSync" | "onStateless" | "onChange" | "onStoreDocument" | "afterStoreDocument" | "onAwarenessUpdate" | "onRequest" | "onDisconnect" | "beforeUnloadDocument" | "afterUnloadDocument" | "onDestroy"; -+type HookName = "onConfigure" | "onListen" | "onUpgrade" | "onConnect" | "connected" | "onAuthenticate" | "onTokenSync" | "onCreateDocument" | "onLoadDocument" | "afterLoadDocument" | "beforeHandleMessage" | "beforeHandleAwareness" | "beforeBroadcastStateless" | "beforeSync" | "onStateless" | "onChange" | "onStoreDocument" | "afterStoreDocument" | "onAwarenessUpdate" | "onRequest" | "onDisconnect" | "beforeUnloadDocument" | "afterUnloadDocument" | "onDestroy"; - type HookPayloadByName = { - onConfigure: onConfigurePayload; - onListen: onListenPayload; -@@ -378,6 +401,7 @@ - onLoadDocument: onLoadDocumentPayload; - afterLoadDocument: afterLoadDocumentPayload; - beforeHandleMessage: beforeHandleMessagePayload; -+ beforeHandleAwareness: beforeHandleAwarenessPayload; - beforeBroadcastStateless: beforeBroadcastStatelessPayload; - beforeSync: beforeSyncPayload; - onStateless: onStatelessPayload; -@@ -540,6 +564,43 @@ - socketId: string; - connection: Connection; - } -+interface beforeHandleAwarenessPayload { -+ awareness: Awareness; -+ clientsCount: number; -+ /** -+ * Connection context populated by `onAuthenticate`. `undefined` when the -+ * update did not originate from a client connection (e.g. server-internal -+ * writes via `DirectConnection`). -+ */ -+ context: Context | undefined; -+ document: Document; -+ documentName: string; -+ instance: Hocuspocus; -+ requestHeaders: Headers; -+ requestParameters: URLSearchParams; -+ /** -+ * Per-client awareness states decoded from the inbound update, keyed by -+ * Yjs clientId. Mutate this map in place to rewrite the update: change -+ * fields on a state object, `states.delete(clientId)` to drop a peer, or -+ * `states.set(clientId, ...)` to add or replace one. The encoded update -+ * sent to peers reflects whatever the map looks like after every hook in -+ * the chain has run. -+ */ -+ states: Map>; -+ socketId: string; -+ /** -+ * The `TransactionOrigin` that will be passed to `applyAwarenessUpdate`. -+ * Use `isTransactionOrigin(origin)` to discriminate sources. Matches the -+ * `transactionOrigin` shape of `onAwarenessUpdatePayload`. -+ */ -+ transactionOrigin: unknown; -+ /** -+ * Convenience shortcut: `origin.connection` when `transactionOrigin` is a -+ * `ConnectionTransactionOrigin`, otherwise `undefined`. Matches the -+ * `connection?` shape of `onAwarenessUpdatePayload`. -+ */ -+ connection?: Connection; -+} - interface beforeSyncPayload { - clientsCount: number; - context: Context; -@@ -801,4 +862,4 @@ - executeNow: (id: string) => any; - }; - //#endregion --export { AwarenessUpdate, Configuration, Connection, ConnectionConfiguration, ConnectionTransactionOrigin, DirectConnection, Document, Extension, Hocuspocus, HookName, HookPayloadByName, IncomingMessage, LocalTransactionOrigin, MessageReceiver, MessageType, OutgoingMessage, RedisTransactionOrigin, Server, ServerConfiguration, StatesArray, TransactionOrigin, WebSocketLike, afterLoadDocumentPayload, afterStoreDocumentPayload, afterUnloadDocumentPayload, beforeBroadcastStatelessPayload, beforeHandleMessagePayload, beforeSyncPayload, beforeUnloadDocumentPayload, connectedPayload, defaultConfiguration, defaultServerConfiguration, fetchPayload, isTransactionOrigin, onAuthenticatePayload, onAwarenessUpdatePayload, onChangePayload, onConfigurePayload, onConnectPayload, onCreateDocumentPayload, onDestroyPayload, onDisconnectPayload, onListenPayload, onLoadDocumentPayload, onRequestPayload, onStatelessPayload, onStoreDocumentPayload, onTokenSyncPayload, onUpgradePayload, shouldSkipStoreHooks, storePayload, useDebounce }; -\ No newline at end of file -+export { AwarenessUpdate, Configuration, Connection, ConnectionConfiguration, ConnectionTransactionOrigin, DirectConnection, Document, Extension, Hocuspocus, HookName, HookPayloadByName, IncomingMessage, LocalTransactionOrigin, MessageReceiver, MessageType, OutgoingMessage, RedisTransactionOrigin, Server, ServerConfiguration, StatesArray, TransactionOrigin, WebSocketLike, afterLoadDocumentPayload, afterStoreDocumentPayload, afterUnloadDocumentPayload, beforeBroadcastStatelessPayload, beforeHandleAwarenessPayload, beforeHandleMessagePayload, beforeSyncPayload, beforeUnloadDocumentPayload, connectedPayload, defaultConfiguration, defaultServerConfiguration, fetchPayload, isTransactionOrigin, onAuthenticatePayload, onAwarenessUpdatePayload, onChangePayload, onConfigurePayload, onConnectPayload, onCreateDocumentPayload, onDestroyPayload, onDisconnectPayload, onListenPayload, onLoadDocumentPayload, onRequestPayload, onStatelessPayload, onStoreDocumentPayload, onTokenSyncPayload, onUpgradePayload, shouldSkipStoreHooks, storePayload, useDebounce }; -\ No newline at end of file diff --git a/dev/docker/hocuspocus/server.js b/dev/docker/hocuspocus/server.js index 816c512d3f..d181050a67 100644 --- a/dev/docker/hocuspocus/server.js +++ b/dev/docker/hocuspocus/server.js @@ -1,8 +1,6 @@ import { Server } from '@hocuspocus/server' -import { SQLite } from '@hocuspocus/extension-sqlite' const port = parseInt(process.env.PORT ?? '1234', 10) -const dbPath = process.env.DB_PATH ?? '/var/lib/hocuspocus/state.db' const opencloudUrl = (process.env.OPENCLOUD_URL ?? 'https://host.docker.internal:9200').replace( /\/$/, '' @@ -112,7 +110,12 @@ const META_KEY = '_oc_meta' const server = new Server({ port, address: '0.0.0.0', - extensions: [new SQLite({ database: dbPath })], + // No server-side persistence: every doc is file-backed via WebDAV. + // Cold-start for a fresh peer = hydrate from `currentContent` in the + // wrapper. The persisted SQLite snapshot would get discarded on stale- + // state recovery anyway (etag drift triggers rehydrate); keeping it + // here is "mostly ceremony" per the migration plan. Stale detection + // moved to the client (see CollaborativeWrapper.onProviderSynced). async onAuthenticate({ token, documentName, requestParameters }) { if (!token) { @@ -182,43 +185,47 @@ const server = new Server({ } }, - // Stale-state detection: SQLite extension has loaded the persisted Y.Doc - // by the time this runs (extension hooks run before the configuration - // hook). onLoadDocument only fires when a doc is loaded into memory for - // the first time after eviction — i.e. nobody else is in the room — so - // we can safely flag-and-rehydrate without racing live peers. + // Stale-state detection — DISABLED. // - // Two staleness dimensions, both produce the same `_oc_meta.isStale = true` - // signal for the wrapper to act on: - // 1. etag drift: external file write happened between sessions - // (persisted etag != caller's native etag) - // 2. app-version drift: persisted Y.Doc was written by an older client - // version whose adapter layout the new client can no longer read - async onLoadDocument({ document, context }) { - const meta = document.getMap(META_KEY) - const persistedEtag = meta.get('etag') - const nativeEtag = context?.nativeEtag - const persistedAppVersion = meta.get('appVersion') - const clientAppVersion = context?.clientAppVersion - - const etagDrift = !!persistedEtag && !!nativeEtag && persistedEtag !== nativeEtag - const versionDrift = - !!persistedAppVersion && !!clientAppVersion && persistedAppVersion !== clientAppVersion - - if (!etagDrift && !versionDrift) return - - const reasons = [] - if (etagDrift) reasons.push(`etag(${persistedEtag}→${nativeEtag})`) - if (versionDrift) reasons.push(`appVersion(${persistedAppVersion}→${clientAppVersion})`) - console.log( - `[onLoadDocument] stale state document="${document.name}" ` + - `${reasons.join(' ')} → marked for rehydrate` - ) - document.transact(() => { - meta.set('isStale', true) - if (nativeEtag) meta.set('nativeEtag', nativeEtag) - }) - }, + // This hook fires once when a doc is loaded into memory. It used to do + // useful work when we shipped the SQLite extension: at cold load it + // compared the persisted `_oc_meta.etag` against the live native etag + // and flagged drift so the wrapper would rehydrate. Without persistence + // the doc is always freshly created at load time, `_oc_meta` is empty, + // and the wrapper's etag mirror runs strictly AFTER this hook — so the + // comparison can never fire. The equivalent check now lives in + // CollaborativeWrapper.onProviderSynced (runs on the client, sees the + // CRDT-synced `_oc_meta.etag` from whichever peer joined first). + // + // Kept commented out as a reference: if persistence is reintroduced + // (extension-sqlite, redis, etc.), uncomment to get the server-side + // cold-load probe back. + // + // async onLoadDocument({ document, context }) { + // const meta = document.getMap(META_KEY) + // const persistedEtag = meta.get('etag') + // const nativeEtag = context?.nativeEtag + // const persistedAppVersion = meta.get('appVersion') + // const clientAppVersion = context?.clientAppVersion + // + // const etagDrift = !!persistedEtag && !!nativeEtag && persistedEtag !== nativeEtag + // const versionDrift = + // !!persistedAppVersion && !!clientAppVersion && persistedAppVersion !== clientAppVersion + // + // if (!etagDrift && !versionDrift) return + // + // const reasons = [] + // if (etagDrift) reasons.push(`etag(${persistedEtag}→${nativeEtag})`) + // if (versionDrift) reasons.push(`appVersion(${persistedAppVersion}→${clientAppVersion})`) + // console.log( + // `[onLoadDocument] stale state document="${document.name}" ` + + // `${reasons.join(' ')} → marked for rehydrate` + // ) + // document.transact(() => { + // meta.set('isStale', true) + // if (nativeEtag) meta.set('nativeEtag', nativeEtag) + // }) + // }, async onConnect({ documentName, requestHeaders }) { console.log(`[onConnect] document="${documentName}" origin=${requestHeaders.origin ?? '-'}`) @@ -235,9 +242,11 @@ const server = new Server({ // Anti-spoof identity stamp: before each inbound awareness update is // applied, overwrite the `user` field on every state in the update with - // the authenticated identity from the connection's context. Provided by - // the patched @hocuspocus/server (see patches/). - async beforeHandleAwareness({ states, connection }) { + // the authenticated identity from the connection's context. This used + // to require a patch on hocuspocus 4.0.0; 4.1.0 ships the + // `beforeHandleAwareness` callback natively with positional args. + async beforeHandleAwareness(_document, states, origin) { + const connection = origin?.source === 'connection' ? origin.connection : null const user = connection?.context?.user if (!user) return const canonical = { @@ -252,5 +261,5 @@ const server = new Server({ }) server.listen().then(() => { - console.log(`hocuspocus v4 listening on :${port}, db=${dbPath}, oc=${opencloudUrl}`) + console.log(`hocuspocus v4 listening on :${port}, oc=${opencloudUrl}`) }) diff --git a/docker-compose.yml b/docker-compose.yml index de4ac3d911..0c1527e10f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -356,7 +356,6 @@ services: - host.docker.internal:${DOCKER_HOST:-host-gateway} environment: PORT: '1234' - DB_PATH: /var/lib/hocuspocus/state.db OPENCLOUD_URL: https://host.docker.internal:9200 # Dev only: self-signed cert from OC's Traefik NODE_TLS_REJECT_UNAUTHORIZED: '0' @@ -364,7 +363,6 @@ services: # Remove for prod. DEV_FAKE_TOKEN: dev-integration-token volumes: - - hocuspocus-data:/var/lib/hocuspocus # Dev override: edit server.js without rebuild; remove for prod - ./dev/docker/hocuspocus/server.js:/app/server.js:ro networks: