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";
// @ts-ignore
import XmarkIcon from "@/assets/fontawesome/svg/xmark.svg?react";
// @ts-ignore
import PlusIcon from "@/assets/fontawesome/svg/plus.svg?react";
function Home() {
const [newFolder] = useState(false);
@ -129,11 +131,14 @@ function Home() {
<div className="h-full lg:w-3xl w-full">
<input
type="text"
id="noteTitle"
name=""
placeholder="Untitled note..."
value={editingNote?.title || ""}
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"
/>
<TagSelector />
{/*<div className="px-4 py-2 border-ctp-surface2 flex items-center gap-2 flex-wrap">
{editingNote?.tags &&
editingNote.tags.map((tag) => (
@ -197,25 +202,99 @@ const Modal = () => {
export const TagSelector = () => {
const [value, setValue] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { data: tagTree, isLoading, error } = useTagTree();
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 () => {
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 (
<div>
<input
type="text"
value={value}
onKeyDown={(e) => {
if (e.key === "Enter") handleEnter();
}}
onChange={(e) => setValue(e.target.value)}
/>
{tagTree && tagTree.map((tag) => <TagTree tag={tag} />)}
<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
ref={inputRef}
value={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"}
`}
/>
</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>
);
};
@ -230,7 +309,10 @@ export const TagTree = ({
const [collapse, setCollapse] = useState(false);
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>
<AnimatePresence>
{collapse && (

View file

@ -24,14 +24,14 @@ export const StatusIndicator = () => {
) : updating ? (
<>
<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...
</span>
</span>*/}
</>
) : (
<>
<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>

View file

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