might work, basic writing page with linking. no saving.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"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"; // Added menubar import back
|
||||
|
||||
interface TextEditorProps {
|
||||
content: string;
|
||||
onSave: (content: string, linkedAppIds: string[]) => void;
|
||||
initialLinkedAppIds?: string[];
|
||||
}
|
||||
|
||||
const getApps = async (): Promise<HomelabApp[]> => {
|
||||
return (await invoke("get_app")) as HomelabApp[];
|
||||
};
|
||||
|
||||
export default function TextEditor({ content, onSave }: TextEditorProps) {
|
||||
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
|
||||
// Track document-wide linked apps here
|
||||
const [linkedAppIds, setLinkedAppIds] = useState<string[]>([]);
|
||||
|
||||
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) // Unlink if already added
|
||||
: [...prev, appId], // Link if not added
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editor) return;
|
||||
const htmlOutput = editor.getHTML();
|
||||
// Pass both the layout structure and the entire linked app set
|
||||
onSave(htmlOutput, linkedAppIds);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{/* Top action bar */}
|
||||
<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>
|
||||
|
||||
{/* 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"
|
||||
}`}
|
||||
>
|
||||
{/* Subtle active border indicator */}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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";
|
||||
|
||||
export default function Menubar({ editor }: { editor: Editor | null }) {
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: (ctx) => {
|
||||
return {
|
||||
// Text formatting
|
||||
isBold: ctx.editor.isActive("bold") ?? false,
|
||||
isItalic: ctx.editor.isActive("italic") ?? false,
|
||||
isStrike: ctx.editor.isActive("strike") ?? false,
|
||||
isHighlight: ctx.editor.isActive("highlight") ?? false,
|
||||
|
||||
// Text alignment
|
||||
isAlignLeft: ctx.editor.isActive({ textAlign: "left" }) ?? false,
|
||||
isAlignCenter: ctx.editor.isActive({ textAlign: "center" }) ?? false,
|
||||
isAlignRight: ctx.editor.isActive({ textAlign: "right" }) ?? false,
|
||||
isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }) ?? false,
|
||||
|
||||
// Block types
|
||||
isParagraph: ctx.editor.isActive("paragraph") ?? false,
|
||||
isHeading1: ctx.editor.isActive("heading", { level: 1 }) ?? false,
|
||||
isHeading2: ctx.editor.isActive("heading", { level: 2 }) ?? false,
|
||||
isHeading3: ctx.editor.isActive("heading", { level: 3 }) ?? false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const options: {
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
pressed: boolean;
|
||||
}[] = [
|
||||
{
|
||||
icon: <Heading1 className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
pressed: editor.isActive("heading", { level: 1 }),
|
||||
},
|
||||
{
|
||||
icon: <Heading2 className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
pressed: editor.isActive("heading", { level: 2 }),
|
||||
},
|
||||
{
|
||||
icon: <Heading3 className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
pressed: editor.isActive("heading", { level: 3 }),
|
||||
},
|
||||
{
|
||||
icon: <Bold className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleBold().run(),
|
||||
pressed: editor.isActive("bold"),
|
||||
},
|
||||
{
|
||||
icon: <Italic className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleItalic().run(),
|
||||
pressed: editor.isActive("italic"),
|
||||
},
|
||||
{
|
||||
icon: <AlignLeft className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleTextAlign("left").run(),
|
||||
pressed: editor.isActive({ textAlign: "left" }),
|
||||
},
|
||||
{
|
||||
icon: <AlignCenter className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleTextAlign("center").run(),
|
||||
pressed: editor.isActive({ textAlign: "center" }),
|
||||
},
|
||||
{
|
||||
icon: <AlignRight className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleTextAlign("right").run(),
|
||||
pressed: editor.isActive({ textAlign: "right" }),
|
||||
},
|
||||
{
|
||||
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("orderList"),
|
||||
},
|
||||
{
|
||||
icon: <Highlighter className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHighlight().run(),
|
||||
pressed: editor.isActive("highlight"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border rounded-md p-1 mb-1 bg-slate-50 space-x-2 z-50">
|
||||
{options.map((option, index) => (
|
||||
<Toggle key={index} pressed={option.pressed} onClick={option.onClick}>
|
||||
{option.icon}
|
||||
</Toggle>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user