From 010dd4a49578aa56e6ef34ab925e8c5ede6a65be Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Wed, 16 Sep 2026 16:31:48 -0500 Subject: [PATCH 1/2] refactor: consume the shared editor through public packages Replace the cloned desktop source and private renderer/store imports with the published @zennotes/app-core, @zennotes/bridge-contract and @zennotes/shared-domain packages. The archives are the desktop release core-2.51.0-core.h49d73b531d346192 (commit 8ff2cb86, tag v2.51.0, clean tree), vendored under vendor/zennotes with their manifest and pinned by the lockfile; npm run boundaries:check verifies checksums, installed versions, singleton React/CodeMirror/Lezer and the absence of private core imports. The .zennotes-commit pin and the source-preparation scripts are gone. Android keeps native storage, Cloud transport, gestures and keyboard behavior, and drives the editor and workspace through public core APIs: toolbar commands, Browse, note actions, deep links, widgets, image paste and attachments no longer touch the store or CodeMirror views. Native vault rename/move run inside core's workspace transition (save draining, reopen, rollback); gesture and picker contexts captured before a transition are rejected afterward. Note, folder and trash operations move content and comment sidecars together with rollback (relocateVaultEntries); folder rename restores its settings on failure; Empty Trash detaches before deleting. SAF reads distinguish missing files, directories, revoked access and provider failures, exercised by an instrumentation fixture; SAF rename-after-move rolls back. A Cloud vault the server reports as deleted is retired locally with its sync state archived (desktop #791). The bridge reports platform 'android' and hostKind 'android'. Carries the desktop audit overrides and the dependabot bumps from #67 and #68. --- .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 7 +- .gitignore | 2 + .zennotes-commit | 1 - README.md | 39 +- .../app/src/androidTest/AndroidManifest.xml | 8 + .../java/md/zennotes/SafFixtureProvider.java | 45 + .../md/zennotes/SafReadInstrumentedTest.java | 58 + .../main/java/md/zennotes/SafFsPlugin.java | 39 +- docs/native-boundary-validation.md | 63 + package-lock.json | 3052 +++++++++++++++-- package.json | 25 +- src/bridge/cloud-layout.ts | 2 +- src/bridge/events.ts | 2 +- src/bridge/imported-assets.ts | 2 +- src/bridge/link-metadata.ts | 2 +- src/bridge/mobile-bridge.ts | 88 +- .../mobile-cloud-sync.integration.test.ts | 145 +- src/bridge/mobile-cloud-sync.ts | 35 +- src/bridge/native-fs.ts | 3 +- src/bridge/remote-client.ts | 4 +- src/bridge/remote-vault.ts | 12 +- src/bridge/remote-workspace.ts | 2 +- src/bridge/tikz.ts | 2 +- src/bridge/vault-core.ts | 4 +- src/bridge/vault-fs.ts | 129 +- src/bridge/vault-lifecycle.test.ts | 110 + src/bridge/widget-snapshot.ts | 4 +- src/bridge/widgets.ts | 78 +- src/main.tsx | 2 + src/ui-mobile/EditorToolbar.tsx | 118 +- src/ui-mobile/MobileDrawer.tsx | 234 +- src/ui-mobile/MobileShell.tsx | 449 +-- src/ui-mobile/attach.ts | 57 +- src/ui-mobile/deep-links.ts | 41 +- src/ui-mobile/editor-host.ts | 41 + src/ui-mobile/editor-keyboard-scroll.ts | 127 +- src/ui-mobile/editor-native-typing.ts | 59 +- src/ui-mobile/image-paste.ts | 74 +- src/ui-mobile/mobile-layout.test.ts | 6 +- src/ui-mobile/mobile.css | 2 +- src/ui-mobile/nav.ts | 67 +- src/ui-mobile/note-actions.tsx | 107 +- src/ui-mobile/note-order.ts | 75 +- src/ui-mobile/note-row-gestures.ts | 33 +- src/ui-mobile/workspace-context.test.ts | 20 + src/ui-mobile/workspace-context.ts | 20 + tailwind.config.js | 3 +- tooling/android-ui-check.tsx | 9 +- tooling/build-native-boundary-fixture.mjs | 15 + tooling/check-core-boundary.mjs | 43 + tooling/load-mobile-module.ts | 4 +- tooling/native-boundary-fixture.ts | 111 + tooling/prepare-zennotes.sh | 61 - tooling/upstream-check.sh | 18 - tsconfig.json | 31 +- vendor/zennotes/manifest.json | 35 + ...app-core-2.51.0-core.h49d73b531d346192.tgz | Bin 0 -> 3101734 bytes ...ct-2.51.0-boundaries.h18d39d9887df8897.tgz | Bin 0 -> 36509 bytes ...in-2.51.0-boundaries.h18d39d9887df8897.tgz | Bin 0 -> 256891 bytes vite.config.ts | 101 +- 61 files changed, 4063 insertions(+), 1865 deletions(-) delete mode 100644 .zennotes-commit create mode 100644 android/app/src/androidTest/AndroidManifest.xml create mode 100644 android/app/src/androidTest/java/md/zennotes/SafFixtureProvider.java create mode 100644 android/app/src/androidTest/java/md/zennotes/SafReadInstrumentedTest.java create mode 100644 docs/native-boundary-validation.md create mode 100644 src/bridge/vault-lifecycle.test.ts create mode 100644 src/ui-mobile/editor-host.ts create mode 100644 src/ui-mobile/workspace-context.test.ts create mode 100644 src/ui-mobile/workspace-context.ts create mode 100644 tooling/build-native-boundary-fixture.mjs create mode 100644 tooling/check-core-boundary.mjs create mode 100644 tooling/native-boundary-fixture.ts delete mode 100755 tooling/prepare-zennotes.sh delete mode 100755 tooling/upstream-check.sh create mode 100644 vendor/zennotes/manifest.json create mode 100644 vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz create mode 100644 vendor/zennotes/zennotes-bridge-contract-2.51.0-boundaries.h18d39d9887df8897.tgz create mode 100644 vendor/zennotes/zennotes-shared-domain-2.51.0-boundaries.h18d39d9887df8897.tgz diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8a76171..a343e67 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ updates: dependency-type: development ignore: # The shell's React (and its types) must match the pinned ZenNotes - # source (.zennotes-commit); a second React or diverging @types/react + # package candidates (vendor/zennotes/manifest.json); a second React or diverging @types/react # breaks the pinned-source typecheck. These move with the pin only. - dependency-name: react - dependency-name: react-dom diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b05486d..d733ec4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: jobs: verify: - name: TypeScript, source pin, and Android build + name: TypeScript, package boundary, and Android build runs-on: ubuntu-latest steps: - name: Check out repository @@ -36,13 +36,12 @@ jobs: - name: Install mobile dependencies run: npm ci - - name: Prepare exact ZenNotes source - run: npm run source:prepare + - name: Verify installed core packages + run: npm run boundaries:check - name: Reject high-severity production advisories run: | npm audit --omit=dev --audit-level=high - npm --prefix .zennotes-source audit --omit=dev --audit-level=high - name: Test and typecheck bridges run: | diff --git a/.gitignore b/.gitignore index 75a54cc..d856911 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ docs/releases/* # Signing (local only — never commit) android/upload-keystore.jks android/keystore.properties + +/dist-boundary-check/ diff --git a/.zennotes-commit b/.zennotes-commit deleted file mode 100644 index 3119a4f..0000000 --- a/.zennotes-commit +++ /dev/null @@ -1 +0,0 @@ -104416f560bb9e709620cabe9e1f225306f7a4ea diff --git a/README.md b/README.md index a51732f..0dda828 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,15 @@ derived from the iPhone shell at `../zennotesiphone` — the two shells share th same structure and bridge modules; platform-specific divergences are noted below. -The zennotes repo is consumed **read-only at the exact commit in -`.zennotes-commit`**. `npm run source:prepare` checks that commit out under the -ignored `.zennotes-source/` directory and installs its locked dependencies. -Every typecheck and release build verifies the pin; no ambient sibling checkout -can silently change a mobile binary. +The app consumes the public `@zennotes/app-core`, `@zennotes/bridge-contract`, +and `@zennotes/shared-domain` packages. The exact archives are vendored under +`vendor/zennotes/` with their source identity and checksums (`manifest.json`), +and `package-lock.json` pins the complete install. No source checkout is used. +The vendored set is the published desktop release +[core-2.51.0-core.h49d73b531d346192](https://github.com/ZenNotes/zennotes/releases/tag/core-2.51.0-core.h49d73b531d346192) +(desktop commit `8ff2cb86`, tag v2.51.0, clean tree). Run `npm run +boundaries:check` to verify archives, installed versions, singleton +editor/React peers, and imports. ## Architecture @@ -31,7 +35,7 @@ src/ widgets.ts publishes it through the ZenWidgets plugin on every change ui-mobile/ MobileShell.tsx bottom nav (capture ⊕ / search / sidebar / palette), - phone drawer behavior via the shared Zustand store + phone drawer behavior through public core APIs mobile.css safe areas, overlay drawers, keyboard handling widget-links.ts the zennotes:// links the widgets fire; deep-links.ts runs them android/ Capacitor-generated Gradle project (appId md.zennotes) @@ -50,9 +54,10 @@ Key decisions (all forced by "don't modify the zennotes repo"): Every desktop-only affordance in app-core gates on `runtime === 'desktop'`, so `'web'` + the capability flags produces correct mobile behavior. When the contract gains `'mobile'` + the new capability flags (spec 02), flip it here. -- **`platform: 'linux'`** (iOS shell reports `'darwin'`) — gives app-core - Ctrl-based keymaps and hides Mac-only chrome; right for Android hardware - keyboards. +- **`platform: 'android'` + `hostKind: 'android'`** (iOS shell reports + `'ios'`) — app-core only special-cases `'darwin'`, so Android still gets + Ctrl-based keymaps and no Mac-only chrome; `hostKind` is how core tells the + native shells apart (it replaced the earlier `'linux'` stand-in in 1.1.21). - **Vault location** — app-scoped external storage: `/Android/data/md.zennotes/files/ZenNotes/` (`Directory.External`), the spec-03 Android default tier: no permission prompt, works under scoped @@ -117,7 +122,7 @@ Key decisions (all forced by "don't modify the zennotes repo"): the misspelling is intentional and load-bearing), same `.zennotes/` metadata (vault.json, workspace.json, comments/), same naming/collision rules, same NoteMeta extraction regexes, `systemFolderPaths` remaps honored - via `@shared/system-folder-paths`. + via `@zennotes/shared-domain/system-folder-paths`. - **Share sheet → quick capture**: Android needs no app extension — a `text/plain` `ACTION_SEND` intent-filter on MainActivity stashes captures in SharedPreferences; the app-local `ShareInbox` plugin (same `jsName` and @@ -163,7 +168,7 @@ Key decisions (all forced by "don't modify the zennotes repo"): task rows, kanban cards, and calendar day cells), subtask rollups, archived notes retiring their tasks, inline mermaid while writing, text replacements, configurable tab size, manual kanban card order, and - absence-aware remote reads (`@shared/remote-absence`). + absence-aware remote reads (`@zennotes/shared-domain/remote-absence`). ## Build & run @@ -183,10 +188,12 @@ points at the SDK. Dev loop against a browser (no emulator): `npm run dev` — Capacitor plugins are absent in a plain browser, so vault I/O won't work; use the emulator for real testing. -`npm run upstream` verifies the generated checkout matches `.zennotes-commit` -and typechecks the bridge against that exact source. To adopt a newer core, -update the pin to a reviewed full commit SHA and commit it with the dependent -mobile changes. +`npm run upstream` verifies the package boundary and typechecks the host. +To adopt a new core candidate, copy its three immutable archives into +`vendor/zennotes/`, update the manifest and `file:` dependencies, then run +`npm install`, the boundary check, tests, typecheck, production build, and native +runtime checks together. Keep the previous validated package version available +for rollback. A package update must not require private core imports. ## Boot-order gotcha (load-bearing) @@ -251,3 +258,5 @@ signed object upload, completion, manifest, and cleanup with a deterministic (`useSystemBackClose` is Android-only: iOS has no system back gesture). Underline stayed out on purpose: ZenNotes markdown has no underline construct on any platform — an upstream schema decision, not a shell one. + +For device-level package checks, see [native boundary validation](docs/native-boundary-validation.md). diff --git a/android/app/src/androidTest/AndroidManifest.xml b/android/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..7d82873 --- /dev/null +++ b/android/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/android/app/src/androidTest/java/md/zennotes/SafFixtureProvider.java b/android/app/src/androidTest/java/md/zennotes/SafFixtureProvider.java new file mode 100644 index 0000000..a828b2f --- /dev/null +++ b/android/app/src/androidTest/java/md/zennotes/SafFixtureProvider.java @@ -0,0 +1,45 @@ +package md.zennotes; + +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract.Document; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.net.Uri; +import android.provider.DocumentsContract; +import java.io.File; +import java.io.FileNotFoundException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** A real Android resolver boundary with deliberate provider failure modes. */ +public class SafFixtureProvider extends ContentProvider { + private static final String[] COLUMNS = { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE, Document.COLUMN_LAST_MODIFIED }; + + @Override public String getType(Uri uri) { return "text/markdown"; } + @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } + @Override public int update(Uri uri, ContentValues values, String selection, String[] args) { throw new UnsupportedOperationException(); } + @Override public int delete(Uri uri, String selection, String[] args) { throw new UnsupportedOperationException(); } + @Override public boolean onCreate() { return true; } + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] args, String sortOrder) { + String parent = DocumentsContract.getTreeDocumentId(uri); + if (parent.equals("null-list")) return null; + if (parent.equals("denied")) throw new SecurityException("Provider access revoked"); + MatrixCursor cursor = new MatrixCursor(COLUMNS); + cursor.addRow(new Object[] {"present", "present.md", "text/markdown", 0, 0}); + cursor.addRow(new Object[] {"directory", "directory", Document.MIME_TYPE_DIR, 0, 0}); + cursor.addRow(new Object[] {"unreadable", "unreadable.md", "text/markdown", 0, 0}); + return cursor; + } + @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + String id = DocumentsContract.getDocumentId(uri); + if (!id.equals("present")) throw new FileNotFoundException("Provider cannot open this document"); + try { + File file = new File(getContext().getCacheDir(), "saf-fixture.md"); + Files.write(file.toPath(), "Exact café 日本語. \n".getBytes(StandardCharsets.UTF_8)); + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } catch (Exception error) { throw new FileNotFoundException(error.getMessage()); } + } +} diff --git a/android/app/src/androidTest/java/md/zennotes/SafReadInstrumentedTest.java b/android/app/src/androidTest/java/md/zennotes/SafReadInstrumentedTest.java new file mode 100644 index 0000000..b3c7fbb --- /dev/null +++ b/android/app/src/androidTest/java/md/zennotes/SafReadInstrumentedTest.java @@ -0,0 +1,58 @@ +package md.zennotes; + +import static org.junit.Assert.*; +import android.content.Context; +import android.provider.DocumentsContract; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SafReadInstrumentedTest { + private static class Result extends PluginCall { + String code, message; + JSObject value; + Result(String root, String path) { + super(null, "SafFs", "fixture", "read", new JSObject().put("root", root).put("path", path)); + } + @Override public void resolve(JSObject data) { value = data; } + @Override public void reject(String message, String code, Exception error, JSObject data) { + this.message = message; this.code = code; + } + } + private Result read(String rootId, String path, boolean base64) { + SafFsPlugin plugin = new SafFsPlugin() { + @Override public Context getContext() { return InstrumentationRegistry.getInstrumentation().getContext(); } + }; + String root = DocumentsContract.buildTreeDocumentUri("md.zennotes.test.saf-fixture", rootId).toString(); + Result result = new Result(root, path); + if (base64) plugin.readBase64(result); else plugin.readText(result); + return result; + } + @Test public void onlyVerifiedAbsenceReturnsNotFound() { + for (boolean base64 : new boolean[] {false, true}) { + assertEquals("ZN-SAF-NOT-FOUND", read("root", "missing.md", base64).code); + assertEquals("ZN-SAF-IS-DIRECTORY", read("root", "directory", base64).code); + for (String root : new String[] {"null-list", "denied"}) { + Result failed = read(root, "missing.md", base64); + assertNotNull(failed.message); + assertNotEquals("ZN-SAF-NOT-FOUND", failed.code); + } + Result unavailable = read("root", "unreadable.md", base64); + assertNotNull(unavailable.message); + assertNotEquals("ZN-SAF-NOT-FOUND", unavailable.code); + } + } + @Test public void nativeProviderReadsPreserveExactBytes() { + Result text = read("root", "present.md", false); + assertNull(text.message); + assertEquals("Exact café 日本語. \n", text.value.getString("data")); + Result binary = read("root", "present.md", true); + assertNull(binary.message); + assertArrayEquals("Exact café 日本語. \n".getBytes(java.nio.charset.StandardCharsets.UTF_8), + android.util.Base64.decode(binary.value.getString("data"), android.util.Base64.DEFAULT)); + } +} diff --git a/android/app/src/main/java/md/zennotes/SafFsPlugin.java b/android/app/src/main/java/md/zennotes/SafFsPlugin.java index e087d79..2dbabf5 100644 --- a/android/app/src/main/java/md/zennotes/SafFsPlugin.java +++ b/android/app/src/main/java/md/zennotes/SafFsPlugin.java @@ -93,7 +93,8 @@ private Map listChildren(Uri tree, String dirDocId) { }, null, null, null )) { - if (c != null) { + if (c == null) throw new IllegalStateException("Document provider did not return a directory listing"); + { while (c.moveToNext()) { String id = c.getString(0); String name = c.getString(1); @@ -249,7 +250,14 @@ public void readText(PluginCall call) { try { String rel = clean(call.getString("path")); Entry e = resolve(tree, rel); - if (e == null || e.isDir) throw new FileNotFoundException(rel); + if (e == null) { + call.reject("File does not exist: " + rel, "ZN-SAF-NOT-FOUND"); + return; + } + if (e.isDir) { + call.reject("Cannot read a directory: " + rel, "ZN-SAF-IS-DIRECTORY"); + return; + } JSObject ret = new JSObject(); ret.put("data", new String(readAll(docUri(tree, e.docId)), StandardCharsets.UTF_8)); call.resolve(ret); @@ -265,7 +273,14 @@ public void readBase64(PluginCall call) { try { String rel = clean(call.getString("path")); Entry e = resolve(tree, rel); - if (e == null || e.isDir) throw new FileNotFoundException(rel); + if (e == null) { + call.reject("File does not exist: " + rel, "ZN-SAF-NOT-FOUND"); + return; + } + if (e.isDir) { + call.reject("Cannot read a directory: " + rel, "ZN-SAF-IS-DIRECTORY"); + return; + } JSObject ret = new JSObject(); ret.put("data", Base64.encodeToString(readAll(docUri(tree, e.docId)), Base64.NO_WRAP)); call.resolve(ret); @@ -385,8 +400,21 @@ public void rename(PluginCall call) { String movedId = DocumentsContract.getDocumentId(moved); String targetName = baseName(to); if (!baseName(from).equals(targetName)) { - if (DocumentsContract.renameDocument(resolver(), docUri(tree, movedId), targetName) == null) { - throw new Exception("Rename after move refused"); + try { + if (DocumentsContract.renameDocument(resolver(), docUri(tree, movedId), targetName) == null) { + throw new Exception("Rename after move refused"); + } + } catch (Exception renameError) { + // A rename promise must not reject after silently changing parents. + try { + Uri restored = DocumentsContract.moveDocument(resolver(), docUri(tree, movedId), + docUri(tree, toParentId), docUri(tree, fromParent.docId)); + if (restored == null) throw new Exception("Rollback move refused"); + } catch (Exception rollbackError) { + throw new Exception("FOLDER_STATE_UNCERTAIN: Rename failed and could not be restored: " + + rollbackError.getMessage(), renameError); + } + throw renameError; } } } @@ -395,6 +423,7 @@ public void rename(PluginCall call) { if (src.isDir) listings.remove(cacheKey(tree, src.docId)); call.resolve(); } catch (Exception e) { + listings.clear(); call.reject("rename failed: " + e.getMessage()); } } diff --git a/docs/native-boundary-validation.md b/docs/native-boundary-validation.md new file mode 100644 index 0000000..7abd324 --- /dev/null +++ b/docs/native-boundary-validation.md @@ -0,0 +1,63 @@ +# Native package boundary validation + +The host consumes the three archives pinned in `vendor/zennotes/manifest.json`. +There is no main-repository checkout or private editor/store import in the build. + +## Package checks + +```sh +npm ci +npm run boundaries:check +npm run typecheck +npm test +npm run build +``` + +Run the same checks in a fresh copy containing the package manifest/lock, vendor +archives, source, public assets, tooling, TypeScript/Vite/Tailwind/PostCSS config, +and native project. Omit `node_modules` and any historical source clone. + +## Disposable native runtime + +Use a newly created simulator/emulator with no real account or vault. The fixture +creates a uniquely named test vault and writes notes and attachments. Do not put +this fixture in a release or install it on a personal device. + +1. Run `npm run build:boundary-fixture`. Only this explicit command adds + `tooling/native-boundary-fixture.ts` to the app; ordinary `npm run build` does not. +2. In a disposable checkout, copy `dist-boundary-check/` into `dist/`, then run + `npx cap sync android` and build the native debug/simulator app as below. +3. Install and launch on the disposable device. The fixture checks native typing, + exact Unicode and trailing whitespace, search, task observation, attachments, + note rename, comments, trash/restore, and whole-vault rename under the public + workspace transition lock. +4. Read `boundary-validation.json` in the private Data directory in the app data container. It must + report `passed-awaiting-restart` with 20 checks and no error. +5. Terminate and relaunch the app without clearing its data. The report must now + be `restart-passed`, including vault identity, selected note and exact bytes. +6. Remove the disposable device when finished. Before a normal build, use + `npm run build` and `npx cap sync android` to replace the fixture assets. + +The fixture uses public core APIs and the native filesystem bridge. It needs no +account credentials. It does not prove live Cloud sync, iCloud account behavior, +or every third-party storage provider; those remain separate release checks. + +## Android native checks + +With a disposable AVD running, set `ANDROID_SERIAL` to its identifier. From +`android/`, using the project's supported JDK and Android SDK, run: + +```sh +./gradlew --no-daemon :app:testDebugUnitTest :app:lintDebug :app:assembleDebug +./gradlew --no-daemon :app:connectedDebugAndroidTest +``` + +Instrumentation includes a test-APK-only content provider. It exercises the real +Android resolver and `SafFsPlugin` for text/base64 reads, verified absence, +directories, revoked access, null listings, and unreadable documents. Provider +failures must never become missing-file results in optional sidecar reads. + +For fixture reports, use the isolated WebView's debug connection to call +`Capacitor.Plugins.Filesystem.readFile` with directory `DATA`, path +`boundary-validation.json`, and encoding `utf8`. Reinstall the debug APK after +instrumentation if Gradle removed it, then launch the fixture separately. diff --git a/package-lock.json b/package-lock.json index a0ad9ca..1d6e453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,9 +28,13 @@ "@codemirror/search": "^6.7.2", "@codemirror/state": "^6.7.4", "@codemirror/view": "^6.43.11", + "@lezer/common": "^1.5.2", "@lezer/highlight": "^1.2.1", "@replit/codemirror-vim": "^6.3.0", "@xyflow/react": "^12.11.6", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.51.0-boundaries.h18d39d9887df8897.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.51.0-boundaries.h18d39d9887df8897.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.15", "function-plot": "^1.25.3", @@ -64,6 +68,7 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^6.1.1", "autoprefixer": "^10.6.0", + "esbuild": "^0.28.2", "harper.js": "^2.10.0", "postcss": "^8.5.28", "tailwindcss": "^3.4.17", @@ -113,6 +118,15 @@ "node": ">=20.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", @@ -677,463 +691,2207 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", - "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^1.1.0", - "@iconify/types": "^2.0.0", - "import-meta-resolve": "^4.2.0" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ionic/cli-framework-output": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", - "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@ionic/utils-terminal": "2.3.5", - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-array": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", - "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-fs": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", - "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/fs-extra": "^8.0.0", - "debug": "^4.0.0", - "fs-extra": "^9.0.0", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-fs/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@ionic/utils-object": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", - "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-process": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", - "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@ionic/utils-object": "2.1.6", - "@ionic/utils-terminal": "2.3.5", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "tree-kill": "^1.2.2", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", - "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-subprocess": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", - "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@ionic/utils-array": "2.1.6", - "@ionic/utils-fs": "3.1.7", - "@ionic/utils-process": "2.1.12", - "@ionic/utils-stream": "3.1.7", - "@ionic/utils-terminal": "2.3.5", - "cross-spawn": "^7.0.3", - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@ionic/utils-terminal": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", - "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/slice-ansi": "^4.0.0", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "slice-ansi": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "tslib": "^2.0.1", - "untildify": "^4.0.0", - "wrap-ansi": "^7.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@excalidraw/excalidraw": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.18.1.tgz", + "integrity": "sha512-6i5Gt7IDTOH//qa0Z315Ly5iVRhjWpu2whrlQFqkuwrkKUWgRsMk0P5qdE7bpyDpai7jeLeWYkyj1eVAfni1lw==", + "license": "MIT", + "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", + "@radix-ui/react-tabs": "1.0.2", + "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", + "jotai-scope": "0.7.2", + "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", + "roughjs": "4.6.4", + "sass": "1.51.0", + "tunnel-rat": "0.1.2" + }, + "peerDependencies": { + "react": "^17.0.2 || ^18.2.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" + } + }, + "node_modules/@excalidraw/excalidraw/node_modules/@braintree/sanitize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz", + "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==", + "license": "MIT" + }, + "node_modules/@excalidraw/excalidraw/node_modules/immutable": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", + "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", + "license": "MIT" + }, + "node_modules/@excalidraw/excalidraw/node_modules/points-on-curve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-1.0.1.tgz", + "integrity": "sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg==", + "license": "MIT" + }, + "node_modules/@excalidraw/excalidraw/node_modules/roughjs": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.4.tgz", + "integrity": "sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/@excalidraw/excalidraw/node_modules/roughjs/node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/@excalidraw/excalidraw/node_modules/sass": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.51.0.tgz", + "integrity": "sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==", + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@excalidraw/laser-pointer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz", + "integrity": "sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==", + "license": "MIT" + }, + "node_modules/@excalidraw/markdown-to-text": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz", + "integrity": "sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==", + "license": "MIT" + }, + "node_modules/@excalidraw/mermaid-to-excalidraw": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-2.2.2.tgz", + "integrity": "sha512-5VKQq5CdRocC82vOIUpQ5ufJOVV9FpBTdHGA+ULqazeIVV+cr299877omQCibsdS3Bpitz2fsnTwnIXEmLVDSg==", + "license": "MIT", + "dependencies": { + "@excalidraw/markdown-to-text": "0.1.2", + "@mermaid-js/parser": "^0.6.3", + "mermaid": "^11.12.1", + "nanoid": "4.0.2" + } + }, + "node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@excalidraw/random-username": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@excalidraw/random-username/-/random-username-1.1.0.tgz", + "integrity": "sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.6.tgz", + "integrity": "sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.6.tgz", + "integrity": "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/go": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz", + "integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.19.tgz", + "integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", + "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", + "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@myriaddreamin/typst-ts-renderer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-renderer/-/typst-ts-renderer-0.7.0.tgz", + "integrity": "sha512-3sXIGxZn9MufPrPn6251DeuLf2FIEILYNzY5lX0XOJmaIYqgvQ3qpfqkCjgQD+splBvt5R1N3BuuNFrZKsHhMw==", + "license": "Apache-2.0" + }, + "node_modules/@myriaddreamin/typst-ts-web-compiler": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-web-compiler/-/typst-ts-web-compiler-0.7.0.tgz", + "integrity": "sha512-nMwMcfOBy5pABPDuVM7/u8425SxhPUM+OJe6daqqgVOT3+neCTz4JKwx2L8XasfEHTNvd0cUI07LR9q5ADo7Gw==", + "license": "Apache-2.0" + }, + "node_modules/@myriaddreamin/typst.ts": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst.ts/-/typst.ts-0.7.0.tgz", + "integrity": "sha512-JtO5Td/1QesH1IBKAZtbayFNK8u2sl3iz18Y67uYqBEdcXiu3z+wpZcDDKlmVJvAcCgSjHTnuk6ZLfkYclrGGQ==", + "license": "Apache-2.0", + "dependencies": { + "idb": "^7.1.1" + }, + "peerDependencies": { + "@myriaddreamin/typst-ts-renderer": "^0.7.0", + "@myriaddreamin/typst-ts-web-compiler": "^0.7.0" + }, + "peerDependenciesMeta": { + "@myriaddreamin/typst-ts-renderer": { + "optional": true + }, + "@myriaddreamin/typst-ts-web-compiler": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz", + "integrity": "sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz", + "integrity": "sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@lezer/common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", - "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", - "license": "MIT" - }, - "node_modules/@lezer/cpp": { + "node_modules/@radix-ui/react-popover": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.6.tgz", - "integrity": "sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "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 + } } }, - "node_modules/@lezer/css": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.6.tgz", - "integrity": "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.0" + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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 + } } }, - "node_modules/@lezer/go": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", - "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.0" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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 + } } }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.3.0" + "@radix-ui/react-slot": "1.1.2" + }, + "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 + } } }, - "node_modules/@lezer/html": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", - "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz", + "integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/java": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", - "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10" } }, - "node_modules/@lezer/javascript": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", - "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.1.3", - "@lezer/lr": "^1.3.0" + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/json": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", - "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/lr": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", - "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/markdown": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz", - "integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/php": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", - "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.1.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/python": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.19.tgz", - "integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/rust": { + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", - "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz", + "integrity": "sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-roving-focus": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/sass": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", - "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz", + "integrity": "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/xml": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", - "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@lezer/yaml": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", - "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.4.0" + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", - "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", - "license": "MIT" + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@mermaid-js/parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", - "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", "license": "MIT", "dependencies": { - "@chevrotain/types": "~11.1.2" + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@radix-ui/react-use-callback-ref": "1.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", "license": "MIT", - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@radix-ui/rect": "1.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@oxc-project/types": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", - "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", - "dev": true, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, "node_modules/@replit/codemirror-vim": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.4.0.tgz", @@ -1415,7 +3173,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3": { @@ -1910,6 +3668,97 @@ "d3-zoom": "^3.0.0" } }, + "node_modules/@zennotes/app-core": { + "version": "2.51.0-core.h49d73b531d346192", + "resolved": "file:vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz", + "integrity": "sha512-WaJLtt9Z0+0KNhMqwL+LAX9fG6JJVeZfHkJjUiL9WyHFBLLPGi0MumfSiGeKpWHabNE/pIdiP1RoU19kNyH7JA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.18.3", + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-cpp": "^6.0.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-java": "^6.0.2", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.3.1", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language-data": "^6.5.1", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.5.8", + "@excalidraw/excalidraw": "^0.18.1", + "@myriaddreamin/typst-ts-renderer": "^0.7.0", + "@myriaddreamin/typst-ts-web-compiler": "^0.7.0", + "@myriaddreamin/typst.ts": "^0.7.0", + "@replit/codemirror-vim": "^6.3.0", + "@xyflow/react": "^12.11.2", + "@zennotes/bridge-contract": "2.51.0-boundaries.h18d39d9887df8897", + "@zennotes/shared-domain": "2.51.0-boundaries.h18d39d9887df8897", + "dompurify": "^3.3.4", + "function-plot": "^1.25.3", + "gray-matter": "^4.0.3", + "harper.js": "^2.7.0", + "hast": "^1.0.0", + "highlight.js": "^11.10.0", + "jsxgraph": "^1.12.2", + "katex": "^0.16.15", + "mdast": "^3.0.0", + "mermaid": "^11.4.1", + "prettier": "^3.8.2", + "rehype-highlight": "^7.0.1", + "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-math": "^6.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vscode-oniguruma": "^2.0.1", + "vscode-textmate": "^9.3.2" + }, + "peerDependencies": { + "@codemirror/language": "^6.10.6", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.35.3", + "@lezer/common": "^1.5.2", + "@lezer/highlight": "^1.2.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "vite": "^6.4.3 || ^7.0.0 || ^8.0.0", + "zustand": "^5.0.2" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/@zennotes/bridge-contract": { + "version": "2.51.0-boundaries.h18d39d9887df8897", + "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.51.0-boundaries.h18d39d9887df8897.tgz", + "integrity": "sha512-ib31p4wDQvcLj+DcoEi6mq0T+1aHXL7b1MOkWpBCtQ9DkiXkHLQj2zvB3U41zc9znWe2JIXgdM6IzHVhvtd7Vw==", + "license": "MIT" + }, + "node_modules/@zennotes/shared-domain": { + "version": "2.51.0-boundaries.h18d39d9887df8897", + "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.51.0-boundaries.h18d39d9887df8897.tgz", + "integrity": "sha512-uocbc9KXy/7XB7MtBJWEG4/8Sod5MJ27MyInQJbT52OJl0RSuvJRcXf4cfnfNvIwVg3pCX8CFFPdtVAoujg4xg==", + "license": "MIT", + "dependencies": { + "@zennotes/bridge-contract": "2.51.0-boundaries.h18d39d9887df8897", + "lz-string": "^1.5.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1947,7 +3796,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -1973,6 +3821,18 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -2098,7 +3958,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2147,7 +4006,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2156,6 +4014,12 @@ "node": ">=8" } }, + "node_modules/browser-fs-access": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.29.1.tgz", + "integrity": "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==", + "license": "Apache-2.0" + }, "node_modules/browserslist": { "version": "4.28.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", @@ -2240,6 +4104,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-roundrect-polyfill": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz", + "integrity": "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==", + "license": "MIT" + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2284,7 +4154,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -2309,7 +4178,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -2334,6 +4202,15 @@ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -2398,17 +4275,43 @@ "layout-base": "^1.0.0" } }, + "node_modules/crc-32": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz", + "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/crelt": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3015,12 +4918,18 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -3133,6 +5042,57 @@ "tests/types" ] }, + "node_modules/es6-promise-pool": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz", + "integrity": "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3271,14 +5231,12 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3309,6 +5267,15 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fractional-indexing": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz", + "integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==", + "license": "CC0-1.0", + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/fs-extra": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", @@ -3328,7 +5295,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3368,6 +5334,23 @@ "interval-arithmetic-eval": "^0.5.3" } }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3399,6 +5382,12 @@ "node": ">=10.13.0" } }, + "node_modules/glur": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/glur/-/glur-1.1.2.tgz", + "integrity": "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==", + "license": "MIT" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3431,7 +5420,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/harper.js/-/harper.js-2.10.0.tgz", "integrity": "sha512-s6BDuqtRbX8M/5exH1G5M7VzAuv6l0TagbRxMY37OsrT/1kWFjWmqHydWrkYFqTUW+fFhu4Hj2yUxvChp862Pg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "fflate": "^0.8.2" @@ -3450,6 +5438,13 @@ "node": ">= 0.4" } }, + "node_modules/hast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hast/-/hast-1.0.0.tgz", + "integrity": "sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==", + "deprecated": "Renamed to rehype", + "license": "MIT" + }, "node_modules/hast-util-from-dom": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", @@ -3689,6 +5684,30 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/image-blob-reduce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/image-blob-reduce/-/image-blob-reduce-3.0.1.tgz", + "integrity": "sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==", + "license": "MIT", + "dependencies": { + "pica": "^7.1.0" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -3703,7 +5722,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -3748,7 +5766,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -3802,7 +5819,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3822,7 +5838,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -3835,7 +5850,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -3870,7 +5884,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jiti": { @@ -3883,6 +5896,37 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jotai": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz", + "integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/jotai-scope": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/jotai-scope/-/jotai-scope-0.7.2.tgz", + "integrity": "sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==", + "license": "MIT", + "peerDependencies": { + "jotai": ">=2.9.2", + "react": ">=17.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3986,7 +6030,7 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -4269,6 +6313,18 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4315,6 +6371,15 @@ "node": ">=12.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -4347,6 +6412,13 @@ "mr-parser": "^0.2.1" } }, + "node_modules/mdast": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast/-/mdast-3.0.0.tgz", + "integrity": "sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g==", + "deprecated": "`mdast` was renamed to `remark`", + "license": "MIT" + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -5311,6 +7383,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multimath": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multimath/-/multimath-2.0.0.tgz", + "integrity": "sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==", + "license": "MIT", + "dependencies": { + "glur": "^1.1.2", + "object-assign": "^4.1.1" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -5327,7 +7409,6 @@ "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, "funding": [ { "type": "github", @@ -5377,6 +7458,15 @@ "double-bits": "^1.1.0" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/node-releases": { "version": "2.0.55", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", @@ -5391,7 +7481,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5401,7 +7490,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5435,6 +7523,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open-color": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/open-color/-/open-color-1.9.1.tgz", + "integrity": "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==", + "license": "MIT" + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5448,6 +7542,12 @@ "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", "license": "MIT" }, + "node_modules/pako": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz", + "integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -5470,7 +7570,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5517,18 +7616,36 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-freehand": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.0.tgz", + "integrity": "sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==", + "license": "MIT" + }, + "node_modules/pica": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/pica/-/pica-7.1.1.tgz", + "integrity": "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==", + "license": "MIT", + "dependencies": { + "glur": "^1.1.2", + "inherits": "^2.0.3", + "multimath": "^2.0.0", + "object-assign": "^4.1.1", + "webworkify": "^1.5.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5572,6 +7689,31 @@ "node": ">=10.4.0" } }, + "node_modules/png-chunk-text": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz", + "integrity": "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==", + "license": "MIT" + }, + "node_modules/png-chunks-encode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz", + "integrity": "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==", + "license": "MIT", + "dependencies": { + "crc-32": "^0.3.0", + "sliced": "^1.0.1" + } + }, + "node_modules/png-chunks-extract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz", + "integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==", + "license": "MIT", + "dependencies": { + "crc-32": "^0.3.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -5592,7 +7734,7 @@ "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -5751,6 +7893,21 @@ "dev": true, "license": "MIT" }, + "node_modules/prettier": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", + "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5785,6 +7942,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/pwacompat": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/pwacompat/-/pwacompat-2.0.17.tgz", + "integrity": "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==", + "license": "Apache-2.0" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5831,6 +7994,75 @@ "react": "^18.3.1" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "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 + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "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 + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5860,7 +8092,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -6111,7 +8342,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.148.0", @@ -6210,6 +8441,63 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.104.1.tgz", + "integrity": "sha512-yDA+1aIG3EHgN4V/BvuhCvu61FF6hEd4e+9DxikUm9U0CAGuvdIZ/UYy7qbOxjhbbWQraoyLlMuzdRGOSV5Bmw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", @@ -6256,7 +8544,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6269,7 +8556,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6332,11 +8618,17 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", + "deprecated": "Unsupported", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6614,7 +8906,7 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -6631,7 +8923,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -6649,7 +8941,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -6662,7 +8954,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6723,6 +9014,43 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "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 + } + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6910,6 +9238,49 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "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 + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "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 + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -6985,7 +9356,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", @@ -7063,7 +9434,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -7100,11 +9471,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/webworkify": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz", + "integrity": "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index c10108d..720c9a4 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,15 @@ "homepage": "https://zennotes.org", "scripts": { "dev": "vite", - "source:prepare": "sh tooling/prepare-zennotes.sh", - "prebuild": "npm run source:prepare", "build": "vite build", "test": "node --test", "e2e:cloud": "node tooling/cloud-direct-upload-e2e.mjs", - "pretypecheck": "npm run source:prepare", "typecheck": "tsc --noEmit", "sync": "npm run build && cap sync android", - "upstream": "sh tooling/upstream-check.sh", - "android": "npm run build && cap sync android && cap open android" + "upstream": "npm run boundaries:check && npm run typecheck", + "android": "npm run build && cap sync android && cap open android", + "boundaries:check": "node tooling/check-core-boundary.mjs", + "build:boundary-fixture": "node tooling/build-native-boundary-fixture.mjs" }, "dependencies": { "@aparajita/capacitor-secure-storage": "^8.0.0", @@ -66,7 +65,11 @@ "unist-util-visit": "^5.0.0", "vscode-oniguruma": "^2.0.1", "vscode-textmate": "^9.3.2", - "zustand": "^5.0.2" + "zustand": "^5.0.2", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.51.0-boundaries.h18d39d9887df8897.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.51.0-boundaries.h18d39d9887df8897.tgz", + "@lezer/common": "^1.5.2" }, "devDependencies": { "@capacitor/cli": "^8.5.2", @@ -79,11 +82,19 @@ "postcss": "^8.5.28", "tailwindcss": "^3.4.17", "typescript": "^5.7.2", - "vite": "^8.3.0" + "vite": "^8.3.0", + "esbuild": "^0.28.2" }, "overrides": { "xcode": { "uuid": "11.1.1" + }, + "@excalidraw/excalidraw": { + "nanoid": "3.3.18" + }, + "@excalidraw/mermaid-to-excalidraw": { + "@mermaid-js/parser": "1.2.1", + "nanoid": "5.1.16" } } } diff --git a/src/bridge/cloud-layout.ts b/src/bridge/cloud-layout.ts index 7f9d692..ee65e45 100644 --- a/src/bridge/cloud-layout.ts +++ b/src/bridge/cloud-layout.ts @@ -22,7 +22,7 @@ * conflict is resolved; the common shapes (fresh device joining a desktop * root vault, or two default vaults) reconcile exactly. */ -import { resolveFolderPath, type SystemFolderPaths } from '@shared/system-folder-paths' +import { resolveFolderPath, type SystemFolderPaths } from '@zennotes/shared-domain/system-folder-paths' import { authenticatedClient } from './mobile-cloud-auth' import { emitVaultChange } from './events' import { diff --git a/src/bridge/events.ts b/src/bridge/events.ts index 7991061..18273d6 100644 --- a/src/bridge/events.ts +++ b/src/bridge/events.ts @@ -4,7 +4,7 @@ * (2) app-foreground rescan, and (3) successful cloud pulls. The subscription * API is identical to desktop/web so app-core consumes it as-is. */ -import type { VaultChangeEvent } from '@bridge-contract/ipc' +import type { VaultChangeEvent } from '@zennotes/bridge-contract/ipc' type VaultChangeListener = (ev: VaultChangeEvent) => void diff --git a/src/bridge/imported-assets.ts b/src/bridge/imported-assets.ts index 1195a1f..9e55028 100644 --- a/src/bridge/imported-assets.ts +++ b/src/bridge/imported-assets.ts @@ -7,7 +7,7 @@ * where a test can reach them. The vault core re-exports them so existing * importers remain unaffected. */ -import type { ImportedAssetKind } from '@bridge-contract/ipc' +import type { ImportedAssetKind } from '@zennotes/bridge-contract/ipc' export const ASSETS_DIR = 'assets' diff --git a/src/bridge/link-metadata.ts b/src/bridge/link-metadata.ts index 57b7fbb..a90425d 100644 --- a/src/bridge/link-metadata.ts +++ b/src/bridge/link-metadata.ts @@ -14,7 +14,7 @@ * (metadata lives in ``, so the whole page is never needed). */ import { CapacitorHttp } from '@capacitor/core' -import type { LinkMetadata } from '@shared/ipc' +import type { LinkMetadata } from '@zennotes/shared-domain/ipc' const TIMEOUT_MS = 6000 const MAX_CHARS = 512 * 1024 diff --git a/src/bridge/mobile-bridge.ts b/src/bridge/mobile-bridge.ts index 678be3e..6d51349 100644 --- a/src/bridge/mobile-bridge.ts +++ b/src/bridge/mobile-bridge.ts @@ -1,3 +1,4 @@ +import { relocateLocalVault } from '@zennotes/app-core/workspace' /** * The mobile `window.zen` — third ZenBridge implementation (after Electron IPC * and the web HTTP bridge). Vault operations run against the on-device vault @@ -36,13 +37,13 @@ import type { VaultTextSearchBackendPreference, VaultTextSearchCapabilities, VaultTextSearchMatch -} from '@shared/ipc' -import { createDatabaseOps } from '@shared/database-ops' +} from '@zennotes/shared-domain/ipc' +import { createDatabaseOps } from '@zennotes/shared-domain/database-ops' import type { CustomCodeLanguage, CustomCodeLanguageInstallInput, CustomCodeLanguageUpdateInput -} from '@shared/custom-code-languages' +} from '@zennotes/shared-domain/custom-code-languages' import type { ApplyWorkflowInput, WorkflowFile, @@ -50,12 +51,12 @@ import type { WorkflowRunSummary, WorkflowUndoResult, WriteWorkflowInput -} from '@bridge-contract/workflows' +} from '@zennotes/bridge-contract/workflows' import type { McpClientStatus, McpInstructionsPayload, McpServerRuntime -} from '@shared/mcp-clients' +} from '@zennotes/shared-domain/mcp-clients' import { MobileVault } from './vault-fs' import { listVaultDirs, VAULTS_DIR, vaultsRoot, initVaultsRoot } from './native-fs' import { Filesystem } from '@capacitor/filesystem' @@ -276,23 +277,22 @@ export async function renameVault(entry: MobileVaultEntry, newName: string): Pro if (!clean) throw new Error('Enter a name.') if (clean === entry.name) return if (entry.tier === 'external') throw new Error('Rename this folder in your file manager.') - await assertNameFree(entry.tier, clean) const wasCurrent = isCurrentVaultEntry(entry) - if (entry.tier === 'icloud') { - await Filesystem.rename({ - from: await icloudVaultUrl(entry.name), - to: await icloudVaultUrl(clean) - }) - if (wasCurrent) await openVaultByName(clean, await icloudVaultUrl(clean)) - } else { - await Filesystem.rename({ - from: `${VAULTS_DIR}/${entry.name}`, - to: `${VAULTS_DIR}/${clean}`, - directory: vaultsRoot(), - toDirectory: vaultsRoot() - }) - if (wasCurrent) await openVaultByName(clean) - } + const token = (name: string): string => entry.tier === 'icloud' + ? `${ICLOUD_VAULT_ROOT_PREFIX}${encodeURIComponent(name)}` : `${VAULT_ROOT_PREFIX}${name}` + let from = '', to = '' + await relocateLocalVault({ + ...(wasCurrent ? { reopen: { source: token(entry.name), destination: token(clean) } } : {}), + move: async () => { + await assertNameFree(entry.tier, clean) + from = entry.tier === 'icloud' ? await icloudVaultUrl(entry.name) : `${VAULTS_DIR}/${entry.name}` + to = entry.tier === 'icloud' ? await icloudVaultUrl(clean) : `${VAULTS_DIR}/${clean}` + await Filesystem.rename({ from, to, ...(entry.tier === 'icloud' ? {} : { directory: vaultsRoot(), toDirectory: vaultsRoot() }) }) + }, + rollback: async () => { + await Filesystem.rename({ from: to, to: from, ...(entry.tier === 'icloud' ? {} : { directory: vaultsRoot(), toDirectory: vaultsRoot() }) }) + } + }) } /** Permanently removes the vault directory and everything in it. The UI owns @@ -326,23 +326,26 @@ export function forgetExternalVault(root: string = EXTERNAL_VAULT_ROOT): void { * the hood, so notes transfer — not copy). Reopens it when it's current. */ export async function moveVault(entry: MobileVaultEntry, to: 'local' | 'icloud'): Promise { if (entry.tier === 'external' || entry.tier === to) return - await assertNameFree(to, entry.name) const wasCurrent = isCurrentVaultEntry(entry) - const localPath = await localVaultPath(entry.name) - if (to === 'icloud') { - const status = await icloudStatus() - if (!status.available) { - throw new Error('iCloud is not available. Sign in to iCloud and turn on iCloud Drive.') + const token = (tier: 'local' | 'icloud'): string => tier === 'icloud' + ? `${ICLOUD_VAULT_ROOT_PREFIX}${encodeURIComponent(entry.name)}` : `${VAULT_ROOT_PREFIX}${entry.name}` + let localPath = '' + await relocateLocalVault({ + ...(wasCurrent ? { reopen: { source: token(entry.tier), destination: token(to) } } : {}), + move: async () => { + await assertNameFree(to, entry.name) + localPath = await localVaultPath(entry.name) + if (to === 'icloud') { + const status = await icloudStatus() + if (!status.available) throw new Error('iCloud is not available. Sign in to iCloud and turn on iCloud Drive.') + await ICloudVault.enable({ localPath, name: entry.name }) + } else await ICloudVault.disable({ name: entry.name, localPath }) + }, + rollback: async () => { + if (to === 'icloud') await ICloudVault.disable({ name: entry.name, localPath }) + else await ICloudVault.enable({ localPath, name: entry.name }) } - await ICloudVault.enable({ localPath, name: entry.name }) - } else { - await ICloudVault.disable({ name: entry.name, localPath }) - } - if (wasCurrent) { - setStoragePref(to) - if (to === 'icloud') await openVaultByName(entry.name, await icloudVaultUrl(entry.name)) - else await openVaultByName(entry.name) - } + }) } const MOBILE_CAPABILITIES: ZenCapabilities = { @@ -376,7 +379,8 @@ function mobileAppInfo(): ZenAppInfo { version: appVersion, description: 'ZenNotes for Android', homepage: 'https://zennotes.org', - runtime: 'web' + runtime: 'web', + hostKind: 'android' } } @@ -798,10 +802,11 @@ export const mobileBridge: ZenBridge = { getCapabilities: (): ZenCapabilities => MOBILE_CAPABILITIES, getAppInfo: (): ZenAppInfo => mobileAppInfo(), - // 'linux' gives app-core Ctrl-based keymaps and hides the Mac-only chrome — - // the right defaults for Android hardware keyboards. - platform: async () => 'linux' as NodeJS.Platform, - platformSync: () => 'linux' as NodeJS.Platform, + // 'android' (with hostKind above) lets app-core tell native shells apart; + // core only special-cases 'darwin', so Android still gets Ctrl-based keymaps + // and no Mac-only chrome — the right defaults for hardware keyboards. + platform: async () => 'android' as const, + platformSync: () => 'android' as const, // WebView-safe system families plus the fonts the shell bundles itself // (public/fonts + index.html @font-face): Android offers no way to install // fonts app-wide, so shipping them is the only path to real choices here. @@ -1093,6 +1098,7 @@ export const mobileBridge: ZenBridge = { // External file links name OS paths outside the iOS sandbox; the exact // 'desktop-only' token makes app-core show its friendly toast. openExternalFile: async () => ({ ok: false, error: 'desktop-only' }), + openExternalUrl: async () => ({ ok: false, error: 'desktop-only' }), openAssetExternally: async () => notImplemented('openAssetExternally'), // Bookmark cards fetch open-graph metadata natively (link-metadata.ts) — // a WKWebView fetch of an arbitrary page would be CORS-blocked. diff --git a/src/bridge/mobile-cloud-sync.integration.test.ts b/src/bridge/mobile-cloud-sync.integration.test.ts index ee50808..28fcc8e 100644 --- a/src/bridge/mobile-cloud-sync.integration.test.ts +++ b/src/bridge/mobile-cloud-sync.integration.test.ts @@ -55,7 +55,7 @@ async function fixture(initial: Record = { 'note.md': 'Original' return item } const remote = { - listVaults: async () => ({ data: [{ id: 'vault-1', name: 'Test vault' }] }), + listVaults: async () => ({ data: [{ id: 'vault-1', name: 'Test vault' }, { id: 'vault-2', name: 'Other vault' }] }), manifest: async (_vaultId: string, options?: unknown) => { manifestRequests.push(options) return { data: [...remoteItems.values()], cursor, next_page: null } @@ -175,7 +175,7 @@ async function fixture(initial: Record = { 'note.md': 'Original' await api.linkMobileCloudVault(vault, 'vault-1') const stateKey = () => [...persisted.keys()].find((path) => path.includes('/states/')) return { - api, vault, files, reads, uploaded, refreshes, remoteText, put, manifestRequests, + api, vault, files, reads, uploaded, refreshes, remoteText, put, manifestRequests, remote, persisted, setAccountStatus: (value: typeof accountStatus) => { accountStatus = value }, sync: () => api.syncMobileCloudVault(vault), setFailWrite: (path: string | null) => { failWritePath = path }, @@ -341,3 +341,144 @@ describe('mobile Cloud adapter wiring', () => { assert.deepEqual(h.reads, ['note.md']) }) }) + + +describe('deleted Cloud vault recovery (#791)', () => { + const serviceError = (status: number, code: string | null) => Object.assign( + new Error('ZenNotes Cloud request failed'), { name: 'CloudServiceRequestError', status, code } + ) + const missing = () => serviceError(404, 'NOT_FOUND') + + it('clears only the missing Cloud association and state while preserving local bytes and another link', async () => { + const h = await fixture({}) + await h.sync() + const ownStateKey = [...h.persisted.keys()].find((path) => path.includes('/states/'))! + const otherVault = { ...h.vault, rootLabel: 'ZenNotes/Other', fs: { ...h.vault.fs, rootPath: 'ZenNotes/Other' } } + const otherLink = await h.api.linkMobileCloudVault(otherVault, 'vault-2') + await h.api.syncMobileCloudVault(otherVault) + const otherStateKey = [...h.persisted.keys()].find((path) => path.includes('/states/') && path !== ownStateKey)! + const otherState = h.persisted.get(otherStateKey) + const note = '---\ntitle: Keep me\n---\r\nUnsent local edit 📝\r\n' + h.put('note.md', note) + h.remote.changes = async () => { throw missing() } + h.remote.manifest = async () => { throw missing() } + + await assert.rejects(h.sync()) + + assert.equal(await h.api.getMobileCloudVaultLink(h.vault), null) + assert.equal(h.persisted.has(ownStateKey), false) + assert.equal(h.files.get('note.md')?.bytes.toString(), note) + assert.deepEqual(await h.api.getMobileCloudVaultLink(otherVault), otherLink) + assert.equal(h.persisted.get(otherStateKey), otherState) + }) + + it('preserves complete conflict drafts in unique inactive state archives when a Cloud vault disappears', async () => { + const h = await fixture({ 'note.md': 'local version' }) + h.remoteText('note.md', 'cloud version') + const savedChanges = h.remote.changes + const savedManifest = h.remote.manifest + const snapshots: string[] = [] + const draft = 'Unsent merge draft\r\nKeep every byte 📝\r\n' + for (let attempt = 1; attempt <= 2; attempt++) { + h.remote.changes = savedChanges + h.remote.manifest = savedManifest + if (attempt > 1) await h.api.linkMobileCloudVault(h.vault, 'vault-1') + const conflict = (await h.sync()).pending_conflicts[0] + assert.ok(conflict) + await h.api.saveMobileCloudConflictDraft(h.vault, conflict.id, `${draft}${attempt}`) + const stateKey = [...h.persisted.keys()].find((path) => path.includes('/states/'))! + snapshots.push(h.persisted.get(stateKey)!) + h.remote.changes = async () => { throw missing() } + h.remote.manifest = async () => { throw missing() } + + await assert.rejects(h.sync()) + + assert.equal(h.persisted.has(stateKey), false) + assert.equal(await h.api.getMobileCloudVaultLink(h.vault), null) + } + const archived = [...h.persisted.entries()].filter(([path]) => !path.includes('/states/')) + for (const snapshot of snapshots) { + assert.equal(archived.filter(([, value]) => value === snapshot).length, 1) + } + assert.equal(h.files.get('note.md')?.bytes.toString(), 'local version') + }) + + it('retires the deleted link when a background metadata probe discovers it', async () => { + const h = await fixture({}) + await h.sync() + h.remote.manifest = async () => { throw missing() } + + await h.api.hasMobileCloudVaultChanges(h.vault).catch(() => undefined) + + assert.equal(await h.api.getMobileCloudVaultLink(h.vault), null) + assert.equal(await h.api.hasMobileCloudVaultChanges(h.vault), false) + }) + + for (const [label, failure] of [ + ['offline', new Error('Network unavailable')], + ['unauthenticated', serviceError(401, 'UNAUTHENTICATED')], + ['forbidden', serviceError(403, 'FORBIDDEN')], + ['server failure', serviceError(503, null)], + ['unstructured proxy 404', serviceError(404, null)] + ] as const) { + it(`preserves the link and cursor after ${label}`, async () => { + const h = await fixture({}) + await h.sync() + const link = await h.api.getMobileCloudVaultLink(h.vault) + const state = structuredClone(h.state) + h.remote.changes = async () => { throw failure } + h.remote.manifest = async () => { throw failure } + + await assert.rejects(h.sync()) + await h.api.hasMobileCloudVaultChanges(h.vault).catch(() => undefined) + + assert.deepEqual(await h.api.getMobileCloudVaultLink(h.vault), link) + assert.deepEqual(h.state, state) + }) + } + + it('retains the link when a missing mutation resource belongs to an existing vault', async () => { + const h = await fixture({}) + await h.sync() + const link = await h.api.getMobileCloudVaultLink(h.vault) + h.put('note.md', 'Local note still exists') + h.remote.mutate = async () => { throw missing() } + + await assert.rejects(h.sync()) + + assert.deepEqual(await h.api.getMobileCloudVaultLink(h.vault), link) + assert.equal(h.files.get('note.md')?.bytes.toString(), 'Local note still exists') + }) + + it('keeps the association when confirmation cannot reach an authenticated vault endpoint', async () => { + const h = await fixture({}) + await h.sync() + const link = await h.api.getMobileCloudVaultLink(h.vault) + h.remote.changes = async () => { throw missing() } + h.remote.manifest = async () => { throw serviceError(401, 'UNAUTHENTICATED') } + + await assert.rejects(h.sync()) + + assert.deepEqual(await h.api.getMobileCloudVaultLink(h.vault), link) + }) + + it('preserves a newer association when an older sync reports a deleted vault', async () => { + const h = await fixture({}) + await h.sync() + let begin!: () => void + let fail!: (error: Error) => void + const started = new Promise((resolve) => { begin = resolve }) + h.remote.changes = async () => { + begin() + return await new Promise((_resolve, reject) => { fail = reject }) + } + h.remote.manifest = async () => { throw missing() } + const rejected = assert.rejects(h.sync()) + await started + const replacement = await h.api.linkMobileCloudVault(h.vault, 'vault-2') + fail(missing()) + await rejected + + assert.deepEqual(await h.api.getMobileCloudVaultLink(h.vault), replacement) + }) +}) diff --git a/src/bridge/mobile-cloud-sync.ts b/src/bridge/mobile-cloud-sync.ts index d2a28e3..15d2687 100644 --- a/src/bridge/mobile-cloud-sync.ts +++ b/src/bridge/mobile-cloud-sync.ts @@ -58,6 +58,23 @@ const persistence: CloudSyncHostPersistence = { }, async saveState(vaultKey: string, baseUrl: string, state: CloudSyncState): Promise { await writeJson(await statePath(vaultKey, baseUrl, state.vault_id), state) + }, + async retireState(vaultKey: string, baseUrl: string, vaultId: string): Promise { + const path = await statePath(vaultKey, baseUrl, vaultId) + let data: string + try { + const result = await Filesystem.readFile({ path, directory: Directory.Data, encoding: Encoding.UTF8 }) + data = typeof result.data === 'string' ? result.data : await result.data.text() + } catch (error) { + if (isNotFoundError(error)) return + throw error + } + // Preserve the complete state, including unsent conflict drafts, before unlinking. + await Filesystem.writeFile({ + path: path.replace('/states/', '/retired-states/').replace(/\.json$/, `.${crypto.randomUUID()}.json`), + directory: Directory.Data, encoding: Encoding.UTF8, data, recursive: true + }) + await deleteDataFile(path) } } @@ -105,17 +122,7 @@ export async function deleteMobileCloudVault(vault: MobileVault): Promise /** Check the server cursor without scanning the vault or downloading attachment bytes. */ export async function hasMobileCloudVaultChanges(vault: MobileVault): Promise { - const link = await getMobileCloudVaultLink(vault) - if (!link) return false - const status = await getMobileCloudAccountStatus() - if (status.state !== 'connected' || status.account?.base_url !== link.base_url) return false - const value = await persistence.loadState(vault.fs.rootPath, link.base_url, link.vault_id) - const state = value as Partial | null - if (!state || state.version !== 1 || state.vault_id !== link.vault_id || - typeof state.cursor !== 'number' || !Number.isInteger(state.cursor) || state.cursor < 0) return true - const client = await authenticatedClient() - const manifest = await client.manifest(link.vault_id, { includeContent: false, perPage: 1 }) - return manifest.cursor !== state.cursor + return service.hasRemoteChanges(hostVault(vault)) } export async function syncMobileCloudVault(vault: MobileVault): Promise { @@ -371,7 +378,11 @@ async function writeJson(path: string, value: unknown): Promise { } async function deleteDataFile(path: string): Promise { - await Filesystem.deleteFile({ path, directory: Directory.Data }).catch(() => {}) + try { + await Filesystem.deleteFile({ path, directory: Directory.Data }) + } catch (error) { + if (!isNotFoundError(error)) throw error + } } async function fingerprint(value: string): Promise { diff --git a/src/bridge/native-fs.ts b/src/bridge/native-fs.ts index b5ab6be..1400511 100644 --- a/src/bridge/native-fs.ts +++ b/src/bridge/native-fs.ts @@ -230,7 +230,8 @@ export class NativeFs { async readTextOrNull(relPath: string): Promise { try { return await this.readText(relPath) - } catch { + } catch (error) { + if (!isNotFoundError(error)) throw error return null } } diff --git a/src/bridge/remote-client.ts b/src/bridge/remote-client.ts index 2894e5a..94b8dca 100644 --- a/src/bridge/remote-client.ts +++ b/src/bridge/remote-client.ts @@ -24,8 +24,8 @@ import type { VaultInfo, VaultSettings, VaultTextSearchMatch -} from '@shared/ipc' -import type { VaultTask } from '@shared/tasks' +} from '@zennotes/shared-domain/ipc' +import type { VaultTask } from '@zennotes/shared-domain/tasks' import { importedAssetFilename } from './imported-assets.ts' export interface RemoteClientOptions { diff --git a/src/bridge/remote-vault.ts b/src/bridge/remote-vault.ts index 5ecdde7..1821ea1 100644 --- a/src/bridge/remote-vault.ts +++ b/src/bridge/remote-vault.ts @@ -28,12 +28,12 @@ import type { VaultInfo, VaultSettings, VaultTextSearchMatch -} from '@shared/ipc' -import type { VaultTask } from '@shared/tasks' -import type { CustomTemplateFile, WriteTemplateInput } from '@bridge-contract/templates' -import type { ImportedAsset } from '@shared/ipc' -import { createAbsenceAwareReader } from '@shared/remote-absence' -import { pastedImageFilename } from '@shared/pasted-image' +} from '@zennotes/shared-domain/ipc' +import type { VaultTask } from '@zennotes/shared-domain/tasks' +import type { CustomTemplateFile, WriteTemplateInput } from '@zennotes/bridge-contract/templates' +import type { ImportedAsset } from '@zennotes/shared-domain/ipc' +import { createAbsenceAwareReader } from '@zennotes/shared-domain/remote-absence' +import { pastedImageFilename } from '@zennotes/shared-domain/pasted-image' import { emitVaultChange } from './events' import { importedAssetFilename } from './imported-assets' import { RemoteClient, RemoteRequestError } from './remote-client' diff --git a/src/bridge/remote-workspace.ts b/src/bridge/remote-workspace.ts index c6c2d41..a2dcc36 100644 --- a/src/bridge/remote-workspace.ts +++ b/src/bridge/remote-workspace.ts @@ -15,7 +15,7 @@ import type { RemoteWorkspaceProfileInput, ServerCapabilities, VaultInfo -} from '@shared/ipc' +} from '@zennotes/shared-domain/ipc' import { RemoteClient, normalizeBaseUrl } from './remote-client' import { RemoteVault } from './remote-vault' diff --git a/src/bridge/tikz.ts b/src/bridge/tikz.ts index 57fe7e6..a6131fe 100644 --- a/src/bridge/tikz.ts +++ b/src/bridge/tikz.ts @@ -16,7 +16,7 @@ * per-source caching, and a serialized render queue (the TeX engine does not * tolerate concurrent runs). */ -import type { TikzRenderResponse } from '@shared/ipc' +import type { TikzRenderResponse } from '@zennotes/shared-domain/ipc' const ASSET_BASE = 'tikzjax/' // Generous: the first render on a cold device includes engine load + wasm diff --git a/src/bridge/vault-core.ts b/src/bridge/vault-core.ts index aad3b3c..9e736ed 100644 --- a/src/bridge/vault-core.ts +++ b/src/bridge/vault-core.ts @@ -6,12 +6,12 @@ * The `attachements` (sic) constant is the intentional, load-bearing legacy * spelling — do not "fix" it and do not add an `attachments` variant. */ -import type { ImportedAssetKind, NoteFolder, VaultSettings } from '@bridge-contract/ipc' +import type { ImportedAssetKind, NoteFolder, VaultSettings } from '@zennotes/bridge-contract/ipc' import { resolveFolderPath, systemFolderForDirName, type SystemFolderPaths -} from '@shared/system-folder-paths' +} from '@zennotes/shared-domain/system-folder-paths' import { ASSETS_DIR } from './imported-assets' // Re-exported so every existing importer keeps reaching them here; they live in diff --git a/src/bridge/vault-fs.ts b/src/bridge/vault-fs.ts index e1b2ddd..3a1c5a2 100644 --- a/src/bridge/vault-fs.ts +++ b/src/bridge/vault-fs.ts @@ -1,3 +1,4 @@ +import { relocateVaultEntries, type VaultRelocation, type VaultRelocationIO } from '@zennotes/shared-domain/vault-relocation' /** * The on-device vault: desktop `vault.ts` semantics reimplemented over the * Capacitor Filesystem (there is no Node `fs` in a WKWebView). Same on-disk @@ -19,21 +20,21 @@ import type { VaultDemoTourResult, VaultSettings, VaultTextSearchMatch -} from '@bridge-contract/ipc' -import { DEFAULT_VAULT_SETTINGS } from '@bridge-contract/ipc' -import type { CustomTemplateFile, WriteTemplateInput } from '@bridge-contract/templates' -import type { VaultTask } from '@shared/tasks' -import { parseTaskFile, parseTasksFromBody } from '@shared/tasks' -import { normalizeHarperVaultState } from '@shared/harper-settings' -import { normalizeNoteComments } from '@shared/note-comments' -import { isFormDirName, isDatabaseInternalPath } from '@shared/databases' -import { emptyExcalidrawDocument } from '@shared/excalidraw' -import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from '@desktop-main/demo-tour-data' +} from '@zennotes/bridge-contract/ipc' +import { DEFAULT_VAULT_SETTINGS } from '@zennotes/bridge-contract/ipc' +import type { CustomTemplateFile, WriteTemplateInput } from '@zennotes/bridge-contract/templates' +import type { VaultTask } from '@zennotes/shared-domain/tasks' +import { parseTaskFile, parseTasksFromBody } from '@zennotes/shared-domain/tasks' +import { normalizeHarperVaultState } from '@zennotes/shared-domain/harper-settings' +import { normalizeNoteComments } from '@zennotes/shared-domain/note-comments' +import { isFormDirName, isDatabaseInternalPath } from '@zennotes/shared-domain/databases' +import { emptyExcalidrawDocument } from '@zennotes/shared-domain/excalidraw' +import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from '@zennotes/shared-domain/demo-tour-data' import { WELCOME_NOTE_PATH, WELCOME_NOTE_BODY } from './welcome-note' import { rewriteWikilinksForRename, type RenameNoteRef -} from '@desktop-main/wikilink-rename' +} from '@zennotes/shared-domain/wikilink-rename' import { NativeFs } from './native-fs' import { ensureDownloaded } from './icloud' import { emitVaultChange } from './events' @@ -76,7 +77,7 @@ import { normalizeSystemFolderPaths, resolveFolderPath, type SystemFolderPaths -} from '@shared/system-folder-paths' +} from '@zennotes/shared-domain/system-folder-paths' const META_CACHE_FILE = `${INTERNAL_VAULT_DIR}/mobile-note-meta-cache-v1.json` /** Restore metadata written next to each deleted asset (desktop parity). */ @@ -672,6 +673,37 @@ export class MobileVault { return await this.metaForPath(rel) } + private relocationIO(): VaultRelocationIO { + return { + stat: path => this.fs.statVerified(path), + mkdir: path => this.fs.mkdir(path), + rename: (from, to) => this.fs.rename(from, to) + } + } + + private async relocateNote(oldPath: string, newPath: string): Promise { + await relocateVaultEntries(this.relocationIO(), [ + { from: oldPath, to: newPath, required: true }, + { from: this.commentsPathFor(oldPath), to: this.commentsPathFor(newPath) } + ]) + this.invalidateMeta(oldPath) + } + + /** Detach both trees before cleanup so a failed move can restore the original. */ + private async detachContent(path: string, comments: string, required = true): Promise { + const temporary = `${INTERNAL_VAULT_DIR}/delete-${uuid()}` + const moves: VaultRelocation[] = [ + { from: path, to: `${temporary}/content`, required }, + { from: comments, to: `${temporary}/comments` } + ] + await relocateVaultEntries(this.relocationIO(), moves) + try { + if (await this.fs.statVerified(temporary) !== null) await this.fs.rmdir(temporary) + } catch (error) { + console.warn('Detached deleted content retained for cleanup', temporary, error) + } + } + async renameNote(relPath: string, nextTitle: string): Promise { const rel = resolveSafeRel(relPath) const folder = await this.folderOf(rel) @@ -686,9 +718,7 @@ export class MobileVault { } // Snapshot for inbound wikilink rewriting before the rename lands. const preNotes = await this.listNotes() - await this.fs.rename(rel, target) - this.invalidateMeta(rel) - await this.moveNoteComments(rel, target) + await this.relocateNote(rel, target) emitVaultChange({ kind: 'unlink', path: rel, folder, scope: 'content' }) emitVaultChange({ kind: 'add', path: target, folder, scope: 'content' }) await this.updateInboundWikilinks(preNotes, rel, trimmed) @@ -753,9 +783,7 @@ export class MobileVault { const baseTitle = stemName(filename) const finalTitle = await this.uniqueTitle(destDir, baseTitle, ext) const destRel = joinPath(destDir, `${finalTitle}${ext}`) - await this.fs.rename(rel, destRel) - this.invalidateMeta(rel) - await this.moveNoteComments(rel, destRel) + await this.relocateNote(rel, destRel) emitVaultChange({ kind: 'unlink', path: rel, folder: sourceFolder, scope: 'content' }) emitVaultChange({ kind: 'add', path: destRel, folder: target, scope: 'content' }) return await this.metaForPath(destRel) @@ -776,25 +804,17 @@ export class MobileVault { async emptyTrash(): Promise { const trashDir = await this.folderRootRel('trash') - const entries = await this.fs.readdir(trashDir) - for (const entry of entries) { - const rel = `${trashDir}/${entry.name}` - await this.removeNoteComments(rel) - if (entry.type === 'directory') { - await this.fs.rmdir(rel).catch(() => {}) - } else { - await this.fs.deleteFile(rel).catch(() => {}) - } - this.invalidateMeta(rel) - emitVaultChange({ kind: 'unlink', path: rel, folder: 'trash', scope: 'content' }) + await this.detachContent(trashDir, `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${trashDir}`, false) + for (const key of [...this.metaCache.keys()]) { + if (key.startsWith(`${trashDir}/`)) this.invalidateMeta(key) } + emitVaultChange({ kind: 'unlink', path: trashDir, folder: 'trash', scope: 'folder' }) } async deleteNote(relPath: string): Promise { const rel = resolveSafeRel(relPath) const folder = (await this.folderOf(rel)) ?? 'trash' - await this.fs.deleteFile(rel) - await this.removeNoteComments(rel) + await this.detachContent(rel, this.commentsPathFor(rel)) this.invalidateMeta(rel) emitVaultChange({ kind: 'unlink', path: rel, folder, scope: 'content' }) } @@ -835,9 +855,7 @@ export class MobileVault { `${await this.uniqueTitle(destDir, baseTitle, ext || '.md')}${ext || '.md'}` ) if (destRel === rel) return await this.metaForPath(rel) - await this.fs.rename(rel, destRel) - this.invalidateMeta(rel) - await this.moveNoteComments(rel, destRel) + await this.relocateNote(rel, destRel) emitVaultChange({ kind: 'unlink', path: rel, folder: sourceFolder, scope: 'content' }) emitVaultChange({ kind: 'add', path: destRel, folder: targetFolder, scope: 'content' }) return await this.metaForPath(destRel) @@ -871,7 +889,6 @@ export class MobileVault { } const parent = dirName(newRel) if (parent) await this.fs.mkdir(parent) - await this.fs.rename(oldRel, newRel) // Re-key folder icons/colors + drop stale meta cache entries under the old path. const settings = await this.getVaultSettings() const rekey = (map: Record): Record => { @@ -885,10 +902,30 @@ export class MobileVault { } return out } - await this.setVaultSettings({ + const settingsPath = `${INTERNAL_VAULT_DIR}/vault.json` + const hadSettings = await this.fs.statVerified(settingsPath) !== null + const originalSettings = hadSettings ? await this.fs.readText(settingsPath) : null + await relocateVaultEntries(this.relocationIO(), [ + { from: oldRel, to: newRel, required: true }, + { from: `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${oldRel}`, + to: `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${newRel}` } + ], async () => { + try { + await this.setVaultSettings({ ...settings, folderIcons: rekey(settings.folderIcons as Record) as VaultSettings['folderIcons'], folderColors: rekey(settings.folderColors as Record) as VaultSettings['folderColors'] + }) + } catch (error) { + try { + if (originalSettings !== null) await this.fs.writeText(settingsPath, originalSettings) + else if (await this.fs.statVerified(settingsPath) !== null) await this.fs.deleteFile(settingsPath) + this.settingsCache = settings + } catch (rollback) { + throw new AggregateError([error, rollback], 'FOLDER_STATE_UNCERTAIN: Could not restore vault settings') + } + throw error + } }) for (const key of [...this.metaCache.keys()]) { if (key.startsWith(`${oldRel}/`)) this.invalidateMeta(key) @@ -903,7 +940,7 @@ export class MobileVault { const clean = subpath.replace(/^\/+|\/+$/g, '') if (!clean) return const rel = resolveSafeRel(joinPath(topRel, clean)) - await this.fs.rmdir(rel) + await this.detachContent(rel, `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${rel}`) for (const key of [...this.metaCache.keys()]) { if (key.startsWith(`${rel}/`)) this.invalidateMeta(key) } @@ -974,24 +1011,6 @@ export class MobileVault { return comments } - private async moveNoteComments(oldRel: string, newRel: string): Promise { - const oldPath = this.commentsPathFor(oldRel) - const raw = await this.fs.readTextOrNull(oldPath) - if (raw === null) return - try { - const parsed = JSON.parse(raw) as { comments?: NoteComment[] } - const comments = (parsed.comments ?? []).map((c) => ({ ...c, notePath: newRel })) - await this.writeCommentsFile(newRel, comments) - } catch { - // unreadable sidecar — drop it - } - await this.fs.deleteFile(oldPath).catch(() => {}) - } - - private async removeNoteComments(rel: string): Promise { - await this.fs.deleteFile(this.commentsPathFor(rel)).catch(() => {}) - } - // ------------------------------------------------------------------- // Workspace state // ------------------------------------------------------------------- diff --git a/src/bridge/vault-lifecycle.test.ts b/src/bridge/vault-lifecycle.test.ts new file mode 100644 index 0000000..6f58675 --- /dev/null +++ b/src/bridge/vault-lifecycle.test.ts @@ -0,0 +1,110 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: { getItem: () => null } }) +const { MobileVault, NativeFs, DEFAULT_VAULT_SETTINGS } = await loadMobileModule([ + './src/bridge/vault-fs', './src/bridge/native-fs', '@zennotes/bridge-contract/ipc' +]) + +class MemoryFs { + files = new Map() + directories = new Set() + failMove: ((from: string, to: string) => boolean) | null = null + failWrite = false + async statVerified(path: string) { + return this.files.has(path) ? 'file' : this.directories.has(path) || [...this.files.keys()].some(key => key.startsWith(`${path}/`)) ? 'directory' : null + } + async exists(path: string) { return await this.statVerified(path) !== null } + async mkdir(path: string) { this.directories.add(path) } + async readText(path: string) { if (!this.files.has(path)) throw new Error('ENOENT'); return this.files.get(path)! } + async readTextOrNull(path: string) { return this.files.get(path) ?? null } + async writeText(path: string, body: string) { + if (this.failWrite) { this.failWrite = false; throw new Error('Settings write refused') } + this.files.set(path, body) + } + async deleteFile(path: string) { this.files.delete(path) } + async rmdir(path: string) { + for (const key of this.files.keys()) if (key === path || key.startsWith(`${path}/`)) this.files.delete(key) + for (const key of this.directories) if (key === path || key.startsWith(`${path}/`)) this.directories.delete(key) + } + async rename(from: string, to: string) { + if (this.failMove?.(from, to)) throw new Error('Provider refused move') + assert.ok(await this.exists(from)); assert.equal(await this.exists(to), false) + for (const [key, body] of [...this.files]) if (key === from || key.startsWith(`${from}/`)) { + this.files.set(to + key.slice(from.length), body); this.files.delete(key) + } + for (const key of [...this.directories]) if (key === from || key.startsWith(`${from}/`)) { + this.directories.add(to + key.slice(from.length)); this.directories.delete(key) + } + } +} +function fixture(rootMode = false) { + const fs = new MemoryFs(), vault = new MobileVault('Fixture') + Object.assign(vault, { + fs, settingsCache: { ...structuredClone(DEFAULT_VAULT_SETTINGS), primaryNotesLocation: rootMode ? 'root' : 'inbox', + systemFolderPaths: { inbox: 'Notes', archive: 'Old', trash: 'Bin', quick: 'Capture' } }, + listNotes: async () => [], invalidateMeta: () => {}, + metaForPath: async (path: string) => ({ path, title: path.split('/').pop() }) + }) + const note = `${rootMode ? '' : 'Notes/'}Work/One.md` + fs.files.set(note, '# One\n\nExact café 日本語. \n') + fs.files.set(`.zennotes/comments/${note}.comments.json`, '{malformed but preserved sidecar}') + return { fs, vault, note, original: new Map(fs.files) } +} +for (const rootMode of [false, true]) { + test(`native move and restore preserve bytes and sidecars with remapping (root=${rootMode})`, async () => { + const s = fixture(rootMode), result = await s.vault.moveToTrash(s.note) + assert.equal(result.path, 'Bin/Work/One.md') + assert.equal(s.fs.files.get(result.path), s.original.get(s.note)) + assert.equal(s.fs.files.get(`.zennotes/comments/${result.path}.comments.json`), '{malformed but preserved sidecar}') + const restored = await s.vault.restoreFromTrash(result.path) + assert.equal(restored.path, s.note) + assert.deepEqual(s.fs.files, s.original) + }) +} +test('native note relocation restores content when the comments provider fails', async () => { + const s = fixture() + s.fs.failMove = from => from.startsWith('.zennotes/comments/') + await assert.rejects(s.vault.moveToTrash(s.note), /Provider refused move/) + assert.deepEqual(s.fs.files, s.original) +}) +test('native folder rename moves database files and nested sidecars together', async () => { + const s = fixture() + s.fs.files.set('Notes/Work/Projects.base/data.csv', 'ID,Name\n1,One\n') + s.fs.files.set('Notes/Work/Projects.base/schema.json', '{"pages":{"1":"One.md"}}') + await s.vault.renameFolder('inbox', 'Work', 'Moved') + assert.equal(s.fs.files.get('Notes/Moved/Projects.base/data.csv'), 'ID,Name\n1,One\n') + assert.equal(s.fs.files.get('.zennotes/comments/Notes/Moved/One.md.comments.json'), '{malformed but preserved sidecar}') + assert.equal(s.fs.files.has(s.note), false) +}) +test('native folder rename restores content, comments and settings on metadata failure', async () => { + const s = fixture() + s.fs.files.set('.zennotes/vault.json', '{"keep":"exact settings"}\n') + s.original.set('.zennotes/vault.json', '{"keep":"exact settings"}\n') + s.fs.failWrite = true + await assert.rejects(s.vault.renameFolder('inbox', 'Work', 'Moved'), /Settings write refused/) + assert.deepEqual(s.fs.files, s.original) +}) +test('native Empty Trash rolls back if comments cannot detach, then deletes nested comments on retry', async () => { + const s = fixture() + await s.vault.moveToTrash(s.note) + const before = new Map(s.fs.files) + s.fs.failMove = from => from === '.zennotes/comments/Bin' + await assert.rejects(s.vault.emptyTrash(), /Provider refused move/) + assert.deepEqual(s.fs.files, before) + s.fs.failMove = null + await s.vault.emptyTrash() + assert.equal(s.fs.files.size, 0) + await s.vault.emptyTrash() +}) +test('native deletion does not report success after a failed sidecar move', async () => { + const s = fixture() + s.fs.failMove = from => from.startsWith('.zennotes/comments/') + await assert.rejects(s.vault.deleteNote(s.note), /Provider refused move/) + assert.deepEqual(s.fs.files, s.original) +}) +test('native absent-file reads propagate provider failures instead of allowing schema adoption', async () => { + const permission = new Error('Permission denied') + await assert.rejects(NativeFs.prototype.readTextOrNull.call({ readText: async () => { throw permission } }, 'schema.json'), permission) +}) diff --git a/src/bridge/widget-snapshot.ts b/src/bridge/widget-snapshot.ts index c9e63d7..d888855 100644 --- a/src/bridge/widget-snapshot.ts +++ b/src/bridge/widget-snapshot.ts @@ -12,8 +12,8 @@ * Only pure selectors live here (node --test covers them). The store/pins * wiring and the native call are in widgets.ts. */ -import type { NoteMeta } from '@bridge-contract/ipc' -import type { VaultTask } from '@shared/tasks' +import type { NoteMeta } from '@zennotes/bridge-contract/ipc' +import type { VaultTask } from '@zennotes/shared-domain/tasks' export const WIDGET_SNAPSHOT_VERSION = 1 /** The list scrolls; a dozen keeps the widget useful at any height. */ diff --git a/src/bridge/widgets.ts b/src/bridge/widgets.ts index 19188ba..9a6740d 100644 --- a/src/bridge/widgets.ts +++ b/src/bridge/widgets.ts @@ -20,14 +20,14 @@ */ import { App as CapApp } from '@capacitor/app' import { Capacitor, registerPlugin } from '@capacitor/core' -import { useStore } from '@zennotes/app-core/store' -import { computeTasksRender } from '@zennotes/app-core/lib/tasks-filter' -import { filterTasksForDisplay, toIsoDateLocal } from '@shared/tasks' +import { getShellSnapshot, subscribeShell, type ShellSnapshot } from '@zennotes/app-core/shell' +import { getTasksSnapshot, subscribeTasks, refreshTasks, getTodayTasks, type TasksSnapshot } from '@zennotes/app-core/tasks' +import { subscribeSettings } from '@zennotes/app-core/settings' +import { toIsoDateLocal } from '@zennotes/shared-domain/tasks' import { isMobileNoteIndexReady } from './mobile-bridge' import { getPinnedNotes, subscribePins } from '../ui-mobile/pins' import { WIDGET_SNAPSHOT_VERSION, - filterLiveTasks, selectWidgetNotes, selectWidgetTasks, themeFromTokens, @@ -45,21 +45,12 @@ interface ZenWidgetsPlugin { export const ZenWidgets = registerPlugin('ZenWidgets') -const NO_COLLAPSE = { - today: false, - upcoming: false, - waiting: false, - forwarded: false, - done: false, - cancelled: false -} - const PUBLISH_DEBOUNCE_MS = 400 const PUBLISH_MIN_INTERVAL_MS = 8000 /** Past this many changed notes one full scan beats per-note rescans. */ const RESCAN_BATCH_LIMIT = 8 -type StoreState = ReturnType +type StoreState = ShellSnapshot let timer = 0 let lastPublishedAt = 0 @@ -79,16 +70,8 @@ function themeMode(): WidgetTheme['mode'] { function buildSnapshot(state: StoreState, now: Date): Omit { const style = getComputedStyle(document.documentElement) - const live = filterLiveTasks( - filterTasksForDisplay(state.vaultTasks, state.showArchivedTasks), - state.notes - ) - const render = computeTasksRender(live, '', now, NO_COLLAPSE) - const { tasks, counts } = selectWidgetTasks( - render.groups.today, - render.groups.overdueCount ?? 0, - toIsoDateLocal(now) - ) + const today = getTodayTasks(now) + const { tasks, counts } = selectWidgetTasks(today.tasks, today.overdueCount, toIsoDateLocal(now)) return { version: WIDGET_SNAPSHOT_VERSION, vaultName: state.vault?.name ?? null, @@ -101,10 +84,10 @@ function buildSnapshot(state: StoreState, now: Date): Omit { - const state = useStore.getState() + const state = getShellSnapshot() // No vault (onboarding, a switch in flight): keep whatever the widgets // already show rather than blanking them. - if (!state.vault) return + if (!state.vault || !state.workspaceRestored) return let body: Omit try { body = buildSnapshot(state, new Date()) @@ -136,17 +119,17 @@ function flush(): void { void publish() } -function reconcileTasks(state: StoreState, prev: StoreState | null): void { - if (!state.vault || !isMobileNoteIndexReady()) return +function reconcileTasks(state: StoreState, prev: StoreState | null, tasks: TasksSnapshot, previousTasks: TasksSnapshot | null): void { + if (!state.vault || !state.workspaceRestored || !isMobileNoteIndexReady()) return const key = pinsKey(state) if (key !== taskVaultKey) { taskVaultKey = key tasksSettled = false knownUpdatedAt = new Map(state.notes.map((n) => [n.path, n.updatedAt])) - if (!state.tasksLoading) void state.refreshTasks() + if (!tasks.loading) void refreshTasks() return } - if (prev && prev.tasksLoading && !state.tasksLoading) tasksSettled = true + if (previousTasks?.loading && !tasks.loading) tasksSettled = true if (!prev || state.notes === prev.notes) return const changed: string[] = [] const next = new Map() @@ -157,38 +140,37 @@ function reconcileTasks(state: StoreState, prev: StoreState | null): void { knownUpdatedAt = next if (changed.length === 0) return if (changed.length > RESCAN_BATCH_LIMIT) { - if (!state.tasksLoading) void state.refreshTasks() + if (!tasks.loading) void refreshTasks() return } - for (const path of changed) void state.rescanTasksForPath(path) + for (const path of changed) void refreshTasks(path) } /** Start publishing; returns the teardown (tests / hot paths — the shell * itself never stops). No-op off the native platform. */ export function installWidgetPublisher(): () => void { if (!Capacitor.isNativePlatform()) return () => {} - const unsubStore = useStore.subscribe((state, prev) => { - reconcileTasks(state, prev) - if ( - state.notes !== prev.notes || - state.vaultTasks !== prev.vaultTasks || - state.showArchivedTasks !== prev.showArchivedTasks || - state.tasksLoading !== prev.tasksLoading || - state.vault !== prev.vault || - state.themeId !== prev.themeId || - state.themeMode !== prev.themeMode - ) { - schedule() - } - }) + let shell = getShellSnapshot() + let tasks = getTasksSnapshot() + const changed = (): void => { + const nextShell = getShellSnapshot(), nextTasks = getTasksSnapshot() + const previousShell = shell, previousTasks = tasks + // Store subscriptions can fire synchronously when refreshTasks starts. + shell = nextShell; tasks = nextTasks + reconcileTasks(nextShell, previousShell, nextTasks, previousTasks) + schedule() + } + const unsubShell = subscribeShell(changed) + const unsubTasks = subscribeTasks(changed) + const unsubSettings = subscribeSettings(schedule) const unsubPins = subscribePins(schedule) const appState = CapApp.addListener('appStateChange', ({ isActive }) => { if (!isActive) flush() }) - reconcileTasks(useStore.getState(), null) + reconcileTasks(getShellSnapshot(), null, getTasksSnapshot(), null) schedule() return () => { - unsubStore() + unsubShell(); unsubTasks(); unsubSettings() unsubPins() void appState.then((handle) => handle.remove()).catch(() => {}) window.clearTimeout(timer) diff --git a/src/main.tsx b/src/main.tsx index 2b13bce..14a5d9b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,6 +10,7 @@ */ import { App as CapApp } from '@capacitor/app' import { Keyboard, KeyboardResize } from '@capacitor/keyboard' +import { installMobileEditorHost } from './ui-mobile/editor-host' import { renderZenNotesApp } from '@zennotes/app-core/main' import { installMobileBridge, @@ -94,6 +95,7 @@ async function boot(): Promise { const root = document.getElementById('root') if (!root) throw new Error('Renderer root element #root was not found') + installMobileEditorHost() renderZenNotesApp(root) mountMobileShell() // Home Screen widgets: publish what they show, and run the links they diff --git a/src/ui-mobile/EditorToolbar.tsx b/src/ui-mobile/EditorToolbar.tsx index 0d357f7..149964d 100644 --- a/src/ui-mobile/EditorToolbar.tsx +++ b/src/ui-mobile/EditorToolbar.tsx @@ -2,96 +2,17 @@ * The mobile editing toolbar (spec 06's marquee input feature): a horizontally * scrollable formatting row docked above the soft keyboard while the * CodeMirror editor is focused. Actions drive the shared editor through the - * store's `editorViewRef` using app-core's own formatting helpers - * (lib/cm-format.ts) plus stock @codemirror/commands — no editor logic is - * duplicated, and no zennotes code changes. + * public semantic commands, without owning editor state or formatting logic. * * With the Capacitor Keyboard in `resize: native` mode the viewport shrinks * when the keyboard shows, so `bottom: 0` docks exactly on the keyboard's top. */ import React, { useEffect, useState } from 'react' import { Keyboard } from '@capacitor/keyboard' -import type { EditorView } from '@codemirror/view' -import { indentLess, indentMore, redo, undo } from '@codemirror/commands' -import { openSearchPanel } from '@codemirror/search' -import { EditorSelection } from '@codemirror/state' -import { useStore } from '@zennotes/app-core/store' -import { - setBlockType, - toggleWrap, - wrapLink, - type BlockType -} from '@zennotes/app-core/lib/cm-format' +import { runEditorCommand } from '@zennotes/app-core/editor' import { promptAttachFiles } from './attach' import { revealCaretAboveKeyboardSoon } from './editor-keyboard-scroll' -function view(): EditorView | null { - return useStore.getState().editorViewRef -} - -function withView(fn: (v: EditorView) => void): void { - const v = view() - if (!v) return - fn(v) - v.focus() -} - -/** Insert text at the cursor, placing the caret `caretOffset` chars in. */ -function insertSnippet(v: EditorView, text: string, caretOffset: number): void { - const { from, to } = v.state.selection.main - v.dispatch({ - changes: { from, to, insert: text }, - selection: EditorSelection.cursor(from + caretOffset) - }) -} - -/** - * Markers app-core's blockPrefix would put on these block types. app-core's - * setBlockType converts existing lines and deliberately skips blank ones, so - * on a fresh line Bullet / Checkbox / Heading did nothing until something was - * typed (Adib, iPhone device testing 2026-09-08; same code here). Desktop - * users just type the marker; on the phone the button IS the way to start a - * list. - */ -const BLANK_LINE_MARKERS: Partial> = { - bullet: '- ', - todo: '- [ ] ', - h1: '# ', - h2: '## ', - h3: '### ' -} - -/** - * setBlockType, plus the blank-line case it skips: a collapsed cursor on an - * empty (or whitespace-only) line gets the marker inserted after the existing - * indentation, caret after the marker. Selections and non-blank lines go - * through setBlockType unchanged. - */ -function applyBlockType(v: EditorView, type: BlockType): void { - const { from, to } = v.state.selection.main - const line = v.state.doc.lineAt(from) - const marker = BLANK_LINE_MARKERS[type] - if (marker !== undefined && from === to && line.text.trim() === '') { - // line.text is whitespace-only here, so it doubles as the indent. - const insert = line.text + marker - v.dispatch({ - changes: { from: line.from, to: line.to, insert }, - selection: EditorSelection.cursor(line.from + insert.length) - }) - return - } - setBlockType(v, type) -} - -/** Cycle the current line's heading level: none → # → ## → ### → none. */ -function cycleHeading(v: EditorView): void { - const line = v.state.doc.lineAt(v.state.selection.main.from) - const m = line.text.match(/^(#{1,6})\s/) - const level = m ? m[1]!.length : 0 - const next = level >= 3 ? 'paragraph' : (['h1', 'h2', 'h3'] as const)[level]! - applyBlockType(v, next) -} - interface ToolButton { key: string label: string @@ -106,13 +27,13 @@ const BUTTONS: ToolButton[] = [ key: 'undo', label: 'Undo', d: 'M9 14L4 9l5-5M4 9h10.5a5.5 5.5 0 015.5 5.5v0a5.5 5.5 0 01-5.5 5.5H11', - run: () => withView((v) => undo(v)) + run: () => runEditorCommand('undo') }, { key: 'redo', label: 'Redo', d: 'M15 14l5-5-5-5M20 9H9.5A5.5 5.5 0 004 14.5v0A5.5 5.5 0 009.5 20H13', - run: () => withView((v) => redo(v)) + run: () => runEditorCommand('redo') }, { key: 'find', @@ -122,10 +43,7 @@ const BUTTONS: ToolButton[] = [ // withView's editor refocus would immediately steal it back. Focus moves // input-to-input, so the keyboard stays up (Discord feedback, 2026-08-20: // "I have to exit the note to search for a word"). - run: () => { - const v = view() - if (v) openSearchPanel(v) - } + run: () => { runEditorCommand('open-search') } }, { key: 'attach', @@ -139,84 +57,84 @@ const BUTTONS: ToolButton[] = [ key: 'todo', label: 'Checkbox', d: 'M9 11l3 3L22 4M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11', - run: () => withView((v) => applyBlockType(v, 'todo')) + run: () => runEditorCommand('set-task-list') }, { key: 'bullet', label: 'Bullet list', d: 'M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01', - run: () => withView((v) => applyBlockType(v, 'bullet')) + run: () => runEditorCommand('set-bullet-list') }, { key: 'heading', label: 'Heading', glyph: 'H', d: '', - run: () => withView((v) => cycleHeading(v)) + run: () => runEditorCommand('cycle-heading') }, { key: 'bold', label: 'Bold', glyph: 'B', d: '', - run: () => withView((v) => toggleWrap(v, '**')) + run: () => runEditorCommand('toggle-bold') }, { key: 'italic', label: 'Italic', glyph: 'I', d: '', - run: () => withView((v) => toggleWrap(v, '*')) + run: () => runEditorCommand('toggle-italic') }, { key: 'strike', label: 'Strikethrough', d: 'M16 4H9a3 3 0 00-2.83 4M14 12a4 4 0 010 8H6M4 12h16', - run: () => withView((v) => toggleWrap(v, '~~')) + run: () => runEditorCommand('toggle-strikethrough') }, { key: 'highlight', label: 'Highlight', d: 'M12 20h9M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z', - run: () => withView((v) => toggleWrap(v, '==')) + run: () => runEditorCommand('toggle-highlight') }, { key: 'code', label: 'Inline code', d: 'M16 18l6-6-6-6M8 6l-6 6 6 6', - run: () => withView((v) => toggleWrap(v, '`')) + run: () => runEditorCommand('toggle-inline-code') }, { key: 'link', label: 'Link', d: 'M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71', - run: () => withView((v) => wrapLink(v)) + run: () => runEditorCommand('insert-link') }, { key: 'wikilink', label: 'Wikilink', glyph: '[[', d: '', - run: () => withView((v) => insertSnippet(v, '[[]]', 2)) + run: () => runEditorCommand('insert-wikilink') }, { key: 'tag', label: 'Tag', glyph: '#', d: '', - run: () => withView((v) => insertSnippet(v, '#', 1)) + run: () => runEditorCommand('insert-tag') }, { key: 'outdent', label: 'Outdent', d: 'M11 8h10M11 12h10M11 16h10M7 8l-4 4 4 4', - run: () => withView((v) => indentLess(v)) + run: () => runEditorCommand('outdent') }, { key: 'indent', label: 'Indent', d: 'M11 8h10M11 12h10M11 16h10M3 8l4 4-4 4', - run: () => withView((v) => indentMore(v)) + run: () => runEditorCommand('indent') } ] diff --git a/src/ui-mobile/MobileDrawer.tsx b/src/ui-mobile/MobileDrawer.tsx index 89eb767..d366a23 100644 --- a/src/ui-mobile/MobileDrawer.tsx +++ b/src/ui-mobile/MobileDrawer.tsx @@ -1,3 +1,4 @@ +import { dirOf } from './note-order' /** * The phone navigation drawer — a purpose-built mobile surface that REPLACES * app-core's desktop sidebar below 768px (which is hidden by CSS). Flat, @@ -7,23 +8,20 @@ */ import React, { useEffect, useMemo, useRef, useState } from 'react' import ReactDOM from 'react-dom/client' -import { useStore } from '@zennotes/app-core/store' -import type { NoteSortOrder } from '@zennotes/app-core/store' -import { confirmApp } from '@zennotes/app-core/lib/confirm-requests' -import { promptApp } from '@zennotes/app-core/lib/prompt-requests' -import { notePathWithinFolder } from '@zennotes/app-core/lib/vault-layout' -import { resolveFolderPath } from '@zennotes/shared-domain/system-folder-paths' -import { - csvPathForFormDir, - databaseTabPath, - FORM_DIR_SUFFIX, - isFormDirName -} from '@zennotes/shared-domain/databases' +import { getShellSnapshot, useShellSnapshot, setNoteSortOrder, type NoteSortOrder } from '@zennotes/app-core/shell' +import { getBrowseSnapshot, useBrowseSnapshot, getBrowseDirectory, requestCreateBrowseFolder, + requestRenameBrowseFolder, requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' +import { useWorkspaceSnapshot, openLocalVault, pickLocalVault, refreshRemoteProfiles, connectRemoteWorkspace, + connectRemoteProfile, changeRemoteVaultPath, deleteRemoteProfile } from '@zennotes/app-core/workspace' +import { openNote, openAppPage } from '@zennotes/app-core/navigation' +import { showSearch } from '@zennotes/app-core/commands' +import { setSettingsVisible } from '@zennotes/app-core/settings' +import { confirm as confirmApp, prompt as promptApp } from '@zennotes/app-core/dialogs' +import { captureMobileWorkspace, reportActionError } from './workspace-context' import { Keyboard } from '@capacitor/keyboard' import { setDrawerOpen, takeDrawerPath, useDrawerOpen } from './drawer-state' import { openMobileSheet } from './sheet-state' import { goHome } from './nav' -import { dirOf, noteComparator, pinnedFirst } from './note-order' import { usePins, toggleNotePin, toggleFolderPin } from './pins' import { archiveNote, openNoteMenu, trashNote } from './note-actions' import { refreshVault } from './refresh' @@ -152,9 +150,7 @@ function NewVaultSheet({ setBusy('create') setError('') dismissKeyboard() - useStore - .getState() - .openLocalVault(`${VAULT_ROOT_PREFIX}${clean}`) + openLocalVault(`${VAULT_ROOT_PREFIX}${clean}`) .then(() => onDone(true)) .catch((err) => { setError(String((err as Error)?.message ?? err)) @@ -170,12 +166,10 @@ function NewVaultSheet({ setBusy('pick') setError('') dismissKeyboard() - const before = useStore.getState().vault?.root ?? null - useStore - .getState() - .openVaultPicker() + const before = getShellSnapshot().vault?.root ?? null + pickLocalVault() .then(() => { - const after = useStore.getState().vault?.root ?? null + const after = getShellSnapshot().vault?.root ?? null if (after !== before) onDone(true) else setBusy(null) }) @@ -285,10 +279,8 @@ const TIER_SECTIONS = [ ] as const export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Element { - const currentName = useStore((s) => s.vault?.name ?? null) - const workspaceMode = useStore((s) => s.workspaceMode) - const remoteProfileId = useStore((s) => s.remoteWorkspaceInfo?.profileId ?? null) - const remoteProfiles = useStore((s) => s.remoteWorkspaceProfiles) + const currentName = useShellSnapshot().vault?.name ?? null + const { mode: workspaceMode, remoteProfileId, remoteProfiles } = useWorkspaceSnapshot() const [entries, setEntries] = useState(null) const [view, setView] = useState({ kind: 'list' }) const [busy, setBusy] = useState(null) @@ -303,7 +295,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele useEffect(() => { reload() - void useStore.getState().refreshRemoteWorkspaceProfiles() + void refreshRemoteProfiles() }, []) // The storage pref tracks whichever tier is open (every switch path sets @@ -347,26 +339,16 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele }) } - const tokenFor = (tier: 'local' | 'icloud', name: string): string => - tier === 'icloud' - ? `${ICLOUD_VAULT_ROOT_PREFIX}${encodeURIComponent(name)}` - : `${VAULT_ROOT_PREFIX}${name}` - const submitRename = (entry: MobileVaultEntry): void => { if (entry.tier === 'external') return - const tier = entry.tier const clean = sanitizeNoteTitle(renameTo.trim()) dismissKeyboard() if (!clean || clean === entry.name) { setView({ kind: 'vault', entry }) return } - const wasCurrent = isCurrent(entry) manage('rename', async () => { await renameVault(entry, clean) - // Renaming the open vault: route the store through its normal switch so - // the whole workspace picks up the new identity. - if (wasCurrent) await useStore.getState().openLocalVault(tokenFor(tier, clean)) }) } @@ -384,7 +366,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele onClose() // Let the sheet unmount so the guided URL/token prompts get focus. window.setTimeout(() => { - void useStore.getState().connectRemoteWorkspace() + void connectRemoteWorkspace() }, 30) } @@ -446,7 +428,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele disabled={current} onClick={() => act(`switch:${entry.root}`, () => - useStore.getState().openLocalVault(entry.root) + openLocalVault(entry.root) ) } > @@ -487,7 +469,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele disabled={current} onClick={() => act(`switch:${profile.id}`, () => - useStore.getState().connectRemoteWorkspaceProfile(profile.id) + connectRemoteProfile(profile.id) ) } > @@ -555,7 +537,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele className="zn-mobile-sheet-row" onClick={() => act(`switch:${view.entry.root}`, () => - useStore.getState().openLocalVault(view.entry.root) + openLocalVault(view.entry.root) ) } > @@ -696,7 +678,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele className="zn-mobile-sheet-row" onClick={() => act(`switch:${view.id}`, () => - useStore.getState().connectRemoteWorkspaceProfile(view.id) + connectRemoteProfile(view.id) ) } > @@ -713,7 +695,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele // Server-side folder browser renders as a modal — leave // the sheet first so it gets focus. window.setTimeout(() => { - void useStore.getState().changeRemoteWorkspaceVaultPath() + void changeRemoteVaultPath() }, 30) }} > @@ -726,7 +708,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele className="zn-mobile-sheet-row zn-danger" onClick={() => manage('remove', () => - useStore.getState().deleteRemoteWorkspaceProfile(view.id) + deleteRemoteProfile(view.id) ) } > @@ -748,27 +730,12 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele export function MobileDrawer(): React.JSX.Element | null { const open = useDrawerOpen() - const vaultName = useStore((s) => s.vault?.name ?? 'ZenNotes') - const vaultRoot = useStore((s) => s.vault?.root ?? null) + const browse = useBrowseSnapshot() + const vaultName = browse.vault?.name ?? 'ZenNotes' + const vaultRoot = browse.vault?.root ?? null const pins = usePins(vaultRoot) - const notes = useStore((s) => s.notes) - const folders = useStore((s) => s.folders) - const primaryAtRoot = useStore((s) => s.vaultSettings.primaryNotesLocation === 'root') - // Primitive selectors only — returning a fresh object from a selector - // re-renders forever (Object.is on a new Set is never equal). - const dailyDir = useStore((s) => - s.vaultSettings.dailyNotes.enabled ? s.vaultSettings.dailyNotes.directory : null - ) - const weeklyDir = useStore((s) => - s.vaultSettings.weeklyNotes.enabled ? s.vaultSettings.weeklyNotes.directory : null - ) - const monthlyDir = useStore((s) => - s.vaultSettings.monthlyNotes.enabled ? s.vaultSettings.monthlyNotes.directory : null - ) - const noteSortOrder = useStore((s) => s.noteSortOrder) - // Stable reference from the store (replaced wholesale on settings reload), - // so this is selector-safe; needed for remap-aware path composition. - const vaultSettings = useStore((s) => s.vaultSettings) + const { daily: dailyDir, weekly: weeklyDir, monthly: monthlyDir } = browse.dateDirectories + const noteSortOrder = browse.noteSortOrder const dateDirs = useMemo(() => { const dirs = new Set() if (dailyDir) dirs.add(dailyDir) @@ -785,45 +752,13 @@ export function MobileDrawer(): React.JSX.Element | null { }, [open]) const { childFolders, childDatabases, childNotes } = useMemo(() => { - const inboxDir = resolveFolderPath('inbox', vaultSettings.systemFolderPaths) - const folderSet = new Map() - const databases: Array<[string, string, string]> = [] - for (const f of folders) { - if (f.folder !== 'inbox') continue - if (dirOf(f.subpath) !== path) continue - const name = f.subpath.split('/').pop() ?? f.subpath - if (isFormDirName(name)) { - // Databases are `.base` folders — surface them as openable rows. - const vaultRel = primaryAtRoot ? f.subpath : `${inboxDir}/${f.subpath}` - databases.push([ - databaseTabPath(csvPathForFormDir(vaultRel)), - name.slice(0, -FORM_DIR_SUFFIX.length), - f.subpath - ]) - continue - } - folderSet.set(f.subpath, name) - } - const noteRows = notes - .filter((n) => { - if (n.folder !== 'inbox') return false - const sub = notePathWithinFolder(n.path, 'inbox', vaultSettings) - return dirOf(sub) === path && !isFormDirName(dirOf(sub).split('/').pop() ?? '') - }) - .sort(noteComparator(noteSortOrder)) - // Pinned rows float to the top of their group, keeping the sort order - // within each half (pins.ts). - const pinnedNoteSet = new Set(pins.notes) - const pinnedFolderSet = new Set(pins.folders) + const rows = getBrowseDirectory(browse, path, pins) return { - childFolders: pinnedFirst( - [...folderSet.entries()].sort((a, b) => a[1].localeCompare(b[1])), - ([subpath]) => pinnedFolderSet.has(subpath) - ), - childDatabases: databases.sort((a, b) => a[1].localeCompare(b[1])), - childNotes: pinnedFirst(noteRows, (n) => pinnedNoteSet.has(n.path)) + childFolders: rows.folders.map(row => [row.directory, row.title] as [string, string]), + childDatabases: rows.databases.map(row => [row.path, row.title, row.directory] as [string, string, string]), + childNotes: [...rows.notes] } - }, [notes, folders, path, primaryAtRoot, noteSortOrder, vaultSettings, pins]) + }, [browse, path, pins]) if (!open) return null @@ -837,7 +772,6 @@ export function MobileDrawer(): React.JSX.Element | null { window.setTimeout(() => void action(), 30) } - const s = (): ReturnType => useStore.getState() return ( <> @@ -858,7 +792,6 @@ export function MobileDrawer(): React.JSX.Element | null { noteSortOrder={noteSortOrder} close={close} go={go} - s={s} onOpenVaults={() => openMobileSheet('vaults')} /> @@ -933,7 +866,6 @@ function MobileDrawerBody(props: { noteSortOrder: NoteSortOrder close: () => void go: (action: () => unknown) => void - s: () => ReturnType }): React.JSX.Element { const { vaultName, @@ -951,8 +883,7 @@ function MobileDrawerBody(props: { childNotes, noteSortOrder, close, - go, - s + go } = props const lp = useLongPress() const [sortOpen, setSortOpen] = useState(false) @@ -962,14 +893,14 @@ function MobileDrawerBody(props: { // Rename/Delete here. Prompts overlay the open drawer (Modal layers above // z-49), so the drawer stays put and its list refreshes in place via the // vault change events. - const [folderMenu, setFolderMenu] = useState<{ subpath: string; name: string } | null>(null) + const [folderMenu, setFolderMenu] = useState<{ subpath: string; name: string; host: ReturnType } | null>(null) const pinNote = (notePath: string): void => { if (!vaultRoot) return toggleNotePin( vaultRoot, notePath, - s().notes.map((n) => n.path) + getShellSnapshot().notes.map((n) => n.path) ) } @@ -978,9 +909,7 @@ function MobileDrawerBody(props: { toggleFolderPin( vaultRoot, subpath, - s() - .folders.filter((f) => f.folder === 'inbox') - .map((f) => f.subpath) + getBrowseSnapshot().folders.map(row => row.directory) ) } @@ -1042,63 +971,20 @@ function MobileDrawerBody(props: { } }, [refreshing]) - const renameFolderFromDrawer = (subpath: string, name: string): void => { + const renameFolderFromDrawer = (subpath: string, _name: string): void => { + const host = folderMenu?.host ?? captureMobileWorkspace() setFolderMenu(null) - void (async () => { - const next = await promptApp({ - title: 'Rename folder', - initialValue: name, - okLabel: 'Rename', - validate: (v: string) => (v.includes('/') ? 'Folder name cannot contain "/"' : null) - }) - const clean = next?.trim() - if (!clean || clean === name) return - const parent = subpath.includes('/') ? subpath.slice(0, subpath.lastIndexOf('/')) : '' - try { - await s().renameFolder('inbox', subpath, parent ? `${parent}/${clean}` : clean) - } catch (err) { - window.alert(err instanceof Error ? err.message : String(err)) - } - })() + void requestRenameBrowseFolder(host, subpath).catch(reportActionError) } - const newFolderHere = (): void => { - void (async () => { - const leaf = path === '' ? '' : (path.split('/').pop() ?? '') - const name = await promptApp({ - title: leaf ? `New folder in ${leaf}` : 'New folder', - placeholder: 'Folder name', - okLabel: 'Create', - validate: (v: string) => (v.includes('/') ? 'Folder name cannot contain "/"' : null) - }) - const clean = name?.trim().replace(/^\/+|\/+$/g, '') - if (!clean) return - await s().createFolder('inbox', path === '' ? clean : `${path}/${clean}`) - })() + void requestCreateBrowseFolder(captureMobileWorkspace(), path).catch(reportActionError) } - - const deleteDatabase = (subpath: string, title: string): void => { - void (async () => { - const ok = await confirmApp({ - title: `Delete "${title}"?`, - description: 'All records will be permanently deleted. This cannot be undone.', - confirmLabel: 'Delete', - danger: true - }) - if (ok) await s().deleteFolder('inbox', subpath) - })() + const deleteDatabase = (subpath: string, _title: string): void => { + void requestDeleteBrowseDirectory(captureMobileWorkspace(), subpath).catch(reportActionError) } - - const deleteFolder = (subpath: string, name: string): void => { - void (async () => { - const ok = await confirmApp({ - title: `Delete "${name}"?`, - description: 'Everything inside will be permanently deleted. This cannot be undone.', - confirmLabel: 'Delete', - danger: true - }) - if (ok) await s().deleteFolder('inbox', subpath) - })() + const deleteFolder = (subpath: string, _name: string): void => { + const host = folderMenu?.host ?? captureMobileWorkspace() + void requestDeleteBrowseDirectory(host, subpath).catch(reportActionError) } return ( @@ -1117,7 +1003,7 @@ function MobileDrawerBody(props: { - @@ -1141,11 +1027,11 @@ function MobileDrawerBody(props: { Home - - @@ -1170,22 +1056,22 @@ function MobileDrawerBody(props: { )} - {/* The assets table only exists as a pane tab (zen://assets) — the palette's "Go to Files" drives the desktop sidebar list, which phones don't render, so this row is the phone's way in. */} - - - @@ -1225,7 +1111,7 @@ function MobileDrawerBody(props: { aria-checked={noteSortOrder === order} className={noteSortOrder === order ? 'is-active' : ''} onClick={() => { - s().setNoteSortOrder(order) + setNoteSortOrder(order) setSortOpen(false) }} > @@ -1248,7 +1134,7 @@ function MobileDrawerBody(props: { diff --git a/src/ui-mobile/MobileShell.tsx b/src/ui-mobile/MobileShell.tsx index 7bed1d2..d64f435 100644 --- a/src/ui-mobile/MobileShell.tsx +++ b/src/ui-mobile/MobileShell.tsx @@ -6,39 +6,24 @@ * full-screen flow. Rendered into its own React root so app-core stays * untouched; state is driven through the shared Zustand store. */ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import ReactDOM from 'react-dom/client' import { App as CapApp } from '@capacitor/app' import { Haptics, ImpactStyle } from '@capacitor/haptics' import { Keyboard } from '@capacitor/keyboard' -import { closeSearchPanel } from '@codemirror/search' -import { useStore } from '@zennotes/app-core/store' -import type { TaskMutation } from '@zennotes/app-core/store' -import type { VaultTask } from '@shared/tasks' -import { toIsoDateLocal } from '@shared/tasks' -import { buildCommands } from '@zennotes/app-core/lib/commands' -import { findLeaf, updateLeaf } from '@zennotes/app-core/lib/pane-layout' -import { - paneModeForPath, - paneModesWithPathMode, - requestPaneMode -} from '@zennotes/app-core/lib/pane-mode' -import { - isSameFileHeadingLink, - resolveWikilinkTarget, - wikilinkHeadingAnchor -} from '@zennotes/app-core/lib/wikilinks' -import { - openDatabaseFromWikilink, - openWikilinkHeading -} from '@zennotes/app-core/lib/wikilink-navigation' +import { getShellSnapshot, useShellSnapshot, subscribeShell, getAdjacentNotePath, + getTagPresenceSnapshot, subscribeTagPresence } from '@zennotes/app-core/shell' +import { getBrowseSnapshot, requestCreateBrowseFolder, requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' +import { getWorkspaceSnapshot, useWorkspaceSnapshot, subscribeWorkspace, readPersistedHomeState, + configureWorkspacePresentation, pickLocalVault, openLocalVault, connectRemoteProfile, refreshRemoteProfiles } from '@zennotes/app-core/workspace' +import { getSettingsSnapshot, useSettingsSnapshot, subscribeSettings, setSettingsVisible, setEditorFontSize } from '@zennotes/app-core/settings' +import { useEditorPresentation, setEditorMode, hasEditorSelection, runEditorCommand } from '@zennotes/app-core/editor' +import { getTasksSnapshot, moveTaskToColumn, type KanbanGroupBy } from '@zennotes/app-core/tasks' +import { runAppCommand, showCommandPalette, showSearch, showTemplates, showOutline } from '@zennotes/app-core/commands' +import { openNote, openWikilink, openTodayDailyNote } from '@zennotes/app-core/navigation' +import { requestTrashNote } from '@zennotes/app-core/notes' import { MobileEditorToolbar } from './EditorToolbar' -import { promptApp } from '@zennotes/app-core/lib/prompt-requests' -import { confirmApp } from '@zennotes/app-core/lib/confirm-requests' -import { notePathWithinFolder } from '@zennotes/app-core/lib/vault-layout' -import { noteTagsForCount } from '@zennotes/app-core/lib/tags' -import { resolveTypstPreambleFolder } from '@zennotes/app-core/lib/typst-preamble' -import { csvPathFromDatabaseTab, formDirFromCsvPath } from '@zennotes/shared-domain/databases' +import { captureMobileWorkspace, reportActionError } from './workspace-context' import { MobileDrawer } from './MobileDrawer' import { isDrawerOpen, setDrawerOpen, useDrawerOpen } from './drawer-state' import { goHome } from './nav' @@ -68,11 +53,6 @@ import { WELCOME_PENDING_KEY, FAB_HINT_KEY } from './Onboarding' import { WELCOME_NOTE_PATH } from '../bridge/welcome-note' import ensoUrl from '../assets/enso.png' import { getStoragePref } from '../bridge/icloud' -import { - createTagsEmptyStateTracker, - type TagsEmptyStateSnapshot -} from './tags-empty-state' -import { siblingNotesInDrawerOrder } from './note-order' import { getPinnedNotes, loadPins } from './pins' import { vaultSettingsAccessForLayout } from './vault-settings-access' import { getLayoutMode, isPhoneViewport, setLayoutMode, type LayoutMode } from '../viewport' @@ -100,10 +80,8 @@ function isPhoneWidth(): boolean { /** Run a command from the shared registry by id (same path the palette uses). */ function runCommand(id: string): void { - const cmd = buildCommands({ includeUnavailable: true }).find((c) => c.id === id) - if (!cmd) return - if (cmd.when && !cmd.when()) return - void cmd.run() + void runAppCommand(id).catch(reportActionError) + } function Icon({ d, filled }: { d: string; filled?: boolean }): React.JSX.Element { @@ -200,56 +178,22 @@ const APP_ROWS: SheetRow[] = [ ] function ActionSheet({ onClose }: { onClose: () => void }): React.JSX.Element { - const selectedPath = useStore((s) => s.selectedPath) - const workspaceMode = useStore((s) => s.workspaceMode) - // Virtual tabs (zen://help, zen://tasks, ...) aren't notes — their rows - // (rename/trash/...) would silently no-op. - const hasNote = Boolean(selectedPath) && !selectedPath?.startsWith('zen://') - const noteFolder = useStore((s) => { - if (!s.selectedPath) return null - return s.notes.find((n) => n.path === s.selectedPath)?.folder ?? null - }) - const calendarAvailable = useStore( - (s) => s.vaultSettings.dailyNotes.enabled || s.vaultSettings.weeklyNotes.enabled - ) - // With a database tab open, offer its removal — every open thing should be - // deletable from •••. - const dbFormDir = useStore((s) => { - const csv = csvPathFromDatabaseTab(s.selectedPath) - return csv ? formDirFromCsvPath(csv) : null - }) - const dbTitle = dbFormDir - ? (dbFormDir.split('/').pop() ?? '').replace(/\.base$/i, '') - : null - const title = useStore((s) => { - if (!s.selectedPath) return 'ZenNotes' - const note = s.notes.find((n) => n.path === s.selectedPath) - return note?.title ?? 'ZenNotes' - }) - + const shell = useShellSnapshot() + const { selectedPath, workspaceMode } = shell + const hasNote = !!shell.selectedNote + const noteFolder = shell.selectedNote?.folder ?? null + const { calendarAvailable } = useSettingsSnapshot() + const database = getBrowseSnapshot().databases.find(row => row.path === selectedPath) + const dbFormDir = database?.directory ?? null + const dbTitle = database?.title ?? null + const title = shell.selectedNote?.title ?? 'ZenNotes' + const workspace = useWorkspaceSnapshot() + const host = useMemo(() => captureMobileWorkspace(), [shell.vault, workspaceMode, workspace.generation, workspace.transitioning]) const deleteOpenDatabase = (): void => { - const formDir = dbFormDir - const label = dbTitle - if (!formDir || label === null) return + if (!dbFormDir) return onClose() window.setTimeout(() => { - void (async () => { - const ok = await confirmApp({ - title: `Delete "${label}"?`, - description: 'All records will be permanently deleted. This cannot be undone.', - confirmLabel: 'Delete', - danger: true - }) - if (!ok) return - // Remap-aware: the inbox may live in a renamed directory - // (vault.json systemFolderPaths), so strip the RESOLVED prefix. - const subpath = notePathWithinFolder( - formDir, - 'inbox', - useStore.getState().vaultSettings - ) - await useStore.getState().deleteFolder('inbox', subpath) - })() + void requestDeleteBrowseDirectory(host, dbFormDir).catch(reportActionError) }, 30) } @@ -259,7 +203,7 @@ function ActionSheet({ onClose }: { onClose: () => void }): React.JSX.Element { // focus lands in the right place. window.setTimeout(() => { if (id === 'zn.palette') { - useStore.getState().setCommandPaletteOpen(true) + showCommandPalette() return } if (id === 'zn.vaults') { @@ -267,26 +211,12 @@ function ActionSheet({ onClose }: { onClose: () => void }): React.JSX.Element { return } if (id === 'zn.pickfolder') { - void useStore.getState().openVaultPicker() + void pickLocalVault() return } + if (!host.isCurrent()) return if (id === 'note.trash') { - // Own the confirm copy ("Delete") — app-core's command would show its - // desktop "Move to Trash?" dialog. The bridge's unlink event closes - // the tab via applyChange. - void (async () => { - const st = useStore.getState() - const path = st.selectedPath - if (!path) return - const noteTitle = st.notes.find((n) => n.path === path)?.title - const ok = await confirmApp({ - title: `Delete "${noteTitle ?? 'this note'}"?`, - description: 'It will move to the trash.', - confirmLabel: 'Delete', - danger: true - }) - if (ok) await window.zen.moveToTrash(path) - })() + if (selectedPath) void requestTrashNote(host, selectedPath).catch(reportActionError) return } runCommand(id) @@ -384,22 +314,14 @@ function ActionSheet({ onClose }: { onClose: () => void }): React.JSX.Element { * quick-action chips are hidden on phones, so this is its one-tap home. */ function CreateSheet({ onClose }: { onClose: () => void }): React.JSX.Element { - const dailyEnabled = useStore((s) => s.vaultSettings.dailyNotes.enabled) + const dailyEnabled = useSettingsSnapshot().dailyNotesEnabled const run = (fn: () => unknown): void => { onClose() window.setTimeout(() => void fn(), 30) } const newFolder = async (): Promise => { - const name = await promptApp({ - title: 'New folder', - placeholder: 'Folder name', - okLabel: 'Create', - validate: (v: string) => (v.includes('/') ? 'Folder name cannot contain "/"' : null) - }) - const clean = name?.trim().replace(/^\/+|\/+$/g, '') - if (!clean) return - await useStore.getState().createFolder('inbox', clean) + await requestCreateBrowseFolder(captureMobileWorkspace(), '').catch(reportActionError) } return ( @@ -430,7 +352,7 @@ function CreateSheet({ onClose }: { onClose: () => void }): React.JSX.Element { {selection && <>
Keyboard toolbar
- {}} onLink={() => {}} onComment={() => {}} onBlockType={() => {}} onDismiss={() => setSelection(false)} /> +
+ Selection toolbar layout stand-in +
} {sheet && <>
setSheet(false)} />
diff --git a/tooling/build-native-boundary-fixture.mjs b/tooling/build-native-boundary-fixture.mjs new file mode 100644 index 0000000..15ebc6d --- /dev/null +++ b/tooling/build-native-boundary-fixture.mjs @@ -0,0 +1,15 @@ +import { build } from 'vite' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +await build({ + root, configFile: resolve(root, 'vite.config.ts'), + plugins: [{ + name: 'isolated-native-boundary-fixture', + transformIndexHtml: { order: 'pre', handler(html) { + return html.replace('', '') + } } + }], + build: { outDir: 'dist-boundary-check' } +}) +console.log('Fixture build ready. Install only on a disposable native simulator or emulator; see docs/native-boundary-validation.md.') diff --git a/tooling/check-core-boundary.mjs b/tooling/check-core-boundary.mjs new file mode 100644 index 0000000..f953cd4 --- /dev/null +++ b/tooling/check-core-boundary.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { readFile, readdir } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const manifest = JSON.parse(await readFile(join(root, 'vendor/zennotes/manifest.json'), 'utf8')) +const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) +for (const entry of [manifest, ...manifest.dependencies]) { + assert.match(entry.file, /^[a-z0-9.-]+\.tgz$/) + const archive = await readFile(join(root, 'vendor/zennotes', entry.file)) + assert.equal(createHash('sha256').update(archive).digest('hex'), entry.sha256, `${entry.name} checksum`) + assert.equal(pkg.dependencies[entry.name], `file:vendor/zennotes/${entry.file}`) + const installed = JSON.parse(await readFile(join(root, 'node_modules', entry.name, 'package.json'), 'utf8')) + assert.equal(installed.version, entry.version, `${entry.name} installed version`) +} +const host = createRequire(join(root, 'package.json')) +const core = createRequire(join(root, 'node_modules/@zennotes/app-core/package.json')) +for (const peer of ['react', 'react-dom', '@codemirror/state', '@codemirror/view', '@codemirror/language', '@lezer/common', '@lezer/highlight']) { + assert.equal(core.resolve(peer), host.resolve(peer), `Duplicate ${peer}`) +} +for (const privatePath of ['store', 'lib/pane-layout', 'src/store.ts', 'components/EditorPane']) { + assert.throws(() => host.resolve(`@zennotes/app-core/${privatePath}`), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' }) +} +async function inspect(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) { await inspect(path); continue } + if (!/\.(?:[cm]?[jt]sx?|css)$/.test(entry.name)) continue + const body = await readFile(path, 'utf8') + const imports = body.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\(\s*|@import\s*)['"]([^'"]+)['"]/g) + for (const [, specifier] of imports) { + assert.ok(!/^@(shared|bridge-contract|desktop-main)\//.test(specifier), `${path}: source alias ${specifier}`) + assert.ok(!specifier.includes('.zennotes-source'), `${path}: source checkout import`) + if (specifier.startsWith('@zennotes/app-core/')) import.meta.resolve(specifier) + } + } +} +await inspect(join(root, 'src')) +await inspect(join(root, 'tooling')) +console.log(`Public core boundary verified: ${manifest.version}`) diff --git a/tooling/load-mobile-module.ts b/tooling/load-mobile-module.ts index 0f91a50..2676bb7 100644 --- a/tooling/load-mobile-module.ts +++ b/tooling/load-mobile-module.ts @@ -3,9 +3,9 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') -const require = createRequire(resolve(root, '.zennotes-source/package.json')) +const require = createRequire(resolve(root, 'package.json')) -/** Load real TypeScript modules with this mobile app's pinned source aliases. */ +/** Load real TypeScript modules with this mobile app's installed public packages. */ export async function loadMobileModule( entry: string | string[], mocks: Record> = {} diff --git a/tooling/native-boundary-fixture.ts b/tooling/native-boundary-fixture.ts new file mode 100644 index 0000000..35fce44 --- /dev/null +++ b/tooling/native-boundary-fixture.ts @@ -0,0 +1,111 @@ +import { Capacitor } from '@capacitor/core' +// Included only by the isolated native-validation build, never by index.html. +import { Filesystem, Directory, Encoding } from '@capacitor/filesystem' +import { getShellSnapshot } from '@zennotes/app-core/shell' +import { getWorkspaceSnapshot, openLocalVault, flushWorkspace, persistWorkspace } from '@zennotes/app-core/workspace' +import { openNote } from '@zennotes/app-core/navigation' +import { runEditorCommand, captureEditorInsertion, attachFiles } from '@zennotes/app-core/editor' +import { requestRenameNote, requestTrashNote, restoreNote } from '@zennotes/app-core/notes' +import { refreshTasks, getTasksSnapshot } from '@zennotes/app-core/tasks' +import { VAULT_ROOT_PREFIX, renameVault } from '../src/bridge/mobile-bridge' +import { captureMobileWorkspace } from '../src/ui-mobile/workspace-context' +import { captureAssetImporter } from '../src/ui-mobile/editor-host' + +const checks: string[] = [] +const vaultName = 'Boundary package validation ' + Date.now() +const key = 'zn:isolated-boundary-validation' +const exact = '# Boundary runtime\n\nExact café 日本語. \n- [ ] Native task\n' +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) +async function until(predicate: () => unknown | Promise, label: string) { + const deadline = Date.now() + 30000 + while (Date.now() < deadline) { if (await predicate()) return; await wait(80) } + throw new Error('Timed out: ' + label) +} +function check(value: unknown, label: string) { if (!value) throw new Error(label); checks.push(label) } +async function report(status: string, error?: unknown) { + const result = {status, checks, error: error instanceof Error ? {message:error.message,stack:error.stack} : error, app: window.zen.getAppInfo(), at: new Date().toISOString()} + await Filesystem.writeFile({directory: Capacitor.getPlatform() === 'android' ? Directory.Data : Directory.Documents, path:'boundary-validation.json', data:JSON.stringify(result,null,2), encoding:Encoding.UTF8}) + console.log('BOUNDARY_VALIDATION', JSON.stringify(result)) +} +async function confirmAction(pending: Promise, label: string) { + await until(() => document.querySelector('.z-modal'), label + ' dialog') + const button = [...document.querySelectorAll('.z-modal button')].find(button => button.textContent?.trim() === label) + if (!button) throw new Error('Missing action button: ' + label) + button.click() + check(await pending === 'completed', label + ' completed through public action') +} +async function run() { + // Exercise native onboarding rather than setting private core preferences. + const onboarding = window.setInterval(() => { + const buttons = [...document.querySelectorAll('.zn-onboard button')] + buttons.find(button => /^(Get started|Start writing|On this iPhone|On this iPad)/.test(button.textContent ?? ''))?.click() + }, 150) + try { await until(() => getShellSnapshot().workspaceRestored, 'native workspace restoration') } + finally { clearInterval(onboarding) } + await wait(500) + const saved = localStorage.getItem(key) + if (saved) { + const expected = JSON.parse(saved) + check(getShellSnapshot().vault?.name === expected.vaultName, 'cold launch retains isolated native vault') + check(getShellSnapshot().selectedPath === expected.path, 'cold launch restores selected note') + check((await window.zen.readNote(expected.path)).body === expected.body, 'cold launch retains exact saved bytes') + await report('restart-passed'); return + } + await openLocalVault(VAULT_ROOT_PREFIX + vaultName) + check(getShellSnapshot().vault?.name === vaultName && !getWorkspaceSnapshot().transitioning, 'public vault switch reaches native storage') + const note = await window.zen.createNote('inbox','Boundary runtime') + await window.zen.writeNote(note.path, '') + await until(() => getShellSnapshot().notes.some(item => item.path === note.path), 'native note index') + await openNote(note.path,{mode:'edit'}) + await until(() => document.querySelector('.cm-content'), 'native editor') + const content = document.querySelector('.cm-content')! + check(content.getAttribute('autocorrect') === 'on' && content.getAttribute('spellcheck') === 'true', 'native typing configured before focus') + content.focus() + document.execCommand('insertText',false,exact) + await until(async () => (await window.zen.readNote(note.path)).body === exact, 'exact native editor save') + check(true, 'editor saves exact Unicode and trailing whitespace through native filesystem') + check(runEditorCommand('open-search'), 'public toolbar opens native editor search') + check(runEditorCommand('close-search'), 'public toolbar closes native editor search') + await refreshTasks() + check(getTasksSnapshot().tasks.some(task => task.sourcePath === note.path && task.content === 'Native task'), 'native task scan uses shared task model') + const importer = captureAssetImporter() + const insertion = importer && captureEditorInsertion(importer) + check(insertion, 'native attachment context captured') + const result = await attachFiles(insertion!,[new File(['native attachment bytes'],'boundary.txt',{type:'text/plain'})]) + check(result.status === 'inserted', 'public attachment inserts into native editor') + check(atob(await window.zen.readVaultAssetBase64(result.assets[0].path)) === 'native attachment bytes', 'native attachment bytes preserved') + await flushWorkspace() + const attachedBody = (await window.zen.readNote(note.path)).body + await window.zen.writeNoteComments(note.path,[{notePath:note.path,anchorStart:0,anchorEnd:1,anchorText:'#',body:'Native discussion'}]) + const renamed = requestRenameNote(captureMobileWorkspace(),note.path) + await until(() => document.querySelector('.z-modal input'), 'rename prompt') + const input = document.querySelector('.z-modal input')! + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')!.set!.call(input,'Boundary renamed') + input.dispatchEvent(new Event('input',{bubbles:true})) + await wait(50) + await confirmAction(renamed,'Rename') + const renamedPath = getShellSnapshot().selectedPath! + check(renamedPath.endsWith('Boundary renamed.md'), 'native canonical rename reconciles editor') + const renamedBody = (await window.zen.readNote(renamedPath)).body + check(renamedBody === (attachedBody.startsWith('# Boundary runtime') ? attachedBody.replace('# Boundary runtime','# Boundary renamed') : attachedBody), 'native rename preserves bytes and follows leading-title rule') + check((await window.zen.readNoteComments(renamedPath))[0]?.body === 'Native discussion', 'native rename retains comments') + await confirmAction(requestTrashNote(captureMobileWorkspace(),renamedPath),'Move to Trash') + const trashed = (await window.zen.listNotes()).find(item => item.folder === 'trash' && item.title === 'Boundary renamed')! + check(trashed, 'native trash returns canonical destination') + check(await restoreNote(captureMobileWorkspace(),trashed.path) === 'completed','public restore completes on native storage') + const restored = (await window.zen.listNotes()).find(item => item.folder === 'inbox' && item.title === 'Boundary renamed')! + check((await window.zen.readNote(restored.path)).body === renamedBody && (await window.zen.readNoteComments(restored.path))[0]?.body === 'Native discussion','native trash and restore retain exact body and comments') + await openNote(restored.path,{mode:'edit'}) + await flushWorkspace(); persistWorkspace(); await wait(800) + const previousHost = captureMobileWorkspace() + const relocatedName = vaultName + ' renamed' + await renameVault({root:VAULT_ROOT_PREFIX+vaultName,name:vaultName,tier:'local'}, relocatedName) + check(getShellSnapshot().vault?.name === relocatedName && !getWorkspaceSnapshot().transitioning, 'native vault rename reopens within shared transition') + check(!previousHost.isCurrent(), 'vault relocation invalidates captured native gestures') + check((await window.zen.readNote(restored.path)).body === renamedBody && (await window.zen.readNoteComments(restored.path))[0]?.body === 'Native discussion', 'vault rename retains exact notes and comments') + await openNote(restored.path,{mode:'edit'}) + await flushWorkspace(); persistWorkspace(); await wait(800) + localStorage.setItem(key,JSON.stringify({path:restored.path,body:renamedBody,vaultName:relocatedName})) + await report('passed-awaiting-restart') +} +void run().catch(error=>report('failed',error)) diff --git a/tooling/prepare-zennotes.sh b/tooling/prepare-zennotes.sh deleted file mode 100755 index 73edc04..0000000 --- a/tooling/prepare-zennotes.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/sh -set -eu - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -PIN_FILE="$ROOT/.zennotes-commit" -SOURCE_DIR="$ROOT/.zennotes-source" -REMOTE="${ZENNOTES_SOURCE_REMOTE:-https://github.com/ZenNotes/zennotes.git}" - -if [ ! -f "$PIN_FILE" ]; then - echo "Missing .zennotes-commit source pin." >&2 - exit 1 -fi - -PIN="$(tr -d '[:space:]' < "$PIN_FILE")" -case "$PIN" in - *[!0-9a-f]*|'') - echo ".zennotes-commit must contain a full lowercase Git commit SHA." >&2 - exit 1 - ;; -esac -if [ "${#PIN}" -ne 40 ]; then - echo ".zennotes-commit must contain a full 40-character Git commit SHA." >&2 - exit 1 -fi - -if [ ! -d "$SOURCE_DIR/.git" ]; then - if [ -e "$SOURCE_DIR" ]; then - echo "$SOURCE_DIR exists but is not a Git checkout; remove or rename it." >&2 - exit 1 - fi - git clone --filter=blob:none "$REMOTE" "$SOURCE_DIR" -fi - -if [ -n "$(git -C "$SOURCE_DIR" status --porcelain)" ]; then - echo "$SOURCE_DIR has local changes; refusing to replace reproducible source." >&2 - exit 1 -fi - -if ! git -C "$SOURCE_DIR" cat-file -e "$PIN^{commit}" 2>/dev/null; then - git -C "$SOURCE_DIR" fetch --depth=1 origin "$PIN" -fi - -git -C "$SOURCE_DIR" checkout --quiet --detach "$PIN" -ACTUAL="$(git -C "$SOURCE_DIR" rev-parse HEAD)" -if [ "$ACTUAL" != "$PIN" ]; then - echo "Expected ZenNotes source $PIN but checked out $ACTUAL." >&2 - exit 1 -fi - -DEPENDENCY_MARKER="$SOURCE_DIR/node_modules/.zennotes-source-commit" -INSTALLED_PIN="" -if [ -f "$DEPENDENCY_MARKER" ]; then - INSTALLED_PIN="$(tr -d '[:space:]' < "$DEPENDENCY_MARKER")" -fi -if [ "$INSTALLED_PIN" != "$PIN" ]; then - echo "Installing dependencies for pinned ZenNotes source..." - npm --prefix "$SOURCE_DIR" ci --ignore-scripts --no-audit --no-fund - printf '%s\n' "$PIN" > "$DEPENDENCY_MARKER" -fi - -echo "ZenNotes source ready at $ACTUAL" diff --git a/tooling/upstream-check.sh b/tooling/upstream-check.sh deleted file mode 100755 index 377c0a7..0000000 --- a/tooling/upstream-check.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# Verify and typecheck against the exact ZenNotes source used by release builds. -set -eu - -DIR="$(cd "$(dirname "$0")/.." && pwd)" -sh "$DIR/tooling/prepare-zennotes.sh" - -PIN="$(tr -d '[:space:]' < "$DIR/.zennotes-commit")" -ACTUAL="$(git -C "$DIR/.zennotes-source" rev-parse HEAD)" -if [ "$PIN" != "$ACTUAL" ]; then - echo "Source mismatch: expected $PIN, found $ACTUAL" >&2 - exit 1 -fi - -echo "Typechecking mobile against pinned ZenNotes source $PIN..." -cd "$DIR" -npx tsc --noEmit -echo "OK: pinned bridge contract satisfied." diff --git a/tsconfig.json b/tsconfig.json index 8448f1e..6f66f58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,11 @@ "compilerOptions": { "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", @@ -15,19 +19,16 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, - "types": ["vite/client", "node"], - "baseUrl": ".", - "paths": { - "@renderer/*": [".zennotes-source/packages/app-core/src/*"], - "@shared/*": [".zennotes-source/packages/shared-domain/src/*"], - "@bridge-contract/*": [".zennotes-source/packages/bridge-contract/src/*"], - "@zennotes/app-core/*": [".zennotes-source/packages/app-core/src/*"], - "@zennotes/shared-domain/*": [".zennotes-source/packages/shared-domain/src/*"], - "@zennotes/bridge-contract/*": [".zennotes-source/packages/bridge-contract/src/*"], - "@desktop-main/*": [".zennotes-source/apps/desktop/src/main/*"], - "@codemirror/*": ["node_modules/@codemirror/*"], - "@lezer/*": ["node_modules/@lezer/*"] - } + "types": [ + "vite/client", + "node" + ], + "baseUrl": "." }, - "include": ["src", "vite.config.ts", "capacitor.config.ts"] + "include": [ + "src", + "vite.config.ts", + "capacitor.config.ts", + "tooling/native-boundary-fixture.ts" + ] } diff --git a/vendor/zennotes/manifest.json b/vendor/zennotes/manifest.json new file mode 100644 index 0000000..3cb9cbe --- /dev/null +++ b/vendor/zennotes/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "@zennotes/app-core", + "version": "2.51.0-core.h49d73b531d346192", + "file": "zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz", + "sha256": "185438be703dac634d89f6ec5c5040ec94cb37962ff46066833c2dd47403ff7e", + "integrity": "sha512-WaJLtt9Z0+0KNhMqwL+LAX9fG6JJVeZfHkJjUiL9WyHFBLLPGi0MumfSiGeKpWHabNE/pIdiP1RoU19kNyH7JA==", + "sourceCommit": "8ff2cb86d7484cd60e6547f851ebf3dff9fb323c", + "workingTreeDirty": false, + "sourceLockSha256": "0f7c50a3010181619aa03e5e91c6f343be1e00dc3cecfb3f9853ea0d3263caeb", + "toolchain": { + "node": "v22.23.2", + "typescript": "5.9.3", + "tailwind": "3.4.19" + }, + "dependencies": [ + { + "name": "@zennotes/bridge-contract", + "version": "2.51.0-boundaries.h18d39d9887df8897", + "file": "zennotes-bridge-contract-2.51.0-boundaries.h18d39d9887df8897.tgz", + "sha256": "aa291c4fd85c180caad4458c0633d42b2c1f217ba92a9a92769ae569af47ecaf", + "integrity": "sha512-ib31p4wDQvcLj+DcoEi6mq0T+1aHXL7b1MOkWpBCtQ9DkiXkHLQj2zvB3U41zc9znWe2JIXgdM6IzHVhvtd7Vw==", + "sourceCommit": "8ff2cb86d7484cd60e6547f851ebf3dff9fb323c", + "workingTreeDirty": false + }, + { + "name": "@zennotes/shared-domain", + "version": "2.51.0-boundaries.h18d39d9887df8897", + "file": "zennotes-shared-domain-2.51.0-boundaries.h18d39d9887df8897.tgz", + "sha256": "c06332862710c15e4304318434bc9d472e4d8315828a3347e24066b4b8e5100b", + "integrity": "sha512-uocbc9KXy/7XB7MtBJWEG4/8Sod5MJ27MyInQJbT52OJl0RSuvJRcXf4cfnfNvIwVg3pCX8CFFPdtVAoujg4xg==", + "sourceCommit": "8ff2cb86d7484cd60e6547f851ebf3dff9fb323c", + "workingTreeDirty": false + } + ] +} diff --git a/vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz b/vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b85cd057259796cc9cebdeb7dffc8bdb9ab2f959 GIT binary patch literal 3101734 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

q>A{GyGlV-} zw43c&GVr~8?=$M9*_SKDy;-X6?pKU|E3ccIkbCwxxIKHsS#fwWsQJ<{Lah0U)D-gk zvH=%CRcZzheJM$aL|>7nj!R!kQUcRgB&osFmr?|r`pUgt`AO1gTZtfExvNAxuAH%( zMLuTJ6I)&flHTN~YKLKWsv1tYvd`BEt1N<6R$rJ|ev?f{3PaBjtpGQ9X#q~thqN_h zgL5IKnh6`nAT3B9osHwhMr?M!rX4?mq+lJ})Lm-0^n;K|F z7XGN`Ug$M@tj&5~;eWsGt@Kf`}gmwus6=# z<0SG$*{xmODXk9G6VG*hkxw1Cvou!&b@3U%iI75P)`txxHSv>Ip9(Hz4hg@~oQNKw zGPP!XX=hO&&lpvU|$Ja16wDGoUTK$6$RAQAwdz!^&HKrUt+Q11|M2%vHxa1$#4 zvRkZ5I-lP;xLf%n>%TXNE9)@HsY!jU_-*s)d?X|g>UE?5kzwk@QyxekH5v(IManR} zH*eUxcRlnHC$CHx1Y-vSeKk3TH&oO!eI-+?z~|KWYm9Yc({J?EigQrQGqwr+^%hLNO+1G%phf0l#*Yae zLwD{%V0Q1rhe_yqLr=KxU7m=ms~%qahmi*=1$i#Z=oa`sFtKYf`ZhFUjw{gvzg(D3 zZkQzc@?$ahS8vHUW|w>

+DNU)n_hkYu8~Jt_1cglJt3|t~bfCP=V_mW4@iJ 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