From 98c8ccef229d6922a40a808f10f2602dfc62cea0 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Thu, 1 Jan 2026 17:13:06 +0000
Subject: [PATCH 1/4] Add WYSIWYG editor for blog posts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- TipTap-based rich text editor at /edit/[slug]
- Matches site typography (Canela fonts, 72ch width, 200% line-height)
- MDX components shown as read-only placeholder blocks
- Save with Cmd+S, writes back to .mdx files
- Conflict detection when file is edited externally (VS Code)
- Dev-mode only (disabled in production)
New files:
- src/pages/edit/[...slug].astro - editor page
- src/components/editor/PostEditor.tsx - TipTap editor
- src/components/editor/editor-styles.css - prose styling
- src/pages/api/save-post.ts - save API with conflict handling
Config changes:
- astro.config.mjs: output: "server" for dynamic routes
- Added prerender: true to static pages to maintain SSG
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
astro.config.mjs | 3 +
package-lock.json | 768 ++++++++++++++++++++++++
package.json | 4 +
src/components/editor/PostEditor.tsx | 529 ++++++++++++++++
src/components/editor/editor-styles.css | 380 ++++++++++++
src/pages/[...slug].astro | 3 +
src/pages/api/save-post.ts | 84 +++
src/pages/edit/[...slug].astro | 140 +++++
src/pages/now-[slug].astro | 3 +
src/pages/og/[...slug].png.ts | 3 +
src/pages/topics/[topic].astro | 3 +
11 files changed, 1920 insertions(+)
create mode 100644 src/components/editor/PostEditor.tsx
create mode 100644 src/components/editor/editor-styles.css
create mode 100644 src/pages/api/save-post.ts
create mode 100644 src/pages/edit/[...slug].astro
diff --git a/astro.config.mjs b/astro.config.mjs
index a2c22936..0c684e60 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -9,6 +9,9 @@ import { remarkWikiLink } from "./src/plugins/remark-wiki-link";
// https://astro.build/config
export default defineConfig({
site: "https://maggieappleton.com",
+ // Server mode with static as default - allows /edit/* pages to be dynamic
+ // Most pages prerender by default; specific pages opt-out with prerender = false
+ output: "server",
image: {
domains: ["res.cloudinary.com"],
},
diff --git a/package-lock.json b/package-lock.json
index 1784a984..56dcd808 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,10 @@
"@astrojs/rss": "^4.0.10",
"@iconify-json/heroicons": "^1.2.1",
"@iconify-json/lucide": "^1.2.62",
+ "@tiptap/extension-placeholder": "^3.14.0",
+ "@tiptap/pm": "^3.14.0",
+ "@tiptap/react": "^3.14.0",
+ "@tiptap/starter-kit": "^3.14.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.2",
@@ -1208,6 +1212,34 @@
"node": ">=12"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@iconify-json/heroicons": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@iconify-json/heroicons/-/heroicons-1.2.1.tgz",
@@ -1771,6 +1803,12 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@remirror/core-constants": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
+ "license": "MIT"
+ },
"node_modules/@rollup/pluginutils": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz",
@@ -2113,6 +2151,453 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@tiptap/core": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.14.0.tgz",
+ "integrity": "sha512-nm0VWVA1Vq/jaKY3wyRXViL/kf78yMdH7qETpv4qZXDQLU+pdWV3IGoRTQTKESc7d8L1wL/2uCeByLNUJfrSIw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-blockquote": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.14.0.tgz",
+ "integrity": "sha512-I7aOqcVLHBgCeRtMaMHA+ILSS8Sli46fjFq8477stOpQ79TPiBd6e4SDuFCAu58M94mVLMvlPKF2Eh5IvbIMyQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bold": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.14.0.tgz",
+ "integrity": "sha512-T4ma6VLoHm9JupglidD3CfZXm89A3HMv99gLplXNizvy1mlr4R3uC3aBqKw6lAP+NoqCqbIgjwc4YYsqZClNwA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.14.0.tgz",
+ "integrity": "sha512-nraHy+5jumT67J7hWrCuVwVTS2vNj4FpV5kO8epVySBmgEBr/7Pyi4w7mQA1VRVOMdjeN9iypbgQ2rKhpfaoTw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.14.0.tgz",
+ "integrity": "sha512-luqPX4u52hiOAHJ95mYsNE+x+9dZxsM461Xny9d/eTXLjAcnwS7MghjrnpljvyYsSXNiwQtxUyEr4uEZZJ5gIQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-code": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.14.0.tgz",
+ "integrity": "sha512-Sx9yLorzS+oqNmXID4jt0G5tDnsEgU0HtEXPLD3KNt/ltVxWJU0AXwCsp1/Dg0HIDL868vWpJ2jC1t/4oaf9kA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-code-block": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.14.0.tgz",
+ "integrity": "sha512-hRSdIhhm3Q9JBMQdKaifRVFnAa4sG+M7l1QcTKR3VSYVy2/oR0U+aiOifi5OvMRBUwhaR71Ro+cMT9FH9s26Kg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-document": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.14.0.tgz",
+ "integrity": "sha512-O3D7/GPB3XrWGy0y/b4LMHiY0eTd+dyIbSdiFtmUnbC/E9lqQLw43GiqvD9Gm6AyKhBA+Z45dKMbaOe1c6eTwQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-dropcursor": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.14.0.tgz",
+ "integrity": "sha512-IwHyiZKLjV9WSBlQFS+afMjucIML8wFAKkG8UKCu+CVOe/Qd1ImDGyv6rzPlCmefJkDHIUWS+c2STapJlUD1VQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-floating-menu": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.14.0.tgz",
+ "integrity": "sha512-+ErwDF74NzX4JV0nXMSIUT9V8FDdo85r0SaBZ8lb2NLmElaA3LDklcNV7SsoKlRcwsAXtFkqQbDwXLNGQLYSPQ==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@floating-ui/dom": "^1.0.0",
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-gapcursor": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.14.0.tgz",
+ "integrity": "sha512-hMg2U59+c9FreYtTvzxx5GWKejdZLRITMLEu4OTfrgQok6uF4qkzGEEqmYqPiHk08TBqAg18Y5bbpyqTsuit9A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-hard-break": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.14.0.tgz",
+ "integrity": "sha512-XKxr8usQp+kFevhDK6Ccmnq1CIkLmPClhKwbt7AClGLKLBtEVAS1qUgcmKudkw8cD8Q2/69twI37LXa23sfuLA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-heading": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.14.0.tgz",
+ "integrity": "sha512-4xpahSo3b1dN2nwA0XKXLQVz9nZ/vE443a/Y5QLWeXiu3v9wkcMs/5kQ5ysFeDZRBTfVUWBqhngI7zhvDUx2zQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-horizontal-rule": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.14.0.tgz",
+ "integrity": "sha512-65O4T9vPKLUKO1fLowh5jqtfQlH5eaIL7qb/uj5sXMMg8O7TCvBIRkwNuYsFTkJmTk4vBy+fjZ0uwSY3DFkO1g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-italic": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.14.0.tgz",
+ "integrity": "sha512-Arl5EaG4wdyipwvKjsI7Krlk3OkmqvLfF0YfGwsd5AVDxTiYuiDGgz7RF8J2kttbBeiUTqwME5xpkryQK3F+fg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-link": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.14.0.tgz",
+ "integrity": "sha512-xaeJIktD42rJ4t9fbQpKe+yYNZ+YFIK96cp1Kdm0hZHv/8MPMNRiF85TRY+9U1aoyh5uRcspgCj7EKQb2Hs7qg==",
+ "license": "MIT",
+ "dependencies": {
+ "linkifyjs": "^4.3.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-list": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.14.0.tgz",
+ "integrity": "sha512-rsjFH0Vd/4UbDsjwMLay7oz72VVu1r35t8ofAzy5587jn5JAjflaZs05XbRRMD2imUTK41dyajVSh8CqSnDEJw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.14.0.tgz",
+ "integrity": "sha512-19Dcp8HCFdhINmRy0KQLFfz9ZEuVwFWGAAjYG7BvMvkd9k4sJ5vCv5fej59G99rhsc+tCmik77w+SLksOcxwKQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-list-keymap": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.14.0.tgz",
+ "integrity": "sha512-1oPbvNnQjeOxkHZcUbWPx/IY9o4fT3QGk/9A9cIjFrJRD2AHzbYfPDHNHINtg7Bj0jWz74cHvAHcaxP+M27jkA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.14.0.tgz",
+ "integrity": "sha512-/fXjVL4JajkJQoc213iiput0bCXC4ztUPUpvNuI62VcgFKHcTvX4eYxED1VflotCx0OdkyY9yYD8PtvyO5lkmA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-paragraph": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.14.0.tgz",
+ "integrity": "sha512-NFxk2yNo3Cvh9g8evea+yTLNV48se7MbMcVizTnVhobqtBKv793qsb5FM5Hu30Y72FQPNfH+LRoap4XZyBPfVw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-placeholder": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.14.0.tgz",
+ "integrity": "sha512-sBiAs1gumdSZXO0ezMSmOkHnlzZNZ1fttm6GriAMIp5xfCvo/0LD6bHPXtvOAbT9ovLQX8mH5+iPZh2jKta7oQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-strike": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.14.0.tgz",
+ "integrity": "sha512-R8BbAhnWpisBml6okMKl98hY4tJjedTTgyTkx8tPabIJ92nS9IURKEk3foWB9uHxdTOBUqTvVT+2ScDf9r6QHg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-text": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.14.0.tgz",
+ "integrity": "sha512-XlpnD87LQ7lLcDcBenHgzxv3uivQzPdVHM16CY4lXR4aKDIp2mxjPZr4twHT+cOnRQHc8VYpRgkEo6LLX6VylA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extension-underline": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.14.0.tgz",
+ "integrity": "sha512-zmnWlsi2g/tMlThHby0Je9O+v24j4d+qcXF3nuzLUUaDsGCEtOyC9RzwITft59ViK+Nc2PD2W/J14rsB0j+qoQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/extensions": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.14.0.tgz",
+ "integrity": "sha512-qQBVKqzU4ZVjRn8W0UbdfE4LaaIgcIWHOMrNnJ+PutrRzQ6ZzhmD/kRONvRWBfG9z3DU7pSKGwVYSR2hztsGuQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ }
+ },
+ "node_modules/@tiptap/pm": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.14.0.tgz",
+ "integrity": "sha512-xrZmqI5jl4yMeAsu8p8gVP9S3An5h2MBi8BQHNnZmpyzkUrlpd40vlT6u13SWIqVi5ZWhBZ6U3rL7mkVLZuRKg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-changeset": "^2.3.0",
+ "prosemirror-collab": "^1.3.1",
+ "prosemirror-commands": "^1.6.2",
+ "prosemirror-dropcursor": "^1.8.1",
+ "prosemirror-gapcursor": "^1.3.2",
+ "prosemirror-history": "^1.4.1",
+ "prosemirror-inputrules": "^1.4.0",
+ "prosemirror-keymap": "^1.2.2",
+ "prosemirror-markdown": "^1.13.1",
+ "prosemirror-menu": "^1.2.4",
+ "prosemirror-model": "^1.24.1",
+ "prosemirror-schema-basic": "^1.2.3",
+ "prosemirror-schema-list": "^1.5.0",
+ "prosemirror-state": "^1.4.3",
+ "prosemirror-tables": "^1.6.4",
+ "prosemirror-trailing-node": "^3.0.0",
+ "prosemirror-transform": "^1.10.2",
+ "prosemirror-view": "^1.38.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@tiptap/react": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.14.0.tgz",
+ "integrity": "sha512-Eo/nLyKxHvnLIF4gI2WFhGJiVrqfA6XL9kismVG9NwBNF/NblMDmZZu6Z2SH/ONJQz2Egn7UBPNp3BMq/qZDcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "fast-equals": "^5.3.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "optionalDependencies": {
+ "@tiptap/extension-bubble-menu": "^3.14.0",
+ "@tiptap/extension-floating-menu": "^3.14.0"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/pm": "^3.14.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tiptap/starter-kit": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.14.0.tgz",
+ "integrity": "sha512-fHsC4oDVzvMU9btg+IUmu/eqPquapjJ341qaNI7cCeSCKjjE6XJEN6WcONLAVId2OZUwML0IX1Jgl+6gJxU9Jw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/core": "^3.14.0",
+ "@tiptap/extension-blockquote": "^3.14.0",
+ "@tiptap/extension-bold": "^3.14.0",
+ "@tiptap/extension-bullet-list": "^3.14.0",
+ "@tiptap/extension-code": "^3.14.0",
+ "@tiptap/extension-code-block": "^3.14.0",
+ "@tiptap/extension-document": "^3.14.0",
+ "@tiptap/extension-dropcursor": "^3.14.0",
+ "@tiptap/extension-gapcursor": "^3.14.0",
+ "@tiptap/extension-hard-break": "^3.14.0",
+ "@tiptap/extension-heading": "^3.14.0",
+ "@tiptap/extension-horizontal-rule": "^3.14.0",
+ "@tiptap/extension-italic": "^3.14.0",
+ "@tiptap/extension-link": "^3.14.0",
+ "@tiptap/extension-list": "^3.14.0",
+ "@tiptap/extension-list-item": "^3.14.0",
+ "@tiptap/extension-list-keymap": "^3.14.0",
+ "@tiptap/extension-ordered-list": "^3.14.0",
+ "@tiptap/extension-paragraph": "^3.14.0",
+ "@tiptap/extension-strike": "^3.14.0",
+ "@tiptap/extension-text": "^3.14.0",
+ "@tiptap/extension-underline": "^3.14.0",
+ "@tiptap/extensions": "^3.14.0",
+ "@tiptap/pm": "^3.14.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -2293,6 +2778,12 @@
"@types/sizzle": "*"
}
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT"
+ },
"node_modules/@types/lodash": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
@@ -2300,6 +2791,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
"node_modules/@types/masonry-layout": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/@types/masonry-layout/-/masonry-layout-4.2.8.tgz",
@@ -2319,6 +2820,12 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT"
+ },
"node_modules/@types/mdx": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz",
@@ -2397,6 +2904,12 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -3583,6 +4096,12 @@
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
"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/crossws": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz",
@@ -4453,6 +4972,15 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
+ "node_modules/fast-equals": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -5729,6 +6257,12 @@
"uc.micro": "^2.0.0"
}
},
+ "node_modules/linkifyjs": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
+ "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
+ "license": "MIT"
+ },
"node_modules/lite-youtube-embed": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/lite-youtube-embed/-/lite-youtube-embed-0.3.3.tgz",
@@ -7435,6 +7969,12 @@
"node": ">=8"
}
},
+ "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/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -7832,6 +8372,213 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/prosemirror-changeset": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
+ "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-collab": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^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-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.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
+ "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
+ "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-markdown": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz",
+ "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/markdown-it": "^14.0.0",
+ "markdown-it": "^14.0.0",
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "node_modules/prosemirror-menu": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
+ "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "crelt": "^1.0.0",
+ "prosemirror-commands": "^1.0.0",
+ "prosemirror-history": "^1.0.0",
+ "prosemirror-state": "^1.0.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-schema-basic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
+ "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "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-trailing-node": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remirror/core-constants": "3.0.0",
+ "escape-string-regexp": "^4.0.0"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.22.1",
+ "prosemirror-state": "^1.4.2",
+ "prosemirror-view": "^1.33.8"
+ }
+ },
+ "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/prosemirror-transform": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz",
+ "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.21.0"
+ }
+ },
+ "node_modules/prosemirror-view": {
+ "version": "1.41.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
+ "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.20.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -8369,6 +9116,12 @@
"fsevents": "~2.3.2"
}
},
+ "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/run-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
@@ -9879,6 +10632,15 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10673,6 +11435,12 @@
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"license": "MIT"
},
+ "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/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
diff --git a/package.json b/package.json
index af227889..d28d12e3 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,10 @@
"@astrojs/rss": "^4.0.10",
"@iconify-json/heroicons": "^1.2.1",
"@iconify-json/lucide": "^1.2.62",
+ "@tiptap/extension-placeholder": "^3.14.0",
+ "@tiptap/pm": "^3.14.0",
+ "@tiptap/react": "^3.14.0",
+ "@tiptap/starter-kit": "^3.14.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.2",
diff --git a/src/components/editor/PostEditor.tsx b/src/components/editor/PostEditor.tsx
new file mode 100644
index 00000000..dd5c486e
--- /dev/null
+++ b/src/components/editor/PostEditor.tsx
@@ -0,0 +1,529 @@
+import { useEditor, EditorContent } from "@tiptap/react";
+import StarterKit from "@tiptap/starter-kit";
+import Placeholder from "@tiptap/extension-placeholder";
+import { useState, useEffect, useCallback, useRef } from "react";
+import "./editor-styles.css";
+
+interface PostEditorProps {
+ initialContent: string;
+ slug: string;
+ collection: string;
+ filePath: string;
+ lastModified: number;
+}
+
+interface ParsedContent {
+ frontmatter: string;
+ imports: string;
+ segments: ContentSegment[];
+}
+
+interface ContentSegment {
+ type: "prose" | "component";
+ content: string;
+ componentName?: string;
+}
+
+// Parse MDX content into frontmatter, imports, and content segments
+function parseMdxContent(content: string): ParsedContent {
+ const lines = content.split("\n");
+ let frontmatter = "";
+ let imports = "";
+ const segments: ContentSegment[] = [];
+
+ let inFrontmatter = false;
+ let frontmatterEnded = false;
+ let importsEnded = false;
+ let currentProse = "";
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // Handle frontmatter
+ if (i === 0 && line.trim() === "---") {
+ inFrontmatter = true;
+ frontmatter += line + "\n";
+ continue;
+ }
+
+ if (inFrontmatter) {
+ frontmatter += line + "\n";
+ if (line.trim() === "---") {
+ inFrontmatter = false;
+ frontmatterEnded = true;
+ }
+ continue;
+ }
+
+ // Handle imports (lines starting with "import")
+ if (frontmatterEnded && !importsEnded) {
+ if (line.trim().startsWith("import ") || line.trim() === "") {
+ imports += line + "\n";
+ continue;
+ } else {
+ importsEnded = true;
+ }
+ }
+
+ // Now we're in the content area - detect block-level MDX components
+ // A block component starts at the beginning of a line with (self-closing)
+ const selfClosingMatch = line.match(new RegExp(`^<${componentName}[^>]*/>`));
+ if (selfClosingMatch) {
+ segments.push({
+ type: "component",
+ content: componentContent,
+ componentName,
+ });
+ continue;
+ }
+
+ // Check if it opens AND closes on the same line
+ // Match pattern: ...
+ const sameLineCloseMatch = line.match(new RegExp(`${componentName}>`));
+ if (sameLineCloseMatch) {
+ segments.push({
+ type: "component",
+ content: componentContent,
+ componentName,
+ });
+ continue;
+ }
+
+ // Multi-line component - find closing tag
+ // We need to track depth for nested same-name components
+ let depth = 1;
+ let j = i + 1;
+
+ while (j < lines.length && depth > 0) {
+ componentContent += "\n" + lines[j];
+
+ // Count opening tags on this line (excluding self-closing)
+ // Look for , or end of pattern
+ const openMatches = lines[j].match(new RegExp(`<${componentName}(?:\\s|>)`, "g")) || [];
+ const selfCloseMatches = lines[j].match(new RegExp(`<${componentName}[^>]*/>`,"g")) || [];
+ const closeMatches = lines[j].match(new RegExp(`${componentName}>`, "g")) || [];
+
+ depth += openMatches.length - selfCloseMatches.length - closeMatches.length;
+ j++;
+ }
+
+ segments.push({
+ type: "component",
+ content: componentContent,
+ componentName,
+ });
+ i = j - 1; // Skip the lines we've consumed
+ } else {
+ currentProse += line + "\n";
+ }
+ }
+
+ // Don't forget trailing prose
+ if (currentProse.trim()) {
+ segments.push({ type: "prose", content: currentProse });
+ }
+
+ return { frontmatter, imports, segments };
+}
+
+// Convert markdown to HTML for TipTap (basic conversion)
+function markdownToHtml(markdown: string): string {
+ let html = markdown
+ // Headers
+ .replace(/^#### (.+)$/gm, "$1
")
+ .replace(/^### (.+)$/gm, "$1
")
+ .replace(/^## (.+)$/gm, "$1
")
+ .replace(/^# (.+)$/gm, "$1
")
+ // Bold and italic
+ .replace(/\*\*\*(.+?)\*\*\*/g, "$1")
+ .replace(/\*\*(.+?)\*\*/g, "$1")
+ .replace(/\*(.+?)\*/g, "$1")
+ .replace(/_(.+?)_/g, "$1")
+ // Links
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ // Wiki links - preserve as-is for now
+ .replace(/\[\[([^\]]+)\]\]/g, "[[$1]]")
+ // Inline code
+ .replace(/`([^`]+)`/g, "$1")
+ // Horizontal rules
+ .replace(/^---$/gm, "
")
+ // Blockquotes
+ .replace(/^> (.+)$/gm, "$1
")
+ // Unordered lists
+ .replace(/^- (.+)$/gm, "$1")
+ // Ordered lists
+ .replace(/^\d+\. (.+)$/gm, "$1");
+
+ // Wrap consecutive in or
+ html = html.replace(/(- .*<\/li>\n?)+/g, (match) => ``);
+
+ // Paragraphs - wrap remaining lines that aren't already wrapped
+ const lines = html.split("\n");
+ const result: string[] = [];
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed) {
+ result.push("");
+ } else if (
+ trimmed.startsWith("${line}
`);
+ }
+ }
+
+ return result.join("\n");
+}
+
+// Convert TipTap HTML back to markdown
+function htmlToMarkdown(html: string): string {
+ const div = document.createElement("div");
+ div.innerHTML = html;
+
+ function processNode(node: Node): string {
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent || "";
+ }
+
+ if (node.nodeType !== Node.ELEMENT_NODE) {
+ return "";
+ }
+
+ const el = node as HTMLElement;
+ const children = Array.from(el.childNodes).map(processNode).join("");
+
+ switch (el.tagName.toLowerCase()) {
+ case "h1":
+ return `# ${children}\n\n`;
+ case "h2":
+ return `## ${children}\n\n`;
+ case "h3":
+ return `### ${children}\n\n`;
+ case "h4":
+ return `#### ${children}\n\n`;
+ case "p":
+ return `${children}\n\n`;
+ case "strong":
+ return `**${children}**`;
+ case "em":
+ return `*${children}*`;
+ case "a":
+ return `[${children}](${el.getAttribute("href") || ""})`;
+ case "code":
+ return `\`${children}\``;
+ case "blockquote":
+ return children
+ .split("\n")
+ .filter((l) => l.trim())
+ .map((l) => `> ${l.replace(/^> /, "")}`)
+ .join("\n") + "\n\n";
+ case "ul":
+ return children;
+ case "ol":
+ return children;
+ case "li":
+ return `- ${children.trim()}\n`;
+ case "hr":
+ return "---\n\n";
+ case "br":
+ return "\n";
+ default:
+ return children;
+ }
+ }
+
+ return processNode(div).replace(/\n{3,}/g, "\n\n").trim();
+}
+
+// Sub-component for individual prose editors
+function ProseEditor({
+ initialContent,
+ onUpdate,
+}: {
+ initialContent: string;
+ onUpdate: (html: string) => void;
+}) {
+ const editor = useEditor({
+ // Disable immediate render to avoid SSR hydration mismatches
+ immediatelyRender: false,
+ extensions: [
+ StarterKit.configure({
+ heading: {
+ levels: [1, 2, 3, 4],
+ },
+ }),
+ Placeholder.configure({
+ placeholder: "Start writing...",
+ }),
+ ],
+ content: markdownToHtml(initialContent),
+ editorProps: {
+ attributes: {
+ class: "prose-editor",
+ },
+ },
+ onUpdate: ({ editor }) => {
+ onUpdate(editor.getHTML());
+ },
+ });
+
+ return ;
+}
+
+export default function PostEditor({
+ initialContent,
+ slug,
+ collection,
+ filePath,
+ lastModified: initialLastModified,
+}: PostEditorProps) {
+ const [parsed] = useState(() =>
+ parseMdxContent(initialContent)
+ );
+ const [saveStatus, setSaveStatus] = useState<
+ "idle" | "saving" | "saved" | "error" | "conflict"
+ >("idle");
+ const [conflictData, setConflictData] = useState<{
+ serverContent: string;
+ serverModified: number;
+ } | null>(null);
+ const lastModifiedRef = useRef(initialLastModified);
+ const editorContentsRef = useRef