Add tag selector and refactor note editing UI

This commit is contained in:
james fitzsimons 2025-12-24 14:35:36 +00:00
parent ffbf485935
commit 40f5a3d794
4 changed files with 98 additions and 16 deletions

Binary file not shown.

View file

@ -13,6 +13,8 @@ import { Note, NoteRead } from "@/api/notes";
import { DecryptedTagNode } from "@/api/encryption"; import { DecryptedTagNode } from "@/api/encryption";
// @ts-ignore // @ts-ignore
import XmarkIcon from "@/assets/fontawesome/svg/xmark.svg?react"; import XmarkIcon from "@/assets/fontawesome/svg/xmark.svg?react";
// @ts-ignore
import PlusIcon from "@/assets/fontawesome/svg/plus.svg?react";
function Home() { function Home() {
const [newFolder] = useState(false); const [newFolder] = useState(false);
@ -129,11 +131,14 @@ function Home() {
<div className="h-full lg:w-3xl w-full"> <div className="h-full lg:w-3xl w-full">
<input <input
type="text" type="text"
id="noteTitle"
name=""
placeholder="Untitled note..." placeholder="Untitled note..."
value={editingNote?.title || ""} value={editingNote?.title || ""}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
className="w-full p-4 pb-0 text-3xl font-semibold bg-transparent focus:outline-none border-transparent focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text" className="w-full p-4 pb-0 text-3xl font-semibold bg-transparent focus:outline-none border-transparent focus:border-ctp-mauve transition-colors placeholder:text-ctp-overlay0 text-ctp-text"
/> />
<TagSelector />
{/*<div className="px-4 py-2 border-ctp-surface2 flex items-center gap-2 flex-wrap"> {/*<div className="px-4 py-2 border-ctp-surface2 flex items-center gap-2 flex-wrap">
{editingNote?.tags && {editingNote?.tags &&
editingNote.tags.map((tag) => ( editingNote.tags.map((tag) => (
@ -197,25 +202,99 @@ const Modal = () => {
export const TagSelector = () => { export const TagSelector = () => {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { data: tagTree, isLoading, error } = useTagTree(); const { data: tagTree, isLoading, error } = useTagTree();
const createTag = useCreateTag(); const createTag = useCreateTag();
const [expanded, setExpanded] = useState(false);
// Close when clicking outside the entire component
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
handleClose();
}
};
if (expanded) {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [expanded]);
// Focus input when expanded
useEffect(() => {
if (expanded && inputRef.current) {
inputRef.current.focus();
}
}, [expanded]);
const handleEnter = async () => { const handleEnter = async () => {
createTag.mutate({ name: value }); createTag.mutate({ name: value });
}; };
function handleClose() {
// If theres a value, submit it; otherwise just collapse
if (value.trim()) {
// onsubmit?.(value.trim());
}
setValue("");
setExpanded(false);
}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
e.preventDefault();
handleClose();
} else if (e.key === "Escape") {
// Abort without submitting
setValue("");
setExpanded(false);
}
}
return ( return (
<div> <div className="relative ml-4" ref={containerRef}>
<div className="inline-flex bg-ctp-surface0 rounded-full px-1 py-0.5 items-center">
<button
type="button"
onClick={() => setExpanded(true)}
className={`text-xs bg-transparent flex items-center justify-center
focus:outline-none transition-all duration-200 ease-out whitespace-nowrap text-ctp-subtext0
${expanded ? "opacity-0 w-0 px-0 pointer-events-none overflow-hidden" : "opacity-100 px-0.5"}
`}
>
<PlusIcon className="w-3 h-3 mr-1 fill-ctp-subtext0" />
{"Add"}
</button>
<input <input
type="text" ref={inputRef}
value={value} value={value}
onKeyDown={(e) => {
if (e.key === "Enter") handleEnter();
}}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKey}
placeholder={"Type here…"}
className={`text-xs z-30
bg-transparent px-1.5
focus:outline-none focus:ring-0
transition-all duration-200 ease-out
${expanded ? "w-32 opacity-100" : "w-0 opacity-0 pointer-events-none"}
`}
/> />
{tagTree && tagTree.map((tag) => <TagTree tag={tag} />)} </div>
<div className="absolute bg-ctp-base z-10">
{tagTree &&
tagTree
.filter((tag) =>
value == "" ? false : (tag.name + tag.parentPath).includes(value),
)
.map((tag) => <TagTree tag={tag} />)}
</div>
</div> </div>
); );
}; };
@ -230,7 +309,10 @@ export const TagTree = ({
const [collapse, setCollapse] = useState(false); const [collapse, setCollapse] = useState(false);
return ( return (
<div key={tag.id} className="flex flex-col relative"> <div
key={tag.id}
className="flex flex-col relative bg-ctp-surface0 pt-3 -translate-y-3 w-[136px]"
>
<div onClick={() => setCollapse(!collapse)}>{tag.name}</div> <div onClick={() => setCollapse(!collapse)}>{tag.name}</div>
<AnimatePresence> <AnimatePresence>
{collapse && ( {collapse && (

View file

@ -24,14 +24,14 @@ export const StatusIndicator = () => {
) : updating ? ( ) : updating ? (
<> <>
<SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" /> <SpinnerIcon className="animate-spin h-4 w-4 [&_.fa-primary]:fill-ctp-blue [&_.fa-secondary]:fill-ctp-sapphire" />
<span className="text-sm text-ctp-subtext0 font-medium"> {/*<span className="text-sm text-ctp-subtext0 font-medium">
Saving... Saving...
</span> </span>*/}
</> </>
) : ( ) : (
<> <>
<CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" /> <CheckIcon className="h-4 w-4 [&_.fa-primary]:fill-ctp-green [&_.fa-secondary]:fill-ctp-teal" />
<span className="text-sm text-ctp-subtext0 font-medium">Saved</span> {/*<span className="text-sm text-ctp-subtext0 font-medium">Saved</span>*/}
</> </>
)} )}
</div> </div>

View file

@ -79,7 +79,7 @@ export const TiptapEditor = ({
} }
return ( return (
<div className="tiptap-editor pt-0"> <div className="tiptap-editor pt-0!">
{/* Toolbar */} {/* Toolbar */}
{/*<div className="editor-toolbar"> {/*<div className="editor-toolbar">
<div className="toolbar-group"> <div className="toolbar-group">
@ -191,7 +191,7 @@ export const TiptapEditor = ({
{/* Editor content */} {/* Editor content */}
<EditorContent <EditorContent
editor={editor} editor={editor}
className="editor-content h-min-screen p-4!" className="editor-content h-min-screen p-4! pt-0!"
/> />
</div> </div>
); );