diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md index fca7f03b..d8bd9d42 100644 --- a/.agents/skills/release/SKILL.md +++ b/.agents/skills/release/SKILL.md @@ -108,6 +108,7 @@ Prepend a new section to `CHANGELOG.md` directly below the `# Changelog` heading - Use present tense ("Add dark mode toggle", not "Added dark mode toggle") - Focus on what the user sees/experiences, not internal implementation details - Use the current date in YYYY-MM-DD format +- **Never use Nostr protocol jargon.** NIP numbers (e.g., "NIP-89", "NIP-17"), kind numbers (e.g., "kind 30078"), and other protocol-level references must not appear in the changelog. Describe the feature in plain language from the user's perspective. For example, write "App cards for Nostr apps" instead of "App cards for Nostr apps (NIP-89)". The changelog audience is end users, not protocol developers. - **Collapse related work into one entry.** If a feature was added and then fixed/tweaked across multiple commits in the same release, present the finished result as a single "Added" entry. Never list something as "Added" and then also list fixes for that same thing -- the user sees the end product, not the development history. - **Omit purely internal changes.** CI fixes, build pipeline tweaks, developer tooling, and infrastructure changes should be omitted from the changelog entirely unless they have a direct, visible impact on the user experience. The changelog is for users, not developers. - **Compare the actual code between versions** to understand what really changed, rather than just reading commit messages. Commit messages may over- or under-represent the significance of changes. diff --git a/AGENTS.md b/AGENTS.md index cf78b483..0dbaae89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -716,6 +716,43 @@ await publishEvent({ kind: 10003, content: freshEvent?.content ?? '', tags: newT This applies to all list-type hooks (bookmarks, pins, interests, follow sets, badges, etc.). See `useFollowActions` and `useMuteList` for complete examples. +### D-Tag Collision Prevention for Addressable Events + +Addressable events (kind 30000-39999) are identified by `pubkey + kind + d-tag`. Publishing an event with the same d-tag as an existing one **silently replaces** it. This is by design for intentional updates (edit flows), but dangerous when creating *new* content with user-derived d-tags (slugs from titles, user-entered identifiers, etc.). + +#### When to Check for Collisions + +**Must check before publishing** when the d-tag is derived from user input (slugified titles, user-entered identifiers, etc.). **No check needed** when the d-tag is a `crypto.randomUUID()`, a canonical format with embedded pubkey prefix, or intentionally the same as an existing event (edit/update flows). + +#### Implementation Pattern + +Before publishing a **new** addressable event with a user-derived d-tag, query for an existing event with that d-tag. If one exists, block the publish and tell the user to change the identifier. + +```typescript +// Before publishing a new addressable event: +const slug = slugify(title, { lower: true, strict: true }); + +const existing = await nostr.query([ + { kinds: [30023], authors: [user.pubkey], '#d': [slug], limit: 1 }, +]); + +if (existing.length > 0) { + toast({ + title: 'Slug already in use', + description: 'Change the slug or edit the existing item.', + variant: 'destructive', + }); + return; +} + +// Safe to publish +publishEvent({ kind: 30023, content, tags: [['d', slug], ...otherTags] }); +``` + +**Skip the check in edit mode** -- when the user explicitly loaded an existing event to update, overwriting is the intended behavior. + +Prefer UUID or canonical formats when the d-tag doesn't need to be human-readable. Only use slugified input when the d-tag will appear in URLs or needs to be meaningful to users, and always add a collision check. + ### Nostr Login To enable login with Nostr, simply use the `LoginArea` component already included in this project. diff --git a/CHANGELOG.md b/CHANGELOG.md index b6678825..2f08afaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [2.3.0] - 2026-04-02 + +### Added +- In-app article editor with a rich text toolbar, image uploads, auto-saving drafts, and a "My Articles" tab to manage drafts and published articles + +### Fixed +- Custom emoji no longer stretch to fill their container +- Mobile drawer now closes when tapping footer links like Changelog or Privacy +- Logged-out users now default to the global tab on content feeds instead of seeing an empty follows tab + +## [2.2.11] - 2026-04-02 + +### Fixed +- Fix crash caused by the "What's new" toast firing outside the router + +## [2.2.10] - 2026-04-02 + +### Added +- App cards for Nostr apps now display in feeds and detail pages with hero images, icons, and quick-launch buttons +- "What's new" toast appears after an app update with a changelog preview and link to the full changelog + +### Changed +- Changelog page redesigned with a hero section for the latest release, collapsible older entries, and category icons inline with each item + +### Fixed +- Compose box now fully resets to its collapsed state after posting, including poll options and media trays + ## [2.2.9] - 2026-04-01 ### Added @@ -36,7 +63,7 @@ ### Added - Encrypted letters now appear as interactive 3D envelopes with Nushu script -- flip and open them to reveal the secret writing inside - Zap receipts and profile metadata events now render in feeds and detail pages -- Remote signer callback page for NIP-46 login flows (Amber, Primal) +- Remote signer callback page for login flows with Amber, Primal, and other signing apps ### Changed - Post action buttons extracted into a reusable PostActionBar component @@ -91,11 +118,11 @@ ## [2.2.2] - 2026-03-29 ### Added -- Dedicated photo upload flow for sharing photos as NIP-68 kind 20 events +- Dedicated photo upload flow for sharing photos - Pull-to-refresh on all feed pages - 3D tilt effect on badge images -- hover over badges to see them pop - Multi-select badge awarding with indicators for already-sent badges -- Badge list recovery dialog for restoring kind 10008 profile badge lists +- Badge list recovery dialog for restoring profile badge lists - Compact badge row preview in embedded profile badges events - Custom emoji usage tracking so your most-used custom emojis appear in the quick-react bar - Release notes now included in Zapstore publishing @@ -114,7 +141,7 @@ - Double-tap reactions now properly show the emoji on the post - Emoji shortcode autocomplete text and highlight colors - Profile skeleton no longer flickers for brand-new users with no metadata -- Addressable event routing now works correctly for replaceable events (kind 10000-19999) +- Event links now route correctly for all event types - Badge notifications are now clickable - Custom profile tab form no longer retains fields from a previously edited tab - Double line under profile tabs in edit mode @@ -141,10 +168,10 @@ - Blobbi shop and inventory system with items that affect your pet's stats - Daily missions with reroll, care streaks, and stage-based rewards - Immersive full-screen divines experience on both mobile and desktop with floating controls -- NIP-11 relay information panel on the network settings page +- Relay information panel on the network settings page - Link preview cards now display inside quoted posts instead of raw URLs - Nsec paste guard warns you before accidentally pasting private keys outside the login field -- Remote signer UX improvements for Amber and NIP-46 users on Android +- Remote signer UX improvements for Amber users on Android - Badge awards now trigger push notifications - Badges display in profile bio section with a "Give badge" option in the profile menu diff --git a/android/app/build.gradle b/android/app/build.gradle index b03b2705..ea93e5c3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -14,7 +14,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "2.2.9" + versionName "2.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 72e002ab..1aaa26d2 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -303,7 +303,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.9; + MARKETING_VERSION = 2.3.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = pub.ditto.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -325,7 +325,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.9; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = pub.ditto.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package-lock.json b/package-lock.json index 26c2d381..216c5ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ditto", - "version": "2.2.9", + "version": "2.2.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ditto", - "version": "2.2.9", + "version": "2.2.11", "dependencies": { "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.1.0", @@ -47,6 +47,17 @@ "@fontsource/special-elite": "^5.2.8", "@getalby/sdk": "^5.1.1", "@hookform/resolvers": "^5.2.2", + "@milkdown/core": "^7.20.0", + "@milkdown/ctx": "^7.20.0", + "@milkdown/plugin-clipboard": "^7.20.0", + "@milkdown/plugin-history": "^7.20.0", + "@milkdown/plugin-listener": "^7.20.0", + "@milkdown/plugin-upload": "^7.20.0", + "@milkdown/preset-commonmark": "^7.20.0", + "@milkdown/preset-gfm": "^7.20.0", + "@milkdown/prose": "^7.20.0", + "@milkdown/react": "^7.20.0", + "@milkdown/utils": "^7.20.0", "@nostrify/nostrify": "^0.51.0", "@nostrify/react": "^0.4.0", "@nostrify/types": "^0.36.9", @@ -111,6 +122,7 @@ "react-router-dom": "^6.26.2", "recharts": "^2.12.7", "rehype-sanitize": "^6.0.0", + "slugify": "^1.6.8", "smol-toml": "^1.6.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", @@ -238,12 +250,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -262,13 +274,13 @@ } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -400,6 +412,416 @@ "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", "license": "ISC" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", + "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-jinja": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.0.tgz", + "integrity": "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.2.tgz", + "integrity": "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", + "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-jinja": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1399,9 +1821,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1414,6 +1836,558 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz", + "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==", + "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.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz", + "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", + "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.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "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.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "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.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@milkdown/components": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/components/-/components-7.20.0.tgz", + "integrity": "sha512-Qn91/oZugGjf17ARE51nbEsH4YklZQaomRSsfxOAtIcwGEJe5osq+zhhKGtgAYFfUb6rU3W86Pe4XDlXN6vFjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/exception": "7.20.0", + "@milkdown/plugin-tooltip": "7.20.0", + "@milkdown/preset-commonmark": "7.20.0", + "@milkdown/preset-gfm": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/transformer": "7.20.0", + "@milkdown/utils": "7.20.0", + "@types/lodash-es": "^4.17.12", + "clsx": "^2.0.0", + "dompurify": "^3.2.5", + "lodash-es": "^4.17.21", + "nanoid": "^5.0.9", + "unist-util-visit": "^5.0.0", + "vue": "^3.5.20" + }, + "peerDependencies": { + "@codemirror/language": "^6", + "@codemirror/state": "^6", + "@codemirror/view": "^6" + } + }, + "node_modules/@milkdown/components/node_modules/nanoid": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@milkdown/core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/core/-/core-7.20.0.tgz", + "integrity": "sha512-X9LaUcIR4Y2oiY2J5tslavlPVOwIB3X8/9z1bOeBjlIPtr+urbkY7YEX86EeLV9LyRQ3+t+jXaLMCIjW1wsV6w==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.20.0", + "@milkdown/exception": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/transformer": "7.20.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.3" + } + }, + "node_modules/@milkdown/crepe": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/crepe/-/crepe-7.20.0.tgz", + "integrity": "sha512-KT+oFF6Q7mI41z01c9v/wUUCyQ2f908TgOsa6mwi25yuxnxQxISZFCjRvlh0sc9p9D3CrMeuJWGCN6DialQdig==", + "license": "MIT", + "dependencies": { + "@codemirror/commands": "^6.2.4", + "@codemirror/language": "^6.10.1", + "@codemirror/language-data": "^6.3.1", + "@codemirror/state": "^6.4.1", + "@codemirror/theme-one-dark": "^6.1.2", + "@codemirror/view": "^6.16.0", + "@milkdown/kit": "7.20.0", + "@types/lodash-es": "^4.17.12", + "clsx": "^2.0.0", + "codemirror": "^6.0.1", + "katex": "^0.16.0", + "lodash-es": "^4.17.21", + "prosemirror-virtual-cursor": "^0.4.2", + "remark-math": "^6.0.0", + "unist-util-visit": "^5.0.0", + "vue": "^3.5.20" + } + }, + "node_modules/@milkdown/ctx": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/ctx/-/ctx-7.20.0.tgz", + "integrity": "sha512-LUK4xdsUngY2xCCvPTyHPifjAknJ5rE6VBjgsP+LySIUKeFUrhqzo/zz2vaOODKzm3DBMIhpZAoW3MAqxoMGIQ==", + "license": "MIT", + "dependencies": { + "@milkdown/exception": "7.20.0" + } + }, + "node_modules/@milkdown/exception": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/exception/-/exception-7.20.0.tgz", + "integrity": "sha512-u8EL7rbqgrWrPpkDhrxUYXauw2DO52JUQmuokrUZvqezmflo7pgIDCr+Rk6AQslzB4Xw+n9eYik5rXX3RXC7Qg==", + "license": "MIT" + }, + "node_modules/@milkdown/kit": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/kit/-/kit-7.20.0.tgz", + "integrity": "sha512-X74KMa0tcDAAMOE9aFtBRN+RCdD/HMXor5YN18e7d0pe4a65MGFklUGlcg1U6zEfeMMYeC3msNvMKLMwk3O5RA==", + "license": "MIT", + "dependencies": { + "@milkdown/components": "7.20.0", + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/plugin-block": "7.20.0", + "@milkdown/plugin-clipboard": "7.20.0", + "@milkdown/plugin-cursor": "7.20.0", + "@milkdown/plugin-history": "7.20.0", + "@milkdown/plugin-indent": "7.20.0", + "@milkdown/plugin-listener": "7.20.0", + "@milkdown/plugin-slash": "7.20.0", + "@milkdown/plugin-tooltip": "7.20.0", + "@milkdown/plugin-trailing": "7.20.0", + "@milkdown/plugin-upload": "7.20.0", + "@milkdown/preset-commonmark": "7.20.0", + "@milkdown/preset-gfm": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/transformer": "7.20.0", + "@milkdown/utils": "7.20.0" + } + }, + "node_modules/@milkdown/plugin-block": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-block/-/plugin-block-7.20.0.tgz", + "integrity": "sha512-jIXfzJ8Zje+6+9ZwQuVmNeYE8KfzqL9YJ/YdMvWQIEiKhy2x9pZMAkkufgmUlq1aouxOV+gk5fX+ovxzEzfSrA==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-clipboard": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-clipboard/-/plugin-clipboard-7.20.0.tgz", + "integrity": "sha512-PyokNvwgWcO6/I/0LxDRnATpnxvs5upFRlp6eO8PhjwBFZftCIU6D15Wg4JAxwW7Y0NyTWfViWjc9TwiBd6KOQ==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0" + } + }, + "node_modules/@milkdown/plugin-cursor": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-cursor/-/plugin-cursor-7.20.0.tgz", + "integrity": "sha512-goCPwUARBzGV6Hvnr3P57Bj5TnyFjYIfDFLvgWTIlsm/dR2Wr4Syy4HDOtaKO9YL/VtZ8gtiZVgeo0vhc4CzMA==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0", + "prosemirror-drop-indicator": "^0.1.0" + } + }, + "node_modules/@milkdown/plugin-history": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-history/-/plugin-history-7.20.0.tgz", + "integrity": "sha512-lqOYQBrxKj4px/i0Cav3zRkCArwnkv8o7fGMh3NtnUXMLSE7/xojK5GFPS4EaS/UKK7/+i1oV2+HRA6+6Ezy7w==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0" + } + }, + "node_modules/@milkdown/plugin-indent": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-indent/-/plugin-indent-7.20.0.tgz", + "integrity": "sha512-KfdIztQMuHv4Rx1JmSQe2vooN4+Zm7MhjQkNolGyiI7BPZbu855hVIC/s96x3Dk04tkbb+M/i9MJhxCazxfd6Q==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0" + } + }, + "node_modules/@milkdown/plugin-listener": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-listener/-/plugin-listener-7.20.0.tgz", + "integrity": "sha512-Sj+B63JfM3NVVS3uGXTPkoz8xx8MQYrR28pI9AaqX5q60tvCvOJw9E1ODvSsBEjeqnN4kablDthIugLlBhOlwQ==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-slash": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-slash/-/plugin-slash-7.20.0.tgz", + "integrity": "sha512-Qm3/ZxkGYd5XN+J/X91lGGu7SBzuQBOTOLjuJdg4qDBZmdEHlGojB+5BhCSAMB3WGyCpQQGbSqKOelUrXtj68w==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-tooltip": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-tooltip/-/plugin-tooltip-7.20.0.tgz", + "integrity": "sha512-BVaXorpmA6ZAS3+xv0rgrtjV1h2K39G5Z9Wun4RxT1YXJTTbzIuFQ3hwBAGLjLMwTsosp7YhRLaMJJAC0jEY5Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0", + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + } + }, + "node_modules/@milkdown/plugin-trailing": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-trailing/-/plugin-trailing-7.20.0.tgz", + "integrity": "sha512-AxDeMSAZfj0Er7RYLvLRf6FKdQtLVmotxML6Se6zgqIa++bFeIXCU22/FC+9r/6d1eUtraTva9ez5K2qPy8qig==", + "license": "MIT", + "dependencies": { + "@milkdown/ctx": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0" + } + }, + "node_modules/@milkdown/plugin-upload": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/plugin-upload/-/plugin-upload-7.20.0.tgz", + "integrity": "sha512-g3UQrD2zfpm86r3BcBDfOdEAyQHhay1nf5wUQgNf4zn6IgRttfEF8tosQsL1B/WBnZB05hH5scLWo4DR2bFhUw==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/exception": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/utils": "7.20.0" + } + }, + "node_modules/@milkdown/preset-commonmark": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/preset-commonmark/-/preset-commonmark-7.20.0.tgz", + "integrity": "sha512-+mPcONXfdjaXdx8JMxDIOT3PWHfy5vewK8iY8j8bUiYD8Iw7YfyWTUh9JHOf4vmOpiKGCJd7+iz7e93u95bQRw==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/exception": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/transformer": "7.20.0", + "@milkdown/utils": "7.20.0", + "remark-inline-links": "^7.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@milkdown/preset-gfm": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/preset-gfm/-/preset-gfm-7.20.0.tgz", + "integrity": "sha512-ulErTWDqrGYYqto4kQO9dPTMRp+q44pdRTPW4MTeiSO7eJ6JIBUKSqtCm1zf7MX6nZPaPhuscmg5CU2moXOwxQ==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/exception": "7.20.0", + "@milkdown/preset-commonmark": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/transformer": "7.20.0", + "@milkdown/utils": "7.20.0", + "prosemirror-safari-ime-span": "^1.0.1", + "remark-gfm": "^4.0.1" + } + }, + "node_modules/@milkdown/prose": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/prose/-/prose-7.20.0.tgz", + "integrity": "sha512-Qe6jmKcXsjOfpk8duDFdkLCEo5044L8HSyKVn7ewAe7XJJPUM6bPQaP130UAznq75/+TiKxFCzurcrBO3LzNRg==", + "license": "MIT", + "dependencies": { + "@milkdown/exception": "7.20.0", + "prosemirror-changeset": "^2.3.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.0", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.1", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.3" + } + }, + "node_modules/@milkdown/react": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/react/-/react-7.20.0.tgz", + "integrity": "sha512-uuMuMfTmp2SZJtmKhX+tmVkJNasKnzxlYoHtUwjxuZcy90cWQVYpGXnmwpFgjcXY38lMLsAOLx2jjXrqe7ZOuQ==", + "license": "MIT", + "dependencies": { + "@milkdown/crepe": "7.20.0", + "@milkdown/kit": "7.20.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@milkdown/transformer": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/transformer/-/transformer-7.20.0.tgz", + "integrity": "sha512-h7KGFr1o5AYwc+hEfnA3Dldo4jRrYOB/7KExaqelcjUz++KYI/9LXMOsV7CpgjtLI3Xtf2IIRTZND1+p2nsOaw==", + "license": "MIT", + "dependencies": { + "@milkdown/exception": "7.20.0", + "@milkdown/prose": "7.20.0", + "remark": "^15.0.1", + "unified": "^11.0.3" + } + }, + "node_modules/@milkdown/utils": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@milkdown/utils/-/utils-7.20.0.tgz", + "integrity": "sha512-ciEhtLKhIW/Kaz/NRE5DUXVoMCdenn7S4mClrO7sZ/nXtmObnk3okJzSDnamQoDOcLOIbpOu1V3E1Btkvc5x9w==", + "license": "MIT", + "dependencies": { + "@milkdown/core": "7.20.0", + "@milkdown/ctx": "7.20.0", + "@milkdown/exception": "7.20.0", + "@milkdown/prose": "7.20.0", + "@milkdown/transformer": "7.20.0", + "nanoid": "^5.0.9" + } + }, + "node_modules/@milkdown/utils/node_modules/nanoid": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -1572,6 +2546,15 @@ "resolved": "https://registry.npmjs.org/@nostrify/types/-/types-0.36.9.tgz", "integrity": "sha512-tMx/r0W+QoVRRgs8d6ltaSgrftasOXuFsi33kW8WirswCy2b3UR1tqRgc0iBU9zRa9XR0nlej/wJZW+6wUFi+Q==" }, + "node_modules/@ocavue/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@ocavue/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-8W3q1hxx9qFdrYgPtbElllG/tqYkO/dMhlRUiqasO0SuDFTj78azSQjhIrBTFWxlBPPsSZN6zXYHmb3RwN2Jtg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ocavue" + } + }, "node_modules/@oxc-project/types": { "version": "0.122.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", @@ -5528,6 +6511,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -6035,6 +7039,130 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "vue": "3.5.31" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "license": "MIT" + }, "node_modules/@webbtc/webln-types": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@webbtc/webln-types/-/webln-types-3.0.0.tgz", @@ -6719,6 +7847,21 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6769,6 +7912,12 @@ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8391,6 +9540,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/katex": { + "version": "0.16.44", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.44.tgz", + "integrity": "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8721,9 +9895,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.castarray": { @@ -8802,12 +9982,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magic-string-ast": { @@ -8825,6 +10005,59 @@ "url": "https://github.com/sponsors/sxzz" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", @@ -8849,6 +10082,126 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -9056,6 +10409,146 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -9690,6 +11183,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10174,6 +11673,198 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-drop-indicator": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/prosemirror-drop-indicator/-/prosemirror-drop-indicator-0.1.3.tgz", + "integrity": "sha512-fJV6G2tHIVXZLUuc60fS9ly1/GuGOlAZUm67S1El+kGFUYh27Hyv6hcGx3rrJ+Q/JZL5jnyAibIZYYWpPqE45g==", + "license": "MIT", + "dependencies": { + "@ocavue/utils": "^1.0.0", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-view": "^1.41.3" + }, + "funding": { + "url": "https://github.com/sponsors/ocavue" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-safari-ime-span": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/prosemirror-safari-ime-span/-/prosemirror-safari-ime-span-1.0.2.tgz", + "integrity": "sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.4.3", + "prosemirror-view": "^1.33.8" + }, + "funding": { + "url": "https://github.com/sponsors/ocavue" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.7", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz", + "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/prosemirror-virtual-cursor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/prosemirror-virtual-cursor/-/prosemirror-virtual-cursor-0.4.2.tgz", + "integrity": "sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ocavue" + }, + "peerDependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + }, + "peerDependenciesMeta": { + "prosemirror-model": { + "optional": true + }, + "prosemirror-state": { + "optional": true + }, + "prosemirror-view": { + "optional": true + } + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10600,6 +12291,71 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-inline-links": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/remark-inline-links/-/remark-inline-links-7.0.0.tgz", + "integrity": "sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-definitions": "^6.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -10633,6 +12389,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11094,6 +12865,12 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -11272,6 +13049,15 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/slugify": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.8.tgz", + "integrity": "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/smol-toml": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", @@ -11413,6 +13199,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -11931,6 +13723,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -13597,6 +15403,33 @@ } } }, + "node_modules/vue": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index f0e46b7e..6586f735 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ditto", "private": true, - "version": "2.2.9", + "version": "2.3.0", "type": "module", "scripts": { "dev": "npm i --silent && vite", @@ -53,6 +53,17 @@ "@fontsource/special-elite": "^5.2.8", "@getalby/sdk": "^5.1.1", "@hookform/resolvers": "^5.2.2", + "@milkdown/core": "^7.20.0", + "@milkdown/ctx": "^7.20.0", + "@milkdown/plugin-clipboard": "^7.20.0", + "@milkdown/plugin-history": "^7.20.0", + "@milkdown/plugin-listener": "^7.20.0", + "@milkdown/plugin-upload": "^7.20.0", + "@milkdown/preset-commonmark": "^7.20.0", + "@milkdown/preset-gfm": "^7.20.0", + "@milkdown/prose": "^7.20.0", + "@milkdown/react": "^7.20.0", + "@milkdown/utils": "^7.20.0", "@nostrify/nostrify": "^0.51.0", "@nostrify/react": "^0.4.0", "@nostrify/types": "^0.36.9", @@ -117,6 +128,7 @@ "react-router-dom": "^6.26.2", "recharts": "^2.12.7", "rehype-sanitize": "^6.0.0", + "slugify": "^1.6.8", "smol-toml": "^1.6.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index b6678825..2f08afaf 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [2.3.0] - 2026-04-02 + +### Added +- In-app article editor with a rich text toolbar, image uploads, auto-saving drafts, and a "My Articles" tab to manage drafts and published articles + +### Fixed +- Custom emoji no longer stretch to fill their container +- Mobile drawer now closes when tapping footer links like Changelog or Privacy +- Logged-out users now default to the global tab on content feeds instead of seeing an empty follows tab + +## [2.2.11] - 2026-04-02 + +### Fixed +- Fix crash caused by the "What's new" toast firing outside the router + +## [2.2.10] - 2026-04-02 + +### Added +- App cards for Nostr apps now display in feeds and detail pages with hero images, icons, and quick-launch buttons +- "What's new" toast appears after an app update with a changelog preview and link to the full changelog + +### Changed +- Changelog page redesigned with a hero section for the latest release, collapsible older entries, and category icons inline with each item + +### Fixed +- Compose box now fully resets to its collapsed state after posting, including poll options and media trays + ## [2.2.9] - 2026-04-01 ### Added @@ -36,7 +63,7 @@ ### Added - Encrypted letters now appear as interactive 3D envelopes with Nushu script -- flip and open them to reveal the secret writing inside - Zap receipts and profile metadata events now render in feeds and detail pages -- Remote signer callback page for NIP-46 login flows (Amber, Primal) +- Remote signer callback page for login flows with Amber, Primal, and other signing apps ### Changed - Post action buttons extracted into a reusable PostActionBar component @@ -91,11 +118,11 @@ ## [2.2.2] - 2026-03-29 ### Added -- Dedicated photo upload flow for sharing photos as NIP-68 kind 20 events +- Dedicated photo upload flow for sharing photos - Pull-to-refresh on all feed pages - 3D tilt effect on badge images -- hover over badges to see them pop - Multi-select badge awarding with indicators for already-sent badges -- Badge list recovery dialog for restoring kind 10008 profile badge lists +- Badge list recovery dialog for restoring profile badge lists - Compact badge row preview in embedded profile badges events - Custom emoji usage tracking so your most-used custom emojis appear in the quick-react bar - Release notes now included in Zapstore publishing @@ -114,7 +141,7 @@ - Double-tap reactions now properly show the emoji on the post - Emoji shortcode autocomplete text and highlight colors - Profile skeleton no longer flickers for brand-new users with no metadata -- Addressable event routing now works correctly for replaceable events (kind 10000-19999) +- Event links now route correctly for all event types - Badge notifications are now clickable - Custom profile tab form no longer retains fields from a previously edited tab - Double line under profile tabs in edit mode @@ -141,10 +168,10 @@ - Blobbi shop and inventory system with items that affect your pet's stats - Daily missions with reroll, care streaks, and stage-based rewards - Immersive full-screen divines experience on both mobile and desktop with floating controls -- NIP-11 relay information panel on the network settings page +- Relay information panel on the network settings page - Link preview cards now display inside quoted posts instead of raw URLs - Nsec paste guard warns you before accidentally pasting private keys outside the login field -- Remote signer UX improvements for Amber and NIP-46 users on Android +- Remote signer UX improvements for Amber users on Android - Badge awards now trigger push notifications - Badges display in profile bio section with a "Give badge" option in the profile menu diff --git a/src/App.tsx b/src/App.tsx index a7526894..60879594 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,8 +16,8 @@ import NostrProvider from "@/components/NostrProvider"; import { NostrSync } from "@/components/NostrSync"; import { PlausibleProvider } from "@/components/PlausibleProvider"; import { SentryProvider } from "@/components/SentryProvider"; -import { VersionCheck } from "@/components/VersionCheck"; -import { Toaster } from "@/components/ui/toaster"; + + import { TooltipProvider } from "@/components/ui/tooltip"; import { useNsecPasteGuard } from "@/hooks/useNsecPasteGuard"; import type { AppConfig } from "@/contexts/AppContext"; @@ -201,12 +201,11 @@ export function App() { - + - diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index eb072b34..4e8fc10f 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -6,8 +6,10 @@ import { MinimizedAudioBar } from "@/components/MinimizedAudioBar"; import { AudioPlayerProvider } from "@/contexts/AudioPlayerContext"; import { BlobbiActionsProvider } from "@/blobbi/companion/interaction/BlobbiActionsProvider"; import { sidebarItemIcon } from "@/lib/sidebarItems"; +import { Toaster } from "./components/ui/toaster"; import { MainLayout } from "./components/MainLayout"; import { ScrollToTop } from "./components/ScrollToTop"; +import { VersionCheck } from "./components/VersionCheck"; import { useCurrentUser } from "./hooks/useCurrentUser"; import { useProfileUrl } from "./hooks/useProfileUrl"; import { getExtraKindDef } from "./lib/extraKinds"; @@ -32,6 +34,7 @@ const HomePage = lazy(() => import("./pages/HomePage").then(m => ({ default: m.H const AdvancedSettingsPage = lazy(() => import("./pages/AdvancedSettingsPage").then(m => ({ default: m.AdvancedSettingsPage }))); const AIChatPage = lazy(() => import("./pages/AIChatPage").then(m => ({ default: m.AIChatPage }))); const ArchivePage = lazy(() => import("./pages/ArchivePage").then(m => ({ default: m.ArchivePage }))); +const ArticleEditorPage = lazy(() => import("./pages/ArticleEditorPage").then(m => ({ default: m.ArticleEditorPage }))); const BadgesPage = lazy(() => import("./pages/BadgesPage").then(m => ({ default: m.BadgesPage }))); const BlobbiPage = lazy(() => import("./pages/BlobbiPage").then(m => ({ default: m.BlobbiPage }))); const BlueskyPage = lazy(() => import("./pages/BlueskyPage").then(m => ({ default: m.BlueskyPage }))); @@ -136,6 +139,8 @@ export function AppRouter() { return ( + + @@ -207,6 +212,8 @@ export function AppRouter() { } /> } /> + } /> + } /> } /> diff --git a/src/components/AppHandlerContent.tsx b/src/components/AppHandlerContent.tsx new file mode 100644 index 00000000..0c8d5688 --- /dev/null +++ b/src/components/AppHandlerContent.tsx @@ -0,0 +1,316 @@ +import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; +import { ExternalLink, GitFork, Package } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { Button } from '@/components/ui/button'; +import { ExternalFavicon } from '@/components/ExternalFavicon'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useLinkPreview } from '@/hooks/useLinkPreview'; +import { NostrURI } from '@/lib/NostrURI'; +import { cn } from '@/lib/utils'; + +/** Get a tag value by name. */ +function getTag(tags: string[][], name: string): string | undefined { + return tags.find(([n]) => n === name)?.[1]; +} + +/** Get all values for a tag name. */ +function getAllTags(tags: string[][], name: string): string[] { + return tags.filter(([n]) => n === name).map(([, v]) => v); +} + +/** Parse kind-0-style metadata from the content field. */ +function parseHandlerMetadata(content: string): NostrMetadata { + if (!content) return {}; + try { + return JSON.parse(content) as NostrMetadata; + } catch { + return {}; + } +} + +/** Get the website URL from web handler tags or metadata. */ +function getWebsiteUrl(tags: string[][], metadata: NostrMetadata): string | undefined { + const webTags = tags.filter(([n]) => n === 'web'); + for (const tag of webTags) { + const url = tag[1]; + if (url) { + const base = url.replace(//g, '').replace(/\/+$/, ''); + return base; + } + } + return metadata.website; +} + +/** Extract the display domain from a URL. */ +function displayDomain(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, ''); + } catch { + return url; + } +} + +/** Build a Shakespeare "Edit with Shakespeare" URL from a kind 30617 `a` tag, if present. */ +function getShakespeareUrl(tags: string[][]): string | undefined { + for (const tag of tags) { + if (tag[0] !== 'a') continue; + const parts = tag[1]?.split(':'); + if (!parts || parts[0] !== '30617' || parts.length < 3) continue; + const pubkey = parts[1]; + const identifier = parts.slice(2).join(':'); + const nostrUri = new NostrURI({ pubkey, identifier }).toString(); + return `https://shakespeare.diy/clone?url=${encodeURIComponent(nostrUri)}`; + } + return undefined; +} + +interface AppHandlerContentProps { + event: NostrEvent; + /** If true, show compact preview (used in NoteCard feed). */ + compact?: boolean; +} + +/** Renders a kind 31990 NIP-89 application handler event as a showcase-style card. */ +export function AppHandlerContent({ event, compact }: AppHandlerContentProps) { + const metadata = useMemo(() => parseHandlerMetadata(event.content), [event.content]); + + const name = metadata.name || getTag(event.tags, 'name') || getTag(event.tags, 'd') || 'Unknown App'; + const about = metadata.about; + const picture = metadata.picture; + const websiteUrl = getWebsiteUrl(event.tags, metadata); + const hashtags = getAllTags(event.tags, 't'); + + const shakespeareUrl = useMemo(() => getShakespeareUrl(event.tags), [event.tags]); + + const { data: preview, isLoading: previewLoading } = useLinkPreview(websiteUrl ?? null); + const thumbnailUrl = preview?.thumbnail_url; + + const [imgError, setImgError] = useState(false); + const showThumbnail = thumbnailUrl && !imgError; + + if (compact) { + return ( +
+
+ {/* Screenshot hero — only shown while loading or when a thumbnail exists */} + {(previewLoading || showThumbnail) && ( +
+ {previewLoading ? ( + + ) : ( + {name} setImgError(true)} + /> + )} +
+ )} + + {/* Content */} +
+ {/* App icon — overlaps the screenshot hero like a profile avatar */} +
+ {picture ? ( + {name} { + (e.currentTarget as HTMLElement).style.display = 'none'; + }} + /> + ) : ( +
+ +
+ )} +
+ + {/* Name + domain */} +
+

{name}

+ {websiteUrl && ( +
+ + {displayDomain(websiteUrl)} +
+ )} +
+ + {/* Description */} + {about && ( +

{about}

+ )} + + {/* Tags */} + {hashtags.length > 0 && ( +
+ {hashtags.slice(0, 4).map((tag) => ( + e.stopPropagation()} + > + #{tag} + + ))} +
+ )} + + {/* Actions */} +
+ {websiteUrl && ( + + )} + {shakespeareUrl && ( + + )} +
+
+
+
+ ); + } + + // Full detail view + return ( +
+
+ {/* Screenshot hero — only shown while loading or when a thumbnail exists */} + {(previewLoading || showThumbnail) && ( +
+ {previewLoading ? ( + + ) : ( + {name} setImgError(true)} + /> + )} +
+ )} + + {/* Content */} +
+ {/* App icon — overlaps the screenshot hero like a profile avatar */} +
+ {picture ? ( + {name} { + (e.currentTarget as HTMLElement).style.display = 'none'; + }} + /> + ) : ( +
+ +
+ )} +
+ + {/* Name + domain */} +
+

{name}

+ {websiteUrl && ( +
+ + {displayDomain(websiteUrl)} +
+ )} +
+ + {/* Description */} + {about && ( +

{about}

+ )} + + {/* Tags */} + {hashtags.length > 0 && ( +
+ {hashtags.map((tag) => ( + e.stopPropagation()} + > + #{tag} + + ))} +
+ )} + + {/* Actions */} +
+ {websiteUrl && ( + + )} + {shakespeareUrl && ( + + )} +
+
+
+
+ ); +} + +/** Skeleton loading state for AppHandlerContent. */ +export function AppHandlerSkeleton() { + return ( +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ ); +} diff --git a/src/components/CommentContext.tsx b/src/components/CommentContext.tsx index a85e7f5b..d2d5cd04 100644 --- a/src/components/CommentContext.tsx +++ b/src/components/CommentContext.tsx @@ -115,6 +115,7 @@ const KIND_LABELS: Record = { 30817: 'a custom NIP', 31922: 'a calendar event', 31923: 'a calendar event', + 31990: 'an app', 32267: 'an app', 34139: 'a playlist', 34236: 'a divine', @@ -157,6 +158,7 @@ const KIND_ICONS: Partial n === 'name')?.[1]; + if (appName) return { text: `${appName} app`, icon }; + } catch { /* fall through */ } + return { text: 'an app', icon }; + } + // Extract a title-like string from tags const title = event.tags.find(([name]) => name === 'title')?.[1]; const name = event.tags.find(([name]) => name === 'name')?.[1]; diff --git a/src/components/ComposeBox.tsx b/src/components/ComposeBox.tsx index ef43d48a..9867baeb 100644 --- a/src/components/ComposeBox.tsx +++ b/src/components/ComposeBox.tsx @@ -224,6 +224,26 @@ export function ComposeBox({ const voiceRecorder = useVoiceRecorder(); const [isPublishingVoice, setIsPublishingVoice] = useState(false); + const resetComposeState = useCallback(() => { + setContent(''); + setCwEnabled(false); + setCwText(''); + setExpanded(false); + setPickerOpen(false); + setTrayOpen(false); + setInternalPreviewMode(false); + setMode(initialMode); + setPollQuestion(''); + setPollOptions([{ id: pollOptionId(), label: '' }, { id: pollOptionId(), label: '' }]); + setPollType('singlechoice'); + setPollDuration(7); + setRemovedEmbeds(new Set()); + setUploadedFileTags([]); + setUploadedFileGroups(new Map()); + setWebxdcUuids(new Map()); + setWebxdcMetas(new Map()); + }, [initialMode]); + // Use controlled preview mode if provided, otherwise use internal state const previewMode = controlledPreviewMode !== undefined ? controlledPreviewMode : internalPreviewMode; @@ -928,15 +948,7 @@ export function ComposeBox({ }); } - setContent(''); - setCwEnabled(false); - setCwText(''); - setExpanded(false); - setRemovedEmbeds(new Set()); - setUploadedFileTags([]); - setUploadedFileGroups(new Map()); - setWebxdcUuids(new Map()); - setWebxdcMetas(new Map()); + resetComposeState(); // Optimistically bump the reply count on the parent event if (replyTo && !(replyTo instanceof URL)) { queryClient.setQueryData(['event-stats', replyTo.id], (prev) => @@ -984,12 +996,7 @@ export function ComposeBox({ try { await createEvent({ kind: 1068, content: pollQuestion.trim(), tags }); - // Reset poll state - setMode('post'); - setPollQuestion(''); - setPollOptions([{ id: pollOptionId(), label: '' }, { id: pollOptionId(), label: '' }]); - setPollType('singlechoice'); - setPollDuration(7); + resetComposeState(); queryClient.invalidateQueries({ queryKey: ['feed'] }); toast({ title: 'Poll published!' }); onSuccess?.(); @@ -1007,7 +1014,7 @@ export function ComposeBox({ if (!user && compact) return null; return ( -
+
{/* Preview toggle at top when not controlled and has previewable content */} {hasPreviewableContent && controlledPreviewMode === undefined && (
diff --git a/src/components/CustomEmoji.tsx b/src/components/CustomEmoji.tsx index a809897b..b1b804ab 100644 --- a/src/components/CustomEmoji.tsx +++ b/src/components/CustomEmoji.tsx @@ -15,7 +15,7 @@ interface CustomEmojiImgProps { /** * Renders a single custom emoji as an inline image. */ -export function CustomEmojiImg({ name, url, className = 'inline h-[1.2em] w-[1.2em] align-text-bottom' }: CustomEmojiImgProps) { +export function CustomEmojiImg({ name, url, className = 'inline h-[1.2em] w-[1.2em] object-contain align-text-bottom' }: CustomEmojiImgProps) { return ( ; + return ; } } @@ -70,7 +70,7 @@ export function ReactionEmoji({ content, tags, className }: ReactionEmojiProps) */ export function RenderResolvedEmoji({ emoji, className }: { emoji: ResolvedEmoji; className?: string }) { if (emoji.url && emoji.name) { - return ; + return ; } return {emoji.content}; } diff --git a/src/components/ExternalContentHeader.tsx b/src/components/ExternalContentHeader.tsx index e2e54617..e02ee4fe 100644 --- a/src/components/ExternalContentHeader.tsx +++ b/src/components/ExternalContentHeader.tsx @@ -1082,6 +1082,7 @@ function hasVideo(tags: string[][]): boolean { /** Fallback labels for well-known kinds not in EXTRA_KINDS. */ const WELL_KNOWN_KIND_LABELS: Record = { + 31990: 'App', 32267: 'App', 30063: 'Release', 15128: 'Nsite', @@ -1109,7 +1110,7 @@ export function AddressableEventPreview({ addr }: { addr: { kind: number; pubkey const KindIcon = useMemo(() => { if (kindDef?.id) return CONTENT_KIND_ICONS[kindDef.id] ?? FileText; // Fallback icons for well-known kinds not in EXTRA_KINDS - if (addr.kind === 32267 || addr.kind === 30063) return Package; + if (addr.kind === 31990 || addr.kind === 32267 || addr.kind === 30063) return Package; if (addr.kind === 15128 || addr.kind === 35128) return Globe; return FileText; }, [kindDef, addr.kind]); diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index c043074e..bfac327c 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -108,10 +108,14 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee const { startSignup } = useOnboarding(); // Kind-specific pages only support Follows + Global. Clamp any other - // persisted tab (e.g. 'ditto', 'communities') back to 'follows'. - const activeTab: FeedTab = kinds && rawActiveTab !== 'follows' && rawActiveTab !== 'global' - ? 'follows' - : rawActiveTab; + // persisted tab (e.g. 'ditto', 'communities') back to the appropriate default. + // Logged-out users must land on 'global' since 'follows' requires a user. + const activeTab: FeedTab = (() => { + if (!kinds) return rawActiveTab; // Home feed: no clamping + if (rawActiveTab === 'global') return 'global'; + if (rawActiveTab === 'follows' && user) return 'follows'; + return user ? 'follows' : 'global'; + })(); // Is the active tab a saved feed? const activeSavedFeed = useMemo( diff --git a/src/components/InteractionsModal.tsx b/src/components/InteractionsModal.tsx index eb1d03f8..94701ec0 100644 --- a/src/components/InteractionsModal.tsx +++ b/src/components/InteractionsModal.tsx @@ -184,7 +184,7 @@ function ReactionsTab({ reactions }: { reactions: ReactionEntry[] }) { {/* Emoji group header */}
{customUrl && customName ? ( - + ) : ( {emoji} )} diff --git a/src/components/LinkFooter.tsx b/src/components/LinkFooter.tsx index dbe4cd01..d92cc63c 100644 --- a/src/components/LinkFooter.tsx +++ b/src/components/LinkFooter.tsx @@ -1,7 +1,12 @@ import { Link } from 'react-router-dom'; +interface LinkFooterProps { + /** Optional callback fired when an internal (React Router) link is clicked. */ + onNavigate?: () => void; +} + /** Shared footer links used in both sidebars. */ -export function LinkFooter() { +export function LinkFooter({ onNavigate }: LinkFooterProps) { return (

@@ -23,7 +28,7 @@ export function LinkFooter() { Docs {' · '} - + Privacy {' · '} @@ -36,7 +41,7 @@ export function LinkFooter() { Source {' · '} - + Changelog {' · '} diff --git a/src/components/MobileDrawer.tsx b/src/components/MobileDrawer.tsx index 1241fdf8..d44bf02f 100644 --- a/src/components/MobileDrawer.tsx +++ b/src/components/MobileDrawer.tsx @@ -319,7 +319,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {

- +
) : ( @@ -363,7 +363,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
- +
)} diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index fe507785..3a35f479 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -74,6 +74,7 @@ import { EncryptedMessageContent } from "@/components/EncryptedMessageContent"; import { EncryptedLetterContent } from "@/components/EncryptedLetterContent"; import { VanishCardCompact } from "@/components/VanishEventContent"; import { ZapstoreAppContent } from "@/components/ZapstoreAppContent"; +import { AppHandlerContent } from "@/components/AppHandlerContent"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { getAvatarShape } from "@/lib/avatarShape"; import { Badge } from "@/components/ui/badge"; @@ -306,6 +307,7 @@ export const NoteCard = memo(function NoteCard({ const isCustomNip = event.kind === 30817; const isNsite = event.kind === 15128 || event.kind === 35128; const isZapstoreApp = event.kind === 32267; + const isAppHandler = event.kind === 31990; const isEncryptedDM = event.kind === 4; const isLetter = event.kind === 8211; const isVanish = event.kind === 62; @@ -336,6 +338,7 @@ export const NoteCard = memo(function NoteCard({ !isAudioKind && !isDevKind && !isZapstoreApp && + !isAppHandler && !isEncryptedDM && !isLetter && !isVanish && @@ -532,6 +535,8 @@ export const NoteCard = memo(function NoteCard({ ) : isZapstoreApp ? ( + ) : isAppHandler ? ( + ) : isEncryptedDM ? ( ) : isLetter ? ( @@ -789,11 +794,11 @@ export const NoteCard = memo(function NoteCard({
{/* Reaction emoji bubble instead of avatar */} -
+
{threaded && ( @@ -870,11 +875,11 @@ export const NoteCard = memo(function NoteCard({ >
{/* Large reaction emoji */} -
+
@@ -2002,6 +2007,10 @@ const KIND_HEADER_MAP: Record = { icon: Package, action: "published an app", }, + 31990: { + icon: Package, + action: "published an app", + }, 30617: { icon: GitBranch, action: "shared a", diff --git a/src/components/NoteContent.tsx b/src/components/NoteContent.tsx index b2a925bf..f6d6bf30 100644 --- a/src/components/NoteContent.tsx +++ b/src/components/NoteContent.tsx @@ -545,7 +545,7 @@ export function NoteContent({ {groupedTokens.map((token, i) => { switch (token.type) { case 'text': - return {linkifyFlags(emojify(token.value, emojiMap, isEmojiOnly ? 'inline h-12 w-12 align-text-bottom' : undefined))}; + return {linkifyFlags(emojify(token.value, emojiMap, isEmojiOnly ? 'inline h-12 w-12 object-contain align-text-bottom' : undefined))}; case 'image-embed': { if (disableEmbeds) { // In preview contexts (e.g. triple-dot menu), replace image URLs diff --git a/src/components/ReactionButton.tsx b/src/components/ReactionButton.tsx index 3d0ab990..61e91d18 100644 --- a/src/components/ReactionButton.tsx +++ b/src/components/ReactionButton.tsx @@ -186,7 +186,7 @@ export function ReactionButton({ {filledHeart ? ( ) : hasReacted && userReaction ? ( - + ) : ( )} diff --git a/src/components/VersionCheck.tsx b/src/components/VersionCheck.tsx index 985a4fbb..e7fd9b17 100644 --- a/src/components/VersionCheck.tsx +++ b/src/components/VersionCheck.tsx @@ -19,8 +19,11 @@ async function fetchChangelogExcerpt(version: string): Promise e.version === version) ?? entries[0]; if (!entry) return undefined; - // Return the first item from the first section. - return entry.sections[0]?.items[0]; + // Return a truncated first item from the first section. + const item = entry.sections[0]?.items[0]; + if (!item) return undefined; + if (item.length <= 60) return item; + return item.slice(0, 60).trimEnd() + '…'; } catch { return undefined; } diff --git a/src/components/articles/ArticleEditor.tsx b/src/components/articles/ArticleEditor.tsx new file mode 100644 index 00000000..c4392dbe --- /dev/null +++ b/src/components/articles/ArticleEditor.tsx @@ -0,0 +1,904 @@ +import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { formatDistanceToNow } from 'date-fns'; +import { + ArrowLeft, + Image, + Save, + Send, + Loader2, + Hash, + FileText, + X, + Clock, + Cloud, + HardDrive, + Trash2, + ChevronRight, +} from 'lucide-react'; +import slugify from 'slugify'; +import { useNostr } from '@nostrify/react'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Textarea } from '@/components/ui/textarea'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { SubHeaderBar } from '@/components/SubHeaderBar'; +import { TabButton } from '@/components/TabButton'; +import { FabButton } from '@/components/FabButton'; +import { toast } from '@/hooks/useToast'; +import { useUploadFile } from '@/hooks/useUploadFile'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useDrafts, type Draft } from '@/hooks/useDrafts'; +import { usePublishedArticles } from '@/hooks/usePublishedArticles'; +import { saveDraft as saveLocalDraft, deleteDraftBySlug, deleteLocalDraftById, getLocalDrafts } from '@/lib/localDrafts'; +import type { ArticleFields } from '@/lib/articleHelpers'; +import { MilkdownEditor } from './MilkdownEditor'; + +export type ArticleData = ArticleFields; + +interface ArticleEditorProps { + /** Pre-filled data for editing an existing article or loading a draft. */ + initialData?: Partial & { publishedAt?: number }; + /** Whether the editor is in edit mode (updating an existing article). */ + editMode?: boolean; +} + +type EditorTab = 'write' | 'drafts'; + +export function ArticleEditor({ initialData, editMode = false }: ArticleEditorProps) { + const navigate = useNavigate(); + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + const { mutate: publishEvent, isPending: isPublishing } = useNostrPublish(); + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + const { drafts: relayDrafts, isLoading: isDraftsLoading, saveDraft: saveRelayDraft, deleteDraft: deleteRelayDraft, isDeleting } = useDrafts(); + const { articles: publishedArticles } = usePublishedArticles(); + + const fileInputRef = useRef(null); + const autoSaveTimeoutRef = useRef | null>(null); + const [activeTab, setActiveTab] = useState('write'); + const [localDrafts, setLocalDrafts] = useState([]); + const [deleteTarget, setDeleteTarget] = useState<{ id: string; slug: string; isLocal: boolean } | null>(null); + const [tagInput, setTagInput] = useState(''); + const slugManuallyEdited = useRef(!!initialData?.slug); + const [isPublished, setIsPublished] = useState(false); + const [lastSaved, setLastSaved] = useState(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [showLeaveDialog, setShowLeaveDialog] = useState(false); + const [isEditMode, setIsEditMode] = useState(editMode); + const [originalPublishedAt, setOriginalPublishedAt] = useState( + initialData?.publishedAt ?? null, + ); + + const [article, setArticle] = useState({ + title: initialData?.title || '', + summary: initialData?.summary || '', + content: initialData?.content || '', + image: initialData?.image || '', + tags: initialData?.tags || [], + slug: initialData?.slug || '', + }); + + // Keep a ref to the latest article data so the auto-save timer doesn't + // need `article` in its dependency array (which would reset it on every keystroke). + const articleRef = useRef(article); + articleRef.current = article; + const mountedRef = useRef(true); + useEffect(() => () => { mountedRef.current = false; }, []); + + /** Save draft to relay (with localStorage fallback). Shared by manual save + auto-save. */ + const persistDraft = useCallback(async (data: ArticleData, { silent }: { silent?: boolean } = {}) => { + if (user) { + try { + await saveRelayDraft(data); + if (!mountedRef.current) return; + setLastSaved(new Date()); + setHasUnsavedChanges(false); + if (!silent) { + toast({ title: 'Draft saved', description: 'Your article has been saved to Nostr relays.' }); + } + } catch (error) { + console.error('Failed to save draft to relay:', error); + saveLocalDraft(data); + if (!mountedRef.current) return; + setLastSaved(new Date()); + setHasUnsavedChanges(false); + if (!silent) { + toast({ title: 'Draft saved locally', description: 'Could not sync to relays. Saved to your browser.', variant: 'destructive' }); + } + } + } else { + saveLocalDraft(data); + if (!mountedRef.current) return; + setLastSaved(new Date()); + setHasUnsavedChanges(false); + if (!silent) { + toast({ title: 'Draft saved', description: 'Your article has been saved locally.' }); + } + } + }, [user, saveRelayDraft]); + + // Auto-save 30s after the first unsaved change. The timer starts once and + // is only reset when `hasUnsavedChanges` transitions, not on every keystroke. + useEffect(() => { + if (!hasUnsavedChanges) return; + + autoSaveTimeoutRef.current = setTimeout(() => { + const current = articleRef.current; + if (current.content.length === 0) return; + persistDraft(current, { silent: true }); + }, 30000); + + return () => { + if (autoSaveTimeoutRef.current) { + clearTimeout(autoSaveTimeoutRef.current); + } + }; + }, [hasUnsavedChanges, persistDraft]); + + // Reference to handlers for keyboard shortcuts + const handlePublishRef = useRef<(() => void) | null>(null); + const handleSaveDraftRef = useRef<(() => void) | null>(null); + + // Warn before leaving page with unsaved changes + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (hasUnsavedChanges && (article.title || article.content)) { + e.preventDefault(); + e.returnValue = ''; + return ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [hasUnsavedChanges, article.title, article.content]); + + // Auto-generate slug from title (skip if user manually edited the slug) + useEffect(() => { + if (article.title && !slugManuallyEdited.current) { + const newSlug = slugify(article.title, { + lower: true, + strict: true, + trim: true, + }); + setArticle((prev) => ({ ...prev, slug: newSlug })); + } + }, [article.title]); + + // Derived stats + const wordCount = useMemo(() => article.content.trim().split(/\s+/).filter(Boolean).length, [article.content]); + const readingTime = Math.ceil(wordCount / 200); + + // Load local drafts when drafts tab is shown + useEffect(() => { + if (activeTab === 'drafts') { + setLocalDrafts(getLocalDrafts()); + } + }, [activeTab]); + + // Combine relay and local drafts, avoiding duplicates by slug + const combinedDrafts = useMemo(() => { + const drafts: (Draft & { isLocal: boolean })[] = []; + const seenSlugs = new Set(); + + for (const draft of relayDrafts) { + if (draft.slug) seenSlugs.add(draft.slug); + drafts.push({ ...draft, isLocal: false }); + } + + for (const draft of localDrafts) { + if (!draft.slug || !seenSlugs.has(draft.slug)) { + drafts.push({ ...draft, isLocal: true }); + } + } + + return drafts.sort((a, b) => b.updatedAt - a.updatedAt); + }, [relayDrafts, localDrafts]); + + /** Load a draft or published article into the editor. */ + const handleLoadItem = useCallback((item: ArticleData & { publishedAt?: number }, isPublishedArticle: boolean) => { + setArticle({ + title: item.title, + summary: item.summary, + content: item.content, + image: item.image, + tags: item.tags, + slug: item.slug, + }); + slugManuallyEdited.current = !!item.slug; + setIsEditMode(isPublishedArticle); + setOriginalPublishedAt(item.publishedAt ?? null); + setHasUnsavedChanges(false); + setActiveTab('write'); + toast({ + title: isPublishedArticle ? 'Article loaded for editing' : 'Draft loaded', + description: isPublishedArticle + ? 'Make changes and publish to update your article.' + : 'Your draft has been loaded into the editor.', + }); + }, []); + + const handleDeleteDraft = useCallback(async () => { + if (!deleteTarget) return; + + if (deleteTarget.isLocal) { + setLocalDrafts(deleteLocalDraftById(deleteTarget.id)); + toast({ title: 'Draft deleted', description: 'Removed from your browser.' }); + } else { + try { + await deleteRelayDraft(deleteTarget.slug); + toast({ title: 'Draft deleted', description: 'Deletion published to relays.' }); + } catch (error) { + const message = error instanceof Error ? error.message : ''; + toast({ title: 'Delete failed', description: message || 'Could not delete draft.', variant: 'destructive' }); + } + } + setDeleteTarget(null); + }, [deleteTarget, deleteRelayDraft]); + + const updateArticle = useCallback( + (field: keyof ArticleData, value: string | string[]) => { + setArticle((prev) => ({ ...prev, [field]: value })); + setHasUnsavedChanges(true); + }, + [], + ); + + const handleAddTag = useCallback(() => { + const newTag = tagInput.trim().toLowerCase().replace(/^#/, ''); + if (newTag && !article.tags.includes(newTag)) { + setArticle((prev) => ({ ...prev, tags: [...prev.tags, newTag] })); + setTagInput(''); + } + }, [tagInput, article.tags]); + + const handleRemoveTag = useCallback((tagToRemove: string) => { + setArticle((prev) => ({ + ...prev, + tags: prev.tags.filter((tag) => tag !== tagToRemove), + })); + }, []); + + const handleImageUpload = useCallback( + async (file: File) => { + try { + const [[, url]] = await uploadFile(file); + return url; + } catch (error) { + console.error('Upload failed:', error); + toast({ + title: 'Upload failed', + description: 'Could not upload the image. Please try again.', + variant: 'destructive', + }); + return null; + } + }, + [uploadFile], + ); + + const handleHeaderImageUpload = useCallback( + async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const url = await handleImageUpload(file); + if (url) { + updateArticle('image', url); + toast({ + title: 'Image uploaded', + description: 'Header image has been set successfully.', + }); + } + }, + [handleImageUpload, updateArticle], + ); + + const handleSaveDraft = useCallback(async () => { + await persistDraft(article); + }, [article, persistDraft]); + + /** Perform the actual publish (called directly or after overwrite confirmation). */ + const doPublish = useCallback(() => { + if (!user) return; + + // Use original published_at when editing, current time for new articles + const publishedAtTimestamp = + isEditMode && originalPublishedAt + ? Math.floor(originalPublishedAt / 1000) + : Math.floor(Date.now() / 1000); + + const tags: string[][] = [ + ['d', article.slug || slugify(article.title, { lower: true, strict: true })], + ['title', article.title], + ['published_at', publishedAtTimestamp.toString()], + ]; + + if (article.summary) { + tags.push(['summary', article.summary]); + } + + if (article.image) { + tags.push(['image', article.image]); + } + + article.tags.forEach((tag) => { + tags.push(['t', tag]); + }); + + publishEvent( + { + kind: 30023, + content: article.content, + tags, + }, + { + onSuccess: async () => { + setIsPublished(true); + setHasUnsavedChanges(false); + + // Remove draft after publishing + if (article.slug) { + deleteDraftBySlug(article.slug); + try { + await deleteRelayDraft(article.slug); + } catch (error) { + console.error('Failed to delete draft from relay:', error); + } + } + + toast({ + title: isEditMode ? 'Article updated' : 'Article published', + description: isEditMode + ? 'Your article has been updated on Nostr.' + : 'Your article is now live on Nostr.', + }); + + // Navigate back to articles feed + navigate('/articles'); + }, + onError: (error) => { + toast({ + title: 'Publishing failed', + description: + error.message || 'Could not publish your article. Please try again.', + variant: 'destructive', + }); + }, + }, + ); + }, [ + user, + article, + publishEvent, + deleteRelayDraft, + isEditMode, + originalPublishedAt, + navigate, + ]); + + const handlePublish = useCallback(async () => { + if (!user) { + toast({ + title: 'Login required', + description: 'Please login to publish your article.', + variant: 'destructive', + }); + return; + } + + if (!article.title.trim()) { + toast({ + title: 'Title required', + description: 'Please add a title to your article.', + variant: 'destructive', + }); + return; + } + + if (!article.content.trim()) { + toast({ + title: 'Content required', + description: 'Please write some content for your article.', + variant: 'destructive', + }); + return; + } + + // In edit mode we're intentionally overwriting, so skip the collision check + if (!isEditMode) { + const slug = article.slug || slugify(article.title, { lower: true, strict: true }); + + try { + const existing = await nostr.query([ + { kinds: [30023], authors: [user.pubkey], '#d': [slug], limit: 1 }, + ]); + + if (existing.length > 0) { + toast({ + title: 'Slug already in use', + description: 'You already have a published article with this slug. Change the slug or edit the existing article from My Articles.', + variant: 'destructive', + }); + return; + } + } catch { + // If the check fails (e.g. relay timeout), proceed anyway + } + } + + doPublish(); + }, [user, article, isEditMode, nostr, doPublish]); + + // Set refs for keyboard shortcuts + handlePublishRef.current = handlePublish; + handleSaveDraftRef.current = handleSaveDraft; + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault(); + handleSaveDraftRef.current?.(); + } + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && user) { + e.preventDefault(); + handlePublishRef.current?.(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [user]); + + // Handle back navigation with unsaved changes check + const handleBack = useCallback(() => { + if (hasUnsavedChanges && (article.title || article.content)) { + setShowLeaveDialog(true); + } else { + navigate('/articles'); + } + }, [hasUnsavedChanges, article.title, article.content, navigate]); + + const handleLeaveWithoutSaving = useCallback(() => { + setShowLeaveDialog(false); + navigate('/articles'); + }, [navigate]); + + const handleSaveAndLeave = useCallback(async () => { + await handleSaveDraft(); + setShowLeaveDialog(false); + navigate('/articles'); + }, [handleSaveDraft, navigate]); + + const statusLabel = isPublished ? ( + + {isEditMode ? 'Updated' : 'Published'} + + ) : isEditMode ? ( + hasUnsavedChanges ? ( + + + Editing + + ) : ( + Editing + ) + ) : hasUnsavedChanges ? ( + + + Unsaved + + ) : lastSaved ? ( + Saved + ) : null; + + const totalDrafts = combinedDrafts.length; + + return ( +
+ {/* Sticky header */} +
+ + + + setActiveTab('write')} + /> + + setActiveTab('drafts')} + /> + +
+ + + {/* ── New article tab ──────────────────────────────────────── */} + {activeTab === 'write' && ( +
+ {/* Header Image */} + {article.image ? ( +
+ Header + {/* Desktop: centered overlay on hover */} +
+ +
+ {/* Mobile: persistent corner button */} + +
+ ) : ( + + )} + + {/* Title */} + updateArticle('title', e.target.value)} + placeholder="Your article title..." + className="w-full text-3xl sm:text-4xl font-bold bg-transparent border-none outline-none placeholder:text-muted-foreground/40" + /> + + {/* Metadata — inline between title and body */} +
+
+ +