diff --git a/plugins/git/package.json b/plugins/git/package.json index d5149130..2cda8cb4 100644 --- a/plugins/git/package.json +++ b/plugins/git/package.json @@ -52,6 +52,8 @@ }, "devDependencies": { "@antfu/design": "catalog:frontend", + "@floating-ui/react": "catalog:frontend", + "@iconify-json/catppuccin": "catalog:frontend", "@pierre/diffs": "catalog:frontend", "@radix-ui/react-scroll-area": "catalog:frontend", "@radix-ui/react-separator": "catalog:frontend", diff --git a/plugins/git/src/client/components/branches-panel.tsx b/plugins/git/src/client/components/branches-panel.tsx deleted file mode 100644 index d6bb8a60..00000000 --- a/plugins/git/src/client/components/branches-panel.tsx +++ /dev/null @@ -1,13 +0,0 @@ -'use client' - -import type { DevframeRpcClient } from 'devframe/client' -import { useCallback } from 'react' -import { useRpcResource } from './use-rpc-resource' -import { BranchesPanelView } from './views/branches-panel-view' - -export function BranchesPanel() { - const loader = useCallback((rpc: DevframeRpcClient) => rpc.call('git:branches'), []) - const { data, loading, refresh } = useRpcResource(loader) - - return -} diff --git a/plugins/git/src/client/components/dashboard.tsx b/plugins/git/src/client/components/dashboard.tsx index 9ce75ad7..df295ff2 100644 --- a/plugins/git/src/client/components/dashboard.tsx +++ b/plugins/git/src/client/components/dashboard.tsx @@ -2,12 +2,10 @@ import type { DevframeRpcClient } from 'devframe/client' import type { PointerEvent as ReactPointerEvent } from 'react' -import type { Branch, GitBranches } from '../../index' +import type { GitBranches } from '../../index' import { useCallback, useEffect, useRef, useState } from 'react' import { nav as navBar, navBrand, tab as tabClass, tabsList } from '../lib/design' -import { cn } from '../lib/utils' import { CommitDetailsPanel } from './commit-details-panel' -import { DiffPanel } from './diff-panel' import { LogPanel } from './log-panel' import { RpcProvider, useRpc } from './rpc-provider' import { StatusPanel } from './status-panel' @@ -15,10 +13,9 @@ import { useTheme } from './theme' import { Badge } from './ui/badge' import { IconButton } from './ui/button' import { Icon } from './ui/icon' -import { Skeleton } from './ui/skeleton' import { useRpcResource } from './use-rpc-resource' -type DashboardPane = 'status' | 'commits' | 'diff' +type DashboardPane = 'commits' | 'changes' interface NavItem { id: DashboardPane @@ -27,9 +24,8 @@ interface NavItem { } const NAV_ITEMS: NavItem[] = [ - { id: 'status', label: 'Status', icon: 'i-ph-tree-view-duotone' }, { id: 'commits', label: 'Commits', icon: 'i-ph-git-commit-duotone' }, - { id: 'diff', label: 'Diff', icon: 'i-ph-git-diff-duotone' }, + { id: 'changes', label: 'Changes', icon: 'i-ph-git-diff-duotone' }, ] function clamp(value: number, min: number, max: number): number { @@ -117,47 +113,36 @@ function ThemeToggle() { ) } -function BranchRow({ - branch, - selected, +/** Branch picker, living in the nav bar. */ +function BranchSelect({ + branches, + disabled, + value, onSelect, }: { - branch: Branch - selected: boolean + branches: GitBranches | null + disabled: boolean + value: string | null onSelect: (name: string) => void }) { return ( -
  • - -
    - -
    - -
    - {commit.subject} -
    - {commit.shortHash} - {commit.author} - · - {relativeTime(commit.date)} + {isMounted && ( + +
    + { + onSelect?.(commit.hash) + setOpen(false) + }} + />
    -
    - + + )}
  • ) }) @@ -233,11 +374,6 @@ function WipRow({ col, color, gutter, changes }: { const mid = ROW_H / 2 return (
  • -
    -
    - +
    -
    +
    - Work in Progress + Work in Progress {changes} {' '} @@ -279,6 +415,7 @@ export function LogPanelView(props: LogPanelViewProps) { onRefresh, onLoadMore, onSelectCommit, + onLoadDetail, } = props const graph = useMemo( @@ -326,21 +463,24 @@ export function LogPanelView(props: LogPanelViewProps) { }, [hasMore, loading, commits.length]) return ( -
    -
    - - {isRepo - ? `${commits.length}${hasMore ? '+' : ''} commits${selectedRef ? ` · ${selectedRef}` : ''}` - : ' '} - +
    +
    +
    +

    Commit History

    + + {isRepo + ? `${commits.length}${hasMore ? '+' : ''}${selectedRef ? ` · ${selectedRef}` : ''}` + : ''} + +
    {!rpcConnected && ( -
    - {Array.from({ length: 5 }).map((_, i) => )} +
    + {Array.from({ length: 6 }).map((_, i) => )}
    )} @@ -357,7 +497,7 @@ export function LogPanelView(props: LogPanelViewProps) { )} {isRepo === true && commits.length > 0 && ( -
    +
      {showWip && headRow && ( @@ -373,6 +513,7 @@ export function LogPanelView(props: LogPanelViewProps) { topStub={i === 0 && showWip} selected={commit.hash === selectedHash} onSelect={onSelectCommit} + onLoadDetail={onLoadDetail} /> ))}
    diff --git a/plugins/git/src/client/components/views/status-panel-view.stories.tsx b/plugins/git/src/client/components/views/status-panel-view.stories.tsx index 65ee26f7..2677fc68 100644 --- a/plugins/git/src/client/components/views/status-panel-view.stories.tsx +++ b/plugins/git/src/client/components/views/status-panel-view.stories.tsx @@ -57,7 +57,7 @@ function Harness(props: Partial>) { } const meta = { - title: 'Panels/Status', + title: 'Panels/Changes', component: Harness, } satisfies Meta @@ -69,3 +69,13 @@ export const ReadOnly: Story = { args: { canWrite: false } } export const Clean: Story = { args: { data: clean } } export const Loading: Story = { args: { data: null, loading: true } } export const NotARepo: Story = { args: { data: { ...clean, isRepo: false } } } +export const FileSelected: Story = { + args: { + selectedKey: 'false:README.md', + patchSlot: ( +
    + diff --git a/README.md b/README.md +
    + ), + }, +} diff --git a/plugins/git/src/client/components/views/status-panel-view.tsx b/plugins/git/src/client/components/views/status-panel-view.tsx index 101a855f..a57609ef 100644 --- a/plugins/git/src/client/components/views/status-panel-view.tsx +++ b/plugins/git/src/client/components/views/status-panel-view.tsx @@ -1,12 +1,15 @@ 'use client' import type { ReactNode } from 'react' -import type { FileStatusCode, GitStatus, StatusFileEntry } from '../../../index' +import type { GitStatus, StatusFileEntry } from '../../../index' +import { cn } from '../../lib/utils' import { Badge } from '../ui/badge' import { Button, IconButton } from '../ui/button' +import { FileIcon } from '../ui/file-icon' import { Icon } from '../ui/icon' import { ScrollArea } from '../ui/scroll-area' import { Skeleton } from '../ui/skeleton' +import { StatusMark } from '../ui/status-mark' import { Textarea } from '../ui/textarea' export interface StatusPanelViewProps { @@ -16,44 +19,47 @@ export interface StatusPanelViewProps { canWrite: boolean message: string note: string | null + /** `${staged}:${path}` of the file whose diff is shown, or `null`. */ + selectedKey?: string | null onRefresh: () => void | Promise onStage: (paths: string[]) => void | Promise onUnstage: (paths: string[]) => void | Promise onCommit: () => void | Promise onMessageChange: (value: string) => void + /** Reveal a file's diff. `staged` picks the index-vs-HEAD diff. */ + onSelectFile?: (path: string, staged: boolean) => void + /** The selected file's diff viewer, rendered below the file list. */ + patchSlot?: ReactNode } -const STATUS_LABEL: Record = { - 'modified': 'M', - 'added': 'A', - 'deleted': 'D', - 'renamed': 'R', - 'copied': 'C', - 'type-changed': 'T', - 'unmerged': 'U', - 'unknown': '?', +function fileKey(path: string, staged: boolean): string { + return `${staged}:${path}` } -function statusColor(code: FileStatusCode): string { - switch (code) { - case 'added': return 'text-success' - case 'deleted': return 'text-error' - case 'modified': return 'text-warning' - case 'unmerged': return 'text-error' - default: return 'color-muted' - } -} - -function FileRow({ entry, action }: { entry: StatusFileEntry, action?: ReactNode }) { +function FileRow({ entry, staged, selected, action, onSelect }: { + entry: StatusFileEntry + staged: boolean + selected: boolean + action?: ReactNode + onSelect?: (path: string, staged: boolean) => void +}) { const label = entry.from ? `${entry.from} → ${entry.path}` : entry.path return ( -
  • - - {STATUS_LABEL[entry.status]} - - - {label} - +
  • + {action}
  • ) @@ -87,7 +93,7 @@ function Section({ } export function StatusPanelView(props: StatusPanelViewProps) { - const { data, loading, busy, canWrite, message, note, onRefresh, onStage, onUnstage, onCommit, onMessageChange } = props + const { data, loading, busy, canWrite, message, note, selectedKey, onRefresh, onStage, onUnstage, onCommit, onMessageChange, onSelectFile, patchSlot } = props const stageBtn = (paths: string[], label: string) => ( onStage(paths)}> @@ -103,48 +109,45 @@ export function StatusPanelView(props: StatusPanelViewProps) { return (
    -
    - {data?.isRepo - ? ( - <> - - - {data.detached ? `detached @ ${data.head}` : data.branch} - - {data.upstream && ( - - {data.upstream} - {data.ahead > 0 && ( - - - {data.ahead} - - )} - {data.behind > 0 && ( - - - {data.behind} - - )} - - )} - {data.clean - ? ( - - - clean - - ) - : working tree dirty} - - ) - : } +
    +

    Changes

    + {data?.isRepo && !data.clean && ( + + {data.staged.length + data.unstaged.length + data.untracked.length} + {' '} + changed + + )} +
    +
    + {data?.isRepo && (data.ahead > 0 || data.behind > 0) && ( + + {data.ahead > 0 && ( + + + {data.ahead} + + )} + {data.behind > 0 && ( + + + {data.behind} + + )} + + )} + + +
    - - -
    + {!data && ( +
    + {Array.from({ length: 4 }).map((_, i) => )} +
    + )} + {data && !data.isRepo && (

    The working directory is not a git repository.

    )} @@ -168,6 +171,9 @@ export function StatusPanelView(props: StatusPanelViewProps) { ))} @@ -184,6 +190,9 @@ export function StatusPanelView(props: StatusPanelViewProps) { ))} @@ -200,6 +209,9 @@ export function StatusPanelView(props: StatusPanelViewProps) { ))} @@ -207,6 +219,12 @@ export function StatusPanelView(props: StatusPanelViewProps) {
    + {patchSlot && ( +
    + {patchSlot} +
    + )} + {canWrite && data.staged.length > 0 && (