a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

small tweaks for buttons and layout

+143 -72
+71 -34
apps/desktop/src/components/AppHeader.tsx
··· 1 - import { FolderOpenIcon, RefreshCwIcon, SearchIcon } from "lucide-react"; 1 + import { Columns2Icon, FolderOpenIcon, ListIcon, RefreshCwIcon, SearchIcon } from "lucide-react"; 2 2 import { Button } from "@/components/ui/button"; 3 3 4 4 interface AppHeaderProps { ··· 6 6 onOpenProject: () => void; 7 7 onSync: () => void; 8 8 onOpenSearch: () => void; 9 + viewMode: 1 | 2; 10 + onChangeViewMode: (mode: 1 | 2) => void; 9 11 isSyncing?: boolean; 10 12 } 11 13 ··· 14 16 onOpenProject, 15 17 onSync, 16 18 onOpenSearch, 19 + viewMode, 20 + onChangeViewMode, 17 21 isSyncing = false, 18 22 }: AppHeaderProps) { 19 23 return ( ··· 21 25 className="h-10 flex items-center justify-between px-3 border-b border-border bg-background shrink-0" 22 26 data-tauri-drag-region 23 27 > 24 - {/* Left: Project/Repository */} 25 - <Button 26 - variant="ghost" 27 - size="sm" 28 - className="h-7 px-2 gap-1.5 text-sm font-medium" 29 - onClick={onOpenProject} 30 - > 31 - <FolderOpenIcon className="size-4" /> 32 - <span className="truncate max-w-[200px]">{projectName ?? "Open Repository"}</span> 33 - </Button> 28 + {/* Left: Project/Repository + view mode */} 29 + <div className="flex items-center gap-1.5 min-w-0"> 30 + <Button 31 + variant="ghost" 32 + size="sm" 33 + className="h-7 px-2 gap-1.5 text-sm font-medium" 34 + onClick={onOpenProject} 35 + > 36 + <FolderOpenIcon className="size-4" /> 37 + <span className="truncate max-w-[200px]">{projectName ?? "Open Repository"}</span> 38 + </Button> 39 + <div 40 + className="relative flex items-center rounded-md border border-border/70 bg-muted/60 p-0.5 shadow-inner" 41 + aria-label="View mode" 42 + > 43 + <div 44 + className={`absolute top-0.5 h-6 w-6 rounded-sm bg-background shadow-sm transition-transform duration-200 ${ 45 + viewMode === 1 ? "translate-x-0" : "translate-x-6" 46 + }`} 47 + /> 48 + <button 49 + type="button" 50 + className="relative z-10 inline-flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground transition-colors disabled:opacity-50 disabled:pointer-events-none" 51 + onClick={() => onChangeViewMode(1)} 52 + title="Overview mode (1)" 53 + aria-label="Overview mode" 54 + disabled={!projectName} 55 + > 56 + <ListIcon className={`size-3.5 ${viewMode === 1 ? "text-foreground" : ""}`} /> 57 + </button> 58 + <button 59 + type="button" 60 + className="relative z-10 inline-flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground transition-colors disabled:opacity-50 disabled:pointer-events-none" 61 + onClick={() => onChangeViewMode(2)} 62 + title="Split mode (2)" 63 + aria-label="Split mode" 64 + disabled={!projectName} 65 + > 66 + <Columns2Icon className={`size-3.5 ${viewMode === 2 ? "text-foreground" : ""}`} /> 67 + </button> 68 + </div> 69 + </div> 34 70 35 - {/* Center: Revision search */} 36 - <Button 37 - variant="ghost" 38 - size="sm" 39 - className="h-7 px-2 gap-1.5 text-sm text-muted-foreground" 40 - onClick={onOpenSearch} 41 - > 42 - <SearchIcon className="size-4" /> 43 - <span>Search revisions...</span> 44 - <kbd className="ml-2 text-xs bg-muted px-1.5 py-0.5 rounded">/</kbd> 45 - </Button> 46 - 47 - {/* Right: Sync */} 48 - <Button 49 - variant="ghost" 50 - size="sm" 51 - className="h-7 px-2 gap-1.5 text-sm" 52 - onClick={onSync} 53 - disabled={isSyncing || !projectName} 54 - > 55 - <RefreshCwIcon className={`size-4 ${isSyncing ? "animate-spin" : ""}`} /> 56 - <span>Sync</span> 57 - </Button> 71 + {/* Right: sync + revision search */} 72 + <div className="flex items-center gap-1 ml-auto"> 73 + <Button 74 + variant="ghost" 75 + size="icon-sm" 76 + className="h-7 w-7" 77 + onClick={onSync} 78 + disabled={isSyncing || !projectName} 79 + title="Sync" 80 + aria-label="Sync" 81 + > 82 + <RefreshCwIcon className={`size-4 ${isSyncing ? "animate-spin" : ""}`} /> 83 + </Button> 84 + <Button 85 + variant="ghost" 86 + size="icon-sm" 87 + className="h-7 w-7 text-muted-foreground" 88 + onClick={onOpenSearch} 89 + title="Search revisions (/)" 90 + aria-label="Search revisions" 91 + > 92 + <SearchIcon className="size-4" /> 93 + </Button> 94 + </div> 58 95 </header> 59 96 ); 60 97 }
+13 -7
apps/desktop/src/components/AppShell.tsx
··· 3 3 import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; 4 4 import { Profiler, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; 5 5 import { Route as ProjectRoute } from "@/routes/project.$projectId"; 6 - import { debouncedChangeIdAtom, expandedStacksAtom, viewModeAtom } from "@/atoms"; 6 + import { debouncedChangeIdAtom, expandedStacksAtom, searchOpenAtom, viewModeAtom } from "@/atoms"; 7 7 8 8 const NARROW_BREAKPOINT = 768; 9 9 ··· 95 95 onOpenProject={() => setProjectPickerOpen(true)} 96 96 onSync={() => {}} 97 97 onOpenSearch={() => {}} 98 + viewMode={1} 99 + onChangeViewMode={() => {}} 98 100 /> 99 101 <div className="flex-1 min-h-0 flex items-center justify-center text-muted-foreground"> 100 102 <p>Select or add a repository to get started</p> ··· 112 114 const rev = useSearch({ from: ProjectRoute.fullPath, select: (s) => s.rev }); 113 115 const [flash, setFlash] = useState<{ changeId: string; key: number } | null>(null); 114 116 const [viewMode, setViewMode] = useAtom(viewModeAtom); 117 + const [, setSearchOpen] = useAtom(searchOpenAtom); 115 118 const [pendingAbandon, setPendingAbandon] = useState<Revision | null>(null); 116 119 const [projectPickerOpen, setProjectPickerOpen] = useState(false); 117 120 const [isSyncing, setIsSyncing] = useState(false); ··· 183 186 clearTimeout(debounceTimerRef.current); 184 187 } 185 188 186 - // Update after 200ms of no changes 189 + // Update after 50ms of no changes 187 190 debounceTimerRef.current = setTimeout(() => { 188 191 setDebouncedChangeId(selectedChangeId); 189 - }, 200); 192 + }, 50); 190 193 191 194 return () => { 192 195 if (debounceTimerRef.current) { ··· 378 381 } 379 382 380 383 function handleOpenSearch() { 381 - // Focus the revision search dialog 382 - // The "/" key already triggers this via Search component 383 - window.dispatchEvent(new KeyboardEvent("keydown", { key: "/" })); 384 + setSearchOpen(true); 384 385 } 385 386 386 387 return ( ··· 414 415 onOpenProject={() => setProjectPickerOpen(true)} 415 416 onSync={handleSync} 416 417 onOpenSearch={handleOpenSearch} 418 + viewMode={viewMode} 419 + onChangeViewMode={setViewMode} 417 420 isSyncing={isSyncing} 418 421 /> 419 422 <div className="flex-1 min-h-0"> ··· 464 467 </Profiler> 465 468 </section> 466 469 </ResizablePanel> 467 - <ResizableHandle withHandle /> 470 + <ResizableHandle 471 + withHandle 472 + orientation={isNarrowScreen ? "vertical" : "horizontal"} 473 + /> 468 474 <ResizablePanel defaultSize={isNarrowScreen ? 60 : 75} minSize={30}> 469 475 <aside className="h-full" aria-label="Diff viewer"> 470 476 <Profiler id="DiffPanel" onRender={onRenderCallback}>
+21 -14
apps/desktop/src/components/DiffPanel.tsx
··· 6 6 import { type DiffStyle, type DiffViewState, diffStyleAtom, diffViewStateAtom } from "@/atoms"; 7 7 import { FileList, RevisionHeader } from "@/components/diff"; 8 8 import { ImageDiff } from "@/components/diff/ImageDiff"; 9 - import { Button } from "@/components/ui/button"; 10 9 import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; 11 10 import { ScrollArea } from "@/components/ui/scroll-area"; 12 11 import { Skeleton } from "@/components/ui/skeleton"; ··· 480 479 481 480 {/* Toolbar */} 482 481 <div className="flex items-center justify-end px-3 py-2 border-b border-border bg-background shrink-0 min-w-0"> 483 - <div className="flex items-center gap-0.5 shrink-0"> 484 - <Button 485 - variant={effectiveDiffStyle === "unified" ? "secondary" : "ghost"} 486 - size="icon-xs" 482 + <div 483 + className="relative flex items-center rounded-md border border-border/70 bg-muted/60 p-0.5 shadow-inner shrink-0" 484 + aria-label="Diff style" 485 + > 486 + <div 487 + className={`absolute top-0.5 h-5 w-5 rounded-sm bg-background shadow-sm transition-transform duration-200 ${ 488 + effectiveDiffStyle === "unified" ? "translate-x-0" : "translate-x-5" 489 + }`} 490 + /> 491 + <button 492 + type="button" 493 + className="relative z-10 inline-flex h-5 w-5 items-center justify-center rounded-sm text-muted-foreground transition-colors disabled:opacity-50 disabled:pointer-events-none" 487 494 onClick={() => handleSetLocalStyle("unified")} 488 495 title="Unified diff view" 489 - className="h-6 w-6" 496 + aria-label="Unified diff view" 490 497 disabled={effectiveSelectedFiles.size === 0} 491 498 > 492 - <RowsIcon className="size-3" /> 493 - </Button> 494 - <Button 495 - variant={effectiveDiffStyle === "split" ? "secondary" : "ghost"} 496 - size="icon-xs" 499 + <RowsIcon className={`size-3 ${effectiveDiffStyle === "unified" ? "text-foreground" : ""}`} /> 500 + </button> 501 + <button 502 + type="button" 503 + className="relative z-10 inline-flex h-5 w-5 items-center justify-center rounded-sm text-muted-foreground transition-colors disabled:opacity-50 disabled:pointer-events-none" 497 504 onClick={() => handleSetLocalStyle("split")} 498 505 title="Split diff view" 499 - className="h-6 w-6" 506 + aria-label="Split diff view" 500 507 disabled={effectiveSelectedFiles.size === 0} 501 508 > 502 - <Columns2Icon className="size-3" /> 503 - </Button> 509 + <Columns2Icon className={`size-3 ${effectiveDiffStyle === "split" ? "text-foreground" : ""}`} /> 510 + </button> 504 511 </div> 505 512 </div> 506 513
+27 -13
apps/desktop/src/components/diff/FileList.tsx
··· 12 12 SearchIcon, 13 13 } from "lucide-react"; 14 14 import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 15 - import { Button } from "@/components/ui/button"; 16 15 import { Input } from "@/components/ui/input"; 17 16 import { ScrollArea } from "@/components/ui/scroll-area"; 18 17 import { useKeyboardShortcut } from "@/hooks/useKeyboard"; ··· 591 590 {" / "} 592 591 <span className="text-red-500">-{totalDeletions}</span> 593 592 </span> 594 - <Button 595 - variant="ghost" 596 - size="icon" 597 - className="size-6" 598 - onClick={() => setViewMode(viewMode === "flat" ? "tree" : "flat")} 599 - title={viewMode === "flat" ? "Switch to tree view" : "Switch to flat list"} 593 + <div 594 + className="relative flex items-center rounded-md border border-border/70 bg-muted/60 p-0.5 shadow-inner" 595 + aria-label="File list view mode" 600 596 > 601 - {viewMode === "flat" ? ( 602 - <FolderTreeIcon className="size-3.5" /> 603 - ) : ( 604 - <ListIcon className="size-3.5" /> 605 - )} 606 - </Button> 597 + <div 598 + className={`absolute top-0.5 h-5 w-5 rounded-sm bg-background shadow-sm transition-transform duration-200 ${ 599 + viewMode === "flat" ? "translate-x-0" : "translate-x-5" 600 + }`} 601 + /> 602 + <button 603 + type="button" 604 + className="relative z-10 inline-flex h-5 w-5 items-center justify-center rounded-sm text-muted-foreground transition-colors" 605 + onClick={() => setViewMode("flat")} 606 + title="Flat file list" 607 + aria-label="Flat file list" 608 + > 609 + <ListIcon className={`size-3 ${viewMode === "flat" ? "text-foreground" : ""}`} /> 610 + </button> 611 + <button 612 + type="button" 613 + className="relative z-10 inline-flex h-5 w-5 items-center justify-center rounded-sm text-muted-foreground transition-colors" 614 + onClick={() => setViewMode("tree")} 615 + title="Tree file list" 616 + aria-label="Tree file list" 617 + > 618 + <FolderTreeIcon className={`size-3 ${viewMode === "tree" ? "text-foreground" : ""}`} /> 619 + </button> 620 + </div> 607 621 </div> 608 622 </div> 609 623 </div>
-3
apps/desktop/src/components/revision-graph/index.tsx
··· 335 335 }, 336 336 ref, 337 337 ) { 338 - const renderCount = useRef(0); 339 - console.log(`RevisionGraph Render #${++renderCount.current}`); 340 - 341 338 const parentRef = useRef<HTMLDivElement>(null); 342 339 const containerRef = useRef<HTMLDivElement>(null); 343 340 const prefetchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+11 -1
apps/desktop/src/components/ui/resizable.tsx
··· 3 3 4 4 import { cn } from "@/lib/utils"; 5 5 6 - function ResizablePanelGroup({ className, ...props }: React.ComponentProps<typeof Group>) { 6 + type PanelDirection = "horizontal" | "vertical"; 7 + 8 + type ResizablePanelGroupProps = React.ComponentProps<typeof Group> & { 9 + /** 10 + * Optional alias for callers that use `direction` naming. 11 + */ 12 + direction?: PanelDirection; 13 + }; 14 + 15 + function ResizablePanelGroup({ className, orientation, direction, ...props }: ResizablePanelGroupProps) { 7 16 return ( 8 17 <Group 9 18 data-slot="resizable-panel-group" 19 + orientation={orientation ?? direction ?? "horizontal"} 10 20 className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)} 11 21 {...props} 12 22 />