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 new file mode 100644 index 0000000000..023d52f081 --- /dev/null +++ b/REALTIME_COLLAB_MIGRATION.md @@ -0,0 +1,302 @@ +# 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. **Drop `DEV_FAKE_TOKEN` from the sidecar.** Currently the integration vitest suite reaches the sidecar with the literal `dev-integration-token` string and the sidecar's `onAuthenticate` short-circuits to a synthetic identity, skipping the Graph permissions probe. This is a test-only shortcut that bloats the production server.js with a code path that has no business in prod. Replace with a real OIDC flow in the integration test: hit the same logon/token endpoints the playwright e2e helpers already use (`support/helpers/api/getToken.ts` in web-extensions / equivalent in web), pass the real bearer to `HocuspocusProvider`. Delete the `DEV_FAKE_TOKEN` env + `devEtag` query-param branch from `server.js`. + +3. **Auto-focus on mount vs accessibility.** Phase 4 disables `useTextEditor`'s "focus the editor at start when mounted" behavior in collab mode (so a peer doesn't see your phantom caret at (0,0) before you've clicked in). For keyboard / screen-reader users this might be a regression — they rely on the editor receiving focus on app-open to jump straight into editing instead of having to tab into it. Single-user path still auto-focuses; only the collab branch skips. Worth checking with an a11y audit: do collab editors need an alternative focus signal (e.g. focus the toolbar's first action, or focus the editor without setting a selection so awareness stays silent until first interaction)? + +4. **External-app collab compatibility.** Web's central vite build bundles every `web-app-*` in a single pass against one resolved pnpm tree, so `yjs` / `y-prosemirror` / `@tiptap/*` are naturally deduplicated at build time (no `resolve.dedupe` entries needed). External, federated apps shipped via `extension-sdk` are built independently and would re-bundle their own copies, breaking `instanceof Y.Doc` / pluginKey identity checks at runtime. Before any third-party collab editor lands, verify the duplication actually triggers in practice and, if it does, decide whether `extension-sdk` should `external` these shared modules (so the host's instance is reused) or whether the wrapper itself should be the only collab carrier. + +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. + +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. + +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. + +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). + +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`. + +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. + +--- + +## 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. +- [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). +- [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. +- [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/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/dev/docker/hocuspocus/Dockerfile b/dev/docker/hocuspocus/Dockerfile new file mode 100644 index 0000000000..fa79dea68d --- /dev/null +++ b/dev/docker/hocuspocus/Dockerfile @@ -0,0 +1,10 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev --no-audit --no-fund --loglevel=error + +COPY server.js ./ + +CMD ["node", "server.js"] diff --git a/dev/docker/hocuspocus/package.json b/dev/docker/hocuspocus/package.json new file mode 100644 index 0000000000..9685d8e1fc --- /dev/null +++ b/dev/docker/hocuspocus/package.json @@ -0,0 +1,12 @@ +{ + "name": "opencloud-hocuspocus-dev", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "@hocuspocus/server": "^4.1.0" + } +} diff --git a/dev/docker/hocuspocus/server.js b/dev/docker/hocuspocus/server.js new file mode 100644 index 0000000000..d181050a67 --- /dev/null +++ b/dev/docker/hocuspocus/server.js @@ -0,0 +1,265 @@ +import { Server } from '@hocuspocus/server' + +const port = parseInt(process.env.PORT ?? '1234', 10) +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 +// "everybody in this room must run the same client version". First connect +// for a documentName sets the baseline; subsequent connects with a different +// appVersion are rejected at authenticate-time. In-memory only; on restart +// the next connecter becomes the new baseline (acceptable for a stateless +// sidecar). Empty appVersion is tolerated for legacy/test clients. +const appVersionByDocument = new Map() + +function deterministicColor(seed) { + let hash = 0 + for (let i = 0; i < seed.length; i++) hash = seed.charCodeAt(i) + ((hash << 5) - hash) + return `hsl(${Math.abs(hash) % 360}, 70%, 50%)` +} + +async function validateTokenAgainstOpenCloud(token) { + const res = await fetch(`${opencloudUrl}/graph/v1.0/me`, { + headers: { Authorization: `Bearer ${token}` } + }) + if (!res.ok) { + const detail = await res.text().catch(() => '') + throw new Error(`graph /me returned ${res.status}: ${detail.slice(0, 200)}`) + } + return res.json() +} + +// Heuristic: a libregraph permission action implies write access when its +// trailing verb is create/update/delete/allTasks on driveItem properties. +const WRITE_ACTION = /\/(update|create|delete|allTasks)$/ + +// Splits OC's canonical composite id `$!` into +// the (driveId, itemId) pair the Graph endpoint expects: driveID = +// `$`, itemID = the FULL composite. +// +// The wrapper now namespaces room names by app id to avoid schema +// collisions between different editors opening the same file +// (e.g. `text-editor::` vs `codemirror::`). Strip +// any `::` prefix before parsing so the Graph probe targets the +// raw file id. +function parseDocumentId(documentName) { + const scopeSep = documentName.indexOf('::') + const fileId = scopeSep >= 0 ? documentName.slice(scopeSep + 2) : documentName + const sep = fileId.indexOf('!') + if (sep <= 0 || sep === fileId.length - 1) { + throw new Error(`malformed documentName="${documentName}"`) + } + return { driveId: fileId.slice(0, sep), itemId: fileId } +} + +// Probes OC's Graph API for the user's effective access AND the file's +// current native etag. Returns `{ canWrite, etag }` on success; `null` when +// OC denies access entirely (401/403/404). +// +// Two parallel calls: +// - Graph /permissions for the effective action set (top-level +// @libre.graph.permissions.actions.allowedValues, which is the merged +// PermissionSet that backs WebDAV's oc:permissions). +// - WebDAV HEAD for the native eTag (Graph's /items endpoint is share-jail- +// only and 400s on personal drives; WebDAV works uniformly). +async function probeFileAccess(token, documentName) { + const { driveId, itemId } = parseDocumentId(documentName) + const permsUrl = + `${opencloudUrl}/graph/v1beta1/drives/${encodeURIComponent(driveId)}` + + `/items/${encodeURIComponent(itemId)}/permissions` + const davUrl = `${opencloudUrl}/remote.php/dav/spaces/${encodeURIComponent(itemId)}` + const headers = { Authorization: `Bearer ${token}` } + + const [permsRes, headRes] = await Promise.all([ + fetch(permsUrl, { headers }), + fetch(davUrl, { method: 'HEAD', headers }) + ]) + + if ([permsRes.status, headRes.status].some((s) => s === 401 || s === 403 || s === 404)) { + return null + } + if (!permsRes.ok) { + const detail = await permsRes.text().catch(() => '') + throw new Error(`graph permissions returned ${permsRes.status}: ${detail.slice(0, 200)}`) + } + if (!headRes.ok) { + const detail = await headRes.text().catch(() => '') + throw new Error(`webdav HEAD returned ${headRes.status}: ${detail.slice(0, 200)}`) + } + + const permsBody = await permsRes.json() + const allowed = Array.isArray(permsBody?.['@libre.graph.permissions.actions.allowedValues']) + ? permsBody['@libre.graph.permissions.actions.allowedValues'] + : [] + const canWrite = allowed.some((a) => WRITE_ACTION.test(a)) + + // WebDAV emits the strong validator under `ETag` (and sometimes `OC-ETag` + // for OC-specific extensions). Strip surrounding quotes for consistency + // with the etag the wrapper sees from `props.resource.etag`. + const rawEtag = headRes.headers.get('etag') || headRes.headers.get('oc-etag') || '' + const etag = rawEtag.replace(/^"(.*)"$/, '$1') + + return { canWrite, etag } +} + +const META_KEY = '_oc_meta' + +const server = new Server({ + port, + address: '0.0.0.0', + // 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) { + throw new Error('missing token') + } + + // App-version gate. First connect to a documentName sets the baseline, + // subsequent connects with a different appVersion are rejected so old + // clients can't poison the room. Empty client appVersion is permitted + // (back-compat for the integration test harness using a raw provider). + const clientAppVersion = requestParameters.get('appVersion') ?? '' + const baselineAppVersion = appVersionByDocument.get(documentName) + if (clientAppVersion && baselineAppVersion && clientAppVersion !== baselineAppVersion) { + throw new Error( + `app version mismatch for document="${documentName}": ` + + `client=${clientAppVersion} room=${baselineAppVersion}, please reload` + ) + } + if (clientAppVersion && !baselineAppVersion) { + appVersionByDocument.set(documentName, clientAppVersion) + } + + // Dev shortcut for integration tests: any token matching DEV_FAKE_TOKEN + // returns a synthetic identity. ACL check is skipped (tests use random + // documentNames that don't exist in OC). Disabled when DEV_FAKE_TOKEN is + // unset. Tests can pass `devEtag` to drive the stale-state detection + // path without touching real OC. + if (devFakeToken && token === devFakeToken) { + const id = 'dev-fake-user' + const nativeEtag = requestParameters.get('devEtag') ?? '' + console.log(`[onAuthenticate] dev-fake document="${documentName}" nativeEtag="${nativeEtag}"`) + return { + nativeEtag, + user: { + id, + displayName: 'Dev Fake User', + color: deterministicColor(id) + } + } + } + + const me = await validateTokenAgainstOpenCloud(token) + const id = me.id ?? me.userPrincipalName ?? 'unknown' + + // ACL + native etag probe via Graph: enforces access AND captures the + // current native etag so onLoadDocument can detect a stale persisted + // Y.Doc snapshot (Hocuspocus persistence vs external file write). + const access = await probeFileAccess(token, documentName) + if (access === null) { + throw new Error(`access denied for document="${documentName}"`) + } + const readOnly = !access.canWrite + + console.log( + `[onAuthenticate] document="${documentName}" user="${me.displayName ?? id}" ` + + `id="${id}" readOnly=${readOnly} nativeEtag="${access.etag}"` + ) + return { + readOnly, + nativeEtag: access.etag, + clientAppVersion, + user: { + id, + displayName: me.displayName ?? me.userPrincipalName ?? id, + color: deterministicColor(id) + } + } + }, + + // Stale-state detection — DISABLED. + // + // 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 ?? '-'}`) + }, + + async onDisconnect({ documentName, clientsCount }) { + console.log(`[onDisconnect] document="${documentName}" remaining=${clientsCount}`) + if (clientsCount === 0) { + // Forget the version baseline once the room empties out so a new + // deploy can start fresh without manual restart. + appVersionByDocument.delete(documentName) + } + }, + + // 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. 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 = { + id: user.id, + name: user.displayName, + color: user.color + } + for (const state of states.values()) { + state.user = canonical + } + } +}) + +server.listen().then(() => { + console.log(`hocuspocus v4 listening on :${port}, oc=${opencloudUrl}`) +}) diff --git a/dev/docker/opencloud.web.config.json b/dev/docker/opencloud.web.config.json index 3838f4896d..6db42416a2 100644 --- a/dev/docker/opencloud.web.config.json +++ b/dev/docker/opencloud.web.config.json @@ -18,6 +18,9 @@ "activities", "preview", "mail", - "contacts" + "contacts", + "codemirror", + "tiptap", + "excalidraw" ] } 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 6783484e2a..0c1527e10f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -350,11 +350,36 @@ services: logging: driver: ${LOG_DRIVER:-local} + hocuspocus: + build: ./dev/docker/hocuspocus + extra_hosts: + - host.docker.internal:${DOCKER_HOST:-host-gateway} + environment: + PORT: '1234' + OPENCLOUD_URL: https://host.docker.internal:9200 + # Dev only: self-signed cert from OC's Traefik + NODE_TLS_REJECT_UNAUTHORIZED: '0' + # Dev only: allows the integration tests to bypass real OIDC tokens. + # Remove for prod. + DEV_FAKE_TOKEN: dev-integration-token + volumes: + # Dev override: edit server.js without rebuild; remove for prod + - ./dev/docker/hocuspocus/server.js:/app/server.js:ro + 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 + volumes: uploads: opencloud-config: opencloud-federated-config: stalwart-data: + hocuspocus-data: networks: traefik: diff --git a/package.json b/package.json index 57c4bd4e23..32200d9fc5 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/luxon": "^3.7.1", "@types/mark.js": "^8.11.12", "@types/qs": "^6.15.0", + "@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-vue": "6.0.7", "@vitest/coverage-v8": "^4.1.2", "@vitest/web-worker": "^4.1.2", @@ -74,6 +75,8 @@ "pino": "10.3.1", "pino-pretty": "13.1.3", "qs": "^6.15.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "tailwindcss": "^4.2.2", "ts-node": "10.9.2", "tslib": "2.8.1", 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..a9ea4f3805 --- /dev/null +++ b/packages/web-app-codemirror/src/App.vue @@ -0,0 +1,41 @@ + + + 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-excalidraw/extension.d.ts b/packages/web-app-excalidraw/extension.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/web-app-excalidraw/extension.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/web-app-excalidraw/l10n/translations.json b/packages/web-app-excalidraw/l10n/translations.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/web-app-excalidraw/l10n/translations.json @@ -0,0 +1 @@ +{} diff --git a/packages/web-app-excalidraw/package.json b/packages/web-app-excalidraw/package.json new file mode 100644 index 0000000000..8c02e5c440 --- /dev/null +++ b/packages/web-app-excalidraw/package.json @@ -0,0 +1,29 @@ +{ + "name": "excalidraw", + "version": "0.1.0", + "private": true, + "description": "OpenCloud Web collaborative Excalidraw whiteboard editor", + "license": "AGPL-3.0", + "type": "module", + "dependencies": { + "@excalidraw/excalidraw": "^0.18.0", + "fractional-indexing": "^3.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "veaury": "^2.6.0", + "y-excalidraw": "^2.0.12", + "y-protocols": "^1.0.7", + "yjs": "^13.6.0" + }, + "devDependencies": { + "@opencloud-eu/web-test-helpers": "workspace:*", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + }, + "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-excalidraw/src/App.vue b/packages/web-app-excalidraw/src/App.vue new file mode 100644 index 0000000000..6e266e1c66 --- /dev/null +++ b/packages/web-app-excalidraw/src/App.vue @@ -0,0 +1,37 @@ + + + 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/packages/web-app-text-editor/package.json b/packages/web-app-text-editor/package.json index a76188aeeb..13ae0a3775 100644 --- a/packages/web-app-text-editor/package.json +++ b/packages/web-app-text-editor/package.json @@ -4,6 +4,14 @@ "private": true, "description": "OpenCloud Web simple text editor", "license": "AGPL-3.0", + "dependencies": { + "@hocuspocus/provider": "^4.0.0", + "@tiptap/core": "^3.20.4", + "@tiptap/extension-collaboration": "^3.20.4", + "@tiptap/vue-3": "^3.20.4", + "y-protocols": "^1.0.7", + "yjs": "^13.6.0" + }, "devDependencies": { "@opencloud-eu/web-test-helpers": "workspace:*" }, diff --git a/packages/web-app-text-editor/src/App.vue b/packages/web-app-text-editor/src/App.vue index e448bfd293..38dca23dc4 100644 --- a/packages/web-app-text-editor/src/App.vue +++ b/packages/web-app-text-editor/src/App.vue @@ -1,48 +1,54 @@ diff --git a/packages/web-app-text-editor/src/TextEditorBinding.vue b/packages/web-app-text-editor/src/TextEditorBinding.vue new file mode 100644 index 0000000000..ed71b84a1b --- /dev/null +++ b/packages/web-app-text-editor/src/TextEditorBinding.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/web-app-text-editor/src/adapters/textEditorAdapter.ts b/packages/web-app-text-editor/src/adapters/textEditorAdapter.ts new file mode 100644 index 0000000000..9eb1e52256 --- /dev/null +++ b/packages/web-app-text-editor/src/adapters/textEditorAdapter.ts @@ -0,0 +1,85 @@ +import { Editor } from '@tiptap/core' +import { Collaboration } from '@tiptap/extension-collaboration' +import type * as Y from 'yjs' +import type { Editor as TiptapVueEditor } from '@tiptap/vue-3' +import type { CollaborativeAdapter } from '@opencloud-eu/web-pkg' +import type { ContentTypeStrategy } from '@opencloud-eu/web-pkg/editor' + +// CollaborativeWrapper binds editor state to a named Y.XmlFragment. We use +// the default field name `'default'`; the editor component (TextEditorBinding, +// which calls useTextEditor with the same ydoc + default fragment) must +// match. +const FRAGMENT = 'default' + +/** + * Bridges the existing `web-pkg/editor` strategy contract to the + * `CollaborativeAdapter` contract the `CollaborativeWrapper` expects. + * + * Each strategy already knows how to convert between its native string + * format (markdown / HTML / plain text / tiptap-json) and a Tiptap editor + * state. The adapter wraps that with the bit of plumbing the wrapper + * needs: hydration through a headless editor that materialises into the + * shared Y.Doc, serialisation that prefers the live editor when bound + * (via the `getAdapterContext()` channel) and only spawns a headless + * fallback when no UI is mounted, and `hasContent` / `reset` against the + * `'default'` Y.XmlFragment that `Collaboration` writes to. + * + * Must be called from a Vue setup context — the strategy reference comes + * from `useContentStrategy()` which in turn calls `useGettext()`. + */ +export function makeTextEditorAdapter(strategy: ContentTypeStrategy): CollaborativeAdapter { + function makeHeadlessEditor(ydoc: Y.Doc): Editor { + const detached = document.createElement('div') + return new Editor({ + element: detached, + extensions: [ + ...strategy.extensions(), + Collaboration.configure({ document: ydoc, field: FRAGMENT }) + ] + }) + } + + function setContentOptions(): Record { + const opts: Record = { emitUpdate: false } + if (strategy.editorContentType) { + opts.contentType = strategy.editorContentType() + } + return opts + } + + return { + hydrate(ydoc, content) { + if (!content) return + const editor = makeHeadlessEditor(ydoc) + try { + editor.commands.setContent( + strategy.deserialize(content) as Parameters[0], + setContentOptions() + ) + } finally { + editor.destroy() + } + }, + + serialize(ydoc, context) { + const live = (context as { editor?: TiptapVueEditor } | undefined)?.editor + if (live) return strategy.serialize(live) + const editor = makeHeadlessEditor(ydoc) + try { + return strategy.serialize(editor as unknown as TiptapVueEditor) + } finally { + editor.destroy() + } + }, + + hasContent(ydoc) { + return ydoc.getXmlFragment(FRAGMENT).length > 0 + }, + + reset(ydoc) { + const frag = ydoc.getXmlFragment(FRAGMENT) + if (frag.length === 0) return + ydoc.transact(() => frag.delete(0, frag.length), 'reset') + } + } +} 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 638d1d6b44..cecab92be2 100644 --- a/packages/web-app-text-editor/tests/unit/app.spec.ts +++ b/packages/web-app-text-editor/tests/unit/app.spec.ts @@ -1,10 +1,47 @@ import { PartialComponentProps, defaultPlugins, mount } from '@opencloud-eu/web-test-helpers' import { mock } from 'vitest-mock-extended' +import { defineComponent, h } from 'vue' import type { Resource } from '@opencloud-eu/web-client' import App from '../../src/App.vue' -vi.mock('@opencloud-eu/web-pkg') -vi.mock('@opencloud-eu/web-pkg/editor') +// Stub CollaborativeWrapper so the test doesn't have to mount a real +// HocuspocusProvider / Y.Doc chain. The stub just renders a div with the +// stable class the prior version of this test asserted against — App.vue +// itself is now a thin shell, so it's enough to verify it mounts the +// wrapper with the correct adapter / editor / realtime contract. +vi.mock('@opencloud-eu/web-pkg', async () => { + return { + CollaborativeWrapper: defineComponent({ + name: 'CollaborativeWrapperStub', + props: [ + 'resource', + 'currentContent', + 'isReadOnly', + 'adapter', + 'editor', + 'appVersion', + 'realtimeUrl' + ], + setup() { + return () => h('div', { class: 'oc-text-editor' }) + } + }) + } +}) + +vi.mock('@opencloud-eu/web-pkg/editor', () => { + return { + useContentStrategy: () => ({ + resolveStrategy: () => ({ + editorContentType: () => 'markdown', + extensions: (): unknown[] => [], + editorActionGroups: (): unknown[] => [], + serialize: () => '', + deserialize: (s: string) => s + }) + }) + } +}) describe('Text editor app', () => { it('shows the editor', () => { 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..4473846f15 --- /dev/null +++ b/packages/web-app-tiptap/src/App.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/web-app-tiptap/src/TiptapEditor.vue b/packages/web-app-tiptap/src/TiptapEditor.vue new file mode 100644 index 0000000000..ac0b6c3826 --- /dev/null +++ b/packages/web-app-tiptap/src/TiptapEditor.vue @@ -0,0 +1,230 @@ + + + + + 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..187cc7daba --- /dev/null +++ b/packages/web-app-tiptap/src/adapters/tiptapMarkdown.ts @@ -0,0 +1,74 @@ +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, context?: unknown): string { + // When the wrapper has a live editor bound to this Y.Doc (the common + // case during interactive editing) we reuse it directly — no DOM, no + // per-keystroke headless spawn. Fall back to a headless editor only + // when nothing is bound (e.g. stale-recovery on a peer that holds the + // doc but never mounted a UI). + const live = (context as { editor?: Editor } | undefined)?.editor + if (live) return live.getMarkdown() + + 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..0c1be06220 100644 --- a/packages/web-pkg/package.json +++ b/packages/web-pkg/package.json @@ -49,11 +49,13 @@ "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:^", "@sentry/vue": "^10.46.0", "@tiptap/core": "^3.20.4", + "@tiptap/extension-collaboration": "^3.20.4", "@tiptap/extension-document": "^3.20.4", "@tiptap/extension-hard-break": "^3.20.4", "@tiptap/extension-image": "^3.20.4", @@ -71,6 +73,7 @@ "@tiptap/starter-kit": "^3.20.4", "@tiptap/suggestion": "^3.20.4", "@tiptap/vue-3": "^3.20.4", + "@tiptap/y-tiptap": "^3.0.0", "@uppy/core": "^5.2.0", "@uppy/tus": "^5.1.1", "@uppy/utils": "^7.2.0", @@ -91,16 +94,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/AppTemplates/AppWrapper.vue b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue index 88d757eca1..8b57bc7169 100644 --- a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue +++ b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue @@ -434,23 +434,71 @@ 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 + // file just before us. Our editor's content (Y.Doc-synced) already + // includes the peer's edits, so simply refetching the file to grab + // the fresh etag and retrying the save lets us keep going without + // bothering the user. We only fall through to the conflict popup + // when the refetch / retry path itself fails. + if (e.statusCode === 412 || e.statusCode === 409) { + try { + const fresh = yield* call(getFileContents(currentFileContext, { ...fileContentOptions })) + const freshEtag = fresh.headers['OC-ETag'] + + if (fresh.body === newContent) { + // No real content divergence — only our etag tracking was + // stale. Reconcile silently. + serverContent.value = newContent + currentETag.value = freshEtag + if (unref(resource)) { + resourcesStore.upsertResource({ ...unref(resource), etag: freshEtag }) + resource.value = { ...unref(resource), etag: freshEtag } + } + return + } + + // Server has a different content (typical collab case: peer + // saved, our Y.Doc has peer's edits + our own additions). Retry + // the PUT with the fresh etag — that publishes our combined + // state. Cross-app or external writers will be overwritten + // here; collaborating editors of the same file in different + // apps remains a known footgun documented in + // REALTIME_COLLAB_MIGRATION.md. + const retry = yield putFileContents(currentFileContext, { + content: newContent as string, + previousEntityTag: freshEtag + }) + 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 + // conflict popup so they can still recover by copying out. + } + errorPopup( + new HttpError( + $gettext( + 'This file was updated outside this window. Please copy your changes or save the file under a new name (»Save As...«).' + ), + e.response + ) + ) + return + } switch (e.statusCode) { case 401: case 403: errorPopup(new HttpError($gettext("You're not authorized to save this file"), e.response)) break - case 409: - case 412: - errorPopup( - new HttpError( - $gettext( - 'This file was updated outside this window. Please copy your changes or save the file under a new name (»Save As...«).' - ), - e.response - ) - ) - break case 507: const space = spacesStore.spaces.find( (space) => space.id === unref(resource).storageId && isProjectSpaceResource(space) @@ -720,6 +768,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 new file mode 100644 index 0000000000..8fb9a513d1 --- /dev/null +++ b/packages/web-pkg/src/components/Collaborative/CollaborativeWrapper.vue @@ -0,0 +1,574 @@ + + + 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..4b82aabfe9 --- /dev/null +++ b/packages/web-pkg/src/components/Collaborative/types.ts @@ -0,0 +1,56 @@ +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. + * + * `context` is an opaque object the bound editor component publishes via + * `defineExpose({ getAdapterContext() })`. The wrapper passes it through + * untyped because adapters may need wildly different shapes — a Tiptap + * adapter wants the live `Editor` instance to call `getMarkdown()` on + * directly (avoiding a per-keystroke headless-editor spawn), a CodeMirror + * adapter just reads `Y.Text.toString()` and needs nothing. Adapters cast + * `context` to their expected shape and treat absence as "fall back to + * Y.Doc-only serialization". + * + * `context` is `undefined` when no editor is bound (e.g. during + * stale-recovery on a peer that holds the doc but never mounted a UI) + * or when the bound editor doesn't expose `getAdapterContext`. + */ + serialize(ydoc: Y.Doc, context?: unknown): 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/packages/web-pkg/src/editor/composables/strategies/html.ts b/packages/web-pkg/src/editor/composables/strategies/html.ts index ebae3396e3..c86e4bf4b7 100644 --- a/packages/web-pkg/src/editor/composables/strategies/html.ts +++ b/packages/web-pkg/src/editor/composables/strategies/html.ts @@ -37,7 +37,8 @@ export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrate const extensions = (): Extension[] => { return [ - StarterKit.configure({ link: false }), + // See markdown strategy for why `undoRedo: false`. + StarterKit.configure({ link: false, undoRedo: false }), Link.configure({ openOnClick: true, autolink: true, diff --git a/packages/web-pkg/src/editor/composables/strategies/markdown.ts b/packages/web-pkg/src/editor/composables/strategies/markdown.ts index 325632e342..a4e22d2655 100644 --- a/packages/web-pkg/src/editor/composables/strategies/markdown.ts +++ b/packages/web-pkg/src/editor/composables/strategies/markdown.ts @@ -29,7 +29,12 @@ export const useStrategyMarkdown = (editorState: TextEditorState): ContentTypeSt const extensions = (): Extension[] => { return [ - StarterKit.configure({ link: false }), + // `undoRedo: false` — when the host wires a Y.Doc through this + // editor instance (collab mode), the `Collaboration` extension brings + // yUndoPlugin from @tiptap/y-tiptap which is the collab-aware undo + // manager. Tiptap warns + double-stacks history if both run together. + // Read-only callers don't exercise undo so the flag is harmless there. + StarterKit.configure({ link: false, undoRedo: false }), Markdown, Link.configure({ openOnClick: true, diff --git a/packages/web-pkg/src/editor/composables/strategies/tiptapJson.ts b/packages/web-pkg/src/editor/composables/strategies/tiptapJson.ts index 29bfd255eb..a7bdae8d28 100644 --- a/packages/web-pkg/src/editor/composables/strategies/tiptapJson.ts +++ b/packages/web-pkg/src/editor/composables/strategies/tiptapJson.ts @@ -37,7 +37,8 @@ export const useStrategyTiptapJson = (editorState: TextEditorState): ContentType const extensions = (): Extension[] => { return [ - StarterKit.configure({ link: false }), + // See markdown strategy for why `undoRedo: false`. + StarterKit.configure({ link: false, undoRedo: false }), Link.configure({ openOnClick: true, autolink: true, diff --git a/packages/web-pkg/src/editor/composables/useTextEditor.ts b/packages/web-pkg/src/editor/composables/useTextEditor.ts index 4286dbd5c6..8cbe9b611d 100644 --- a/packages/web-pkg/src/editor/composables/useTextEditor.ts +++ b/packages/web-pkg/src/editor/composables/useTextEditor.ts @@ -1,12 +1,51 @@ import { ref, computed, onBeforeUnmount, watch, unref, onMounted, triggerRef } from 'vue' import { useEditor } from '@tiptap/vue-3' +import { Extension } from '@tiptap/core' import { Placeholder } from '@tiptap/extension-placeholder' +import { Collaboration } from '@tiptap/extension-collaboration' +import { yCursorPlugin } from '@tiptap/y-tiptap' +import type { Awareness } from 'y-protocols/awareness' import type { ShallowRef } from 'vue' import type { Editor } from '@tiptap/vue-3' import type { TextEditorOptions, TextEditorInstance, TextEditorState } from '../types' import { SlashCommands } from '../extensions' import { useContentStrategy } from './useContentStrategy' +// Custom Tiptap extension that wires y-tiptap's yCursorPlugin to a given +// Awareness. We bypass `@tiptap/extension-collaboration-cursor` because +// its 3.0.0 release still imports `yCursorPlugin` from the upstream +// `y-prosemirror` package — a different module with a different +// `ySyncPluginKey` than the `@tiptap/y-tiptap` fork that +// `@tiptap/extension-collaboration` uses. Mixing them throws +// "Cannot read properties of undefined (reading 'doc')" on first paint. +// y-tiptap's yCursorPlugin shares ySyncPluginKey with Collaboration so +// the cursor plugin can find the sync state. +function makeCollabCursorExtension(awareness: Awareness): Extension { + return Extension.create({ + name: 'yCollaborationCursor', + addProseMirrorPlugins() { + return [ + yCursorPlugin(awareness, { + // Emit the same `.collaboration-cursor__caret/__label` DOM the + // (broken) upstream extension would have, so consumer CSS keeps + // working unchanged. + cursorBuilder: (user: { name?: string; color?: string }) => { + const cursor = document.createElement('span') + cursor.classList.add('collaboration-cursor__caret') + cursor.setAttribute('style', `border-color: ${user.color ?? '#ffa500'}`) + const label = document.createElement('div') + label.classList.add('collaboration-cursor__label') + label.setAttribute('style', `background-color: ${user.color ?? '#ffa500'}`) + label.insertBefore(document.createTextNode(user.name ?? ''), null) + cursor.insertBefore(label, null) + return cursor + } + }) + ] + } + }) +} + export function useTextEditor(options: TextEditorOptions): TextEditorInstance { const { resolveStrategy } = useContentStrategy() const state: TextEditorState = { @@ -16,10 +55,29 @@ export function useTextEditor(options: TextEditorOptions): TextEditorInstance { const contentType = ref(options.contentType) const readonly = ref(options.readonly ?? false) const strategy = resolveStrategy(options.contentType, state) + const collabFragment = options.ydocFragment ?? 'default' let debounceTimer: ReturnType | null = null const extensions = strategy.extensions() + if (options.ydoc) { + // Bind ProseMirror state to the shared Y.Doc. With Collaboration active, + // the editor's initial content is read from the Y.Doc (not from the + // `content` option), so we skip the `content` assignment below. The + // strategies already disable `StarterKit.undoRedo` so yUndoPlugin can + // take over without conflict. + extensions.push( + Collaboration.configure({ + document: options.ydoc, + field: collabFragment + }) as (typeof extensions)[number] + ) + if (options.awareness) { + // Render remote peers' carets + labels via y-tiptap's yCursorPlugin. + // Skipped when only ydoc is provided (local mode, no remote peers). + extensions.push(makeCollabCursorExtension(options.awareness) as (typeof extensions)[number]) + } + } if (options.slashCommands !== false) { const resolvedGroups = strategy.editorActionGroups() if (resolvedGroups.length > 0) { @@ -39,7 +97,14 @@ export function useTextEditor(options: TextEditorOptions): TextEditorInstance { // to satisfy TextEditorInstance. The destroy() method sets it to null explicitly. const editorOptions: Record = { extensions, - content: unref(options.modelValue) ? strategy.deserialize(unref(options.modelValue)) : '', + // In collab mode the wrapper hydrates the Y.Doc — passing `content` here + // would race against the CRDT and produce duplicated state. Leave the + // editor blank; Collaboration will paint Y.Doc state into it. + content: options.ydoc + ? '' + : unref(options.modelValue) + ? strategy.deserialize(unref(options.modelValue)) + : '', editable: !readonly.value } @@ -47,6 +112,9 @@ export function useTextEditor(options: TextEditorOptions): TextEditorInstance { if (!unref(editor) || unref(editor)?.isFocused) { return } + // In collab mode the Y.Doc is the source of truth — never round-trip + // `modelValue` back into the editor (would clobber peer edits). + if (options.ydoc) return setContent(content) }) @@ -129,9 +197,12 @@ export function useTextEditor(options: TextEditorOptions): TextEditorInstance { onMounted(() => { editor.value?.on('selectionUpdate', triggerEditorUpdate) editor.value?.on('transaction', triggerEditorUpdate) - if (!unref(readonly)) { - focus() - } + // Auto-focus on mount used to live here — moved to the consumer. + // The composable's job is to build an Editor; deciding when (or + // whether) to put the cursor in it is UX policy and belongs with the + // caller. All current consumers either rely on the user clicking in + // (text-editor) or render read-only previews (app-store description, + // files list/space headers) and never wanted auto-focus anyway. }) onBeforeUnmount(() => { diff --git a/packages/web-pkg/src/editor/index.ts b/packages/web-pkg/src/editor/index.ts index 2aa1782b42..6102c07623 100644 --- a/packages/web-pkg/src/editor/index.ts +++ b/packages/web-pkg/src/editor/index.ts @@ -1,5 +1,7 @@ export type { ContentType, TextEditorOptions, TextEditorInstance } from './types' export { useTextEditor } from './composables/useTextEditor' +export { useContentStrategy } from './composables/useContentStrategy' +export type { ContentTypeStrategy } from './composables/strategies/types' export { default as TextEditorProvider } from './components/TextEditorProvider.vue' export { default as TextEditorContent } from './components/TextEditorContent.vue' export { default as TextEditorToolbar } from './components/TextEditorToolbar.vue' diff --git a/packages/web-pkg/src/editor/styles/collab-cursor.css b/packages/web-pkg/src/editor/styles/collab-cursor.css new file mode 100644 index 0000000000..033fe7b608 --- /dev/null +++ b/packages/web-pkg/src/editor/styles/collab-cursor.css @@ -0,0 +1,31 @@ +/* Remote-peer cursor + name label rendered by yCursorPlugin from + * @tiptap/y-tiptap, wired by `useTextEditor` when an Awareness is bound. + * + * Without these the default browser block layout makes the label a + * full-width band across the line. Selectors are unscoped so the same + * styles apply to any consumer of the composable. */ + +.text-editor-content .collaboration-cursor__caret { + position: relative; + margin-left: -1px; + margin-right: -1px; + border-left: 1px solid; + border-right: 1px solid; + word-break: normal; + pointer-events: none; +} + +.text-editor-content .collaboration-cursor__label { + position: absolute; + top: -1.4em; + left: -1px; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: normal; + user-select: none; + color: white; + padding: 0.1rem 0.3rem; + border-radius: 3px 3px 3px 0; + white-space: nowrap; +} diff --git a/packages/web-pkg/src/editor/styles/content.css b/packages/web-pkg/src/editor/styles/content.css index 5aef3e34ed..eef76866d4 100644 --- a/packages/web-pkg/src/editor/styles/content.css +++ b/packages/web-pkg/src/editor/styles/content.css @@ -1,2 +1,3 @@ @import './text-editor.css'; @import './markdown-source-mode.css'; +@import './collab-cursor.css'; diff --git a/packages/web-pkg/src/editor/types.ts b/packages/web-pkg/src/editor/types.ts index 38c341d354..74bbfb9b19 100644 --- a/packages/web-pkg/src/editor/types.ts +++ b/packages/web-pkg/src/editor/types.ts @@ -1,5 +1,7 @@ import type { ShallowRef, Ref, ComputedRef } from 'vue' import { Editor } from '@tiptap/vue-3' +import type * as Y from 'yjs' +import type { Awareness } from 'y-protocols/awareness' import { EditorActionGroup } from './composables' export type ContentType = 'plain-text' | 'markdown' | 'html' | 'tiptap-json' @@ -13,6 +15,26 @@ export interface TextEditorOptions { onUpdate?: (content: string) => void onRequestLinkUrl?: (currentUrl?: string) => Promise onRequestImageUrl?: () => Promise + /** + * When set, the editor binds its ProseMirror state to this Y.Doc via the + * `@tiptap/extension-collaboration` extension. Initial content is taken + * from the Y.Doc state (populated by the host's hydration path) instead + * of from `modelValue`. The undo manager comes from `yUndoPlugin` via + * `@tiptap/y-tiptap` — `StarterKit`'s built-in `undoRedo` is already + * disabled in every strategy to avoid conflict. + */ + ydoc?: Y.Doc + /** + * Y.XmlFragment field name inside the Y.Doc. Matches the + * `CollaborativeWrapper` adapter convention. Defaults to `'default'`. + */ + ydocFragment?: string + /** + * Awareness instance from the same room as `ydoc`. When set (collab + * mode), the editor renders remote peer cursors via `yCursorPlugin` + * from `@tiptap/y-tiptap`. Ignored when `ydoc` is not also set. + */ + awareness?: Awareness } export interface TextEditorState { 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) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90cc532d0d..549fbd0c9a 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 @@ -63,9 +63,12 @@ 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.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) @@ -111,12 +114,18 @@ 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 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 +134,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 +207,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 +231,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)(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: @@ -258,26 +267,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 +416,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': @@ -469,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': @@ -735,6 +830,9 @@ importers: packages/web-app-text-editor: 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 @@ -744,12 +842,73 @@ importers: '@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/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)) vue-concurrency: specifier: 5.0.3 version: 5.0.3(vue@3.5.34(typescript@6.0.3)) 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-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:* @@ -809,13 +968,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 +984,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 +1001,64 @@ 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-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/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)) + '@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) '@uppy/core': specifier: ^5.2.0 version: 5.2.0 @@ -951,6 +1119,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 +1134,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 +1152,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 +1301,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: @@ -1228,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: @@ -1267,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'} @@ -1279,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==} @@ -1291,6 +1484,51 @@ 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==} + + '@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'} @@ -1598,15 +1836,49 @@ 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==} + '@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'} @@ -1629,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'} @@ -1656,6 +1931,39 @@ 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==} + + '@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==} @@ -1832,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} @@ -2248,163 +2838,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-dropcursor@3.23.4': + resolution: {integrity: sha512-ujJQUIENk0RwVFCh5g/TOSEv1a7Pnam/cjHmSUqHWUNZkYS9aOqjm+JfURJPCinRS2oHvo3AARul5mkKgDJYcA==} + peerDependencies: + '@tiptap/extensions': 3.23.4 - '@tiptap/extension-floating-menu@3.23.5': - resolution: {integrity: sha512-kP0bZKH/lxNogfvoIy/YJZ5gkty0OwqFVtQUwoc85vXYUfvy5Jh1VdO053tCE1iDzmvOITUpcb+MdWryP8dBxA==} + '@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 +3016,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==} @@ -2459,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==} @@ -2468,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==} @@ -2508,8 +3218,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==} @@ -2517,9 +3227,20 @@ 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==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2634,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} @@ -2920,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'} @@ -3018,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==} @@ -3093,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==} @@ -3120,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'} @@ -3147,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==} @@ -3195,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==} @@ -3223,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==} @@ -3235,6 +4012,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'} @@ -3242,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==} @@ -3267,10 +4052,166 @@ packages: custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} + engines: {node: '>=0.10'} + + 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'} @@ -3278,6 +4219,9 @@ packages: 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'} @@ -3308,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'} @@ -3323,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==} @@ -3417,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'} @@ -3424,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'} @@ -3670,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==} @@ -3690,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'} @@ -3698,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'} @@ -3737,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'} @@ -3744,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'} @@ -3828,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==} @@ -3839,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'} @@ -3867,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'} @@ -3960,6 +4955,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'} @@ -3983,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'} @@ -4035,19 +5051,41 @@ 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'} + 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'} @@ -4151,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==} @@ -4178,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==} @@ -4240,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'} @@ -4265,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==} @@ -4346,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==} @@ -4360,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} @@ -4470,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'} @@ -4507,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'} @@ -4514,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'} @@ -4532,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'} @@ -4574,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==} @@ -4628,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'} @@ -4696,8 +5797,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==} @@ -4734,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'} @@ -4757,16 +5861,55 @@ packages: randomfill@1.0.4: resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} - read-installed-packages@2.0.1: - resolution: {integrity: sha512-t+fJOFOYaZIjBpTVxiV8Mkt7yQyy4E6MSrrnt5FmPd4enYvpU/9DYGirDmN1XQwkfeuWIhM/iu0t2rm6iSr0CA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - read-package-json@6.0.4: - resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - deprecated: This package is no longer supported. Please use @npmcli/package-json instead. + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 - read-package-up@12.0.0: + 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} + + read-package-json@6.0.4: + resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. Please use @npmcli/package-json instead. + + read-package-up@12.0.0: resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} engines: {node: '>=20'} @@ -4851,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} @@ -4864,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==} @@ -4878,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==} @@ -4954,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==} @@ -5057,6 +6227,12 @@ packages: strnum@2.3.0: resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + 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'} @@ -5182,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'} @@ -5289,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==} @@ -5308,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==} @@ -5461,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==} @@ -5545,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'} @@ -5628,11 +6861,34 @@ 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-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'} + 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'} @@ -5647,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==} @@ -5770,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 @@ -5806,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 @@ -5818,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 @@ -5831,6 +7113,98 @@ 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 + '@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 @@ -6019,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' @@ -6030,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' @@ -6158,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 @@ -6167,8 +7596,30 @@ 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': + 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': @@ -6186,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 @@ -6221,6 +7678,51 @@ 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': {} + + '@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))': @@ -6276,7 +7778,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 +7786,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 +7794,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 +7802,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 @@ -6403,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 @@ -6684,159 +8466,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,74 +8638,200 @@ 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/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.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': {} - '@tiptap/suggestion@3.23.5(@tiptap/core@3.23.5(@tiptap/pm@3.23.5))(@tiptap/pm@3.23.5)': + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': dependencies: - '@tiptap/core': 3.23.5(@tiptap/pm@3.23.5) - '@tiptap/pm': 3.23.5 + '@types/geojson': 7946.0.16 - '@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))': + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': dependencies: - '@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) - 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) + '@types/d3-color': 3.1.3 - '@transloadit/prettier-bytes@0.3.5': {} + '@types/d3-path@3.1.1': {} - '@tsconfig/node10@1.0.12': {} + '@types/d3-polygon@3.0.2': {} - '@tsconfig/node12@1.0.11': {} + '@types/d3-quadtree@3.0.6': {} - '@tsconfig/node14@1.0.3': {} + '@types/d3-random@3.0.3': {} - '@tsconfig/node16@1.0.4': {} + '@types/d3-scale-chromatic@3.1.0': {} - '@tybys/wasm-util@0.10.2': + '@types/d3-scale@4.0.9': dependencies: - tslib: 2.8.1 - optional: true + '@types/d3-time': 3.0.4 - '@types/chai@5.2.3': + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@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': {} @@ -6919,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 @@ -6958,7 +8880,7 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/node@25.9.1': + '@types/node@25.9.0': dependencies: undici-types: 7.24.6 @@ -6966,8 +8888,18 @@ 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': {} + '@types/trusted-types@2.0.7': optional: true @@ -6979,7 +8911,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 +9060,29 @@ 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))': + '@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: - vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(sass@1.99.0)(yaml@2.9.0) + '@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.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 +9097,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 +9108,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 +9143,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: @@ -7447,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: {} @@ -7543,6 +9489,8 @@ snapshots: brorand@1.1.0: {} + browser-fs-access@0.29.1: {} + browser-resolve@2.0.0: dependencies: resolve: 1.22.11 @@ -7647,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 @@ -7670,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 @@ -7707,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: @@ -7743,6 +9709,10 @@ snapshots: commander@14.0.3: {} + commander@7.2.0: {} + + commander@8.3.0: {} + confbox@0.1.8: {} confbox@0.2.4: {} @@ -7766,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 @@ -7790,6 +9770,8 @@ snapshots: create-require@1.1.1: {} + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -7799,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 @@ -7834,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 @@ -7872,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: {} @@ -7883,6 +10059,8 @@ snapshots: detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -7990,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 @@ -8003,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 @@ -8270,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 @@ -8284,6 +10468,8 @@ snapshots: fuse.js@7.3.0: {} + fuzzy@0.1.3: {} + generator-function@2.0.1: {} get-intrinsic@1.3.0: @@ -8299,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 @@ -8354,13 +10542,17 @@ 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.1 + '@types/node': 25.9.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -8464,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: {} @@ -8486,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 @@ -8564,6 +10772,8 @@ snapshots: dependencies: ws: 8.18.0 + isomorphic.js@0.2.5: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -8589,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: {} @@ -8628,21 +10848,43 @@ 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 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 @@ -8734,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: @@ -8759,6 +11003,8 @@ snapshots: lodash.clonedeep@4.5.0: {} + lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -8812,6 +11058,8 @@ snapshots: markdown-it-container@4.0.0: {} + marked@16.4.2: {} + marked@17.0.6: {} marks-pane@1.0.9: {} @@ -8844,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 @@ -8913,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 @@ -8925,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: {} @@ -9054,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 @@ -9092,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 @@ -9118,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: {} @@ -9155,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: {} @@ -9228,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: @@ -9273,7 +11592,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 +11605,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 +11621,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 @@ -9358,6 +11677,8 @@ snapshots: punycode@2.3.1: {} + pwacompat@2.0.17: {} + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -9379,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 @@ -9488,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 @@ -9542,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: {} @@ -9554,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 @@ -9562,6 +11943,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.6 + scheduler@0.27.0: {} + scule@1.3.0: {} search-insights@2.17.3: {} @@ -9642,6 +12025,8 @@ snapshots: signal-exit@4.1.0: {} + sliced@1.0.1: {} + slide@1.1.6: {} sonic-boom@4.2.1: @@ -9750,6 +12135,10 @@ snapshots: strnum@2.3.0: {} + style-mod@4.1.3: {} + + stylis@4.4.0: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -9834,14 +12223,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 @@ -9856,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 @@ -9971,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: {} @@ -9992,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 @@ -10002,34 +12423,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,22 +12458,22 @@ 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)(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 '@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 +12482,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 +12517,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 +12539,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: @@ -10129,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: {} @@ -10240,6 +12678,8 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webworkify@1.5.0: {} + whatwg-mimetype@3.0.0: {} whatwg-url@5.0.0: @@ -10300,8 +12740,30 @@ 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-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 + 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: {} @@ -10315,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/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/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/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..74cdb04611 --- /dev/null +++ b/tests/e2e/cucumber/steps/ui/collaboration.ts @@ -0,0 +1,226 @@ +import { When, Then } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import { World } from '../../environment' +import { + awaitCollabStatus, + collabContent, + codemirrorLine, + remoteCaretCount, + remoteCaretLabelText, + excalidrawSceneElementCount, + awaitExcalidrawMounted, + 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( + /^"([^"]+)" 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 ( + 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..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'] 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/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..4d693443e2 --- /dev/null +++ b/tests/e2e/support/objects/app-files/utils/collab.ts @@ -0,0 +1,63 @@ +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() + +// 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 43f95e05d5..cf3c9ecd80 100644 --- a/tests/woodpecker/config-opencloud.json +++ b/tests/woodpecker/config-opencloud.json @@ -4,6 +4,9 @@ "apps": [ "files", "text-editor", + "codemirror", + "tiptap", + "excalidraw", "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 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',