WIP: blogging. you have a markdown editor with linking capabilities, along with the ability to view, edit, and create other blogs. #1

Draft
pure_sagacity wants to merge 5 commits from blogging into main
19 changed files with 1317 additions and 18 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"cSpell.words": ["tauri"]
"cSpell.words": ["Homelab", "Insertable", "tauri"]
}
+9
View File
@@ -127,4 +127,13 @@
html {
@apply font-sans;
}
h1 {
@apply text-4xl font-bold;
}
h2 {
@apply text-3xl font-bold;
}
h3 {
@apply text-2xl font-bold;
}
}
+173
View File
@@ -0,0 +1,173 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import Link from "next/link";
import { HomelabApp, Blog } from "@/types";
export default function WritePage() {
const params = useSearchParams();
const blogId = params.get("id") || null;
if (!blogId) {
return (
<div className="flex items-center justify-center min-h-100">
<span className="text-sm text-zinc-400 animate-pulse">
No document ID provided.
</span>
</div>
);
}
const [blog, setBlog] = useState<Blog | null>(null);
const [linkedApps, setLinkedApps] = useState<HomelabApp[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!blogId) return;
const fetchBlogAndApps = async () => {
try {
setLoading(true);
const currentBlog = (await invoke("get_blog", { id: blogId })) as
| Blog
| undefined;
if (!currentBlog) {
setError("Blog post not found.");
return;
}
setBlog(currentBlog);
// 2. Fetch the metadata for any bound/linked apps if they exist
if (currentBlog.reference && currentBlog.reference.length > 0) {
const apps = (await invoke("get_apps_by_id", {
ids: currentBlog.reference,
})) as HomelabApp[];
setLinkedApps(apps);
}
} catch (err) {
console.error("Failed to load blog page data:", err);
setError("An error occurred while loading the document.");
} finally {
setLoading(false);
}
};
fetchBlogAndApps();
}, [blogId]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-100">
<span className="text-sm text-zinc-400 animate-pulse">
Loading document...
</span>
</div>
);
}
if (error || !blog) {
return (
<div className="p-6 text-center max-w-xl mx-auto mt-12 border border-red-200 bg-red-50/50 rounded-lg">
<p className="text-sm text-red-600 font-medium">
{error || "Something went wrong."}
</p>
<Link
href="/"
className="inline-block mt-4 text-xs font-semibold text-zinc-600 hover:text-zinc-800 underline"
>
Go Back Home
</Link>
</div>
);
}
return (
<div className="max-w-6xl mx-auto px-4 py-8 w-full flex flex-col gap-6">
{/* Top Header Controls */}
<div className="flex justify-between items-center pb-4 border-b border-zinc-100 gap-4">
<div>
<h1 className="text-2xl font-bold text-zinc-900 tracking-tight">
{blog.title || "Untitled Document"}
</h1>
</div>
<Link
href={`/write/edit/${blog.id}`}
className="px-4 py-1.5 bg-zinc-900 hover:bg-zinc-800 text-white font-medium text-sm rounded-md transition-colors shrink-0 flex items-center gap-1.5 shadow-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
Edit Document
</Link>
</div>
{/* Main Reading Workspace Layout */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 items-start">
{/* Left Side: Styled Canvas Viewport */}
<div className="md:col-span-3 border border-zinc-100 rounded-xl p-6 md:p-8 bg-white shadow-sm min-h-[400px]">
{/* Using your exact editor style class 'prose max-w-none' to ensure uniform markdown formatting */}
<article
className="prose max-w-none text-zinc-800 prose-headings:text-zinc-900 prose-a:text-emerald-600 prose-strong:text-zinc-900"
dangerouslySetInnerHTML={{ __html: blog.content }}
/>
</div>
{/* Right Side: Associated Connected Homelab Apps */}
{linkedApps.length > 0 && (
<div className="md:col-span-1 bg-zinc-50 border border-zinc-200/60 rounded-xl p-4 flex flex-col gap-3 sticky top-4">
<div>
<h3 className="font-semibold text-zinc-700 text-xs uppercase tracking-wider">
Linked Assets
</h3>
<p className="text-[11px] text-zinc-400 mt-0.5">
Infrastructure context bound to this document.
</p>
</div>
<div className="flex flex-col gap-2 mt-1">
{linkedApps.map((app) => (
<div
key={app.id}
className="w-full bg-white border border-zinc-200 p-3 rounded-lg shadow-2vsm"
>
<div className="font-semibold text-xs text-zinc-800 flex items-center justify-between">
<span className="truncate">{app.name}</span>
<span
className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0 ml-1"
title="Bound asset"
/>
</div>
<div className="text-[10px] text-zinc-400 font-mono truncate mt-0.5 selection:bg-zinc-100">
{app.address}
</div>
{app.description && (
<div className="text-[11px] text-zinc-500 mt-1.5 line-clamp-2 border-t border-zinc-50 pt-1.5">
{app.description}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
+103
View File
@@ -0,0 +1,103 @@
"use client";
import { useEffect, useState, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { invoke } from "@tauri-apps/api/core";
import TextEditor from "@/components/text-editor";
import { HomelabApp, Blog } from "@/types";
function WritePageContent() {
const searchParams = useSearchParams();
const appQuery = searchParams.get("app");
const [initialAppIds, setInitialAppIds] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
// Track the document ID to allow updating instead of duplicating
const [documentId, setDocumentId] = useState<string | null>(null);
useEffect(() => {
const checkQueryParam = async () => {
if (!appQuery) {
setLoading(false);
return;
}
try {
const apps = (await invoke("get_app")) as HomelabApp[];
const matchedApp = apps.find(
(app) =>
app.id === appQuery ||
app.name.toLowerCase().trim() === appQuery.toLowerCase().trim(),
);
if (matchedApp) {
setInitialAppIds([matchedApp.id]);
}
} catch (error) {
console.error(
"Failed to fetch apps for query parameter auto-link:",
error,
);
} finally {
setLoading(false);
}
};
checkQueryParam();
}, [appQuery]);
// Updated onSave signature matching what TextEditor emits
const onSave = async (
content: string,
linkedAppIds: string[],
title: string,
) => {
try {
const savedBlog = await invoke<Blog>("save_blog", {
id: documentId,
title: title || "Untitled Document",
content: content,
linkedAppIds: linkedAppIds,
});
// Update state with the backend-generated/returned ID
if (savedBlog && savedBlog.id) {
setDocumentId(savedBlog.id);
}
} catch (error) {
console.error("Failed to save document:", error);
}
};
if (loading) {
return (
<div className="p-4 text-sm text-zinc-400">Loading workspace...</div>
);
}
return (
<TextEditor
content=""
onSave={onSave}
initialLinkedAppIds={initialAppIds}
/>
);
}
export default function WritePage() {
return (
<div className="flex flex-col p-2">
<Suspense
fallback={
<div className="p-4 text-sm text-zinc-400">
Loading router state...
</div>
}
>
<WritePageContent />
</Suspense>
</div>
);
}
+30 -2
View File
@@ -1,3 +1,31 @@
export default function WritePage() {
return <h1>Write Page</h1>;
"use client";
import { invoke } from "@tauri-apps/api/core";
import { Blog } from "@/types";
import { useEffect, useState } from "react";
import BlogCard from "@/components/blog-card";
export default function ViewBlogPage() {
const [blogs, setBlogs] = useState<Blog[]>([]);
useEffect(() => {
const fetchBlogs = async () => {
return await invoke("get_blogs");
};
fetchBlogs().then((blogs) => {
console.log("Blogs:", blogs);
setBlogs(blogs as Blog[]);
});
}, []);
return (
<div className="flex flex-col p-2">
<div className="grid xs:grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 m-4">
{blogs.map((blog) => (
<BlogCard key={blog.id} blog={blog} />
))}
</div>
</div>
);
}
+159
View File
@@ -8,6 +8,17 @@
"@tanstack/react-query": "^5.101.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tiptap/extension-highlight": "^3.27.1",
"@tiptap/extension-mention": "^3.27.1",
"@tiptap/extension-placeholder": "^3.27.1",
"@tiptap/extension-text-align": "^3.27.1",
"@tiptap/extension-typography": "^3.27.1",
"@tiptap/extensions": "^3.27.1",
"@tiptap/markdown": "^3.27.1",
"@tiptap/pm": "^3.27.1",
"@tiptap/react": "^3.27.1",
"@tiptap/starter-kit": "^3.27.1",
"@tiptap/suggestion": "^3.27.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.21.0",
@@ -20,6 +31,8 @@
"shadcn": "^4.12.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.9.0",
"tw-animate-css": "^1.4.0",
},
"devDependencies": {
@@ -217,6 +230,8 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="],
@@ -405,14 +420,92 @@
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="],
"@tiptap/core": ["@tiptap/core@3.27.1", "", { "peerDependencies": { "@tiptap/pm": "3.27.1" } }, "sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ=="],
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-VMF7xJx6qEGiX6DTKNiL31NLqypOcd/4sNjFSe8rb41PwejBJh/nOqVIbBvWkiT6NMGFLxMhj7zJ8/zPo1hXeg=="],
"@tiptap/extension-bold": ["@tiptap/extension-bold@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ=="],
"@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.27.1", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw=="],
"@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-faCUHnRP47o9Zh9VZZX6EX/569udw9Vopm2PgEKPWuKLE2qaS5WBuUVU0iItdJmKUqaWiOZkpoW4jvnDmj0dfg=="],
"@tiptap/extension-code": ["@tiptap/extension-code@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w=="],
"@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA=="],
"@tiptap/extension-document": ["@tiptap/extension-document@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-8FbBTkfnRP4iVaoj+2h3iWa+H0eGDD3yTyVCwrmue/sQTkqUNUoSuAZa3GDG4Sd41xdPwTJxl9nUWGgM1qDCnw=="],
"@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-blFf9x9RG0Qr7P3FoAH/033ffa+mMLZn34trVs8Vi0Ppk6FmJAg5HpYFOtmYoeREdNDJ5rHJKV7SoACbOHgskQ=="],
"@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.27.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q=="],
"@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-QoezN0wdvXIwLQ4ee2ccWDaX3RG0lzgQpIMpMz55oPDhpUVax1+19ApsS53LkcktpS4EbnPL4xO4DaJk0Vp7PQ=="],
"@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-iv/m9hzl6jfSj9Q8UEjAxONvCoUDaP7M9SRCPx3PaLNxA230TTD6RE0Ye4zFJ8ze7ZVoJJMAqg9Qpq1iYg2JOQ=="],
"@tiptap/extension-heading": ["@tiptap/extension-heading@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-SrC4l1kEIyv9ZXFaI/8LQqU2MyMmjczw7XXsWUQOTN4YXv0JyVgMNR3cI/wz0d2xsTfBdZ1N85Tdng+Ga1t0Sg=="],
"@tiptap/extension-highlight": ["@tiptap/extension-highlight@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-IeMIUKbW4oyRkgn92BPYQ0fifkbTwVigKwa107nGDHMokz+pHQPFHy8xG3U7gtUOra/8OUo57bFgNGuP0TaH4A=="],
"@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg=="],
"@tiptap/extension-italic": ["@tiptap/extension-italic@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA=="],
"@tiptap/extension-link": ["@tiptap/extension-link@3.27.1", "", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-/2jBfsxBZUDGJmpZifqRQPz7f1E5qpS1BckTZ39TADzUJX+feKy7RJ3DtQ02+8y6SSMzvP9loGVjrk6zEMTk4g=="],
"@tiptap/extension-list": ["@tiptap/extension-list@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-c2Upru7lj0/ZV/Ibww6cNz6sUS8m6Dp/9uygFhYcZOd3X8M0xBIEk42c6m6SQehkPziVA8QOgNJz7sMqsbz1OQ=="],
"@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-zwRl01ETfCkWUvtvK5fw9bXtAajMPkvlkE3Cq6JvH3LF7XXJwDtNj5Tj7exacMpCaSZmlNc43vFb2rAYnrnwMA=="],
"@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-OIMZNlzPSO8WRd4ic73Fxckzl4N1tesjjLL2XApaNA/uMpO0LoF6WSRPAWv+Z24Wp92ARRJAnRP7iZoI5+Jxig=="],
"@tiptap/extension-mention": ["@tiptap/extension-mention@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1", "@tiptap/suggestion": "3.27.1" } }, "sha512-QfaKdl8PET01JvEFrIjMcOC1iYrBm2tsZEn07J1AaCqClW9iWcg/nCAdkdFhVir1fC54SLuaui43xslBawQRFg=="],
"@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-GYrKqD//9nHJ2r80uXqbDMzRnFpGzbaEQRTSGaO/SH7DvXWFMow8evkOdjQ7PCQO07jNjJo75+A85Jwu3Ov3AA=="],
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q=="],
"@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-lhcNDcczQ75yJOSywCHb58Hmtg1aPL/2TdYmeLPxVrP048D7rRs133sfONcgyyw0AvhnfmPOkHLTv3QtKSowhw=="],
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q=="],
"@tiptap/extension-text": ["@tiptap/extension-text@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg=="],
"@tiptap/extension-text-align": ["@tiptap/extension-text-align@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-EXawuJBO55wd8WcTbHTMoPhv0CGQxza4yCCPB5Hqz4ZPQwahIr3ej+8yp/kimIl0xokabwZ0/Fu8STQ4AkZv5g=="],
"@tiptap/extension-typography": ["@tiptap/extension-typography@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-hkZyiiv45XQGizdEYxJn86g8yKJxG45kkTztg0m6qjjcdDzjhUAcwVPTZ4ecg3hxwy/XNY7gEBOMGI7iqJl2sw=="],
"@tiptap/extension-underline": ["@tiptap/extension-underline@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ=="],
"@tiptap/extensions": ["@tiptap/extensions@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw=="],
"@tiptap/markdown": ["@tiptap/markdown@3.27.1", "", { "dependencies": { "marked": "^17.0.1" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-4m2LZcj/uN0uLlGnXr3i7sfdxbQNNmeMn4wFyFrP2xpshcKPL5WujUAcqT2rgKfaDjKQ8gIK/GpgSQKuRENFwg=="],
"@tiptap/pm": ["@tiptap/pm@3.27.1", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "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.3", "prosemirror-model": "^1.25.7", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.0", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.41.8" } }, "sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw=="],
"@tiptap/react": ["@tiptap/react@3.27.1", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.27.1", "@tiptap/extension-floating-menu": "^3.27.1" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/Wn2fc9zMtX08MXYScDFsm4wJ8lzfhfPEdbtls7WCDlbtrop48PWlkHDBBJrywARfAQTB2mFs9KiFy9yrQm5Lg=="],
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.27.1", "", { "dependencies": { "@tiptap/core": "^3.27.1", "@tiptap/extension-blockquote": "^3.27.1", "@tiptap/extension-bold": "^3.27.1", "@tiptap/extension-bullet-list": "^3.27.1", "@tiptap/extension-code": "^3.27.1", "@tiptap/extension-code-block": "^3.27.1", "@tiptap/extension-document": "^3.27.1", "@tiptap/extension-dropcursor": "^3.27.1", "@tiptap/extension-gapcursor": "^3.27.1", "@tiptap/extension-hard-break": "^3.27.1", "@tiptap/extension-heading": "^3.27.1", "@tiptap/extension-horizontal-rule": "^3.27.1", "@tiptap/extension-italic": "^3.27.1", "@tiptap/extension-link": "^3.27.1", "@tiptap/extension-list": "^3.27.1", "@tiptap/extension-list-item": "^3.27.1", "@tiptap/extension-list-keymap": "^3.27.1", "@tiptap/extension-ordered-list": "^3.27.1", "@tiptap/extension-paragraph": "^3.27.1", "@tiptap/extension-strike": "^3.27.1", "@tiptap/extension-text": "^3.27.1", "@tiptap/extension-underline": "^3.27.1", "@tiptap/extensions": "^3.27.1", "@tiptap/pm": "^3.27.1" } }, "sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow=="],
"@tiptap/suggestion": ["@tiptap/suggestion@3.27.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-GNBPRav+lAfXzqmmUAS6ylRAn3G8JfsP6XosjoORxJIQJLx1ktDqwp6tm1Vgz9aGIM2TrBxLS1uBbI1Gb2/1VA=="],
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="],
"@types/markdown-it": ["@types/markdown-it@13.0.9", "", { "dependencies": { "@types/linkify-it": "^3", "@types/mdurl": "^1" } }, "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw=="],
"@types/mdurl": ["@types/mdurl@1.0.5", "", {}, "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA=="],
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -537,6 +630,8 @@
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
@@ -567,6 +662,8 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
@@ -717,6 +814,10 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="],
"linkifyjs": ["linkifyjs@4.3.3", "", {}, "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="],
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
@@ -727,8 +828,16 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="],
"markdown-it-task-lists": ["markdown-it-task-lists@2.1.1", "", {}, "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="],
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
@@ -787,6 +896,8 @@
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
"orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
@@ -827,8 +938,38 @@
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"prosemirror-changeset": ["prosemirror-changeset@2.4.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="],
"prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
"prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
"prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="],
"prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="],
"prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="],
"prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
"prosemirror-markdown": ["prosemirror-markdown@1.13.4", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw=="],
"prosemirror-model": ["prosemirror-model@1.25.9", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA=="],
"prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
"prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="],
"prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="],
"prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="],
"prosemirror-view": ["prosemirror-view@1.41.9", "", { "dependencies": { "prosemirror-model": "^1.25.8", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@@ -859,6 +1000,8 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
@@ -929,6 +1072,10 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="],
"tiptap-markdown": ["tiptap-markdown@0.9.0", "", { "dependencies": { "@types/markdown-it": "^13.0.7", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "prosemirror-markdown": "^1.11.1" }, "peerDependencies": { "@tiptap/core": "^3.0.1" } }, "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@@ -945,6 +1092,8 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
@@ -961,12 +1110,16 @@
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
@@ -1033,6 +1186,8 @@
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"prosemirror-markdown/@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
@@ -1056,5 +1211,9 @@
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
}
}
+6 -1
View File
@@ -11,6 +11,7 @@ import {
} from "@/components/ui/tooltip";
import DeleteDialog from "./delete-dialog";
import { useState } from "react";
import { useRouter } from "next/navigation";
type Props = {
id: string;
@@ -22,6 +23,8 @@ export default function ActionMenu({ id }: Props) {
const [open, setOpen] = useState(false);
const router = useRouter();
return (
<>
<TooltipProvider delayDuration={150}>
@@ -69,7 +72,9 @@ export default function ActionMenu({ id }: Props) {
<button
className={buttonClass}
onClick={() => {
// Blog
let link = `/write/new?app=${id}`;
console.log(link);
router.push(link);
}}
>
<Pencil className="h-5 w-5" />
+1 -5
View File
@@ -90,11 +90,7 @@ export default function AppCard({ app }: Props) {
</div>
<h3 className="text-2xl font-bold">{app.name}</h3>
<a
className={`${hovered && "text-blue-600"} transition-colors duration-200`}
target="_blank"
href={`http://${app.address}`}
>
<a target="_blank" href={`http://${app.address}`}>
{app.address}
</a>
+84
View File
@@ -0,0 +1,84 @@
"use client";
import { Blog, HomelabApp } from "@/types";
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useRouter } from "next/navigation";
interface BlogCardProps {
blog: Blog & { linked_app_ids?: string[] };
}
export default function BlogCard({ blog }: BlogCardProps) {
const [referencedApps, setReferencedApps] = useState<HomelabApp[]>([]);
const appIds = blog.linked_app_ids;
const router = useRouter();
useEffect(() => {
if (appIds && appIds.length > 0) {
const fetchReferences = async () => {
try {
const apps = await invoke<HomelabApp[]>("get_apps_by_id", {
ids: appIds,
});
setReferencedApps(apps);
} catch (err) {
console.error("Failed to fetch referenced apps:", err);
}
};
fetchReferences();
}
}, [appIds]);
return (
<div
className="group flex flex-col justify-between p-6 border border-slate-200/80 rounded-2xl bg-gradient-to-b from-white to-slate-50/50 dark:from-slate-900 dark:to-slate-950 text-card-foreground shadow-sm hover:shadow-xl hover:-translate-y-1 hover:border-emerald-600/40 transition-all duration-300 h-64 relative overflow-hidden"
onClick={() => router.push(`/write/edit?id=${blog.id}`)}
>
{/* Decorative background glow that triggers on hover */}
<div className="absolute -right-10 -top-10 w-24 h-24 bg-emerald-500/10 rounded-full blur-2xl group-hover:bg-emerald-500/20 transition-all duration-500" />
<div>
{/* Category / Meta indicator */}
<span className="text-[10px] font-bold tracking-wider uppercase text-emerald-700/80 dark:text-emerald-400 mb-1 block">
Homelab Log
</span>
{/* Title */}
<h3 className="text-xl font-bold tracking-tight line-clamp-1 mb-2 text-slate-800 dark:text-slate-100 group-hover:text-emerald-700 dark:group-hover:text-emerald-400 transition-colors duration-200">
{blog.title}
</h3>
{/* Truncated Tiptap HTML Content */}
<div
className="prose prose-sm dark:prose-invert line-clamp-3 text-slate-500 dark:text-slate-400 overflow-hidden max-w-none leading-relaxed"
dangerouslySetInnerHTML={{ __html: blog.content }}
/>
</div>
{/* Footer / References */}
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 flex flex-wrap gap-2 items-center overflow-hidden max-h-12">
{!appIds || appIds.length === 0 ? (
<span className="text-xs text-slate-400 italic">
No linked applications
</span>
) : (
referencedApps.map((app) => (
<span
key={app.id}
title={app.address}
className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500/20 transition-colors"
>
{/* App Avatar Placeholder (Uses first letter of app name) */}
<span className="w-4 h-4 rounded bg-emerald-700 text-white dark:bg-emerald-500 dark:text-slate-950 font-bold text-[9px] flex items-center justify-center mr-1.5 shadow-sm uppercase shrink-0">
{app.name.charAt(0)}
</span>
{app.name}
</span>
))
)}
</div>
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
/* Custom component node pill styling inside the editor wrapper */
.homelab-app-reference-pill {
background-color: rgba(
16,
185,
129,
0.1
); /* Emerald tint matching your UI accents */
color: #059669;
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 0.375rem;
padding: 0.1rem 0.4rem;
font-weight: 500;
font-size: 0.875rem;
display: inline-flex;
align-items: center;
user-select: none;
}
/* Visually highlight the whole card pill block if a user hits delete or selects it */
.ProseMirror-selectednode .homelab-app-reference-pill {
outline: 2px solid #10b981;
outline-offset: 1px;
}
+142
View File
@@ -0,0 +1,142 @@
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Highlight from "@tiptap/extension-highlight";
import TextAlign from "@tiptap/extension-text-align";
import { Markdown } from "@tiptap/markdown";
import { HomelabApp } from "./types";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import Menubar from "./menu-bar";
interface TextEditorProps {
content: string;
onSave: (content: string, linkedAppIds: string[]) => void;
initialLinkedAppIds?: string[]; // Added prop
}
const getApps = async (): Promise<HomelabApp[]> => {
return (await invoke("get_app")) as HomelabApp[];
};
export default function TextEditor({
content,
onSave,
initialLinkedAppIds = [],
}: TextEditorProps) {
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
// Seed state instantly from the prop if it exists
const [linkedAppIds, setLinkedAppIds] =
useState<string[]>(initialLinkedAppIds);
useEffect(() => {
const fetchApps = async () => {
const apps = await getApps();
setAvailableApps(apps);
};
fetchApps();
}, []);
const editor = useEditor({
extensions: [
StarterKit.configure({
bulletList: { HTMLAttributes: { class: "list-disc ml-3" } },
orderedList: { HTMLAttributes: { class: "list-decimal ml-3" } },
}),
Highlight.configure({ HTMLAttributes: { class: "bg-yellow-200" } }),
TextAlign.configure({ types: ["heading", "paragraph"] }),
Markdown,
],
immediatelyRender: false,
content: content || "<p>Start typing your documentation notes here...</p>",
editorProps: {
attributes: {
class: "prose max-w-none focus:outline-none min-h-[300px]",
},
},
});
const toggleAppLink = (appId: string) => {
setLinkedAppIds((prev) =>
prev.includes(appId)
? prev.filter((id) => id !== appId)
: [...prev, appId],
);
};
const handleSave = () => {
if (!editor) return;
const htmlOutput = editor.getHTML();
onSave(htmlOutput, linkedAppIds);
};
return (
<div className="flex flex-col gap-4 w-full">
<div className="flex justify-between items-center pb-2 border-b border-zinc-100">
<span className="text-sm text-zinc-400 font-medium">Draft Session</span>
<button
onClick={handleSave}
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-sm rounded-md transition-colors"
>
Save Changes
</button>
</div>
<Menubar editor={editor} />
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 items-start">
<div className="md:col-span-3 border border-zinc-200 rounded-lg p-4 bg-white shadow-sm min-h-100">
<EditorContent editor={editor} />
</div>
<div className="md:col-span-1 bg-zinc-50 border border-zinc-200 rounded-lg p-4 flex flex-col gap-3">
<div>
<h3 className="font-semibold text-zinc-700 text-sm">
Document Assets
</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Click apps to bind/unbind them to this entire document.
</p>
</div>
<div className="flex flex-col gap-2 mt-2">
{availableApps.map((app) => {
const isLinked = linkedAppIds.includes(app.id);
return (
<button
key={app.id}
type="button"
onClick={() => toggleAppLink(app.id)}
className={`w-full text-left p-3 border rounded-md transition relative overflow-hidden ${
isLinked
? "border-emerald-500 bg-emerald-50/30"
: "bg-white border-zinc-200 hover:border-zinc-300"
}`}
>
{isLinked && (
<div className="absolute top-0 right-0 h-0 w-0 border-t-[16px] border-t-emerald-500 border-l-[16px] border-l-transparent" />
)}
<div
className={`font-medium text-xs ${isLinked ? "text-emerald-700" : "text-zinc-800"}`}
>
{app.name}
</div>
<div className="text-[10px] text-zinc-400 font-mono truncate mt-0.5">
{app.id}
</div>
{app.description && (
<div className="text-[11px] text-zinc-500 mt-1 line-clamp-1">
{app.description}
</div>
)}
</button>
);
})}
</div>
</div>
</div>
</div>
);
}
+195
View File
@@ -0,0 +1,195 @@
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Highlight from "@tiptap/extension-highlight";
import TextAlign from "@tiptap/extension-text-align";
import { Markdown } from "@tiptap/markdown";
import { HomelabApp } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import Menubar from "./menu-bar";
interface TextEditorProps {
content: string;
initialTitle?: string;
onSave: (content: string, linkedAppIds: string[], title: string) => void;
initialLinkedAppIds?: string[];
}
const getApps = async (): Promise<HomelabApp[]> => {
return (await invoke("get_app")) as HomelabApp[];
};
export default function TextEditor({
content,
initialTitle = "",
onSave,
initialLinkedAppIds = [],
}: TextEditorProps) {
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
const [linkedAppIds, setLinkedAppIds] =
useState<string[]>(initialLinkedAppIds);
// Title control state
const [title, setTitle] = useState<string>(initialTitle);
// Save control
// Will be saved on completed handleSave, error if func fails, and saving if the func is in progress
const [isSaved, setIsSaved] = useState<"saved" | "error" | "saving">(
"saving",
);
useEffect(() => {
if (initialLinkedAppIds.length > 0) {
setLinkedAppIds(initialLinkedAppIds);
}
}, [initialLinkedAppIds]);
useEffect(() => {
const fetchApps = async () => {
const apps = await getApps();
setAvailableApps(apps);
};
fetchApps();
}, []);
const editor = useEditor({
extensions: [
StarterKit.configure({
bulletList: { HTMLAttributes: { class: "list-disc ml-3" } },
orderedList: { HTMLAttributes: { class: "list-decimal ml-3" } },
}),
Highlight.configure({ HTMLAttributes: { class: "bg-yellow-200" } }),
TextAlign.configure({ types: ["heading", "paragraph"] }),
Markdown,
],
immediatelyRender: false,
content,
editorProps: {
attributes: {
class: "prose max-w-none focus:outline-none min-h-[300px]",
},
},
});
const toggleAppLink = (appId: string) => {
setLinkedAppIds((prev) =>
prev.includes(appId)
? prev.filter((id) => id !== appId)
: [...prev, appId],
);
};
const handleSave = () => {
setIsSaved("saving");
try {
if (!editor) return;
const htmlOutput = editor.getHTML();
// Dispatch text body layout, assets collection array, and document title string
onSave(htmlOutput, linkedAppIds, title);
setIsSaved("saved");
} catch (error) {
console.error("Error saving document:", error);
setIsSaved("error");
}
};
// create debounced save, every 15 seconds
useEffect(() => {
const interval = setInterval(() => {
handleSave();
}, 15000);
return () => clearInterval(interval);
}, [editor, linkedAppIds, title]);
return (
<div className="flex flex-col gap-4 w-full">
{/* Top action bar updated with live Title Input */}
<div className="flex justify-between items-center pb-2 border-b border-zinc-100 gap-4">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Untitled Document..."
className="bg-transparent text-zinc-800 font-semibold text-base placeholder-zinc-300 focus:outline-none flex-1 py-1"
/>
<div className="flex items-center gap-2">
{isSaved === "saving" && (
<span className="text-xs text-zinc-400">Saving...</span>
)}
{isSaved === "saved" && (
<span className="text-xs text-emerald-600">Saved</span>
)}
{isSaved === "error" && (
<span className="text-xs text-red-600">Error saving</span>
)}
<button
onClick={handleSave}
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-sm rounded-md transition-colors shrink-0"
>
Save Changes
</button>
</div>
</div>
{/* Editor Controls */}
<Menubar editor={editor} />
{/* Main split-view workspace */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 items-start">
{/* Left Side: Text Canvas */}
<div className="md:col-span-3 border border-zinc-200 rounded-lg p-4 bg-white shadow-sm min-h-100">
<EditorContent editor={editor} />
</div>
{/* Right Side: Document-Wide Reference Panel */}
<div className="md:col-span-1 bg-zinc-50 border border-zinc-200 rounded-lg p-4 flex flex-col gap-3">
<div>
<h3 className="font-semibold text-zinc-700 text-sm">
Document Assets
</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Click apps to bind/unbind them to this entire document.
</p>
</div>
<div className="flex flex-col gap-2 mt-2">
{availableApps.map((app) => {
const isLinked = linkedAppIds.includes(app.id);
return (
<button
key={app.id}
type="button"
onClick={() => toggleAppLink(app.id)}
className={`w-full text-left p-3 border rounded-md transition relative overflow-hidden ${
isLinked
? "border-emerald-500 bg-emerald-50/30"
: "bg-white border-zinc-200 hover:border-zinc-300"
}`}
>
{isLinked && (
<div className="absolute top-0 right-0 h-0 w-0 border-t-16 border-t-emerald-500 border-l-16 border-l-transparent" />
)}
<div
className={`font-medium text-xs ${isLinked ? "text-emerald-700" : "text-zinc-800"}`}
>
{app.name}
</div>
<div className="text-[10px] text-zinc-400 font-mono truncate mt-0.5">
{app.id}
</div>
{app.description && (
<div className="text-[11px] text-zinc-500 mt-1 line-clamp-1">
{app.description}
</div>
)}
</button>
);
})}
</div>
</div>
</div>
</div>
);
}
+122
View File
@@ -0,0 +1,122 @@
import { Editor, useEditorState } from "@tiptap/react";
import {
AlignCenter,
AlignLeft,
AlignRight,
Bold,
Heading1,
Heading2,
Heading3,
Highlighter,
Italic,
List,
ListOrdered,
} from "lucide-react";
import { Toggle } from "../ui/toggle";
interface MenubarProps {
editor: Editor | null;
}
// 1. The main exported component remains safe and handles the null state cleanly
export default function Menubar({ editor }: MenubarProps) {
if (!editor) {
return null;
}
// Only render the inner menu once the editor is explicitly available
return <MenubarInner editor={editor} />;
}
// 2. The inner component runs safely because 'editor' is guaranteed to be an Editor instance
function MenubarInner({ editor }: { editor: Editor }) {
const editorState = useEditorState({
editor, // Now safely guaranteed never to be null or undefined
selector: (ctx) => {
return {
isBold: ctx.editor.isActive("bold"),
isItalic: ctx.editor.isActive("italic"),
isStrike: ctx.editor.isActive("strike"),
isHighlight: ctx.editor.isActive("highlight"),
isAlignLeft: ctx.editor.isActive({ textAlign: "left" }),
isAlignCenter: ctx.editor.isActive({ textAlign: "center" }),
isAlignRight: ctx.editor.isActive({ textAlign: "right" }),
isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }),
isParagraph: ctx.editor.isActive("paragraph"),
isHeading1: ctx.editor.isActive("heading", { level: 1 }),
isHeading2: ctx.editor.isActive("heading", { level: 2 }),
isHeading3: ctx.editor.isActive("heading", { level: 3 }),
};
},
});
const options = [
{
icon: <Heading1 className="size-4" />,
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
pressed: editorState.isHeading1,
},
{
icon: <Heading2 className="size-4" />,
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
pressed: editorState.isHeading2,
},
{
icon: <Heading3 className="size-4" />,
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
pressed: editorState.isHeading3,
},
{
icon: <Bold className="size-4" />,
onClick: () => editor.chain().focus().toggleBold().run(),
pressed: editorState.isBold,
},
{
icon: <Italic className="size-4" />,
onClick: () => editor.chain().focus().toggleItalic().run(),
pressed: editorState.isItalic,
},
{
icon: <AlignLeft className="size-4" />,
onClick: () => editor.chain().focus().toggleTextAlign("left").run(),
pressed: editorState.isAlignLeft,
},
{
icon: <AlignCenter className="size-4" />,
onClick: () => editor.chain().focus().toggleTextAlign("center").run(),
pressed: editorState.isAlignCenter,
},
{
icon: <AlignRight className="size-4" />,
onClick: () => editor.chain().focus().toggleTextAlign("right").run(),
pressed: editorState.isAlignRight,
},
{
icon: <List className="size-4" />,
onClick: () => editor.chain().focus().toggleBulletList().run(),
pressed: editor.isActive("bulletList"),
},
{
icon: <ListOrdered className="size-4" />,
onClick: () => editor.chain().focus().toggleOrderedList().run(),
pressed: editor.isActive("orderedList"),
},
{
icon: <Highlighter className="size-4" />,
onClick: () => editor.chain().focus().toggleHighlight().run(),
pressed: editorState.isHighlight,
},
];
return (
<div className="border rounded-md p-1 mb-1 bg-slate-50 space-x-2 z-50 flex items-center">
{options.map((option, index) => (
<Toggle key={index} pressed={option.pressed} onClick={option.onClick}>
{option.icon}
</Toggle>
))}
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Toggle as TogglePrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-2xl text-sm font-medium whitespace-nowrap transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-muted",
},
size: {
default:
"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-7 min-w-7 px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Toggle({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants };
+13
View File
@@ -14,6 +14,17 @@
"@tanstack/react-query": "^5.101.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tiptap/extension-highlight": "^3.27.1",
"@tiptap/extension-mention": "^3.27.1",
"@tiptap/extension-placeholder": "^3.27.1",
"@tiptap/extension-text-align": "^3.27.1",
"@tiptap/extension-typography": "^3.27.1",
"@tiptap/extensions": "^3.27.1",
"@tiptap/markdown": "^3.27.1",
"@tiptap/pm": "^3.27.1",
"@tiptap/react": "^3.27.1",
"@tiptap/starter-kit": "^3.27.1",
"@tiptap/suggestion": "^3.27.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.21.0",
@@ -26,6 +37,8 @@
"shadcn": "^4.12.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.9.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
+111
View File
@@ -53,3 +53,114 @@ pub fn delete_app(conn: &mut SqliteConnection, app_id: &str) -> usize {
.execute(conn)
.expect("Error deleting app")
}
pub fn create_blog_db(
conn: &mut SqliteConnection,
title: &str,
content: &str,
linked_app_ids: Option<&[String]>,
) -> models::Blog {
use crate::db::schema::blog;
// json seralize the referenced app ids, store as string
if let Some(ids) = linked_app_ids {
for id in ids {
// Check if the app exists in the database
let app_exists = crate::db::schema::app::dsl::app
.filter(crate::db::schema::app::dsl::id.eq(id))
.first::<models::App>(conn)
.optional()
.expect("Error checking if app exists");
if app_exists.is_none() {
panic!("App with id {} does not exist", id);
}
}
}
// if there are linked app ids, serialize them to json, else set to None
let reference: Option<String> = if let Some(ids) = linked_app_ids {
Some(serde_json::to_string(&ids).expect("Failed to convert to JSON"))
} else {
None
};
let new_blog = models::Blog {
id: uuid::Uuid::new_v4().to_string(),
title: title.to_string(),
content: content.to_string(),
reference,
};
diesel::insert_into(blog::table)
.values(&new_blog)
.execute(conn)
.expect("Error saving new blog");
new_blog
}
// Will be used to update an existing blog post OR create a new one if it doesn't exist
pub fn save_blog(
conn: &mut SqliteConnection,
blog_id: Option<&str>,
title: &str,
content: &str,
linked_app_ids: Option<&[String]>,
) -> models::Blog {
use crate::db::schema::blog;
// json seralize the referenced app ids, store as string
let reference: Option<String> = if let Some(ids) = linked_app_ids {
Some(serde_json::to_string(&ids).expect("Failed to convert to JSON"))
} else {
None
};
if let Some(id) = blog_id {
// Update existing blog
diesel::update(blog::table.filter(blog::id.eq(id)))
.set((
blog::title.eq(title),
blog::content.eq(content),
blog::reference.eq(reference),
))
.execute(conn)
.expect("Error updating blog");
blog::table
.filter(blog::id.eq(id))
.first::<models::Blog>(conn)
.expect("Error loading updated blog")
} else {
// Create new blog
create_blog_db(conn, title, content, linked_app_ids)
}
}
pub fn get_all_blogs(conn: &mut SqliteConnection) -> Vec<models::Blog> {
use crate::db::schema::blog;
blog::table
.load::<models::Blog>(conn)
.expect("Error loading apps")
}
pub fn get_apps_by_ids(conn: &mut SqliteConnection, ids: &[String]) -> Vec<models::App> {
use crate::db::schema::app;
app::table
.filter(app::id.eq_any(ids))
.load::<models::App>(conn)
.expect("Error loading apps by ids")
}
pub fn get_blog_by_id(conn: &mut SqliteConnection, blog_id: &str) -> Option<models::Blog> {
use crate::db::schema::blog;
blog::table
.filter(blog::id.eq(blog_id))
.first::<models::Blog>(conn)
.optional()
.expect("Error loading blog by id")
}
+2 -2
View File
@@ -12,7 +12,7 @@ pub struct App {
pub ssh_address: Option<String>, // default to normal address
}
#[derive(Queryable, Selectable, Debug, Serialize, Deserialize)]
#[derive(Queryable, Selectable, Debug, Insertable, Serialize, Deserialize)]
#[diesel(table_name = crate::db::schema::blog)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Blog {
@@ -22,7 +22,7 @@ pub struct Blog {
pub reference: Option<String>, // uuid
}
#[derive(Queryable, Selectable, Debug, Serialize, Deserialize)]
#[derive(Queryable, Selectable, Debug, Insertable, Serialize, Deserialize)]
#[diesel(table_name = crate::db::schema::identities)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Identity {
+75 -7
View File
@@ -27,14 +27,12 @@ async fn ping_target(address: &str) -> bool {
let ip_target: IpAddr = socket_addr.ip();
// 2. Configure unprivileged ICMP requestor
let timeout = Some(Duration::from_secs(2));
let pinger = match IcmpEchoRequestor::new(ip_target, None, None, timeout) {
Ok(p) => p,
Err(_) => return false,
};
// 3. Send ICMP Echo request asynchronously
match pinger.send().await {
Ok(reply) => {
// Check if the status returned is a valid success response
@@ -46,7 +44,7 @@ async fn ping_target(address: &str) -> bool {
#[tauri::command]
fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Option<&str>) -> App {
let mut conn = &mut establish_connection();
let mut conn = establish_connection();
let app = create_app(&mut conn, name, address, description, ssh_address);
println!("{:#?}", app);
@@ -55,7 +53,7 @@ fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Op
#[tauri::command]
fn get_app() -> Vec<App> {
let mut conn = &mut establish_connection();
let mut conn = establish_connection();
let apps = get_apps(&mut conn);
println!("{:#?}", apps);
@@ -64,8 +62,8 @@ fn get_app() -> Vec<App> {
#[tauri::command]
fn delete_apps(id: &str) {
let conn = &mut establish_connection();
delete_app(conn, id);
let mut conn = establish_connection();
delete_app(&mut conn, id);
}
#[tauri::command]
@@ -76,6 +74,72 @@ async fn ping(address: &str) -> Result<bool, String> {
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct TempBlog {
id: String,
title: String,
content: String,
linked_app_ids: Option<Vec<String>>,
}
#[tauri::command]
fn get_blogs() -> Vec<TempBlog> {
// I'll need to decode the linked_app_ids from a json encoded string to a Vec<String>.
let mut conn = establish_connection();
let encoded_blogs = db::get_all_blogs(&mut conn);
let mut decoded_blogs: Vec<TempBlog> = Vec::new();
encoded_blogs.iter().for_each(|blog| {
let linked_app_ids: Option<Vec<String>> = match &blog.reference {
Some(ids) => serde_json::from_str(ids).ok(),
None => None,
};
decoded_blogs.push(TempBlog {
id: blog.id.clone(),
title: blog.title.clone(),
content: blog.content.clone(),
linked_app_ids,
});
});
decoded_blogs
}
#[tauri::command]
fn save_blog(
id: Option<&str>,
title: &str,
content: &str,
linked_app_ids: Option<Vec<String>>,
) -> db::models::Blog {
let mut conn = establish_connection();
db::save_blog(&mut conn, id, title, content, linked_app_ids.as_deref())
}
#[tauri::command]
fn get_apps_by_id(ids: Vec<String>) -> Vec<App> {
let mut conn = establish_connection();
db::get_apps_by_ids(&mut conn, &ids)
}
#[tauri::command]
fn get_blog(id: &str) -> Option<TempBlog> {
let mut conn = establish_connection();
let blog = db::get_blog_by_id(&mut conn, id);
blog.map(|b| {
let linked_app_ids: Option<Vec<String>> = match &b.reference {
Some(ids) => serde_json::from_str(ids).ok(),
None => None,
};
TempBlog {
id: b.id.clone(),
title: b.title.clone(),
content: b.content.clone(),
linked_app_ids,
}
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -84,7 +148,11 @@ pub fn run() {
new_app,
get_app,
delete_apps,
ping
ping,
get_blogs,
save_blog,
get_apps_by_id,
get_blog
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Vendored
+20
View File
@@ -0,0 +1,20 @@
export interface HomelabApp {
id: string; // uuid
name: string;
description: string | null;
address: string;
ssh_address: string | null;
}
export interface Blog {
id: string;
title: string;
content: string;
reference: String[]; // list of app ids
}
// Defines the custom attributes Tiptap will store in the DOM/JSON schema
export interface AppMentionAttributes {
id: string;
label: string;
}