diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx index 7f66c554..cd4ac593 100644 --- a/src/components/command-palette/CommandPalette.tsx +++ b/src/components/command-palette/CommandPalette.tsx @@ -95,7 +95,7 @@ export function CommandPalette({ notesFolder, } = useNotes(); const { setTheme } = useTheme(); - const { status, gitAvailable, gitEnabled, commit, sync, isSyncing } = useGit(); + const { status, gitAvailable, gitEnabled, sync, isSyncing } = useGit(); const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -376,15 +376,12 @@ export function CommandPalette({ if (hasChanges) { baseCommands.push({ id: "git-commit", - label: "Git: Quick Commit", + label: "Git: Commit Changes", icon: , action: async () => { - const success = await commit("Quick commit from Scratch"); - if (success) { - toast.success("Changes committed"); - } else { - toast.error("Failed to commit"); - } + window.dispatchEvent( + new CustomEvent("open-commit-panel") + ) onClose(); }, }); @@ -514,7 +511,6 @@ export function CommandPalette({ gitEnabled, gitAvailable, status, - commit, sync, isSyncing, selectNote, diff --git a/src/components/git/CommitPanel.tsx b/src/components/git/CommitPanel.tsx new file mode 100644 index 00000000..5222b0bd --- /dev/null +++ b/src/components/git/CommitPanel.tsx @@ -0,0 +1,118 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type KeyboardEvent, +} from "react"; +import { toast } from "sonner"; +import { useGit } from "../../context/GitContext"; +import { Button, Textarea } from "../ui"; + +const DEFAULT_COMMIT_MESSAGE = "Quick Commit from Scratch"; + +interface CommitPanelProps { + open: boolean; + onClose: () => void; +} + +export function CommitPanel({ open, onClose }: CommitPanelProps) { + const { commit, isCommitting } = useGit(); + + const [message, setMessage] = useState(DEFAULT_COMMIT_MESSAGE); + + const textareaRef = useRef(null); + + useEffect(() => { + if (!open) { + return; + } + + setMessage(DEFAULT_COMMIT_MESSAGE); + + requestAnimationFrame(() => { + textareaRef.current?.focus(); + textareaRef.current?.select(); + }); + }, [open]); + + const canCommit = useMemo(() => message.trim().length > 0, [message]); + + const handleCommit = useCallback(async () => { + const commitMessage = message.trim(); + + if (!commitMessage || isCommitting) { + return; + } + + try { + const success = await commit(commitMessage); + + if (success) { + toast.success("Changes committed"); + onClose(); + } else { + toast.error("Failed to commit"); + } + } catch { + toast.error("Failed to commit"); + } + }, [commit, isCommitting, message, onClose]); + + const handleKeyDown = useCallback( + async (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + onClose(); + return; + } + + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + await handleCommit(); + } + }, + [handleCommit, onClose], + ); + + if (!open) { + return null; + } + + return ( + + + Commit Changes + + + Enter to commit • Shift+Enter for newline + + + + setMessage(event.target.value)} + onKeyDown={handleKeyDown} + rows={4} + placeholder="Commit message" + className="min-h-24" + /> + + + + Cancel + + + + {isCommitting ? "Committing..." : "Commit"} + + + + ); +} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 8faac354..3b5f65a1 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -15,9 +15,13 @@ import { mod, isMac } from "../../lib/platform"; interface FooterProps { onOpenSettings?: () => void; + onOpenCommit?: () => void; } -export const Footer = memo(function Footer({ onOpenSettings }: FooterProps) { +export const Footer = memo(function Footer({ + onOpenSettings, + onOpenCommit, +}: FooterProps) { const { status, isLoading, @@ -27,24 +31,13 @@ export const Footer = memo(function Footer({ onOpenSettings }: FooterProps) { gitEnabled, sync, initRepo, - commit, lastError, clearError, } = useGit(); - const handleCommit = useCallback(async () => { - if (isCommitting) return; - try { - const success = await commit("Quick commit from Scratch"); - if (success) { - toast.success("Changes committed"); - } else { - toast.error("Failed to commit"); - } - } catch { - toast.error("Failed to commit"); - } - }, [commit, isCommitting]); + const handleCommit = useCallback(() => { + onOpenCommit?.(); + }, [onOpenCommit]); const handleSync = useCallback(async () => { if (isSyncing) return; @@ -209,7 +202,7 @@ export const Footer = memo(function Footer({ onOpenSettings }: FooterProps) { {isCommitting ? ( diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index d2ad1884..91d90f01 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -26,6 +26,7 @@ import { import { mod, shift, isMac } from "../../lib/platform"; import * as notesService from "../../services/notes"; import { FolderNameDialog } from "../notes/FolderNameDialog"; +import { CommitPanel } from "../git/CommitPanel"; interface SidebarProps { onOpenSettings?: () => void; @@ -53,6 +54,7 @@ export function Sidebar({ onOpenSettings }: SidebarProps) { const [dragCount, setDragCount] = useState(1); const [multiSelectedNoteIds, setMultiSelectedNoteIds] = useState>(new Set()); const [lastClickedNoteId, setLastClickedNoteId] = useState(null); + const [commitPanelOpen, setCommitPanelOpen] = useState(false); const debounceRef = useRef(null); const searchInputRef = useRef(null); const multiSelectedRef = useRef(multiSelectedNoteIds) as RefObject>; @@ -303,6 +305,22 @@ export function Sidebar({ onOpenSettings }: SidebarProps) { window.removeEventListener("create-new-folder", handleCreateFolder); }, [selectedNoteId]); + const handleOpenCommitPanel = useCallback(() => { + setCommitPanelOpen(true); + }, []); + + const handleCloseCommitPanel = useCallback(() => { + setCommitPanelOpen(false); + }, []); + + useEffect(() => { + window.addEventListener("open-commit-panel", handleOpenCommitPanel); + + return () => { + window.removeEventListener("open-commit-panel", handleOpenCommitPanel); + }; + }, [handleOpenCommitPanel]); + return ( + + {/* Footer with git status, commit, and settings */} - + {/* Folder name dialog */} {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ); + }, +); + +Textarea.displayName = "Textarea"; + +export { Textarea }; diff --git a/src/components/ui/index.tsx b/src/components/ui/index.tsx index 8de365e7..a961ebff 100644 --- a/src/components/ui/index.tsx +++ b/src/components/ui/index.tsx @@ -15,6 +15,7 @@ export { export { Button } from "./Button"; export { CodeCopyButton } from "./CodeCopyButton"; export { Input } from "./Input"; +export { Textarea } from "./Textarea"; export { Select } from "./Select"; export { Toaster } from "./Toaster"; export { @@ -52,7 +53,7 @@ export function ToolbarButton({ isActive ? "bg-bg-muted text-text" : "hover:bg-bg-muted text-text-muted", - className + className, )} tabIndex={-1} aria-label={title} @@ -70,8 +71,7 @@ export function ToolbarButton({ } // Icon button (for sidebar actions, etc.) -export interface IconButtonProps - extends React.ButtonHTMLAttributes { +export interface IconButtonProps extends React.ButtonHTMLAttributes { children: ReactNode; size?: "xs" | "sm" | "md" | "lg" | "xl"; variant?: "primary" | "default" | "secondary" | "ghost" | "outline"; @@ -98,7 +98,7 @@ const iconButtonVariants = { export const IconButton = React.forwardRef( ( { className, children, title, size = "sm", variant = "ghost", ...props }, - ref + ref, ) => { const button = ( ( "disabled:pointer-events-none disabled:opacity-50 cursor-pointer", iconButtonSizes[size], iconButtonVariants[variant], - className + className, )} tabIndex={-1} aria-label={title} @@ -124,7 +124,7 @@ export const IconButton = React.forwardRef( } return button; - } + }, ); IconButton.displayName = "IconButton"; @@ -166,7 +166,7 @@ export function ListItem({ "focus:outline-none focus-visible:outline-none", isSelected ? "bg-bg-muted group-focus/notelist:ring-1 group-focus/notelist:ring-text-muted" - : "hover:bg-bg-muted" + : "hover:bg-bg-muted", )} > @@ -184,7 +184,7 @@ export function ListItem({ {meta} @@ -194,7 +194,7 @@ export function ListItem({ className={cn( "text-xs line-clamp-1 min-h-5", hasSubtitle ? "text-text-muted" : "text-transparent", - isSelected ? "opacity-100" : "opacity-70" + isSelected ? "opacity-100" : "opacity-70", )} > {hasSubtitle ? cleanSubtitle : "\u00A0"} @@ -233,7 +233,7 @@ export function CommandItem({ tabIndex={-1} className={cn( "w-full text-left px-3 py-2 rounded-lg flex items-center justify-between transition-colors cursor-pointer", - isSelected ? "bg-bg-muted text-text" : "text-text hover:bg-bg-muted" + isSelected ? "bg-bg-muted text-text" : "text-text hover:bg-bg-muted", )} > @@ -242,7 +242,7 @@ export function CommandItem({ className={cn( "shrink-0 flex items-center justify-center text-text-muted", variant === "note" && - "w-9 h-9 rounded-md bg-bg-emphasis flex items-center justify-center" + "w-9 h-9 rounded-md bg-bg-emphasis flex items-center justify-center", )} > {iconText ? ( @@ -265,7 +265,9 @@ export function CommandItem({ {shortcut}
+ Enter to commit • Shift+Enter for newline +