A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add TipTap editor with frontmatter support and auto-save

- Install TipTap editor with markdown extensions (StarterKit, Typography, Link, CodeBlock)
- Create TipTapEditor component with custom styling matching design system
- Create MenuBar component with formatting controls
- Create FrontmatterEditor component for YAML metadata editing
- Create EditorContainer component managing editor state and auto-save
- Add file content API client and React Query hooks
- Implement debounced auto-save (2 second delay)
- Add unsaved changes warning before page unload
- Integrate editor into DashboardApp
- Convert HTML to markdown using turndown library

+980 -26
+170 -4
frontend/bun.lock
··· 7 7 "@astrojs/react": "^4.4.2", 8 8 "@astrojs/tailwind": "^6.0.2", 9 9 "@tanstack/react-query": "^5.90.20", 10 + "@tiptap/extension-code-block-lowlight": "^3.18.0", 11 + "@tiptap/extension-document": "^3.18.0", 12 + "@tiptap/extension-link": "^3.18.0", 13 + "@tiptap/extension-paragraph": "^3.18.0", 14 + "@tiptap/extension-placeholder": "^3.18.0", 15 + "@tiptap/extension-text": "^3.18.0", 16 + "@tiptap/extension-typography": "^3.18.0", 17 + "@tiptap/pm": "^3.18.0", 18 + "@tiptap/react": "^3.18.0", 19 + "@tiptap/starter-kit": "^3.18.0", 10 20 "@types/react": "^19.2.10", 11 21 "@types/react-dom": "^19.2.3", 12 22 "astro": "^5.17.1", 13 23 "axios": "^1.13.4", 14 24 "class-variance-authority": "^0.7.1", 15 25 "clsx": "^2.1.1", 26 + "lowlight": "^3.3.0", 16 27 "react": "^19.2.4", 17 28 "react-dom": "^19.2.4", 29 + "remark": "^15.0.1", 30 + "remark-parse": "^11.0.0", 31 + "remark-stringify": "^11.0.0", 18 32 "tailwind-merge": "^3.4.0", 19 33 "tailwindcss": "^3.4.0", 34 + "turndown": "^7.2.2", 35 + "unified": "^11.0.5", 20 36 }, 21 37 }, 22 38 }, ··· 130 146 "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 131 147 132 148 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 149 + 150 + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], 151 + 152 + "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], 153 + 154 + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], 133 155 134 156 "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], 135 157 ··· 191 213 192 214 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 193 215 216 + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], 217 + 194 218 "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 195 219 196 220 "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], ··· 198 222 "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 199 223 200 224 "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], 225 + 226 + "@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="], 201 227 202 228 "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], 203 229 ··· 271 297 272 298 "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="], 273 299 300 + "@tiptap/core": ["@tiptap/core@3.18.0", "", { "peerDependencies": { "@tiptap/pm": "^3.18.0" } }, "sha512-Gczd4GbK1DNgy/QUPElMVozoa0GW9mW8E31VIi7Q4a9PHHz8PcrxPmuWwtJ2q0PF8MWpOSLuBXoQTWaXZRPRnQ=="], 301 + 302 + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-1HjEoM5vZDfFnq2OodNpW13s56a9pbl7jolUv1V9FrE3X5s7n0HCfDzIVpT7z1HgTdPtlN5oSt5uVyBwuwSUfA=="], 303 + 304 + "@tiptap/extension-bold": ["@tiptap/extension-bold@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-xUgOvHCdGXh9Lfxd7DtgsSr0T/egIwBllWHIBWDjQEQQ0b+ICn+0+i703btHMB4hjdduZtgVDrhK8jAW3U6swA=="], 305 + 306 + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.18.0", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-9kYG1fVYQcA3Kp5Bq96lrKCp9oLpQqceDsK688r7iT1yymQlBPMunaqaqb5ZLQGhnNYbhfG+8xcQsvEKjklErA=="], 307 + 308 + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.18.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.18.0" } }, "sha512-8sEpY0nxAGGFDYlF+WVFPKX00X2dAAjmoi0+2eWvK990PdQqwXrQsRs7pkUbpE2mDtATV8+GlDXk9KDkK/ZXhA=="], 309 + 310 + "@tiptap/extension-code": ["@tiptap/extension-code@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-0SU53O0NRmdtRM2Hgzm372dVoHjs2F40o/dtB7ls4kocf4W89FyWeC2R6ZsFQqcXisNh9RTzLtYfbNyizGuZIw=="], 311 + 312 + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-fCx1oT95ikGfoizw+XCjeglQxlLK4lWgUcB4Dcn5TdaCoFBQMEaZs7Q0jVajxxxULnyArkg60uarc1ac/IF2Hw=="], 313 + 314 + "@tiptap/extension-code-block-lowlight": ["@tiptap/extension-code-block-lowlight@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/extension-code-block": "^3.18.0", "@tiptap/pm": "^3.18.0", "highlight.js": "^11", "lowlight": "^2 || ^3" } }, "sha512-euUvh9r1KNSua9X4VdMS6lcWgUkcd0YznCFhp4b5gSqT5/5F7tGlvEg5mNpBeNhOIreDQV6zfBc7HvLfh7cLEA=="], 315 + 316 + "@tiptap/extension-document": ["@tiptap/extension-document@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-e0hOGrjTMpCns8IC5p+c5CEiE1BBmFBFL+RpIxU/fjT2SaZ7q2xsFguBu94lQDT0cD6fdZokFRpGwEMxZNVGCg=="], 317 + 318 + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.18.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.18.0" } }, "sha512-pIW/K9fGth221dkfA5SInHcqfnCr0aG9LGkRiEh4gwM4cf6ceUBrvcD+QlemSZ4q9oktNGJmXT+sEXVOQ8QoeQ=="], 319 + 320 + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.18.0", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-a2cBQi0I/X0o3a9b+adwJvkdxLzQzJIkP9dc/v25qGTSCjC1+ycois5WQOn8T4T8t4g/fAH1UOXEWnkWyTxLIg=="], 321 + 322 + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.18.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.18.0" } }, "sha512-covioXPPHX3SnlTwC/1rcHUHAc7/JFd4vN0kZQmZmvGHlxqq2dPmtrPh8D7TuDuhG0k/3Z6i8dJFP0phfRAhuA=="], 323 + 324 + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-IXLiOHEmbU2Wn1jFRZC6apMxiJQvSRWhwoiubAvRxyiPSnFTeaEgT8Qgo5DjwB39NckP+o7XX7RrgzlkwdFPQQ=="], 325 + 326 + "@tiptap/extension-heading": ["@tiptap/extension-heading@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-MTamVnYsFWVndLSq5PRQ7ZmbF6AExsFS9uIvGtUAwuhzvR4of/WHh6wpvWYjA+BLXTWRrfuGHaZTl7UXBN13fg=="], 327 + 328 + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-fEq7DwwQZ496RHNbMQypBVNqoWnhDEERbzWMBqlmfCfc/0FvJrHtsQkk3k4lgqMYqmBwym3Wp0SrRYiyKCPGTw=="], 329 + 330 + "@tiptap/extension-italic": ["@tiptap/extension-italic@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-1C4nB08psiRo0BPxAbpYq8peUOKnjQWtBCLPbE6B9ToTK3vmUk0AZTqLO11FvokuM1GF5l2Lg3sKrKFuC2hcjQ=="], 331 + 332 + "@tiptap/extension-link": ["@tiptap/extension-link@3.18.0", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-1J28C4+fKAMQi7q/UsTjAmgmKTnzjExXY98hEBneiVzFDxqF69n7+Vb7nVTNAIhmmJkZMA0DEcMhSiQC/1/u4A=="], 333 + 334 + "@tiptap/extension-list": ["@tiptap/extension-list@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-9lQBo45HNqIFcLEHAk+CY3W51eMMxIJjWbthm2CwEWr4PB3+922YELlvq8JcLH1nVFkBVpmBFmQe/GxgnCkzwQ=="], 335 + 336 + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.18.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.18.0" } }, "sha512-auTSt+NXoUnT0xofzFa+FnXsrW1TPdT1OB3U1OqQCIWkumZqL45A8OK9kpvyQsWj/xJ8fy1iZwFlKXPtxjLd2w=="], 337 + 338 + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.18.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.18.0" } }, "sha512-ZzO5r/cW7G0zpL/eM69WPnMpzb0YsSjtI60CYGA0iQDRJnK9INvxu0RU0ewM2faqqwASmtjuNJac+Fjk6scdXg=="], 339 + 340 + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.18.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.18.0" } }, "sha512-5bUAfklYLS5o6qvLLfreGyGvD1JKXqOQF0YntLyPuCGrXv7+XjPWQL2BmEf59fOn2UPT2syXLQ1WN5MHTArRzg=="], 341 + 342 + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-uvFhdwiur4NhhUdBmDsajxjGAIlg5qga55fYag2DzOXxIQE2M7/aVMRkRpuJzb88GY4EHSh8rY34HgMK2FJt2Q=="], 343 + 344 + "@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.18.0", "", { "peerDependencies": { "@tiptap/extensions": "^3.18.0" } }, "sha512-jhN1Xa+MpfrTcCYZsFSvZYpUuMutPTC20ms0IsH1yN0y9tbAS+T6PHPC+dsvyAinYdA8yKElM6OO+jpyz4X1cw=="], 345 + 346 + "@tiptap/extension-strike": ["@tiptap/extension-strike@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-kl/fa68LZg8NWUqTkRTfgyCx+IGqozBmzJxQDc1zxurrIU+VFptDV9UuZim587sbM2KGjCi/PNPjPGk1Uu0PVg=="], 347 + 348 + "@tiptap/extension-text": ["@tiptap/extension-text@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-9TvctdnBCwK/zyTi9kS7nGFNl5OvGM8xE0u38ZmQw5t79JOqJHgOroyqMjw8LHK/1PWrozfNCmsZbpq4IZuKXw=="], 349 + 350 + "@tiptap/extension-typography": ["@tiptap/extension-typography@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-zTNGJjhJG3lObUhhTbDC1IOyi1DCiCd6i11xsnJDPy5BODYc7t7ZP6VMOWU0LIuMsK9kX02dXoNU7OarJgLpCg=="], 351 + 352 + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0" } }, "sha512-009IeXURNJ/sm1pBqbj+2YQgjQaBtNlJR3dbl6xu49C+qExqCmI7klhKQuwsVVGLR7ahsYlp7d9RlftnhCXIcQ=="], 353 + 354 + "@tiptap/extensions": ["@tiptap/extensions@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-uSRIE9HGshBN6NRFR3LX2lZqBLvX92SgU5A9AvUbJD4MqU63E+HdruJnRjsVlX3kPrmbIDowxrzXlUcg3K0USQ=="], 355 + 356 + "@tiptap/pm": ["@tiptap/pm@3.18.0", "", { "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" } }, "sha512-8RoI5gW0xBVCsuxahpK8vx7onAw6k2/uR3hbGBBnH+HocDMaAZKot3nTyY546ij8ospIC1mnQ7k4BhVUZesZDQ=="], 357 + 358 + "@tiptap/react": ["@tiptap/react@3.18.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.18.0", "@tiptap/extension-floating-menu": "^3.18.0" }, "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.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" } }, "sha512-VC20YhoiWe2E03D1BRH+AVMgXeA7li+bzIoaBtpK9+AdizAC+TvWCb2I/9mQCy9m31zGYTD0vv0e7bVlJi+aKA=="], 359 + 360 + "@tiptap/starter-kit": ["@tiptap/starter-kit@3.18.0", "", { "dependencies": { "@tiptap/core": "^3.18.0", "@tiptap/extension-blockquote": "^3.18.0", "@tiptap/extension-bold": "^3.18.0", "@tiptap/extension-bullet-list": "^3.18.0", "@tiptap/extension-code": "^3.18.0", "@tiptap/extension-code-block": "^3.18.0", "@tiptap/extension-document": "^3.18.0", "@tiptap/extension-dropcursor": "^3.18.0", "@tiptap/extension-gapcursor": "^3.18.0", "@tiptap/extension-hard-break": "^3.18.0", "@tiptap/extension-heading": "^3.18.0", "@tiptap/extension-horizontal-rule": "^3.18.0", "@tiptap/extension-italic": "^3.18.0", "@tiptap/extension-link": "^3.18.0", "@tiptap/extension-list": "^3.18.0", "@tiptap/extension-list-item": "^3.18.0", "@tiptap/extension-list-keymap": "^3.18.0", "@tiptap/extension-ordered-list": "^3.18.0", "@tiptap/extension-paragraph": "^3.18.0", "@tiptap/extension-strike": "^3.18.0", "@tiptap/extension-text": "^3.18.0", "@tiptap/extension-underline": "^3.18.0", "@tiptap/extensions": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-LctpCelqI/5nHEeZgCPiwI1MmTjGr6YCIBGWmS5s4DJE7NfevEkwomR/C05QKdVUwPhpCXIMeS1+h/RYqRo1KA=="], 361 + 274 362 "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 275 363 276 364 "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], ··· 284 372 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 285 373 286 374 "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], 375 + 376 + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], 377 + 378 + "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], 287 379 288 380 "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], 289 381 382 + "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], 383 + 290 384 "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 291 385 292 386 "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], ··· 296 390 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 297 391 298 392 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], 393 + 394 + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], 299 395 300 396 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], 301 397 ··· 388 484 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 389 485 390 486 "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], 487 + 488 + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], 391 489 392 490 "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], 393 491 ··· 445 543 446 544 "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], 447 545 448 - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 546 + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 449 547 450 548 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 451 549 ··· 461 559 462 560 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 463 561 464 - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 562 + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 465 563 466 564 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 467 565 468 566 "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], 469 567 470 568 "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], 569 + 570 + "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], 471 571 472 572 "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 473 573 ··· 535 635 536 636 "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], 537 637 638 + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], 639 + 538 640 "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], 539 641 540 642 "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], ··· 581 683 582 684 "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 583 685 686 + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], 687 + 688 + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], 689 + 584 690 "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], 691 + 692 + "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], 585 693 586 694 "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], 587 695 588 696 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 589 697 590 698 "magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="], 699 + 700 + "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], 591 701 592 702 "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], 593 703 ··· 620 730 "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], 621 731 622 732 "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], 733 + 734 + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], 623 735 624 736 "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 625 737 ··· 719 831 720 832 "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], 721 833 834 + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], 835 + 722 836 "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], 723 837 724 838 "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], ··· 763 877 764 878 "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], 765 879 880 + "prosemirror-changeset": ["prosemirror-changeset@2.3.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ=="], 881 + 882 + "prosemirror-collab": ["prosemirror-collab@1.3.1", "", { "dependencies": { "prosemirror-state": "^1.0.0" } }, "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ=="], 883 + 884 + "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="], 885 + 886 + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="], 887 + 888 + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.0", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ=="], 889 + 890 + "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], 891 + 892 + "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="], 893 + 894 + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], 895 + 896 + "prosemirror-markdown": ["prosemirror-markdown@1.13.3", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ=="], 897 + 898 + "prosemirror-menu": ["prosemirror-menu@1.2.5", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ=="], 899 + 900 + "prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="], 901 + 902 + "prosemirror-schema-basic": ["prosemirror-schema-basic@1.2.4", "", { "dependencies": { "prosemirror-model": "^1.25.0" } }, "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ=="], 903 + 904 + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], 905 + 906 + "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], 907 + 908 + "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "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" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], 909 + 910 + "prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "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" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="], 911 + 912 + "prosemirror-transform": ["prosemirror-transform@1.11.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw=="], 913 + 914 + "prosemirror-view": ["prosemirror-view@1.41.5", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA=="], 915 + 766 916 "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], 917 + 918 + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], 767 919 768 920 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 769 921 ··· 793 945 794 946 "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], 795 947 948 + "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], 949 + 796 950 "remark-gfm": ["remark-gfm@4.0.1", "", { "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" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], 797 951 798 952 "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], ··· 817 971 818 972 "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], 819 973 974 + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], 975 + 820 976 "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 821 977 822 978 "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], ··· 875 1031 876 1032 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 877 1033 1034 + "turndown": ["turndown@7.2.2", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ=="], 1035 + 878 1036 "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 879 1037 880 1038 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 1039 + 1040 + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], 881 1041 882 1042 "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], 883 1043 ··· 911 1071 912 1072 "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], 913 1073 1074 + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], 1075 + 914 1076 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 915 1077 916 1078 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], ··· 922 1084 "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 923 1085 924 1086 "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], 1087 + 1088 + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], 925 1089 926 1090 "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], 927 1091 ··· 969 1133 970 1134 "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], 971 1135 972 - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 973 - 974 1136 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 975 1137 1138 + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 1139 + 976 1140 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 1141 + 1142 + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 977 1143 978 1144 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 979 1145
+17 -1
frontend/package.json
··· 12 12 "@astrojs/react": "^4.4.2", 13 13 "@astrojs/tailwind": "^6.0.2", 14 14 "@tanstack/react-query": "^5.90.20", 15 + "@tiptap/extension-code-block-lowlight": "^3.18.0", 16 + "@tiptap/extension-document": "^3.18.0", 17 + "@tiptap/extension-link": "^3.18.0", 18 + "@tiptap/extension-paragraph": "^3.18.0", 19 + "@tiptap/extension-placeholder": "^3.18.0", 20 + "@tiptap/extension-text": "^3.18.0", 21 + "@tiptap/extension-typography": "^3.18.0", 22 + "@tiptap/pm": "^3.18.0", 23 + "@tiptap/react": "^3.18.0", 24 + "@tiptap/starter-kit": "^3.18.0", 15 25 "@types/react": "^19.2.10", 16 26 "@types/react-dom": "^19.2.3", 17 27 "astro": "^5.17.1", 18 28 "axios": "^1.13.4", 19 29 "class-variance-authority": "^0.7.1", 20 30 "clsx": "^2.1.1", 31 + "lowlight": "^3.3.0", 21 32 "react": "^19.2.4", 22 33 "react-dom": "^19.2.4", 34 + "remark": "^15.0.1", 35 + "remark-parse": "^11.0.0", 36 + "remark-stringify": "^11.0.0", 23 37 "tailwind-merge": "^3.4.0", 24 - "tailwindcss": "^3.4.0" 38 + "tailwindcss": "^3.4.0", 39 + "turndown": "^7.2.2", 40 + "unified": "^11.0.5" 25 41 } 26 42 }
+8 -21
frontend/src/components/dashboard/DashboardApp.tsx
··· 3 3 import { Header } from '../layout/Header'; 4 4 import { SetupWizard } from './SetupWizard'; 5 5 import { FileTree } from './FileTree'; 6 + import { EditorContainer } from '../editor/EditorContainer'; 6 7 import { useFiles } from '../../lib/hooks/useRepos'; 7 8 import { useCurrentUser } from '../../lib/hooks/useAuth'; 8 9 ··· 122 123 </aside> 123 124 124 125 {/* Main Content */} 125 - <main className="flex-1 overflow-y-auto"> 126 + <main className="flex-1 overflow-hidden"> 126 127 {!selectedFile ? ( 127 128 <div className="h-full flex items-center justify-center"> 128 129 <div className="text-center max-w-md"> ··· 151 152 </div> 152 153 </div> 153 154 ) : ( 154 - <div className="p-8"> 155 - <div className="max-w-4xl mx-auto"> 156 - <div className="bg-white rounded-lg border border-gray-200 p-8"> 157 - <h2 className="text-xl font-bold text-gray-900 mb-4"> 158 - {selectedFile.split('/').pop()} 159 - </h2> 160 - <p className="text-gray-600"> 161 - Editor will be implemented in Phase 2. For now, you can browse your files. 162 - </p> 163 - <div className="mt-4 p-4 bg-gray-50 rounded border border-gray-200"> 164 - <div className="text-sm text-gray-600"> 165 - <strong>Selected:</strong> {selectedFile} 166 - </div> 167 - <div className="text-sm text-gray-600 mt-1"> 168 - <strong>Repository:</strong> {repoConfig.owner}/{repoConfig.repo} 169 - </div> 170 - </div> 171 - </div> 172 - </div> 173 - </div> 155 + <EditorContainer 156 + owner={repoConfig.owner} 157 + repo={repoConfig.repo} 158 + path={selectedFile} 159 + onClose={() => setSelectedFile(null)} 160 + /> 174 161 )} 175 162 </main> 176 163 </div>
+185
frontend/src/components/editor/EditorContainer.tsx
··· 1 + import { useState, useEffect, useCallback, useRef } from 'react'; 2 + import { TipTapEditor } from './TipTapEditor'; 3 + import { FrontmatterEditor } from './FrontmatterEditor'; 4 + import { useFileContent, useUpdateFile } from '../../lib/hooks/useFileContent'; 5 + import { debounce } from '../../lib/utils/debounce'; 6 + 7 + interface EditorContainerProps { 8 + owner: string; 9 + repo: string; 10 + path: string; 11 + onClose?: () => void; 12 + } 13 + 14 + export function EditorContainer({ owner, repo, path, onClose }: EditorContainerProps) { 15 + const { data: fileData, isLoading, error } = useFileContent(owner, repo, path); 16 + const updateFile = useUpdateFile(); 17 + 18 + const [content, setContent] = useState(''); 19 + const [frontmatter, setFrontmatter] = useState<Record<string, any>>({}); 20 + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); 21 + const [lastSaved, setLastSaved] = useState<Date | null>(null); 22 + const [isSaving, setIsSaving] = useState(false); 23 + 24 + // Keep track of initial content to detect changes 25 + const initialContentRef = useRef<{ content: string; frontmatter: Record<string, any> } | null>(null); 26 + 27 + // Load file content when data is available 28 + useEffect(() => { 29 + if (fileData) { 30 + setContent(fileData.content || ''); 31 + setFrontmatter(fileData.frontmatter || {}); 32 + initialContentRef.current = { 33 + content: fileData.content || '', 34 + frontmatter: fileData.frontmatter || {}, 35 + }; 36 + setHasUnsavedChanges(false); 37 + } 38 + }, [fileData]); 39 + 40 + // Auto-save function 41 + const saveChanges = useCallback(async () => { 42 + if (!hasUnsavedChanges) return; 43 + 44 + setIsSaving(true); 45 + try { 46 + await updateFile.mutateAsync({ 47 + owner, 48 + repo, 49 + path, 50 + content, 51 + frontmatter, 52 + }); 53 + setLastSaved(new Date()); 54 + setHasUnsavedChanges(false); 55 + } catch (error) { 56 + console.error('Failed to save:', error); 57 + } finally { 58 + setIsSaving(false); 59 + } 60 + }, [owner, repo, path, content, frontmatter, hasUnsavedChanges, updateFile]); 61 + 62 + // Debounced auto-save (2 seconds) 63 + const debouncedSave = useCallback( 64 + debounce(() => { 65 + saveChanges(); 66 + }, 2000), 67 + [saveChanges] 68 + ); 69 + 70 + // Handle content changes 71 + const handleContentChange = (newContent: string) => { 72 + setContent(newContent); 73 + setHasUnsavedChanges(true); 74 + debouncedSave(); 75 + }; 76 + 77 + // Handle frontmatter changes 78 + const handleFrontmatterChange = (newFrontmatter: Record<string, any>) => { 79 + setFrontmatter(newFrontmatter); 80 + setHasUnsavedChanges(true); 81 + debouncedSave(); 82 + }; 83 + 84 + // Warn before leaving if there are unsaved changes 85 + useEffect(() => { 86 + const handleBeforeUnload = (e: BeforeUnloadEvent) => { 87 + if (hasUnsavedChanges) { 88 + e.preventDefault(); 89 + e.returnValue = ''; 90 + } 91 + }; 92 + 93 + window.addEventListener('beforeunload', handleBeforeUnload); 94 + return () => window.removeEventListener('beforeunload', handleBeforeUnload); 95 + }, [hasUnsavedChanges]); 96 + 97 + if (isLoading) { 98 + return ( 99 + <div className="flex items-center justify-center h-full"> 100 + <div className="text-center"> 101 + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto mb-4"></div> 102 + <div className="text-gray-600">Loading file...</div> 103 + </div> 104 + </div> 105 + ); 106 + } 107 + 108 + if (error) { 109 + return ( 110 + <div className="flex items-center justify-center h-full"> 111 + <div className="text-center max-w-md"> 112 + <div className="text-red-600 text-xl font-bold mb-2">Error Loading File</div> 113 + <div className="text-gray-600 mb-4"> 114 + {error instanceof Error ? error.message : 'Unknown error'} 115 + </div> 116 + {onClose && ( 117 + <button 118 + onClick={onClose} 119 + className="px-4 py-2 bg-gray-900 text-white font-semibold hover:bg-gray-800" 120 + > 121 + Go Back 122 + </button> 123 + )} 124 + </div> 125 + </div> 126 + ); 127 + } 128 + 129 + return ( 130 + <div className="h-full flex flex-col"> 131 + {/* Header */} 132 + <div className="bg-white border-b-2 border-gray-900 px-6 py-4 flex items-center justify-between"> 133 + <div className="flex-1"> 134 + <h2 135 + className="text-xl font-bold mb-1" 136 + style={{ fontFamily: 'Archivo Black, sans-serif' }} 137 + > 138 + {path.split('/').pop()} 139 + </h2> 140 + <div className="text-sm text-gray-600 font-mono">{path}</div> 141 + </div> 142 + 143 + <div className="flex items-center gap-4"> 144 + {/* Save status */} 145 + <div className="text-sm"> 146 + {isSaving ? ( 147 + <span className="text-amber-600 font-semibold">Saving...</span> 148 + ) : hasUnsavedChanges ? ( 149 + <span className="text-gray-600">Unsaved changes</span> 150 + ) : lastSaved ? ( 151 + <span className="text-green-600 font-semibold"> 152 + Saved {lastSaved.toLocaleTimeString()} 153 + </span> 154 + ) : null} 155 + </div> 156 + 157 + {/* Manual save button */} 158 + <button 159 + onClick={saveChanges} 160 + disabled={!hasUnsavedChanges || isSaving} 161 + className="px-4 py-2 bg-gray-900 text-white font-semibold hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors border-2 border-gray-900" 162 + > 163 + {isSaving ? 'Saving...' : 'Save'} 164 + </button> 165 + </div> 166 + </div> 167 + 168 + {/* Editor Content */} 169 + <div className="flex-1 overflow-y-auto p-6 bg-gray-50"> 170 + <div className="max-w-4xl mx-auto space-y-6"> 171 + <FrontmatterEditor 172 + frontmatter={frontmatter} 173 + onChange={handleFrontmatterChange} 174 + /> 175 + 176 + <TipTapEditor 177 + content={content} 178 + onChange={handleContentChange} 179 + placeholder="Start writing your post..." 180 + /> 181 + </div> 182 + </div> 183 + </div> 184 + ); 185 + }
+170
frontend/src/components/editor/FrontmatterEditor.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + 3 + interface FrontmatterEditorProps { 4 + frontmatter: Record<string, any>; 5 + onChange: (frontmatter: Record<string, any>) => void; 6 + className?: string; 7 + } 8 + 9 + export function FrontmatterEditor({ frontmatter, onChange, className = '' }: FrontmatterEditorProps) { 10 + const [fields, setFields] = useState<Record<string, any>>(frontmatter || {}); 11 + const [newKey, setNewKey] = useState(''); 12 + const [newValue, setNewValue] = useState(''); 13 + 14 + useEffect(() => { 15 + setFields(frontmatter || {}); 16 + }, [frontmatter]); 17 + 18 + const updateField = (key: string, value: any) => { 19 + const updated = { ...fields, [key]: value }; 20 + setFields(updated); 21 + onChange(updated); 22 + }; 23 + 24 + const deleteField = (key: string) => { 25 + const updated = { ...fields }; 26 + delete updated[key]; 27 + setFields(updated); 28 + onChange(updated); 29 + }; 30 + 31 + const addField = () => { 32 + if (!newKey.trim()) return; 33 + 34 + const updated = { ...fields, [newKey]: newValue }; 35 + setFields(updated); 36 + onChange(updated); 37 + setNewKey(''); 38 + setNewValue(''); 39 + }; 40 + 41 + const inferType = (value: any): string => { 42 + if (Array.isArray(value)) return 'array'; 43 + if (typeof value === 'boolean') return 'boolean'; 44 + if (typeof value === 'number') return 'number'; 45 + return 'string'; 46 + }; 47 + 48 + const renderFieldInput = (key: string, value: any) => { 49 + const type = inferType(value); 50 + 51 + switch (type) { 52 + case 'boolean': 53 + return ( 54 + <input 55 + type="checkbox" 56 + checked={value} 57 + onChange={(e) => updateField(key, e.target.checked)} 58 + className="w-5 h-5 text-amber-600 border-2 border-gray-900 focus:ring-0 focus:ring-offset-0" 59 + /> 60 + ); 61 + 62 + case 'array': 63 + return ( 64 + <input 65 + type="text" 66 + value={Array.isArray(value) ? value.join(', ') : ''} 67 + onChange={(e) => updateField(key, e.target.value.split(',').map((v) => v.trim()))} 68 + className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 69 + placeholder="comma, separated, values" 70 + /> 71 + ); 72 + 73 + case 'number': 74 + return ( 75 + <input 76 + type="number" 77 + value={value} 78 + onChange={(e) => updateField(key, parseFloat(e.target.value) || 0)} 79 + className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 80 + /> 81 + ); 82 + 83 + default: 84 + return ( 85 + <input 86 + type="text" 87 + value={value} 88 + onChange={(e) => updateField(key, e.target.value)} 89 + className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 90 + /> 91 + ); 92 + } 93 + }; 94 + 95 + return ( 96 + <div className={`bg-amber-50 border-2 border-gray-900 p-6 ${className}`}> 97 + <div className="flex items-center justify-between mb-4"> 98 + <h3 className="text-lg font-bold" style={{ fontFamily: 'Archivo Black, sans-serif' }}> 99 + Frontmatter 100 + </h3> 101 + <span className="text-xs text-gray-600 font-mono">YAML Metadata</span> 102 + </div> 103 + 104 + {Object.keys(fields).length === 0 ? ( 105 + <div className="text-sm text-gray-600 italic mb-4 p-4 bg-white border-2 border-dashed border-gray-300"> 106 + No frontmatter fields. Add one below. 107 + </div> 108 + ) : ( 109 + <div className="space-y-3 mb-4"> 110 + {Object.entries(fields).map(([key, value]) => ( 111 + <div key={key} className="flex items-center gap-2"> 112 + <div className="flex-1 grid grid-cols-2 gap-2"> 113 + <div className="px-3 py-2 bg-gray-900 text-white font-mono text-sm font-semibold flex items-center"> 114 + {key} 115 + </div> 116 + {renderFieldInput(key, value)} 117 + </div> 118 + <button 119 + onClick={() => deleteField(key)} 120 + className="px-3 py-2 bg-red-600 text-white font-semibold hover:bg-red-700 transition-colors border-2 border-gray-900" 121 + title="Delete field" 122 + > 123 + × 124 + </button> 125 + </div> 126 + ))} 127 + </div> 128 + )} 129 + 130 + <div className="border-t-2 border-gray-900 pt-4"> 131 + <div className="text-sm font-semibold mb-2" style={{ fontFamily: 'Archivo Black, sans-serif' }}> 132 + Add Field 133 + </div> 134 + <div className="flex gap-2"> 135 + <input 136 + type="text" 137 + value={newKey} 138 + onChange={(e) => setNewKey(e.target.value)} 139 + placeholder="Key (e.g., title)" 140 + className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 141 + onKeyDown={(e) => { 142 + if (e.key === 'Enter' && newKey.trim()) { 143 + addField(); 144 + } 145 + }} 146 + /> 147 + <input 148 + type="text" 149 + value={newValue} 150 + onChange={(e) => setNewValue(e.target.value)} 151 + placeholder="Value" 152 + className="flex-1 px-3 py-2 border-2 border-gray-900 bg-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500" 153 + onKeyDown={(e) => { 154 + if (e.key === 'Enter' && newKey.trim()) { 155 + addField(); 156 + } 157 + }} 158 + /> 159 + <button 160 + onClick={addField} 161 + disabled={!newKey.trim()} 162 + className="px-4 py-2 bg-gray-900 text-white font-semibold hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors border-2 border-gray-900" 163 + > 164 + + Add 165 + </button> 166 + </div> 167 + </div> 168 + </div> 169 + ); 170 + }
+143
frontend/src/components/editor/MenuBar.tsx
··· 1 + import { Editor } from '@tiptap/react'; 2 + 3 + interface MenuBarProps { 4 + editor: Editor; 5 + } 6 + 7 + export function MenuBar({ editor }: MenuBarProps) { 8 + const buttonClass = (isActive: boolean) => 9 + `px-3 py-1.5 text-sm font-semibold transition-colors border-2 border-transparent ${ 10 + isActive 11 + ? 'bg-gray-900 text-white' 12 + : 'bg-gray-100 text-gray-900 hover:bg-gray-200' 13 + }`; 14 + 15 + return ( 16 + <div className="border-b-2 border-gray-900 p-3 bg-amber-50 flex flex-wrap gap-1"> 17 + <button 18 + onClick={() => editor.chain().focus().toggleBold().run()} 19 + disabled={!editor.can().chain().focus().toggleBold().run()} 20 + className={buttonClass(editor.isActive('bold'))} 21 + title="Bold (Cmd+B)" 22 + > 23 + <strong>B</strong> 24 + </button> 25 + 26 + <button 27 + onClick={() => editor.chain().focus().toggleItalic().run()} 28 + disabled={!editor.can().chain().focus().toggleItalic().run()} 29 + className={buttonClass(editor.isActive('italic'))} 30 + title="Italic (Cmd+I)" 31 + > 32 + <em>I</em> 33 + </button> 34 + 35 + <button 36 + onClick={() => editor.chain().focus().toggleCode().run()} 37 + disabled={!editor.can().chain().focus().toggleCode().run()} 38 + className={buttonClass(editor.isActive('code'))} 39 + title="Inline Code" 40 + > 41 + {'</>'} 42 + </button> 43 + 44 + <div className="w-px bg-gray-900 mx-1"></div> 45 + 46 + <button 47 + onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} 48 + className={buttonClass(editor.isActive('heading', { level: 1 }))} 49 + title="Heading 1" 50 + > 51 + H1 52 + </button> 53 + 54 + <button 55 + onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} 56 + className={buttonClass(editor.isActive('heading', { level: 2 }))} 57 + title="Heading 2" 58 + > 59 + H2 60 + </button> 61 + 62 + <button 63 + onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} 64 + className={buttonClass(editor.isActive('heading', { level: 3 }))} 65 + title="Heading 3" 66 + > 67 + H3 68 + </button> 69 + 70 + <div className="w-px bg-gray-900 mx-1"></div> 71 + 72 + <button 73 + onClick={() => editor.chain().focus().toggleBulletList().run()} 74 + className={buttonClass(editor.isActive('bulletList'))} 75 + title="Bullet List" 76 + > 77 + • List 78 + </button> 79 + 80 + <button 81 + onClick={() => editor.chain().focus().toggleOrderedList().run()} 82 + className={buttonClass(editor.isActive('orderedList'))} 83 + title="Numbered List" 84 + > 85 + 1. List 86 + </button> 87 + 88 + <button 89 + onClick={() => editor.chain().focus().toggleBlockquote().run()} 90 + className={buttonClass(editor.isActive('blockquote'))} 91 + title="Quote" 92 + > 93 + " Quote 94 + </button> 95 + 96 + <button 97 + onClick={() => editor.chain().focus().toggleCodeBlock().run()} 98 + className={buttonClass(editor.isActive('codeBlock'))} 99 + title="Code Block" 100 + > 101 + {'{ } Code'} 102 + </button> 103 + 104 + <div className="w-px bg-gray-900 mx-1"></div> 105 + 106 + <button 107 + onClick={() => editor.chain().focus().setHorizontalRule().run()} 108 + className={buttonClass(false)} 109 + title="Horizontal Rule" 110 + > 111 + ─── 112 + </button> 113 + 114 + <button 115 + onClick={() => editor.chain().focus().setHardBreak().run()} 116 + className={buttonClass(false)} 117 + title="Line Break" 118 + > 119 + 120 + </button> 121 + 122 + <div className="w-px bg-gray-900 mx-1"></div> 123 + 124 + <button 125 + onClick={() => editor.chain().focus().undo().run()} 126 + disabled={!editor.can().chain().focus().undo().run()} 127 + className="px-3 py-1.5 text-sm font-semibold bg-gray-100 text-gray-900 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border-2 border-transparent transition-colors" 128 + title="Undo (Cmd+Z)" 129 + > 130 + ↶ Undo 131 + </button> 132 + 133 + <button 134 + onClick={() => editor.chain().focus().redo().run()} 135 + disabled={!editor.can().chain().focus().redo().run()} 136 + className="px-3 py-1.5 text-sm font-semibold bg-gray-100 text-gray-900 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border-2 border-transparent transition-colors" 137 + title="Redo (Cmd+Shift+Z)" 138 + > 139 + ↷ Redo 140 + </button> 141 + </div> 142 + ); 143 + }
+187
frontend/src/components/editor/TipTapEditor.tsx
··· 1 + import { useEditor, EditorContent } from '@tiptap/react'; 2 + import StarterKit from '@tiptap/starter-kit'; 3 + import Typography from '@tiptap/extension-typography'; 4 + import Placeholder from '@tiptap/extension-placeholder'; 5 + import Link from '@tiptap/extension-link'; 6 + import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; 7 + import { common, createLowlight } from 'lowlight'; 8 + import { MenuBar } from './MenuBar'; 9 + import { htmlToMarkdown } from '../../lib/utils/markdown'; 10 + 11 + const lowlight = createLowlight(common); 12 + 13 + interface TipTapEditorProps { 14 + content: string; 15 + onChange: (content: string) => void; 16 + placeholder?: string; 17 + className?: string; 18 + } 19 + 20 + export function TipTapEditor({ 21 + content, 22 + onChange, 23 + placeholder = 'Start writing...', 24 + className = '' 25 + }: TipTapEditorProps) { 26 + const editor = useEditor({ 27 + extensions: [ 28 + StarterKit.configure({ 29 + codeBlock: false, // Disable default code block, use lowlight version 30 + }), 31 + Typography, 32 + Placeholder.configure({ 33 + placeholder, 34 + }), 35 + Link.configure({ 36 + openOnClick: false, 37 + HTMLAttributes: { 38 + class: 'text-amber-600 hover:text-amber-700 underline decoration-wavy', 39 + }, 40 + }), 41 + CodeBlockLowlight.configure({ 42 + lowlight, 43 + HTMLAttributes: { 44 + class: 'bg-gray-900 text-gray-100 rounded-lg p-4 font-mono text-sm overflow-x-auto', 45 + }, 46 + }), 47 + ], 48 + content, 49 + editorProps: { 50 + attributes: { 51 + class: 'prose prose-lg max-w-none focus:outline-none min-h-[400px]', 52 + }, 53 + }, 54 + onUpdate: ({ editor }) => { 55 + // Convert HTML to markdown before calling onChange 56 + const html = editor.getHTML(); 57 + const markdown = htmlToMarkdown(html); 58 + onChange(markdown); 59 + }, 60 + }); 61 + 62 + return ( 63 + <div className={`border-2 border-gray-900 bg-white ${className}`}> 64 + {editor && <MenuBar editor={editor} />} 65 + 66 + <div className="p-8"> 67 + <EditorContent editor={editor} /> 68 + </div> 69 + 70 + <style>{` 71 + .ProseMirror { 72 + font-family: 'Crimson Pro', serif; 73 + } 74 + 75 + .ProseMirror p.is-editor-empty:first-child::before { 76 + content: attr(data-placeholder); 77 + float: left; 78 + color: #9ca3af; 79 + pointer-events: none; 80 + height: 0; 81 + } 82 + 83 + .ProseMirror h1, 84 + .ProseMirror h2, 85 + .ProseMirror h3 { 86 + font-family: 'Archivo Black', sans-serif; 87 + font-weight: 900; 88 + } 89 + 90 + .ProseMirror h1 { 91 + font-size: 2.5rem; 92 + line-height: 1.1; 93 + margin-top: 1.5rem; 94 + margin-bottom: 1rem; 95 + } 96 + 97 + .ProseMirror h2 { 98 + font-size: 2rem; 99 + line-height: 1.2; 100 + margin-top: 1.5rem; 101 + margin-bottom: 0.75rem; 102 + } 103 + 104 + .ProseMirror h3 { 105 + font-size: 1.5rem; 106 + line-height: 1.3; 107 + margin-top: 1.25rem; 108 + margin-bottom: 0.5rem; 109 + } 110 + 111 + .ProseMirror p { 112 + margin-bottom: 1rem; 113 + line-height: 1.75; 114 + } 115 + 116 + .ProseMirror ul, 117 + .ProseMirror ol { 118 + margin-left: 1.5rem; 119 + margin-bottom: 1rem; 120 + } 121 + 122 + .ProseMirror ul li { 123 + list-style-type: disc; 124 + margin-bottom: 0.5rem; 125 + } 126 + 127 + .ProseMirror ol li { 128 + list-style-type: decimal; 129 + margin-bottom: 0.5rem; 130 + } 131 + 132 + .ProseMirror blockquote { 133 + border-left: 4px solid #d97706; 134 + padding-left: 1.5rem; 135 + margin: 1.5rem 0; 136 + font-style: italic; 137 + color: #6b7280; 138 + } 139 + 140 + .ProseMirror code { 141 + background: #fef3c7; 142 + color: #92400e; 143 + padding: 0.125rem 0.375rem; 144 + border-radius: 0.25rem; 145 + font-size: 0.875em; 146 + font-family: 'Monaco', 'Courier New', monospace; 147 + } 148 + 149 + .ProseMirror pre { 150 + background: #1f2937; 151 + color: #f3f4f6; 152 + padding: 1rem; 153 + border-radius: 0.5rem; 154 + overflow-x: auto; 155 + margin: 1rem 0; 156 + } 157 + 158 + .ProseMirror pre code { 159 + background: transparent; 160 + color: inherit; 161 + padding: 0; 162 + font-size: 0.875rem; 163 + } 164 + 165 + .ProseMirror hr { 166 + border: none; 167 + border-top: 2px solid #e5e7eb; 168 + margin: 2rem 0; 169 + } 170 + 171 + .ProseMirror strong { 172 + font-weight: 700; 173 + color: #111827; 174 + } 175 + 176 + .ProseMirror em { 177 + font-style: italic; 178 + } 179 + 180 + /* Focus styles */ 181 + .ProseMirror:focus { 182 + outline: none; 183 + } 184 + `}</style> 185 + </div> 186 + ); 187 + }
+35
frontend/src/lib/api/files.ts
··· 1 + import { apiClient } from './client'; 2 + import type { FileContent } from '../types/api'; 3 + 4 + export interface UpdateFileParams { 5 + owner: string; 6 + repo: string; 7 + path: string; 8 + content: string; 9 + frontmatter: Record<string, any>; 10 + } 11 + 12 + export const filesApi = { 13 + async getFileContent( 14 + owner: string, 15 + repo: string, 16 + path: string, 17 + branch?: string 18 + ): Promise<FileContent> { 19 + const params = branch ? { branch } : {}; 20 + const response = await apiClient.get<FileContent>( 21 + `/api/repos/${owner}/${repo}/files/${path}`, 22 + { params } 23 + ); 24 + return response.data; 25 + }, 26 + 27 + async updateFile(params: UpdateFileParams): Promise<{ success: boolean; draft_saved: boolean; saved_at: string }> { 28 + const { owner, repo, path, content, frontmatter } = params; 29 + const response = await apiClient.put( 30 + `/api/repos/${owner}/${repo}/files/${path}`, 31 + { content, frontmatter } 32 + ); 33 + return response.data; 34 + }, 35 + };
+24
frontend/src/lib/hooks/useFileContent.ts
··· 1 + import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { filesApi, type UpdateFileParams } from '../api/files'; 3 + 4 + export function useFileContent(owner: string, repo: string, path: string, branch?: string) { 5 + return useQuery({ 6 + queryKey: ['fileContent', owner, repo, path, branch], 7 + queryFn: () => filesApi.getFileContent(owner, repo, path, branch), 8 + enabled: !!owner && !!repo && !!path, 9 + }); 10 + } 11 + 12 + export function useUpdateFile() { 13 + const queryClient = useQueryClient(); 14 + 15 + return useMutation({ 16 + mutationFn: (params: UpdateFileParams) => filesApi.updateFile(params), 17 + onSuccess: (_, variables) => { 18 + // Invalidate the file content query to refetch 19 + queryClient.invalidateQueries({ 20 + queryKey: ['fileContent', variables.owner, variables.repo, variables.path], 21 + }); 22 + }, 23 + }); 24 + }
+18
frontend/src/lib/utils/debounce.ts
··· 1 + export function debounce<T extends (...args: any[]) => any>( 2 + func: T, 3 + wait: number 4 + ): (...args: Parameters<T>) => void { 5 + let timeout: ReturnType<typeof setTimeout> | null = null; 6 + 7 + return function executedFunction(...args: Parameters<T>) { 8 + const later = () => { 9 + timeout = null; 10 + func(...args); 11 + }; 12 + 13 + if (timeout) { 14 + clearTimeout(timeout); 15 + } 16 + timeout = setTimeout(later, wait); 17 + }; 18 + }
+23
frontend/src/lib/utils/markdown.ts
··· 1 + import TurndownService from 'turndown'; 2 + 3 + // Initialize Turndown for HTML to Markdown conversion 4 + const turndownService = new TurndownService({ 5 + headingStyle: 'atx', 6 + codeBlockStyle: 'fenced', 7 + fence: '```', 8 + emDelimiter: '_', 9 + strongDelimiter: '**', 10 + bulletListMarker: '-', 11 + }); 12 + 13 + export function htmlToMarkdown(html: string): string { 14 + return turndownService.turndown(html); 15 + } 16 + 17 + // For markdown to HTML, we'll use the browser's built-in markdown rendering 18 + // via TipTap's setContent which accepts markdown 19 + export function markdownToHtml(markdown: string): string { 20 + // This is a simple conversion - TipTap will handle the actual rendering 21 + // We just need to preserve the markdown structure 22 + return markdown; 23 + }