From 9f35e74d0f0e5e83aa7d26cca535ef122bfc2d86 Mon Sep 17 00:00:00 2001 From: hugo Date: Tue, 10 Feb 2026 13:40:44 +0100 Subject: [PATCH] feat: more feature implementations --- VISION.md | 25 +- package-lock.json | 1416 ++++++++++++++++- package.json | 11 + src/main/database/connection.ts | 10 + src/main/database/schema.ts | 11 + src/main/engine/MediaEngine.ts | 140 +- src/main/engine/PostEngine.ts | 141 +- src/main/ipc/handlers.ts | 35 + src/main/preload.ts | 8 + .../CredentialsPanel/CredentialsPanel.css | 106 ++ .../CredentialsPanel/CredentialsPanel.tsx | 283 ++++ .../components/CredentialsPanel/index.ts | 1 + src/renderer/components/Editor/Editor.css | 73 + src/renderer/components/Editor/Editor.tsx | 96 +- src/renderer/components/Lightbox/Lightbox.css | 253 +++ src/renderer/components/Lightbox/Lightbox.tsx | 235 +++ src/renderer/components/Lightbox/index.ts | 1 + .../components/PostLinks/PostLinks.css | 107 ++ .../components/PostLinks/PostLinks.tsx | 117 ++ src/renderer/components/PostLinks/index.ts | 2 + .../ResizablePanel/ResizablePanel.css | 73 + .../ResizablePanel/ResizablePanel.tsx | 123 ++ .../components/ResizablePanel/index.ts | 1 + src/renderer/components/Sidebar/Sidebar.css | 38 +- src/renderer/components/Sidebar/Sidebar.tsx | 100 +- .../components/TaskPopup/TaskPopup.css | 230 +++ .../components/TaskPopup/TaskPopup.tsx | 190 +++ src/renderer/components/TaskPopup/index.ts | 1 + .../WysiwygEditor/WysiwygEditor.css | 296 ++++ .../WysiwygEditor/WysiwygEditor.tsx | 379 +++++ .../components/WysiwygEditor/index.ts | 1 + src/renderer/components/index.ts | 6 + src/renderer/store/appStore.ts | 181 ++- 33 files changed, 4560 insertions(+), 130 deletions(-) create mode 100644 src/renderer/components/CredentialsPanel/CredentialsPanel.css create mode 100644 src/renderer/components/CredentialsPanel/CredentialsPanel.tsx create mode 100644 src/renderer/components/CredentialsPanel/index.ts create mode 100644 src/renderer/components/Lightbox/Lightbox.css create mode 100644 src/renderer/components/Lightbox/Lightbox.tsx create mode 100644 src/renderer/components/Lightbox/index.ts create mode 100644 src/renderer/components/PostLinks/PostLinks.css create mode 100644 src/renderer/components/PostLinks/PostLinks.tsx create mode 100644 src/renderer/components/PostLinks/index.ts create mode 100644 src/renderer/components/ResizablePanel/ResizablePanel.css create mode 100644 src/renderer/components/ResizablePanel/ResizablePanel.tsx create mode 100644 src/renderer/components/ResizablePanel/index.ts create mode 100644 src/renderer/components/TaskPopup/TaskPopup.css create mode 100644 src/renderer/components/TaskPopup/TaskPopup.tsx create mode 100644 src/renderer/components/TaskPopup/index.ts create mode 100644 src/renderer/components/WysiwygEditor/WysiwygEditor.css create mode 100644 src/renderer/components/WysiwygEditor/WysiwygEditor.tsx create mode 100644 src/renderer/components/WysiwygEditor/index.ts diff --git a/VISION.md b/VISION.md index 14f99c9..20e0566 100644 --- a/VISION.md +++ b/VISION.md @@ -84,6 +84,21 @@ This is a short-form article that does not need a full-article-page, because it a short comment that is shown after the link. This is meant for link collections and should be rendered in a compact form in the overview pages. More on rendering in the publishing pipelin description. +### category "page" + +This is a post that behaves mostly like an article, but is ignored in overview pages, because it is just +meant to be linked to menues. So menu editing needs to be able to reference posts of category page +and overview templates need to ignore posts of category page. Other than that, they are just like article, +so have long form text, short form summary and title. Pages can be assigned different header images that +override the project image. + +### Project definition + +A project is not just the collection of posts, media and publish settings, it is also a base project +container with title, author and other elements like that. So there needs to be project related settings +that allow to give the project a title, set up a main author that can be referenced in metadat and for +example set up header images that can be used when publishing. + ## Migrating Prepare a proper mass-data importer that can read wordpress backup files, so the user can bring in old @@ -141,6 +156,12 @@ system and also create blog posts with AI support. All SDK tools must also be made available as MCP server that is hosted inside the application, so that I can hook the app into a normal AI coding agent. +Also the AI should be available to create summaries just with a button click in the post editor area, so +that the summary is filled in based on AI summarization to help speed up the blogging process. Also the +AI can be used to generate a good slurl that is not just generically from the title and even the title +can be AI-generated from the text. That way the user can focus on writing the core text and if all matches, +just accept AI summary and title and post it. + ## Publishing Publishing should target static HTML/CSS/JavaScript situations. There must be a asnyc exporter, that will render @@ -168,7 +189,9 @@ maybe even with easy importing from a central bootstrap site or something like t Check the site https://hugo.rfc1437.de/ for its structure, this is the structure of blog I want to be capable of building with this tooling. So we need templates for overview pages and ways to manage menues that reference overview pages and structure the menu according to site structure. Also support calendar views -to allow users to go to specific months and years of the blog. +to allow users to go to specific months and years of the blog. Build up a sensible set of templates that +come with a new project, so that the user can start right away without a lot of hassle. Base the templates +on the structure of above website, but keep out website title and images, of course. Categories and tags must be able to define a template selection for post templates, so that different types can be represented differently. this is especially important for the standard categories "article", "picture" diff --git a/package-lock.json b/package-lock.json index 1ab5860..12d5d0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,25 @@ "dependencies": { "@libsql/client": "^0.4.0", "@monaco-editor/react": "^4.7.0", + "@tiptap/extension-image": "^3.19.0", + "@tiptap/extension-link": "^3.19.0", + "@tiptap/extension-placeholder": "^3.19.0", + "@tiptap/extension-underline": "^3.19.0", + "@tiptap/pm": "^3.19.0", + "@tiptap/react": "^3.19.0", + "@tiptap/starter-kit": "^3.19.0", + "@types/turndown": "^5.0.6", "date-fns": "^4.1.0", "drizzle-orm": "^0.29.0", "electron-store": "^8.1.0", "gray-matter": "^4.0.3", + "marked-react": "^3.0.2", "monaco-editor": "^0.55.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.6.0", + "sharp": "^0.34.5", + "turndown": "^7.2.2", "uuid": "^9.0.1", "zustand": "^4.4.7" }, @@ -688,6 +699,16 @@ "node": ">= 10.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -1566,6 +1587,23 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@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/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -1590,6 +1628,471 @@ "zod": "^3.19.1" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2441,6 +2944,12 @@ "node": ">= 10.0.0" } }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@monaco-editor/loader": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", @@ -2527,6 +3036,12 @@ "dev": true, "license": "MIT" }, + "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/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2917,6 +3432,470 @@ "node": ">=10" } }, + "node_modules/@tiptap/core": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz", + "integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.19.0.tgz", + "integrity": "sha512-y3UfqY9KD5XwWz3ndiiJ089Ij2QKeiXy/g1/tlAN/F1AaWsnkHEHMLxCP1BIqmMpwsX7rZjMLN7G5Lp7c9682A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.19.0.tgz", + "integrity": "sha512-UZgb1d0XK4J/JRIZ7jW+s4S6KjuEDT2z1PPM6ugcgofgJkWQvRZelCPbmtSFd3kwsD+zr9UPVgTh9YIuGQ8t+Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.19.0.tgz", + "integrity": "sha512-klNVIYGCdznhFkrRokzGd6cwzoi8J7E5KbuOfZBwFwhMKZhlz/gJfKmYg9TJopeUhrr2Z9yHgWTk8dh/YIJCdQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.19.0.tgz", + "integrity": "sha512-F9uNnqd0xkJbMmRxVI5RuVxwB9JaCH/xtRqOUNQZnRBt7IdAElCY+Dvb4hMCtiNv+enGM/RFGJuFHR9TxmI7rw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.19.0.tgz", + "integrity": "sha512-2kqqQIXBXj2Or+4qeY3WoE7msK+XaHKL6EKOcKlOP2BW8eYqNTPzNSL+PfBDQ3snA7ljZQkTs/j4GYDj90vR1A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.19.0.tgz", + "integrity": "sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.19.0.tgz", + "integrity": "sha512-AOf0kHKSFO0ymjVgYSYDncRXTITdTcrj1tqxVazrmO60KNl1Rc2dAggDvIVTEBy5NvceF0scc7q3sE/5ZtVV7A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.19.0.tgz", + "integrity": "sha512-sf3dEZXiLvsGqVK2maUIzXY6qtYYCvBumag7+VPTMGQ0D4hiZ1X/4ukt4+6VXDg5R2WP1CoIt/QvUetUjWNhbQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.19.0.tgz", + "integrity": "sha512-JaoEkVRkt+Slq3tySlIsxnMnCjS0L5n1CA1hctjLy0iah8edetj3XD5mVv5iKqDzE+LIjF4nwLRRVKJPc8hFBg==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.19.0.tgz", + "integrity": "sha512-w7DACS4oSZaDWjz7gropZHPc9oXqC9yERZTcjWxyORuuIh1JFf0TRYspleK+OK28plK/IftojD/yUDn1MTRhvA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz", + "integrity": "sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.19.0.tgz", + "integrity": "sha512-uLpLlfyp086WYNOc0ekm1gIZNlEDfmzOhKzB0Hbyi6jDagTS+p9mxUNYeYOn9jPUxpFov43+Wm/4E24oY6B+TQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz", + "integrity": "sha512-iqUHmgMGhMgYGwG6L/4JdelVQ5Mstb4qHcgTGd/4dkcUOepILvhdxajPle7OEdf9sRgjQO6uoAU5BVZVC26+ng==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.19.0.tgz", + "integrity": "sha512-/rGl8nBziBPVJJ/9639eQWFDKcI3RQsDM3s+cqYQMFQfMqc7sQB9h4o4sHCBpmKxk3Y0FV/0NjnjLbBVm8OKdQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz", + "integrity": "sha512-6GffxOnS/tWyCbDkirWNZITiXRta9wrCmrfa4rh+v32wfaOL1RRQNyqo9qN6Wjyl1R42Js+yXTzTTzZsOaLMYA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.19.0.tgz", + "integrity": "sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.19.0.tgz", + "integrity": "sha512-N6nKbFB2VwMsPlCw67RlAtYSK48TAsAUgjnD+vd3ieSlIufdQnLXDFUP6hFKx9mwoUVUgZGz02RA6bkxOdYyTw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.19.0.tgz", + "integrity": "sha512-VsSKuJz4/Tb6ZmFkXqWpDYkRzmaLTyE6dNSEpNmUpmZ32sMqo58mt11/huADNwfBFB0Ve7siH/VnFNIJYY3xvg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.19.0.tgz", + "integrity": "sha512-bxgmAgA3RzBGA0GyTwS2CC1c+QjkJJq9hC+S6PSOWELGRiTbwDN3MANksFXLjntkTa0N5fOnL27vBHtMStURqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.19.0.tgz", + "integrity": "sha512-cxGsINquwHYE1kmhAcLNLHAofmoDEG6jbesR5ybl7tU5JwtKVO7S/xZatll2DU1dsDAXWPWEeeMl4e/9svYjCg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.19.0.tgz", + "integrity": "sha512-xWa6gj82l5+AzdYyrSk9P4ynySaDzg/SlR1FarXE5yPXibYzpS95IWaVR0m2Qaz7Rrk+IiYOTGxGRxcHLOelNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.19.0.tgz", + "integrity": "sha512-i15OfgyI4IDCYAcYSKUMnuZkYuUInfanjf9zquH8J2BETiomf/jZldVCp/QycMJ8DOXZ38fXDc99wOygnSNySg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.19.0.tgz", + "integrity": "sha512-xYpabHsv7PccLUBQaP8AYiFCnYbx6P93RHPd0lgNwhdOjYFd931Zy38RyoxPHAgbYVmhf1iyx7lpuLtBnhS5dA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz", + "integrity": "sha512-K95+SnbZy0h6hNFtfy23n8t/nOcTFEf69In9TSFVVmwn/Nwlke+IfiESAkqbt1/7sKJeegRXYO7WzFEmFl9Q/g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.19.0.tgz", + "integrity": "sha512-800MGEWfG49j10wQzAFiW/ele1HT04MamcL8iyuPNu7ZbjbGN2yknvdrJlRy7hZlzIrVkZMr/1tz62KN33VHIw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.19.0.tgz", + "integrity": "sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz", + "integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==", + "license": "MIT", + "peer": true, + "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.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.19.0.tgz", + "integrity": "sha512-GQQMUUXMpNd8tRjc1jDK3tDRXFugJO7C928EqmeBcBzTKDrFIJ3QUoZKEPxUNb6HWhZ2WL7q00fiMzsv4DNSmg==", + "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.19.0", + "@tiptap/extension-floating-menu": "^3.19.0" + }, + "peerDependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.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.19.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.19.0.tgz", + "integrity": "sha512-dTCkHEz+Y8ADxX7h+xvl6caAj+3nII/wMB1rTQchSuNKqJTOrzyUsCWm094+IoZmLT738wANE0fRIgziNHs/ug==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.19.0", + "@tiptap/extension-blockquote": "^3.19.0", + "@tiptap/extension-bold": "^3.19.0", + "@tiptap/extension-bullet-list": "^3.19.0", + "@tiptap/extension-code": "^3.19.0", + "@tiptap/extension-code-block": "^3.19.0", + "@tiptap/extension-document": "^3.19.0", + "@tiptap/extension-dropcursor": "^3.19.0", + "@tiptap/extension-gapcursor": "^3.19.0", + "@tiptap/extension-hard-break": "^3.19.0", + "@tiptap/extension-heading": "^3.19.0", + "@tiptap/extension-horizontal-rule": "^3.19.0", + "@tiptap/extension-italic": "^3.19.0", + "@tiptap/extension-link": "^3.19.0", + "@tiptap/extension-list": "^3.19.0", + "@tiptap/extension-list-item": "^3.19.0", + "@tiptap/extension-list-keymap": "^3.19.0", + "@tiptap/extension-ordered-list": "^3.19.0", + "@tiptap/extension-paragraph": "^3.19.0", + "@tiptap/extension-strike": "^3.19.0", + "@tiptap/extension-text": "^3.19.0", + "@tiptap/extension-underline": "^3.19.0", + "@tiptap/extensions": "^3.19.0", + "@tiptap/pm": "^3.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3029,6 +4008,28 @@ "@types/node": "*" } }, + "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/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/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/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3071,14 +4072,12 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -3090,8 +4089,8 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3113,6 +4112,18 @@ "license": "MIT", "optional": true }, + "node_modules/@types/turndown": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", + "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", + "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/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -3697,7 +4708,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/assert-plus": { @@ -4605,6 +5615,12 @@ "node": ">= 10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5437,6 +6453,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -5633,9 +6661,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, @@ -5819,6 +6845,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.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -7119,6 +8154,21 @@ "@libsql/win32-x64-msvc": "0.2.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "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/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -7301,6 +8351,23 @@ "node": ">=10" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "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" + } + }, "node_modules/marked": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", @@ -7313,6 +8380,30 @@ "node": ">= 18" } }, + "node_modules/marked-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked-react/-/marked-react-3.0.2.tgz", + "integrity": "sha512-uLuH5P5i1H+igH+Y7toWX+sM2UGDdYMIPZ9JtBNajkyFiQBoAMJJY7gBlm3nHXHiPDZHkhZ9p/cb9XO0F1qnCA==", + "license": "MIT", + "dependencies": { + "marked": "^16.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || >=17.0.0" + } + }, + "node_modules/marked-react/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -7336,6 +8427,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/memfs": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", @@ -7777,6 +8874,12 @@ "node": ">=6" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -8061,6 +9164,204 @@ "node": ">=10" } }, + "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.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "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", + "peer": true, + "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", + "peer": true, + "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-transform": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", + "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -8082,6 +9383,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8367,6 +9677,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-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8520,6 +9836,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9180,7 +10561,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, + "devOptional": true, "license": "0BSD", "peer": true }, @@ -9637,6 +11018,15 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/turndown": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", + "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", @@ -9680,6 +11070,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/ufo": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", @@ -10377,6 +11773,12 @@ } } }, + "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/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", diff --git a/package.json b/package.json index d205ff7..1f2524e 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,25 @@ "dependencies": { "@libsql/client": "^0.4.0", "@monaco-editor/react": "^4.7.0", + "@tiptap/extension-image": "^3.19.0", + "@tiptap/extension-link": "^3.19.0", + "@tiptap/extension-placeholder": "^3.19.0", + "@tiptap/extension-underline": "^3.19.0", + "@tiptap/pm": "^3.19.0", + "@tiptap/react": "^3.19.0", + "@tiptap/starter-kit": "^3.19.0", + "@types/turndown": "^5.0.6", "date-fns": "^4.1.0", "drizzle-orm": "^0.29.0", "electron-store": "^8.1.0", "gray-matter": "^4.0.3", + "marked-react": "^3.0.2", "monaco-editor": "^0.55.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.6.0", + "sharp": "^0.34.5", + "turndown": "^7.2.2", "uuid": "^9.0.1", "zustand": "^4.4.7" }, diff --git a/src/main/database/connection.ts b/src/main/database/connection.ts index 7d423b3..d44e7b2 100644 --- a/src/main/database/connection.ts +++ b/src/main/database/connection.ts @@ -190,12 +190,22 @@ export class DatabaseConnection { updated_at INTEGER NOT NULL ); + CREATE TABLE IF NOT EXISTS post_links ( + id TEXT PRIMARY KEY, + source_post_id TEXT NOT NULL, + target_post_id TEXT NOT NULL, + link_text TEXT, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug); CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status); CREATE INDEX IF NOT EXISTS idx_posts_sync_status ON posts(sync_status); CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at); CREATE INDEX IF NOT EXISTS idx_media_sync_status ON media(sync_status); CREATE INDEX IF NOT EXISTS idx_sync_log_status ON sync_log(status); + CREATE INDEX IF NOT EXISTS idx_post_links_source ON post_links(source_post_id); + CREATE INDEX IF NOT EXISTS idx_post_links_target ON post_links(target_post_id); `); // Check if project_id column exists in posts table, add if missing (migration) diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 4ced904..91bcceb 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -72,6 +72,15 @@ export const settings = sqliteTable('settings', { updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), }); +// Post links - tracks internal links between posts +export const postLinks = sqliteTable('post_links', { + id: text('id').primaryKey(), + sourcePostId: text('source_post_id').notNull(), // Post containing the link + targetPostId: text('target_post_id').notNull(), // Post being linked to + linkText: text('link_text'), // The text of the link + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), +}); + // Types for TypeScript export type Project = typeof projects.$inferSelect; export type NewProject = typeof projects.$inferInsert; @@ -83,3 +92,5 @@ export type SyncLogEntry = typeof syncLog.$inferSelect; export type NewSyncLogEntry = typeof syncLog.$inferInsert; export type Setting = typeof settings.$inferSelect; export type NewSetting = typeof settings.$inferInsert; +export type PostLink = typeof postLinks.$inferSelect; +export type NewPostLink = typeof postLinks.$inferInsert; diff --git a/src/main/engine/MediaEngine.ts b/src/main/engine/MediaEngine.ts index cb85479..cca1190 100644 --- a/src/main/engine/MediaEngine.ts +++ b/src/main/engine/MediaEngine.ts @@ -7,6 +7,15 @@ import { eq } from 'drizzle-orm'; import { app } from 'electron'; import { getDatabase } from '../database'; import { media, Media, NewMedia } from '../database/schema'; + +// Thumbnail sizes +const THUMBNAIL_SIZES = { + small: { width: 150, height: 150 }, + medium: { width: 400, height: 400 }, + large: { width: 800, height: 800 }, +} as const; + +type ThumbnailSize = keyof typeof THUMBNAIL_SIZES; import { taskManager, Task } from './TaskManager'; export interface MediaData { @@ -88,6 +97,105 @@ export class MediaEngine extends EventEmitter { return crypto.createHash('md5').update(buffer).digest('hex'); } + /** + * Get the thumbnails directory for the current project + */ + private getThumbnailsDir(): string { + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'projects', this.currentProjectId, 'thumbnails'); + } + + /** + * Generate thumbnails for an image file + */ + async generateThumbnails(mediaId: string, sourcePath: string): Promise> { + const thumbnailsDir = this.getThumbnailsDir(); + await fs.mkdir(thumbnailsDir, { recursive: true }); + + const thumbnails: Record = {} as Record; + + try { + // Dynamic import of sharp (it's a native module) + const sharp = (await import('sharp')).default; + + for (const [size, dimensions] of Object.entries(THUMBNAIL_SIZES) as [ThumbnailSize, { width: number; height: number }][]) { + const thumbnailPath = path.join(thumbnailsDir, `${mediaId}-${size}.webp`); + + await sharp(sourcePath) + .resize(dimensions.width, dimensions.height, { + fit: 'inside', + withoutEnlargement: true, + }) + .webp({ quality: 80 }) + .toFile(thumbnailPath); + + thumbnails[size] = thumbnailPath; + } + + this.emit('thumbnailsGenerated', { mediaId, thumbnails }); + } catch (error) { + console.error('Failed to generate thumbnails:', error); + // Return empty thumbnails on error - non-critical failure + } + + return thumbnails; + } + + /** + * Get existing thumbnail paths for a media item + */ + async getThumbnailPaths(mediaId: string): Promise> { + const thumbnailsDir = this.getThumbnailsDir(); + const result: Record = { + small: null, + medium: null, + large: null, + }; + + for (const size of Object.keys(THUMBNAIL_SIZES) as ThumbnailSize[]) { + const thumbnailPath = path.join(thumbnailsDir, `${mediaId}-${size}.webp`); + try { + await fs.access(thumbnailPath); + result[size] = thumbnailPath; + } catch { + // Thumbnail doesn't exist + } + } + + return result; + } + + /** + * Get thumbnail as base64 data URL for renderer + */ + async getThumbnailDataUrl(mediaId: string, size: ThumbnailSize = 'small'): Promise { + const thumbnailsDir = this.getThumbnailsDir(); + const thumbnailPath = path.join(thumbnailsDir, `${mediaId}-${size}.webp`); + + try { + const data = await fs.readFile(thumbnailPath); + return `data:image/webp;base64,${data.toString('base64')}`; + } catch { + return null; + } + } + + /** + * Delete thumbnails for a media item + */ + private async deleteThumbnails(mediaId: string): Promise { + const thumbnailsDir = this.getThumbnailsDir(); + + for (const size of Object.keys(THUMBNAIL_SIZES) as ThumbnailSize[]) { + const thumbnailPath = path.join(thumbnailsDir, `${mediaId}-${size}.webp`); + try { + await fs.unlink(thumbnailPath); + } catch { + // Thumbnail doesn't exist, ignore + } + } + } + private async writeSidecarFile(mediaData: MediaData, mediaPath: string): Promise { const sidecarPath = `${mediaPath}.meta`; @@ -239,14 +347,30 @@ export class MediaEngine extends EventEmitter { // Copy file to media directory await fs.copyFile(sourcePath, destPath); + const mimeType = metadata?.mimeType || this.getMimeType(originalName); + let width = metadata?.width; + let height = metadata?.height; + + // Get image dimensions using sharp if it's an image + if (mimeType.startsWith('image/') && !mimeType.includes('svg')) { + try { + const sharp = (await import('sharp')).default; + const imageMetadata = await sharp(destPath).metadata(); + width = imageMetadata.width; + height = imageMetadata.height; + } catch (error) { + console.error('Failed to get image dimensions:', error); + } + } + const mediaData: MediaData = { id, filename, originalName, - mimeType: metadata?.mimeType || this.getMimeType(originalName), + mimeType, size: sourceBuffer.length, - width: metadata?.width, - height: metadata?.height, + width, + height, alt: metadata?.alt, caption: metadata?.caption, createdAt: now, @@ -257,6 +381,13 @@ export class MediaEngine extends EventEmitter { const sidecarPath = await this.writeSidecarFile(mediaData, destPath); const checksum = this.calculateChecksum(sourceBuffer); + // Generate thumbnails for images (async, non-blocking) + if (mimeType.startsWith('image/') && !mimeType.includes('svg')) { + this.generateThumbnails(id, destPath).catch(err => { + console.error('Failed to generate thumbnails:', err); + }); + } + const dbMedia: NewMedia = { id: mediaData.id, projectId: this.currentProjectId, @@ -339,6 +470,9 @@ export class MediaEngine extends EventEmitter { // File might not exist } + // Delete thumbnails + await this.deleteThumbnails(id); + await db.delete(media).where(eq(media.id, id)); this.emit('mediaDeleted', id); diff --git a/src/main/engine/PostEngine.ts b/src/main/engine/PostEngine.ts index 9282336..b1146f3 100644 --- a/src/main/engine/PostEngine.ts +++ b/src/main/engine/PostEngine.ts @@ -7,7 +7,7 @@ import matter from 'gray-matter'; import { eq, and, desc, gte, lte, like } from 'drizzle-orm'; import { app } from 'electron'; import { getDatabase } from '../database'; -import { posts, Post, NewPost } from '../database/schema'; +import { posts, Post, NewPost, postLinks } from '../database/schema'; import { taskManager, Task } from './TaskManager'; export interface PostData { @@ -289,6 +289,11 @@ export class PostEngine extends EventEmitter { }); } + // Update post links if content changed + if (data.content) { + await this.updatePostLinks(id, updated.content); + } + this.emit('postUpdated', updated); return updated; } @@ -635,6 +640,140 @@ export class PostEngine extends EventEmitter { await taskManager.runTask(task); } + + /** + * Extract internal post links from content (links to other posts in the blog) + */ + extractInternalLinks(content: string): { slug: string; text: string }[] { + const links: { slug: string; text: string }[] = []; + + // Match markdown links: [text](/posts/slug) or [text](/year/month/slug) + const markdownLinkRegex = /\[([^\]]+)\]\(\/(?:posts\/)?(?:\d{4}\/\d{2}\/)?([a-z0-9-]+)(?:\.html?)?\)/gi; + let match; + while ((match = markdownLinkRegex.exec(content)) !== null) { + links.push({ text: match[1], slug: match[2] }); + } + + // Match HTML links: text + const htmlLinkRegex = /]+href=["']\/(?:posts\/)?(?:\d{4}\/\d{2}\/)?([a-z0-9-]+)(?:\.html?)?["'][^>]*>([^<]+)<\/a>/gi; + while ((match = htmlLinkRegex.exec(content)) !== null) { + links.push({ text: match[2], slug: match[1] }); + } + + return links; + } + + /** + * Update post links in the database based on content analysis + */ + async updatePostLinks(postId: string, content: string): Promise { + const db = getDatabase().getLocal(); + const extractedLinks = this.extractInternalLinks(content); + + // Delete existing links from this post + await db.delete(postLinks).where(eq(postLinks.sourcePostId, postId)); + + if (extractedLinks.length === 0) return; + + // Get all posts to resolve slugs to IDs + const allPosts = await db.select({ id: posts.id, slug: posts.slug }) + .from(posts) + .where(eq(posts.projectId, this.currentProjectId)); + + const slugToId = new Map(allPosts.map(p => [p.slug, p.id])); + + // Insert new links + for (const link of extractedLinks) { + const targetId = slugToId.get(link.slug); + if (targetId && targetId !== postId) { + await db.insert(postLinks).values({ + id: uuidv4(), + sourcePostId: postId, + targetPostId: targetId, + linkText: link.text, + createdAt: new Date(), + }); + } + } + } + + /** + * Get posts that link TO the specified post ("linked by") + */ + async getLinkedBy(postId: string): Promise<{ id: string; title: string; slug: string }[]> { + const db = getDatabase().getLocal(); + + const links = await db + .select({ + sourcePostId: postLinks.sourcePostId, + linkText: postLinks.linkText, + }) + .from(postLinks) + .where(eq(postLinks.targetPostId, postId)); + + if (links.length === 0) return []; + + const sourceIds = links.map(l => l.sourcePostId); + const sourcePosts = await db + .select({ id: posts.id, title: posts.title, slug: posts.slug }) + .from(posts) + .where(eq(posts.projectId, this.currentProjectId)); + + return sourcePosts.filter(p => sourceIds.includes(p.id)); + } + + /** + * Get posts that the specified post links TO ("links to") + */ + async getLinksTo(postId: string): Promise<{ id: string; title: string; slug: string }[]> { + const db = getDatabase().getLocal(); + + const links = await db + .select({ + targetPostId: postLinks.targetPostId, + linkText: postLinks.linkText, + }) + .from(postLinks) + .where(eq(postLinks.sourcePostId, postId)); + + if (links.length === 0) return []; + + const targetIds = links.map(l => l.targetPostId); + const targetPosts = await db + .select({ id: posts.id, title: posts.title, slug: posts.slug }) + .from(posts) + .where(eq(posts.projectId, this.currentProjectId)); + + return targetPosts.filter(p => targetIds.includes(p.id)); + } + + /** + * Rebuild all post links from content analysis + */ + async rebuildAllPostLinks(): Promise { + const db = getDatabase().getLocal(); + + // Clear all existing links + await db.delete(postLinks); + + // Get all posts + const allPosts = await db + .select({ id: posts.id, filePath: posts.filePath }) + .from(posts) + .where(eq(posts.projectId, this.currentProjectId)); + + for (const post of allPosts) { + try { + const fileContent = await fs.readFile(post.filePath, 'utf-8'); + const { content } = matter(fileContent); + await this.updatePostLinks(post.id, content); + } catch (error) { + console.error(`Failed to update links for post ${post.id}:`, error); + } + } + + this.emit('postLinksRebuilt'); + } } // Singleton instance diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index b1e0586..77a2700 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -1,10 +1,12 @@ import { ipcMain, dialog, shell } from 'electron'; +import { eq } from 'drizzle-orm'; import { getPostEngine, PostData, PostFilter } from '../engine/PostEngine'; import { getMediaEngine, MediaData } from '../engine/MediaEngine'; import { getSyncEngine, SyncConfig, SyncDirection } from '../engine/SyncEngine'; import { getProjectEngine, ProjectData } from '../engine/ProjectEngine'; import { taskManager, TaskProgress } from '../engine/TaskManager'; import { getDatabase } from '../database'; +import { media } from '../database/schema'; export function registerIpcHandlers(): void { // ============ Project Handlers ============ @@ -126,6 +128,21 @@ export function registerIpcHandlers(): void { return engine.getPostsByYearMonth(); }); + ipcMain.handle('posts:getLinksTo', async (_, id: string) => { + const engine = getPostEngine(); + return engine.getLinksTo(id); + }); + + ipcMain.handle('posts:getLinkedBy', async (_, id: string) => { + const engine = getPostEngine(); + return engine.getLinkedBy(id); + }); + + ipcMain.handle('posts:rebuildLinks', async () => { + const engine = getPostEngine(); + return engine.rebuildAllPostLinks(); + }); + // ============ Media Handlers ============ ipcMain.handle('media:import', async (_, sourcePath: string, metadata?: Partial) => { @@ -187,6 +204,24 @@ export function registerIpcHandlers(): void { return engine.rebuildDatabaseFromFiles(); }); + ipcMain.handle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => { + const engine = getMediaEngine(); + return engine.getThumbnailDataUrl(id, size || 'small'); + }); + + ipcMain.handle('media:regenerateThumbnails', async (_, id: string) => { + const engine = getMediaEngine(); + const mediaItem = await engine.getMedia(id); + if (mediaItem && mediaItem.mimeType.startsWith('image/')) { + const db = getDatabase().getLocal(); + const dbMedia = await db.select().from(media).where(eq(media.id, id)).get(); + if (dbMedia) { + return engine.generateThumbnails(id, dbMedia.filePath); + } + } + return null; + }); + // ============ Sync Handlers ============ ipcMain.handle('sync:configure', async (_, config: SyncConfig) => { diff --git a/src/main/preload.ts b/src/main/preload.ts index 7663b7e..739f61c 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -30,6 +30,9 @@ contextBridge.exposeInMainWorld('electronAPI', { getTags: () => ipcRenderer.invoke('posts:getTags'), getCategories: () => ipcRenderer.invoke('posts:getCategories'), getByYearMonth: () => ipcRenderer.invoke('posts:getByYearMonth'), + getLinksTo: (id: string) => ipcRenderer.invoke('posts:getLinksTo', id), + getLinkedBy: (id: string) => ipcRenderer.invoke('posts:getLinkedBy', id), + rebuildLinks: () => ipcRenderer.invoke('posts:rebuildLinks'), }, // Media @@ -41,6 +44,8 @@ contextBridge.exposeInMainWorld('electronAPI', { get: (id: string) => ipcRenderer.invoke('media:get', id), getAll: () => ipcRenderer.invoke('media:getAll'), rebuildFromFiles: () => ipcRenderer.invoke('media:rebuildFromFiles'), + getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => ipcRenderer.invoke('media:getThumbnail', id, size), + regenerateThumbnails: (id: string) => ipcRenderer.invoke('media:regenerateThumbnails', id), }, // Sync @@ -107,6 +112,9 @@ export interface ElectronAPI { getTags: () => Promise; getCategories: () => Promise; getByYearMonth: () => Promise<{ year: number; month: number; count: number }[]>; + getLinksTo: (id: string) => Promise<{ id: string; title: string; slug: string }[]>; + getLinkedBy: (id: string) => Promise<{ id: string; title: string; slug: string }[]>; + rebuildLinks: () => Promise; }; media: { import: (sourcePath: string, metadata?: unknown) => Promise; diff --git a/src/renderer/components/CredentialsPanel/CredentialsPanel.css b/src/renderer/components/CredentialsPanel/CredentialsPanel.css new file mode 100644 index 0000000..8c6262a --- /dev/null +++ b/src/renderer/components/CredentialsPanel/CredentialsPanel.css @@ -0,0 +1,106 @@ +.credentials-panel { + display: flex; + flex-direction: column; + height: 100%; +} + +.credentials-tabs { + display: flex; + gap: 4px; + padding: 8px 12px; + border-bottom: 1px solid var(--vscode-panel-border); + background-color: var(--vscode-sideBar-background); +} + +.credentials-tabs button { + padding: 6px 12px; + background-color: transparent; + border: none; + border-radius: 4px; + color: var(--vscode-foreground); + font-size: 12px; + cursor: pointer; + transition: background-color 0.15s; +} + +.credentials-tabs button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.credentials-tabs button.active { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.credentials-content { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +.credentials-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.credentials-header h4 { + margin: 0 0 8px; + font-size: 16px; + font-weight: 600; + color: var(--vscode-foreground); +} + +.credentials-header p { + font-size: 12px; + margin: 0; + line-height: 1.5; +} + +.credentials-field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.credentials-field label { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + font-weight: 500; + color: var(--vscode-descriptionForeground); +} + +.credentials-field input { + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; +} + +.toggle-visibility { + background: none; + border: none; + padding: 2px 4px; + cursor: pointer; + font-size: 14px; + opacity: 0.7; + transition: opacity 0.15s; +} + +.toggle-visibility:hover { + opacity: 1; +} + +.credentials-actions { + display: flex; + gap: 8px; + margin-top: 8px; + padding-top: 16px; + border-top: 1px solid var(--vscode-panel-border); +} + +.credentials-actions button { + padding: 8px 16px; + font-size: 12px; +} diff --git a/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx b/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx new file mode 100644 index 0000000..6af55b9 --- /dev/null +++ b/src/renderer/components/CredentialsPanel/CredentialsPanel.tsx @@ -0,0 +1,283 @@ +import React, { useState, useEffect } from 'react'; +import { showToast } from '../Toast'; +import './CredentialsPanel.css'; + +interface Credentials { + tursoUrl: string; + tursoToken: string; + ftpHost?: string; + ftpUser?: string; + ftpPassword?: string; + sshHost?: string; + sshUser?: string; + sshKeyPath?: string; +} + +export const CredentialsPanel: React.FC = () => { + const [credentials, setCredentials] = useState({ + tursoUrl: '', + tursoToken: '', + ftpHost: '', + ftpUser: '', + ftpPassword: '', + sshHost: '', + sshUser: '', + sshKeyPath: '', + }); + const [activeTab, setActiveTab] = useState<'sync' | 'ftp' | 'ssh'>('sync'); + const [showTokens, setShowTokens] = useState(false); + + // Load saved credentials (in a real app, use secure storage) + useEffect(() => { + const loadCredentials = async () => { + try { + const savedCreds = localStorage.getItem('bds-credentials'); + if (savedCreds) { + setCredentials(JSON.parse(savedCreds)); + } + } catch (error) { + console.error('Failed to load credentials:', error); + } + }; + loadCredentials(); + }, []); + + const handleSave = async () => { + try { + // Save to localStorage (in production, use secure storage) + localStorage.setItem('bds-credentials', JSON.stringify(credentials)); + + // Configure sync if Turso credentials are set + if (credentials.tursoUrl && credentials.tursoToken) { + await window.electronAPI?.sync.configure({ + tursoUrl: credentials.tursoUrl, + tursoAuthToken: credentials.tursoToken, + autoSync: true, + syncInterval: 5, + }); + } + + showToast.success('Credentials saved'); + } catch (error) { + console.error('Failed to save credentials:', error); + showToast.error('Failed to save credentials'); + } + }; + + const handleClear = (type: 'sync' | 'ftp' | 'ssh') => { + const newCreds = { ...credentials }; + switch (type) { + case 'sync': + newCreds.tursoUrl = ''; + newCreds.tursoToken = ''; + break; + case 'ftp': + newCreds.ftpHost = ''; + newCreds.ftpUser = ''; + newCreds.ftpPassword = ''; + break; + case 'ssh': + newCreds.sshHost = ''; + newCreds.sshUser = ''; + newCreds.sshKeyPath = ''; + break; + } + setCredentials(newCreds); + }; + + const handleTestConnection = async (type: 'sync' | 'ftp' | 'ssh') => { + showToast.loading(`Testing ${type.toUpperCase()} connection...`); + + // Simulate connection test + await new Promise(resolve => setTimeout(resolve, 1500)); + + // In a real implementation, this would test the actual connection + if (type === 'sync' && credentials.tursoUrl && credentials.tursoToken) { + showToast.dismiss(); + showToast.success('Sync connection successful'); + } else { + showToast.dismiss(); + showToast.error('Connection failed - check credentials'); + } + }; + + return ( +
+
+ + + +
+ +
+ {activeTab === 'sync' && ( +
+
+

Turso/LibSQL Cloud Sync

+

+ Connect to Turso for cloud database synchronization. +

+
+ +
+ + setCredentials({ ...credentials, tursoUrl: e.target.value })} + /> +
+ +
+ + setCredentials({ ...credentials, tursoToken: e.target.value })} + /> +
+ +
+ + + +
+
+ )} + + {activeTab === 'ftp' && ( +
+
+

FTP Publishing

+

+ Configure FTP for publishing your blog to a web server. +

+
+ +
+ + setCredentials({ ...credentials, ftpHost: e.target.value })} + /> +
+ +
+ + setCredentials({ ...credentials, ftpUser: e.target.value })} + /> +
+ +
+ + setCredentials({ ...credentials, ftpPassword: e.target.value })} + /> +
+ +
+ + + +
+
+ )} + + {activeTab === 'ssh' && ( +
+
+

SSH Publishing

+

+ Configure SSH for secure publishing to your server. +

+
+ +
+ + setCredentials({ ...credentials, sshHost: e.target.value })} + /> +
+ +
+ + setCredentials({ ...credentials, sshUser: e.target.value })} + /> +
+ +
+ + setCredentials({ ...credentials, sshKeyPath: e.target.value })} + /> +
+ +
+ + + +
+
+ )} +
+
+ ); +}; + +export default CredentialsPanel; diff --git a/src/renderer/components/CredentialsPanel/index.ts b/src/renderer/components/CredentialsPanel/index.ts new file mode 100644 index 0000000..b1404d5 --- /dev/null +++ b/src/renderer/components/CredentialsPanel/index.ts @@ -0,0 +1 @@ +export { CredentialsPanel } from './CredentialsPanel'; diff --git a/src/renderer/components/Editor/Editor.css b/src/renderer/components/Editor/Editor.css index f42d12f..3fbb207 100644 --- a/src/renderer/components/Editor/Editor.css +++ b/src/renderer/components/Editor/Editor.css @@ -186,6 +186,22 @@ color: var(--vscode-button-foreground); } +.gallery-button { + padding: 4px 12px; + font-size: 12px; + border-radius: 4px; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: none; + cursor: pointer; + transition: background-color 0.15s; + margin-left: auto; +} + +.gallery-button:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + .editor-preview { flex: 1; background-color: var(--vscode-input-background); @@ -196,6 +212,63 @@ line-height: 1.6; } +.editor-preview .preview-content { + max-width: 800px; + margin: 0 auto; +} + +.editor-preview h1, +.editor-preview h2, +.editor-preview h3 { + margin-top: 1.5em; + margin-bottom: 0.5em; + color: var(--vscode-foreground); +} + +.editor-preview h1 { font-size: 2em; } +.editor-preview h2 { font-size: 1.5em; } +.editor-preview h3 { font-size: 1.25em; } + +.editor-preview code { + background-color: var(--vscode-textCodeBlock-background); + padding: 2px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); +} + +.editor-preview pre { + background-color: var(--vscode-textCodeBlock-background); + padding: 12px; + border-radius: 6px; + overflow-x: auto; +} + +.editor-preview pre code { + background: none; + padding: 0; +} + +.editor-preview blockquote { + border-left: 3px solid var(--vscode-textBlockQuote-border); + padding-left: 16px; + margin-left: 0; + color: var(--vscode-descriptionForeground); +} + +.editor-preview a { + color: var(--vscode-textLink-foreground); +} + +.editor-preview a:hover { + color: var(--vscode-textLink-activeForeground); +} + +.editor-preview img { + max-width: 100%; + border-radius: 6px; + cursor: pointer; +} + .editor-field-row { display: flex; gap: 12px; diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 28cdfc1..8570126 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -2,8 +2,43 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import MonacoEditor from '@monaco-editor/react'; import { useAppStore, PostData } from '../../store'; import { showToast } from '../Toast'; +import { WysiwygEditor } from '../WysiwygEditor'; +import { Lightbox, useMarkdownImages } from '../Lightbox'; +import { PostLinks } from '../PostLinks'; import './Editor.css'; +// Simple markdown to HTML converter for preview +const markdownToHtml = (markdown: string): string => { + return markdown + // Escape HTML + .replace(//g, '>') + // Headers + .replace(/^### (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^# (.*$)/gim, '

$1

') + // Bold + .replace(/\*\*(.*?)\*\*/gim, '$1') + // Italic + .replace(/\*(.*?)\*/gim, '$1') + // Images + .replace(/!\[(.*?)\]\((.*?)\)/gim, '$1') + // Links + .replace(/\[(.*?)\]\((.*?)\)/gim, '$1') + // Code blocks + .replace(/```([\s\S]*?)```/gim, '
$1
') + // Inline code + .replace(/`(.*?)`/gim, '$1') + // Blockquotes + .replace(/^\> (.*$)/gim, '
$1
') + // Horizontal rules + .replace(/^---$/gim, '
') + // Line breaks + .replace(/\n/g, '
'); +}; + +type EditorMode = 'markdown' | 'wysiwyg' | 'preview'; + interface PostEditorProps { post: PostData; } @@ -15,9 +50,14 @@ const PostEditor: React.FC = ({ post }) => { const [tags, setTags] = useState(post.tags.join(', ')); const [categories, setCategories] = useState(post.categories.join(', ')); const [isDirty, setIsDirty] = useState(false); - const [editorMode, setEditorMode] = useState<'markdown' | 'preview'>('markdown'); + const [editorMode, setEditorMode] = useState('wysiwyg'); + const [lightboxOpen, setLightboxOpen] = useState(false); + const [lightboxIndex, setLightboxIndex] = useState(0); const editorRef = useRef(null); + // Extract images from content for lightbox + const images = useMarkdownImages(content); + // Reset when post changes useEffect(() => { setTitle(post.title); @@ -200,27 +240,59 @@ const PostEditor: React.FC = ({ post }) => { /> + + useAppStore.getState().setSelectedPost(id)} + />
- +
+
+ {images.length > 0 && ( + + )}
- {editorMode === 'markdown' ? ( + + {editorMode === 'wysiwyg' && ( + + )} + + {editorMode === 'markdown' && ( = ({ post }) => { cursorBlinking: 'smooth', }} /> - ) : ( + )} + + {editorMode === 'preview' && (
- {/* Simple markdown preview - could be enhanced with a proper renderer */} -
{content}
+
)}
+ + {/* Lightbox for viewing images in content */} + setLightboxOpen(false)} + />
diff --git a/src/renderer/components/Lightbox/Lightbox.css b/src/renderer/components/Lightbox/Lightbox.css new file mode 100644 index 0000000..09764f4 --- /dev/null +++ b/src/renderer/components/Lightbox/Lightbox.css @@ -0,0 +1,253 @@ +/* Lightbox Overlay */ +.lightbox-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.9); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.lightbox-container { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* Image Container */ +.lightbox-image-container { + max-width: 90%; + max-height: 80%; + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-image-container.zoomed { + max-width: 100%; + max-height: 100%; + overflow: auto; + cursor: zoom-out; +} + +.lightbox-image { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 4px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + cursor: zoom-in; + animation: scaleIn 0.2s ease-out; +} + +.lightbox-image-container.zoomed .lightbox-image { + max-width: none; + max-height: none; + cursor: zoom-out; +} + +@keyframes scaleIn { + from { + transform: scale(0.9); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +/* Close Button */ +.lightbox-close { + position: absolute; + top: 16px; + right: 16px; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 50%; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + color: white; + cursor: pointer; + transition: background-color 0.2s; + z-index: 10; +} + +.lightbox-close:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Navigation Arrows */ +.lightbox-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 50%; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + color: white; + cursor: pointer; + transition: background-color 0.2s; + z-index: 10; +} + +.lightbox-nav:hover { + background: rgba(255, 255, 255, 0.2); +} + +.lightbox-prev { + left: 16px; +} + +.lightbox-next { + right: 16px; +} + +/* Footer */ +.lightbox-footer { + position: absolute; + bottom: 60px; + left: 50%; + transform: translateX(-50%); + text-align: center; + color: white; + max-width: 80%; +} + +.lightbox-caption { + font-size: 14px; + margin: 0 0 8px; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); +} + +.lightbox-counter { + font-size: 12px; + opacity: 0.7; +} + +/* Thumbnails */ +.lightbox-thumbnails { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 8px; + padding: 8px; + background: rgba(0, 0, 0, 0.5); + border-radius: 8px; +} + +.lightbox-thumbnail { + width: 48px; + height: 48px; + padding: 0; + border: 2px solid transparent; + border-radius: 4px; + overflow: hidden; + cursor: pointer; + transition: border-color 0.2s, opacity 0.2s; + opacity: 0.6; +} + +.lightbox-thumbnail:hover { + opacity: 0.9; +} + +.lightbox-thumbnail.active { + border-color: white; + opacity: 1; +} + +.lightbox-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Image Gallery Grid */ +.image-gallery { + display: grid; + gap: 8px; + border-radius: 8px; + overflow: hidden; +} + +.image-gallery.gallery-2 { + grid-template-columns: repeat(2, 1fr); +} + +.image-gallery.gallery-3 { + grid-template-columns: repeat(3, 1fr); +} + +.image-gallery.gallery-4 { + grid-template-columns: repeat(2, 1fr); +} + +.gallery-item { + aspect-ratio: 1; + overflow: hidden; + cursor: pointer; + transition: transform 0.2s; +} + +.gallery-item:hover { + transform: scale(1.02); +} + +.gallery-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Single Image */ +.single-image { + cursor: pointer; + display: inline-block; + max-width: 100%; +} + +.single-image img { + max-width: 100%; + border-radius: 6px; + transition: box-shadow 0.2s; +} + +.single-image:hover img { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +.image-caption { + font-size: 12px; + color: var(--vscode-descriptionForeground); + text-align: center; + margin-top: 8px; +} diff --git a/src/renderer/components/Lightbox/Lightbox.tsx b/src/renderer/components/Lightbox/Lightbox.tsx new file mode 100644 index 0000000..6adc74d --- /dev/null +++ b/src/renderer/components/Lightbox/Lightbox.tsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import './Lightbox.css'; + +interface LightboxImage { + src: string; + alt?: string; + caption?: string; +} + +interface LightboxProps { + images: LightboxImage[]; + initialIndex?: number; + isOpen: boolean; + onClose: () => void; +} + +export const Lightbox: React.FC = ({ + images, + initialIndex = 0, + isOpen, + onClose, +}) => { + const [currentIndex, setCurrentIndex] = useState(initialIndex); + const [isZoomed, setIsZoomed] = useState(false); + + useEffect(() => { + setCurrentIndex(initialIndex); + }, [initialIndex]); + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (!isOpen) return; + + switch (e.key) { + case 'Escape': + onClose(); + break; + case 'ArrowLeft': + setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1)); + break; + case 'ArrowRight': + setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0)); + break; + } + }, [isOpen, images.length, onClose]); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + if (!isOpen || images.length === 0) { + return null; + } + + const currentImage = images[currentIndex]; + const hasMultiple = images.length > 1; + + const handlePrev = () => { + setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1)); + }; + + const handleNext = () => { + setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0)); + }; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const toggleZoom = () => { + setIsZoomed(!isZoomed); + }; + + return ( +
+
+ {/* Close button */} + + + {/* Navigation arrows */} + {hasMultiple && ( + <> + + + + )} + + {/* Main image */} +
+ {currentImage.alt +
+ + {/* Caption and counter */} +
+ {currentImage.caption && ( +

{currentImage.caption}

+ )} + {hasMultiple && ( +
+ {currentIndex + 1} / {images.length} +
+ )} +
+ + {/* Thumbnail strip for galleries */} + {hasMultiple && images.length <= 10 && ( +
+ {images.map((image, index) => ( + + ))} +
+ )} +
+
+ ); +}; + +// Hook to extract images from markdown content +export function useMarkdownImages(content: string): LightboxImage[] { + const [images, setImages] = useState([]); + + useEffect(() => { + // Match markdown image syntax: ![alt](src) + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + const matches: LightboxImage[] = []; + let match; + + while ((match = imageRegex.exec(content)) !== null) { + matches.push({ + alt: match[1] || undefined, + src: match[2], + }); + } + + setImages(matches); + }, [content]); + + return images; +} + +// Component to render images with lightbox support +interface ImageGalleryProps { + images: LightboxImage[]; +} + +export const ImageGallery: React.FC = ({ images }) => { + const [lightboxOpen, setLightboxOpen] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); + + if (images.length === 0) { + return null; + } + + const openLightbox = (index: number) => { + setSelectedIndex(index); + setLightboxOpen(true); + }; + + if (images.length === 1) { + return ( + <> +
openLightbox(0)}> + {images[0].alt + {images[0].caption &&

{images[0].caption}

} +
+ setLightboxOpen(false)} + /> + + ); + } + + return ( + <> +
+ {images.map((image, index) => ( +
openLightbox(index)} + > + {image.alt +
+ ))} +
+ setLightboxOpen(false)} + /> + + ); +}; + +export default Lightbox; diff --git a/src/renderer/components/Lightbox/index.ts b/src/renderer/components/Lightbox/index.ts new file mode 100644 index 0000000..cbb86a9 --- /dev/null +++ b/src/renderer/components/Lightbox/index.ts @@ -0,0 +1 @@ +export { Lightbox, ImageGallery, useMarkdownImages } from './Lightbox'; diff --git a/src/renderer/components/PostLinks/PostLinks.css b/src/renderer/components/PostLinks/PostLinks.css new file mode 100644 index 0000000..08c5221 --- /dev/null +++ b/src/renderer/components/PostLinks/PostLinks.css @@ -0,0 +1,107 @@ +.post-links { + margin-top: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-sideBar-background); + overflow: hidden; +} + +.post-links-loading { + padding: 8px 12px; + color: var(--vscode-descriptionForeground); + font-size: 12px; +} + +.post-links-toggle { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 12px; + background: none; + border: none; + color: var(--vscode-foreground); + font-size: 12px; + cursor: pointer; + transition: background-color 0.15s; +} + +.post-links-toggle:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.post-links-icon { + font-size: 14px; +} + +.post-links-count { + flex: 1; + text-align: left; + color: var(--vscode-descriptionForeground); +} + +.post-links-chevron { + font-size: 10px; + color: var(--vscode-descriptionForeground); + transition: transform 0.15s; +} + +.post-links-content { + border-top: 1px solid var(--vscode-panel-border); + padding: 8px 0; +} + +.post-links-section { + padding: 4px 0; +} + +.post-links-section:not(:last-child) { + border-bottom: 1px solid var(--vscode-panel-border); + margin-bottom: 4px; + padding-bottom: 8px; +} + +.post-links-heading { + display: flex; + align-items: center; + gap: 6px; + margin: 0 0 4px 0; + padding: 0 12px; + font-size: 11px; + font-weight: 500; + color: var(--vscode-descriptionForeground); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.post-links-arrow { + font-size: 12px; +} + +.post-links-list { + list-style: none; + margin: 0; + padding: 0; +} + +.post-link-item { + display: block; + width: 100%; + padding: 4px 12px 4px 24px; + background: none; + border: none; + color: var(--vscode-textLink-foreground); + font-size: 12px; + text-align: left; + cursor: pointer; + transition: background-color 0.15s; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.post-link-item:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; +} diff --git a/src/renderer/components/PostLinks/PostLinks.tsx b/src/renderer/components/PostLinks/PostLinks.tsx new file mode 100644 index 0000000..c3136f8 --- /dev/null +++ b/src/renderer/components/PostLinks/PostLinks.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect } from 'react'; +import './PostLinks.css'; + +interface PostLinkInfo { + id: string; + title: string; + slug: string; +} + +interface PostLinksProps { + postId: string; + onPostClick?: (postId: string) => void; +} + +export const PostLinks: React.FC = ({ postId, onPostClick }) => { + const [linksTo, setLinksTo] = useState([]); + const [linkedBy, setLinkedBy] = useState([]); + const [loading, setLoading] = useState(true); + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + const loadLinks = async () => { + setLoading(true); + try { + const [to, by] = await Promise.all([ + window.electronAPI?.posts.getLinksTo(postId), + window.electronAPI?.posts.getLinkedBy(postId), + ]); + setLinksTo(to || []); + setLinkedBy(by || []); + } catch (error) { + console.error('Failed to load post links:', error); + } finally { + setLoading(false); + } + }; + + loadLinks(); + }, [postId]); + + const totalLinks = linksTo.length + linkedBy.length; + + if (loading) { + return ( +
+
Loading links...
+
+ ); + } + + if (totalLinks === 0) { + return null; + } + + return ( +
+ + + {expanded && ( +
+ {linksTo.length > 0 && ( +
+

+ + Links to ({linksTo.length}) +

+
    + {linksTo.map(link => ( +
  • + +
  • + ))} +
+
+ )} + + {linkedBy.length > 0 && ( +
+

+ + Linked by ({linkedBy.length}) +

+
    + {linkedBy.map(link => ( +
  • + +
  • + ))} +
+
+ )} +
+ )} +
+ ); +}; + +export default PostLinks; diff --git a/src/renderer/components/PostLinks/index.ts b/src/renderer/components/PostLinks/index.ts new file mode 100644 index 0000000..f9dd49e --- /dev/null +++ b/src/renderer/components/PostLinks/index.ts @@ -0,0 +1,2 @@ +export { PostLinks } from './PostLinks'; +export { default } from './PostLinks'; diff --git a/src/renderer/components/ResizablePanel/ResizablePanel.css b/src/renderer/components/ResizablePanel/ResizablePanel.css new file mode 100644 index 0000000..a4c965a --- /dev/null +++ b/src/renderer/components/ResizablePanel/ResizablePanel.css @@ -0,0 +1,73 @@ +.resizable-panel { + position: relative; + display: flex; + flex-shrink: 0; +} + +.resizable-panel.horizontal { + flex-direction: row; + height: 100%; +} + +.resizable-panel.vertical { + flex-direction: column; + width: 100%; +} + +.resizable-panel.resizing { + pointer-events: none; +} + +.resizable-panel.resizing .resizable-panel-content { + pointer-events: none; +} + +.resizable-panel-content { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* Resizer handle */ +.resizer { + flex-shrink: 0; + background: transparent; + transition: background-color 0.15s; + z-index: 10; +} + +.resizer.horizontal { + width: 4px; + cursor: col-resize; +} + +.resizer.vertical { + height: 4px; + cursor: row-resize; +} + +.resizer:hover, +.resizable-panel.resizing .resizer { + background-color: var(--vscode-sash-hoverBorder, #0078d4); +} + +/* Double-click to reset */ +.resizer::after { + content: ''; + position: absolute; +} + +.resizer.horizontal::after { + top: 0; + bottom: 0; + left: -2px; + right: -2px; +} + +.resizer.vertical::after { + left: 0; + right: 0; + top: -2px; + bottom: -2px; +} diff --git a/src/renderer/components/ResizablePanel/ResizablePanel.tsx b/src/renderer/components/ResizablePanel/ResizablePanel.tsx new file mode 100644 index 0000000..a9d8ad1 --- /dev/null +++ b/src/renderer/components/ResizablePanel/ResizablePanel.tsx @@ -0,0 +1,123 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import './ResizablePanel.css'; + +interface ResizablePanelProps { + children: React.ReactNode; + direction: 'horizontal' | 'vertical'; + initialSize: number; + minSize?: number; + maxSize?: number; + storageKey?: string; + className?: string; + resizerPosition?: 'start' | 'end'; +} + +export const ResizablePanel: React.FC = ({ + children, + direction, + initialSize, + minSize = 150, + maxSize = 600, + storageKey, + className = '', + resizerPosition = 'end', +}) => { + // Load saved size from localStorage + const getSavedSize = () => { + if (storageKey) { + const saved = localStorage.getItem(`bds-panel-${storageKey}`); + if (saved) { + const parsed = parseInt(saved, 10); + if (!isNaN(parsed) && parsed >= minSize && parsed <= maxSize) { + return parsed; + } + } + } + return initialSize; + }; + + const [size, setSize] = useState(getSavedSize); + const [isResizing, setIsResizing] = useState(false); + const panelRef = useRef(null); + const startPosRef = useRef(0); + const startSizeRef = useRef(0); + + // Save size to localStorage + useEffect(() => { + if (storageKey && !isResizing) { + localStorage.setItem(`bds-panel-${storageKey}`, size.toString()); + } + }, [size, storageKey, isResizing]); + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setIsResizing(true); + startPosRef.current = direction === 'horizontal' ? e.clientX : e.clientY; + startSizeRef.current = size; + }, [direction, size]); + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!isResizing) return; + + const currentPos = direction === 'horizontal' ? e.clientX : e.clientY; + let delta = currentPos - startPosRef.current; + + // Reverse delta if resizer is at start + if (resizerPosition === 'start') { + delta = -delta; + } + + const newSize = Math.max(minSize, Math.min(maxSize, startSizeRef.current + delta)); + setSize(newSize); + }, [isResizing, direction, minSize, maxSize, resizerPosition]); + + const handleMouseUp = useCallback(() => { + setIsResizing(false); + }, []); + + useEffect(() => { + if (isResizing) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize'; + document.body.style.userSelect = 'none'; + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + }, [isResizing, handleMouseMove, handleMouseUp, direction]); + + const style: React.CSSProperties = direction === 'horizontal' + ? { width: size } + : { height: size }; + + return ( +
+ {resizerPosition === 'start' && ( +
+ )} +
+ {children} +
+ {resizerPosition === 'end' && ( +
+ )} +
+ ); +}; + +export default ResizablePanel; diff --git a/src/renderer/components/ResizablePanel/index.ts b/src/renderer/components/ResizablePanel/index.ts new file mode 100644 index 0000000..15f2dd4 --- /dev/null +++ b/src/renderer/components/ResizablePanel/index.ts @@ -0,0 +1 @@ +export { ResizablePanel } from './ResizablePanel'; diff --git a/src/renderer/components/Sidebar/Sidebar.css b/src/renderer/components/Sidebar/Sidebar.css index 5805a86..d92f20f 100644 --- a/src/renderer/components/Sidebar/Sidebar.css +++ b/src/renderer/components/Sidebar/Sidebar.css @@ -67,7 +67,10 @@ } .sidebar-item { - padding: 6px 12px 6px 24px; + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 12px 6px 12px; cursor: pointer; border-left: 2px solid transparent; } @@ -81,6 +84,39 @@ border-left-color: var(--vscode-focusBorder); } +.post-type-icon { + font-size: 14px; + line-height: 1.4; + flex-shrink: 0; + opacity: 0.85; +} + +.sidebar-item-content { + flex: 1; + min-width: 0; +} + +/* Post type specific styling */ +.sidebar-item.post-type-picture { + background: linear-gradient(90deg, rgba(139, 92, 246, 0.05) 0%, transparent 100%); +} + +.sidebar-item.post-type-aside { + background: linear-gradient(90deg, rgba(245, 158, 11, 0.05) 0%, transparent 100%); +} + +.sidebar-item.post-type-quote { + background: linear-gradient(90deg, rgba(34, 197, 94, 0.05) 0%, transparent 100%); +} + +.sidebar-item.post-type-link { + background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, transparent 100%); +} + +.sidebar-item.post-type-video { + background: linear-gradient(90deg, rgba(239, 68, 68, 0.05) 0%, transparent 100%); +} + .sidebar-item-title { font-size: 13px; color: var(--vscode-sideBar-foreground); diff --git a/src/renderer/components/Sidebar/Sidebar.tsx b/src/renderer/components/Sidebar/Sidebar.tsx index 1821c72..6fd101d 100644 --- a/src/renderer/components/Sidebar/Sidebar.tsx +++ b/src/renderer/components/Sidebar/Sidebar.tsx @@ -14,6 +14,28 @@ const formatFileSize = (bytes: number) => { return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; }; +// Get post type icon based on categories +const getPostTypeIcon = (categories: string[]): { icon: string; type: string } => { + const lowerCategories = categories.map(c => c.toLowerCase()); + if (lowerCategories.includes('picture') || lowerCategories.includes('photo') || lowerCategories.includes('image')) { + return { icon: '🖼️', type: 'picture' }; + } + if (lowerCategories.includes('aside') || lowerCategories.includes('note') || lowerCategories.includes('quick')) { + return { icon: '📝', type: 'aside' }; + } + if (lowerCategories.includes('link') || lowerCategories.includes('bookmark')) { + return { icon: '🔗', type: 'link' }; + } + if (lowerCategories.includes('video')) { + return { icon: '🎬', type: 'video' }; + } + if (lowerCategories.includes('quote')) { + return { icon: '💬', type: 'quote' }; + } + // Default to article + return { icon: '📄', type: 'article' }; +}; + const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; interface CalendarViewProps { @@ -399,16 +421,22 @@ const PostsList: React.FC = () => { Drafts ({groupedPosts.draft.length})
- {groupedPosts.draft.map(post => ( -
setSelectedPost(post.id)} - > -
{post.title}
-
{formatDate(post.updatedAt)}
-
- ))} + {groupedPosts.draft.map(post => { + const postType = getPostTypeIcon(post.categories); + return ( +
setSelectedPost(post.id)} + > + {postType.icon} +
+
{post.title}
+
{formatDate(post.updatedAt)}
+
+
+ ); + })}
)} @@ -420,16 +448,22 @@ const PostsList: React.FC = () => { Published ({groupedPosts.published.length})
- {groupedPosts.published.map(post => ( -
setSelectedPost(post.id)} - > -
{post.title}
-
{formatDate(post.publishedAt || post.updatedAt)}
-
- ))} + {groupedPosts.published.map(post => { + const postType = getPostTypeIcon(post.categories); + return ( +
setSelectedPost(post.id)} + > + {postType.icon} +
+
{post.title}
+
{formatDate(post.publishedAt || post.updatedAt)}
+
+
+ ); + })}
)} @@ -441,16 +475,22 @@ const PostsList: React.FC = () => { Archived ({groupedPosts.archived.length})
- {groupedPosts.archived.map(post => ( -
setSelectedPost(post.id)} - > -
{post.title}
-
{formatDate(post.updatedAt)}
-
- ))} + {groupedPosts.archived.map(post => { + const postType = getPostTypeIcon(post.categories); + return ( +
setSelectedPost(post.id)} + > + {postType.icon} +
+
{post.title}
+
{formatDate(post.updatedAt)}
+
+
+ ); + })}
)} diff --git a/src/renderer/components/TaskPopup/TaskPopup.css b/src/renderer/components/TaskPopup/TaskPopup.css new file mode 100644 index 0000000..6a72deb --- /dev/null +++ b/src/renderer/components/TaskPopup/TaskPopup.css @@ -0,0 +1,230 @@ +.task-popup-wrapper { + position: relative; +} + +.task-popup-trigger { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: transparent; + border: none; + border-radius: 4px; + color: var(--vscode-statusBar-foreground); + font-size: 12px; + cursor: pointer; + transition: background-color 0.15s; +} + +.task-popup-trigger:hover { + background-color: var(--vscode-statusBarItem-hoverBackground); +} + +.task-popup-trigger.active { + background-color: var(--vscode-statusBarItem-activeBackground); +} + +.task-popup { + position: absolute; + bottom: 100%; + left: 0; + margin-bottom: 8px; + width: 360px; + max-height: 400px; + background-color: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-editorWidget-border); + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + overflow: hidden; + animation: slideUp 0.15s ease-out; + z-index: 100; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.task-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--vscode-panel-border); +} + +.task-popup-header h4 { + margin: 0; + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); +} + +.task-popup-header .text-button { + background: none; + border: none; + color: var(--vscode-textLink-foreground); + font-size: 11px; + cursor: pointer; + padding: 0; +} + +.task-popup-header .text-button:hover { + text-decoration: underline; +} + +.task-section { + padding: 8px 0; +} + +.task-section:not(:last-child) { + border-bottom: 1px solid var(--vscode-panel-border); +} + +.task-section-title { + padding: 4px 16px 8px; + font-size: 11px; + font-weight: 600; + color: var(--vscode-descriptionForeground); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.task-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + gap: 12px; +} + +.task-item:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.task-item-info { + display: flex; + align-items: flex-start; + gap: 10px; + flex: 1; + min-width: 0; +} + +.task-item-details { + flex: 1; + min-width: 0; +} + +.task-item-message { + font-size: 12px; + color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-item-error { + font-size: 11px; + color: var(--vscode-notificationsErrorIcon-foreground); + margin-top: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-progress-bar { + height: 4px; + background-color: var(--vscode-progressBar-background); + border-radius: 2px; + margin-top: 6px; + overflow: hidden; +} + +.task-progress-fill { + height: 100%; + background-color: var(--vscode-button-background); + border-radius: 2px; + transition: width 0.3s ease-out; +} + +.task-cancel { + background: none; + border: none; + color: var(--vscode-descriptionForeground); + font-size: 12px; + cursor: pointer; + padding: 4px; + border-radius: 4px; + opacity: 0; + transition: opacity 0.15s, background-color 0.15s; +} + +.task-item:hover .task-cancel { + opacity: 1; +} + +.task-cancel:hover { + background-color: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-notificationsErrorIcon-foreground); +} + +.task-time { + font-size: 11px; + color: var(--vscode-descriptionForeground); + flex-shrink: 0; +} + +.task-icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 12px; + flex-shrink: 0; +} + +.task-icon.success { + color: var(--vscode-testing-iconPassed); +} + +.task-icon.error { + color: var(--vscode-notificationsErrorIcon-foreground); +} + +.task-icon.pending { + color: var(--vscode-descriptionForeground); +} + +.task-icon.cancelled { + color: var(--vscode-descriptionForeground); +} + +.task-spinner { + width: 14px; + height: 14px; + border: 2px solid var(--vscode-panel-border); + border-top-color: var(--vscode-button-background); + border-radius: 50%; + animation: spin 1s linear infinite; + flex-shrink: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.task-empty { + padding: 24px 16px; + text-align: center; + font-size: 12px; + color: var(--vscode-descriptionForeground); +} diff --git a/src/renderer/components/TaskPopup/TaskPopup.tsx b/src/renderer/components/TaskPopup/TaskPopup.tsx new file mode 100644 index 0000000..ff2a16b --- /dev/null +++ b/src/renderer/components/TaskPopup/TaskPopup.tsx @@ -0,0 +1,190 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useAppStore } from '../../store'; +import './TaskPopup.css'; + +export const TaskPopup: React.FC = () => { + const { tasks } = useAppStore(); + const [isOpen, setIsOpen] = useState(false); + const popupRef = useRef(null); + + const runningTasks = tasks.filter(t => t.status === 'running'); + const pendingTasks = tasks.filter(t => t.status === 'pending'); + const recentTasks = tasks + .filter(t => t.status === 'completed' || t.status === 'failed') + .sort((a, b) => { + const aTime = a.endTime ? new Date(a.endTime).getTime() : 0; + const bTime = b.endTime ? new Date(b.endTime).getTime() : 0; + return bTime - aTime; + }) + .slice(0, 5); + + const hasActiveTasks = runningTasks.length > 0 || pendingTasks.length > 0; + + // Close popup when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (popupRef.current && !popupRef.current.contains(e.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + const handleCancel = async (taskId: string) => { + await window.electronAPI?.tasks.cancel(taskId); + }; + + const handleClearCompleted = async () => { + await window.electronAPI?.tasks.clearCompleted(); + }; + + const formatTime = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'running': + return ; + case 'completed': + return ; + case 'failed': + return ; + case 'pending': + return ; + case 'cancelled': + return ; + default: + return null; + } + }; + + if (!hasActiveTasks && recentTasks.length === 0) { + return null; + } + + return ( +
+ + + {isOpen && ( +
+
+

Background Tasks

+ {recentTasks.length > 0 && ( + + )} +
+ + {runningTasks.length > 0 && ( +
+
Running
+ {runningTasks.map(task => ( +
+
+ {getStatusIcon(task.status)} +
+
{task.message}
+
+
+
+
+
+ +
+ ))} +
+ )} + + {pendingTasks.length > 0 && ( +
+
Pending
+ {pendingTasks.map(task => ( +
+
+ {getStatusIcon(task.status)} +
+
{task.message}
+
+
+ +
+ ))} +
+ )} + + {recentTasks.length > 0 && ( +
+
Recent
+ {recentTasks.map(task => ( +
+
+ {getStatusIcon(task.status)} +
+
{task.message}
+ {task.error && ( +
{task.error}
+ )} +
+
+ {task.endTime && ( + {formatTime(task.endTime)} + )} +
+ ))} +
+ )} + + {runningTasks.length === 0 && pendingTasks.length === 0 && recentTasks.length === 0 && ( +
No active tasks
+ )} +
+ )} +
+ ); +}; + +export default TaskPopup; diff --git a/src/renderer/components/TaskPopup/index.ts b/src/renderer/components/TaskPopup/index.ts new file mode 100644 index 0000000..ceee7d9 --- /dev/null +++ b/src/renderer/components/TaskPopup/index.ts @@ -0,0 +1 @@ +export { TaskPopup } from './TaskPopup'; diff --git a/src/renderer/components/WysiwygEditor/WysiwygEditor.css b/src/renderer/components/WysiwygEditor/WysiwygEditor.css new file mode 100644 index 0000000..edf8872 --- /dev/null +++ b/src/renderer/components/WysiwygEditor/WysiwygEditor.css @@ -0,0 +1,296 @@ +.wysiwyg-editor { + display: flex; + flex-direction: column; + flex: 1; + background-color: var(--vscode-input-background); + border-radius: 4px; + overflow: hidden; +} + +/* Toolbar */ +.wysiwyg-toolbar { + display: flex; + align-items: center; + padding: 8px 12px; + background-color: var(--vscode-sideBar-background); + border-bottom: 1px solid var(--vscode-panel-border); + flex-wrap: wrap; + gap: 4px; +} + +.toolbar-group { + display: flex; + align-items: center; + gap: 2px; +} + +.toolbar-divider { + width: 1px; + height: 20px; + background-color: var(--vscode-panel-border); + margin: 0 8px; +} + +.wysiwyg-toolbar button { + display: flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 28px; + padding: 4px 8px; + background-color: transparent; + border: none; + border-radius: 4px; + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; + transition: background-color 0.15s; +} + +.wysiwyg-toolbar button:hover:not(:disabled) { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.wysiwyg-toolbar button.is-active { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.wysiwyg-toolbar button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* Editor Content */ +.wysiwyg-content { + flex: 1; + padding: 16px; + overflow-y: auto; +} + +.wysiwyg-content .ProseMirror { + outline: none; + min-height: 100%; + color: var(--vscode-editor-foreground); + font-size: 15px; + line-height: 1.7; +} + +.wysiwyg-content .ProseMirror > * + * { + margin-top: 0.75em; +} + +/* Placeholder */ +.wysiwyg-content .ProseMirror p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + float: left; + color: var(--vscode-descriptionForeground); + pointer-events: none; + height: 0; +} + +/* Typography */ +.wysiwyg-content h1 { + font-size: 2em; + font-weight: 600; + margin-top: 1em; + margin-bottom: 0.5em; + color: var(--vscode-editor-foreground); +} + +.wysiwyg-content h2 { + font-size: 1.5em; + font-weight: 600; + margin-top: 1em; + margin-bottom: 0.5em; + color: var(--vscode-editor-foreground); +} + +.wysiwyg-content h3 { + font-size: 1.25em; + font-weight: 600; + margin-top: 1em; + margin-bottom: 0.5em; + color: var(--vscode-editor-foreground); +} + +.wysiwyg-content p { + margin: 0.5em 0; +} + +.wysiwyg-content strong { + font-weight: 600; +} + +.wysiwyg-content em { + font-style: italic; +} + +.wysiwyg-content u { + text-decoration: underline; +} + +.wysiwyg-content s { + text-decoration: line-through; +} + +/* Links */ +.wysiwyg-content a, +.wysiwyg-content .editor-link { + color: var(--vscode-textLink-foreground); + text-decoration: underline; + cursor: pointer; +} + +.wysiwyg-content a:hover { + color: var(--vscode-textLink-activeForeground); +} + +/* Lists */ +.wysiwyg-content ul, +.wysiwyg-content ol { + padding-left: 1.5em; + margin: 0.5em 0; +} + +.wysiwyg-content li { + margin: 0.25em 0; +} + +.wysiwyg-content ul { + list-style-type: disc; +} + +.wysiwyg-content ol { + list-style-type: decimal; +} + +/* Blockquote */ +.wysiwyg-content blockquote { + border-left: 3px solid var(--vscode-textBlockQuote-border); + padding-left: 1em; + margin: 1em 0; + color: var(--vscode-textBlockQuote-foreground); + font-style: italic; +} + +/* Code */ +.wysiwyg-content code { + background-color: var(--vscode-textCodeBlock-background); + padding: 2px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + font-size: 0.9em; +} + +.wysiwyg-content pre { + background-color: var(--vscode-textCodeBlock-background); + padding: 12px 16px; + border-radius: 6px; + overflow-x: auto; + margin: 1em 0; +} + +.wysiwyg-content pre code { + padding: 0; + background: none; + font-size: 0.9em; + line-height: 1.5; +} + +/* Images */ +.wysiwyg-content img, +.wysiwyg-content .editor-image { + max-width: 100%; + height: auto; + border-radius: 6px; + margin: 1em 0; + cursor: pointer; + transition: box-shadow 0.2s; +} + +.wysiwyg-content img:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* Horizontal Rule */ +.wysiwyg-content hr { + border: none; + border-top: 1px solid var(--vscode-panel-border); + margin: 2em 0; +} + +/* Bubble Menu */ +.bubble-menu { + display: flex; + background-color: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-editorWidget-border); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + padding: 4px; + gap: 2px; +} + +.bubble-menu button { + display: flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 28px; + padding: 4px 8px; + background-color: transparent; + border: none; + border-radius: 4px; + color: var(--vscode-foreground); + font-size: 13px; + cursor: pointer; +} + +.bubble-menu button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.bubble-menu button.is-active { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.bubble-menu .divider { + width: 1px; + background-color: var(--vscode-panel-border); + margin: 2px 4px; +} + +/* Floating Menu */ +.floating-menu { + display: flex; + background-color: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-editorWidget-border); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + padding: 4px; + gap: 2px; +} + +.floating-menu button { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + background-color: transparent; + border: none; + border-radius: 4px; + color: var(--vscode-foreground); + font-size: 12px; + cursor: pointer; + white-space: nowrap; +} + +.floating-menu button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.floating-menu button.is-active { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} diff --git a/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx b/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx new file mode 100644 index 0000000..ec94629 --- /dev/null +++ b/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx @@ -0,0 +1,379 @@ +import React, { useEffect, useCallback } from 'react'; +import { useEditor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Link from '@tiptap/extension-link'; +import Image from '@tiptap/extension-image'; +import Underline from '@tiptap/extension-underline'; +import Placeholder from '@tiptap/extension-placeholder'; +import TurndownService from 'turndown'; +import './WysiwygEditor.css'; + +// Convert HTML to Markdown +const turndownService = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', +}); + +// Add custom rules for turndown +turndownService.addRule('strikethrough', { + filter: ['del', 's', 'strike'], + replacement: (content) => `~~${content}~~`, +}); + +interface WysiwygEditorProps { + content: string; + onChange: (markdown: string) => void; + placeholder?: string; +} + +// Simple markdown to HTML converter for TipTap +function markdownToHtml(markdown: string): string { + // Simple markdown parser - for production use a proper library + let html = markdown + // Headers + .replace(/^### (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^# (.*$)/gim, '

$1

') + // Bold + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/__(.+?)__/g, '$1') + // Italic + .replace(/\*(.+?)\*/g, '$1') + .replace(/_(.+?)_/g, '$1') + // Strikethrough + .replace(/~~(.+?)~~/g, '$1') + // Code blocks + .replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
') + // Inline code + .replace(/`([^`]+)`/g, '$1') + // Links + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + // Images + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + // Blockquotes + .replace(/^> (.*$)/gim, '
$1
') + // Unordered lists + .replace(/^\s*[-*+] (.+)$/gim, '
  • $1
  • ') + // Ordered lists + .replace(/^\d+\. (.+)$/gim, '
  • $1
  • ') + // Horizontal rule + .replace(/^(?:---+|___+|\*\*\*+)$/gim, '
    ') + // Paragraphs (double newlines) + .replace(/\n\n/g, '

    ') + // Single line breaks + .replace(/\n/g, '
    '); + + // Wrap in paragraphs if not starting with a block element + if (!html.startsWith('<')) { + html = '

    ' + html + '

    '; + } + + // Fix consecutive blockquotes + html = html.replace(/<\/blockquote>\s*
    /g, '
    '); + + // Wrap list items in ul/ol + html = html.replace(/(
  • .*<\/li>)+/g, (match) => { + // Check if it's ordered (starts with number) or unordered + return '
      ' + match + '
    '; + }); + + return html; +} + +export const WysiwygEditor: React.FC = ({ + content, + onChange, + placeholder = 'Start writing your content...', +}) => { + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + heading: { + levels: [1, 2, 3, 4, 5, 6], + }, + }), + Link.configure({ + openOnClick: false, + HTMLAttributes: { + class: 'editor-link', + }, + }), + Image.configure({ + HTMLAttributes: { + class: 'editor-image', + }, + }), + Underline, + Placeholder.configure({ + placeholder, + }), + ], + content: markdownToHtml(content), + onUpdate: ({ editor }) => { + const html = editor.getHTML(); + const markdown = turndownService.turndown(html); + onChange(markdown); + }, + }); + + useEffect(() => { + if (editor && content) { + const currentHtml = editor.getHTML(); + const newHtml = markdownToHtml(content); + // Only update if content is significantly different + if (turndownService.turndown(currentHtml) !== content) { + editor.commands.setContent(newHtml); + } + } + }, [content]); + + const addImage = useCallback(() => { + const url = window.prompt('Enter image URL:'); + if (url && editor) { + editor.chain().focus().setImage({ src: url }).run(); + } + }, [editor]); + + const setLink = useCallback(() => { + if (!editor) return; + + const previousUrl = editor.getAttributes('link').href; + const url = window.prompt('Enter URL:', previousUrl); + + if (url === null) return; + + if (url === '') { + editor.chain().focus().extendMarkRange('link').unsetLink().run(); + return; + } + + editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); + }, [editor]); + + if (!editor) { + return null; + } + + return ( +
    + {/* Bubble menu appears when text is selected */} + {editor && ( + + + + + +
    + + + + )} + + {/* Floating menu appears on empty lines */} + {editor && ( + + + + + + + + + )} + + {/* Toolbar */} +
    +
    + + + +
    + +
    + +
    + + + + +
    + +
    + +
    + + + + +
    + +
    + +
    + + + +
    + +
    + +
    + + +
    +
    + + {/* Editor Content */} + +
    + ); +}; + +export default WysiwygEditor; diff --git a/src/renderer/components/WysiwygEditor/index.ts b/src/renderer/components/WysiwygEditor/index.ts new file mode 100644 index 0000000..9d50854 --- /dev/null +++ b/src/renderer/components/WysiwygEditor/index.ts @@ -0,0 +1 @@ +export { WysiwygEditor } from './WysiwygEditor'; diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index 814f36a..374edae 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -5,3 +5,9 @@ export { StatusBar } from './StatusBar'; export { Panel } from './Panel'; export { ToastContainer, toast, showToast, type ToastType } from './Toast'; export { ProjectSelector } from './ProjectSelector'; +export { WysiwygEditor } from './WysiwygEditor'; +export { Lightbox, ImageGallery, useMarkdownImages } from './Lightbox'; +export { TaskPopup } from './TaskPopup'; +export { ResizablePanel } from './ResizablePanel'; +export { CredentialsPanel } from './CredentialsPanel'; +export { PostLinks } from './PostLinks'; diff --git a/src/renderer/store/appStore.ts b/src/renderer/store/appStore.ts index 661e465..f54f32e 100644 --- a/src/renderer/store/appStore.ts +++ b/src/renderer/store/appStore.ts @@ -1,4 +1,8 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +// Storage key for persisted state +const STORAGE_KEY = 'bds-app-state'; // Types export interface ProjectData { @@ -113,84 +117,99 @@ interface AppState { setError: (error: string | null) => void; } -export const useAppStore = create((set) => ({ - // Initial Project State - projects: [], - activeProject: null, - - // Initial UI State - activeView: 'posts', - sidebarVisible: true, - panelVisible: false, - selectedPostId: null, - selectedMediaId: null, - - // Initial Data - posts: [], - media: [], - tasks: [], - - // Initial Sync State - syncStatus: 'idle', - syncConfigured: false, - pendingChanges: { posts: 0, media: 0 }, - - // Initial Loading State - isLoading: false, - error: null, - - // Project Actions - setProjects: (projects) => set({ projects }), - setActiveProject: (activeProject) => set({ activeProject }), - addProject: (project) => set((state) => ({ projects: [...state.projects, project] })), - updateProject: (id, updatedProject) => set((state) => ({ - projects: state.projects.map((p) => (p.id === id ? { ...p, ...updatedProject } : p)), - })), - removeProject: (id) => set((state) => ({ - projects: state.projects.filter((p) => p.id !== id), - })), - - // UI Actions - setActiveView: (view) => set({ activeView: view }), - toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })), - togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })), - setSelectedPost: (id) => set({ selectedPostId: id }), - setSelectedMedia: (id) => set({ selectedMediaId: id }), - - // Post Actions - setPosts: (posts) => set({ posts }), - addPost: (post) => set((state) => ({ posts: [...state.posts, post] })), - updatePost: (id, updatedPost) => set((state) => ({ - posts: state.posts.map((p) => (p.id === id ? { ...p, ...updatedPost } : p)), - })), - removePost: (id) => set((state) => ({ - posts: state.posts.filter((p) => p.id !== id), - selectedPostId: state.selectedPostId === id ? null : state.selectedPostId, - })), - - // Media Actions - setMedia: (media) => set({ media }), - addMedia: (media) => set((state) => ({ media: [...state.media, media] })), - updateMedia: (id, updatedMedia) => set((state) => ({ - media: state.media.map((m) => (m.id === id ? { ...m, ...updatedMedia } : m)), - })), - removeMedia: (id) => set((state) => ({ - media: state.media.filter((m) => m.id !== id), - selectedMediaId: state.selectedMediaId === id ? null : state.selectedMediaId, - })), - - // Task Actions - setTasks: (tasks) => set({ tasks }), - updateTask: (taskId, task) => set((state) => ({ - tasks: state.tasks.map((t) => (t.taskId === taskId ? { ...t, ...task } : t)), - })), - - // Sync Actions - setSyncStatus: (syncStatus) => set({ syncStatus }), - setSyncConfigured: (syncConfigured) => set({ syncConfigured }), - setPendingChanges: (pendingChanges) => set({ pendingChanges }), - - // Loading Actions - setLoading: (isLoading) => set({ isLoading }), - setError: (error) => set({ error }), -})); +export const useAppStore = create()( + persist( + (set) => ({ + // Initial Project State + projects: [], + activeProject: null, + + // Initial UI State + activeView: 'posts', + sidebarVisible: true, + panelVisible: false, + selectedPostId: null, + selectedMediaId: null, + + // Initial Data + posts: [], + media: [], + tasks: [], + + // Initial Sync State + syncStatus: 'idle', + syncConfigured: false, + pendingChanges: { posts: 0, media: 0 }, + + // Initial Loading State + isLoading: false, + error: null, + + // Project Actions + setProjects: (projects) => set({ projects }), + setActiveProject: (activeProject) => set({ activeProject }), + addProject: (project) => set((state) => ({ projects: [...state.projects, project] })), + updateProject: (id, updatedProject) => set((state) => ({ + projects: state.projects.map((p) => (p.id === id ? { ...p, ...updatedProject } : p)), + })), + removeProject: (id) => set((state) => ({ + projects: state.projects.filter((p) => p.id !== id), + })), + + // UI Actions + setActiveView: (view) => set({ activeView: view }), + toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })), + togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })), + setSelectedPost: (id) => set({ selectedPostId: id }), + setSelectedMedia: (id) => set({ selectedMediaId: id }), + + // Post Actions + setPosts: (posts) => set({ posts }), + addPost: (post) => set((state) => ({ posts: [...state.posts, post] })), + updatePost: (id, updatedPost) => set((state) => ({ + posts: state.posts.map((p) => (p.id === id ? { ...p, ...updatedPost } : p)), + })), + removePost: (id) => set((state) => ({ + posts: state.posts.filter((p) => p.id !== id), + selectedPostId: state.selectedPostId === id ? null : state.selectedPostId, + })), + + // Media Actions + setMedia: (media) => set({ media }), + addMedia: (media) => set((state) => ({ media: [...state.media, media] })), + updateMedia: (id, updatedMedia) => set((state) => ({ + media: state.media.map((m) => (m.id === id ? { ...m, ...updatedMedia } : m)), + })), + removeMedia: (id) => set((state) => ({ + media: state.media.filter((m) => m.id !== id), + selectedMediaId: state.selectedMediaId === id ? null : state.selectedMediaId, + })), + + // Task Actions + setTasks: (tasks) => set({ tasks }), + updateTask: (taskId, task) => set((state) => ({ + tasks: state.tasks.map((t) => (t.taskId === taskId ? { ...t, ...task } : t)), + })), + + // Sync Actions + setSyncStatus: (syncStatus) => set({ syncStatus }), + setSyncConfigured: (syncConfigured) => set({ syncConfigured }), + setPendingChanges: (pendingChanges) => set({ pendingChanges }), + + // Loading Actions + setLoading: (isLoading) => set({ isLoading }), + setError: (error) => set({ error }), + }), + { + name: STORAGE_KEY, + // Only persist UI state, not data (which is loaded from backend) + partialize: (state) => ({ + activeView: state.activeView, + sidebarVisible: state.sidebarVisible, + panelVisible: state.panelVisible, + selectedPostId: state.selectedPostId, + selectedMediaId: state.selectedMediaId, + }), + } + ) +);