feat(ui): redesign workspace shell with context panel, tabbed sidebars, and faster diff UX (#433)
* feat: tabbed right sidebar, context panel, floating diff comments * fix: auto-close left sidebar when context panel opens - Increase default context panel width from 520 to 600 pixels - Increase sidebar minimum width from 200 to 300 pixels - Replace collapsible component with custom button in diff view * refactoring: rework sidebars, tabs, and file tree layout - Rewrite AnimatedTabs as segment-style with sliding indicator - Upgrade SidebarFilesTree to match FilesView features (context menus, git status, file icons, CRUD dialogs, fuzzy search ranking) - Restructure FilesView header: tabs row + actions row, remove breadcrumbs - Show relative path in context panel header, track active tab - Allow left sidebar to stay open alongside context panel - Hide diff/files tabs from header on desktop (mobile-only) - Move chevron after group name in session sidebar - Compact tab heights in right sidebar and git view - Size PreviewToggleButton to match other action buttons - Remove directory loading spinner from folder icons * feat: add project icon and color customization - Enable users to assign custom icons to projects - Allow users to choose accent colors for projects - Stabilize repo status UI during project switching * feat: add scroll fade indicators to editor tabs * style: reduce spacing and icon sizes in header * style: adjust tab component padding from uniform to vertical-horizontal * feat: Add session state indicators to project tabs * feat: Enhance session status handling and improve UI responsiveness * fix: preserve upstream tracking on branch rename * fix: improve initial remote selection for pull requests - Uses saved remote name from previous session when available - Selects remote based on tracking branch when possible - Falls back to origin or first available remote * perf(diff): faster highlight, stable stacked scroll - split/unified Pierre worker pools; prefer shiki-wasm - align diff CSS line-height; disable scroll anchoring; drop WebKit compositing hacks - harden stacked pin/align (cancel on user scroll/input); prevent overscroll - make overlay scrollbar MutationObserver optional; disable for diff container * feat: handle binary files in diff view * fix: adjust project tabs layout and drag regions * style: update drag overlay visual styling * feat: enable number keys to switch projects in the sidebar * fix: recognize octet-stream as text-based MIME type * feat: add keyboard navigation to context panel * feat: add session pinning to sidebar - Pin important sessions to keep them at the top - Pinned sessions persist across browser sessions * refactor: move context usage display from chat input to header
This commit is contained in:
committed by
GitHub
parent
12606b9e53
commit
47c943b487
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
|
||||
import { InlineCommentCard } from './InlineCommentCard';
|
||||
import { InlineCommentInput } from './InlineCommentInput';
|
||||
|
||||
type SelectedLineRange = { start: number; end: number };
|
||||
|
||||
type CommentPos = {
|
||||
top: number;
|
||||
flipUp: boolean;
|
||||
};
|
||||
|
||||
const COMMENT_POPOVER_HEIGHT = 200;
|
||||
|
||||
function getLineTop(view: EditorView, wrapper: HTMLElement, lineNumber: number, position: 'top' | 'bottom'): number | undefined {
|
||||
const lineCount = view.state.doc.lines;
|
||||
if (lineNumber < 1 || lineNumber > lineCount) return undefined;
|
||||
|
||||
const line = view.state.doc.line(lineNumber);
|
||||
const coords = view.coordsAtPos(line.from);
|
||||
if (!coords) return undefined;
|
||||
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
if (position === 'bottom') {
|
||||
return coords.bottom - wrapperRect.top;
|
||||
}
|
||||
return coords.top - wrapperRect.top;
|
||||
}
|
||||
|
||||
function shouldFlipUp(view: EditorView, endLine: number, scrollContainer: HTMLElement | null): boolean {
|
||||
const lineCount = view.state.doc.lines;
|
||||
if (endLine < 1 || endLine > lineCount) return false;
|
||||
|
||||
const line = view.state.doc.line(endLine);
|
||||
const coords = view.coordsAtPos(line.from);
|
||||
if (!coords) return false;
|
||||
|
||||
const viewportBottom = scrollContainer
|
||||
? scrollContainer.getBoundingClientRect().bottom
|
||||
: window.innerHeight;
|
||||
|
||||
return (coords.bottom + COMMENT_POPOVER_HEIGHT + 30) > viewportBottom;
|
||||
}
|
||||
|
||||
function computePosition(
|
||||
view: EditorView,
|
||||
wrapper: HTMLElement,
|
||||
scrollContainer: HTMLElement | null,
|
||||
range: { start: number; end: number },
|
||||
): CommentPos | undefined {
|
||||
const flipUp = shouldFlipUp(view, range.end, scrollContainer);
|
||||
|
||||
const top = flipUp
|
||||
? getLineTop(view, wrapper, range.start, 'top')
|
||||
: getLineTop(view, wrapper, range.end, 'bottom');
|
||||
|
||||
if (top === undefined) return undefined;
|
||||
return { top, flipUp };
|
||||
}
|
||||
|
||||
type FloatingCommentsProps = {
|
||||
editorView: EditorView | null;
|
||||
wrapperRef: React.RefObject<HTMLElement | null>;
|
||||
fileDrafts: InlineCommentDraft[];
|
||||
editingDraftId: string | null;
|
||||
commentText: string;
|
||||
lineSelection: SelectedLineRange | null;
|
||||
isDragging: boolean;
|
||||
fileLabel: string;
|
||||
onSaveComment: (text: string, range?: SelectedLineRange) => void;
|
||||
onCancelComment: () => void;
|
||||
onEditDraft: (draft: InlineCommentDraft) => void;
|
||||
onDeleteDraft: (draft: InlineCommentDraft) => void;
|
||||
};
|
||||
|
||||
export function useFloatingComments({
|
||||
editorView,
|
||||
wrapperRef,
|
||||
fileDrafts,
|
||||
editingDraftId,
|
||||
commentText,
|
||||
lineSelection,
|
||||
isDragging,
|
||||
fileLabel,
|
||||
onSaveComment,
|
||||
onCancelComment,
|
||||
onEditDraft,
|
||||
onDeleteDraft,
|
||||
}: FloatingCommentsProps): React.ReactNode {
|
||||
const [positions, setPositions] = React.useState<Record<string, CommentPos | undefined>>({});
|
||||
|
||||
const updatePositions = React.useCallback(() => {
|
||||
const view = editorView;
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!view || !wrapper) return;
|
||||
|
||||
const scrollContainer = wrapper.closest('.overlay-scrollbar-container') as HTMLElement | null;
|
||||
const next: Record<string, CommentPos | undefined> = {};
|
||||
|
||||
for (const d of fileDrafts) {
|
||||
next[d.id] = computePosition(view, wrapper, scrollContainer, {
|
||||
start: d.startLine,
|
||||
end: d.endLine,
|
||||
});
|
||||
}
|
||||
|
||||
if (lineSelection && !editingDraftId && !isDragging) {
|
||||
next['__new__'] = computePosition(view, wrapper, scrollContainer, {
|
||||
start: lineSelection.start,
|
||||
end: lineSelection.end,
|
||||
});
|
||||
}
|
||||
|
||||
setPositions(next);
|
||||
}, [editorView, wrapperRef, fileDrafts, editingDraftId, lineSelection, isDragging]);
|
||||
|
||||
React.useEffect(() => {
|
||||
requestAnimationFrame(updatePositions);
|
||||
}, [updatePositions]);
|
||||
|
||||
// Also update on scroll
|
||||
React.useEffect(() => {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
|
||||
const scrollContainer = wrapper.closest('.overlay-scrollbar-container') as HTMLElement | null;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
const onScroll = () => requestAnimationFrame(updatePositions);
|
||||
scrollContainer.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => scrollContainer.removeEventListener('scroll', onScroll);
|
||||
}, [wrapperRef, updatePositions]);
|
||||
|
||||
const popoverStyle = (flipUp: boolean): React.CSSProperties => flipUp
|
||||
? { position: 'absolute', bottom: 'calc(100% + 4px)', right: -8, zIndex: 40, width: 380, maxWidth: 'min(380px, calc(100vw - 48px))', borderRadius: 14 }
|
||||
: { position: 'absolute', top: 'calc(100% + 4px)', right: -8, zIndex: 40, width: 380, maxWidth: 'min(380px, calc(100vw - 48px))', borderRadius: 14 };
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileDrafts.map((d) => {
|
||||
const pos = positions[d.id];
|
||||
if (!pos) return null;
|
||||
|
||||
if (d.id === editingDraftId) {
|
||||
return (
|
||||
<div
|
||||
key={`edit-${d.id}`}
|
||||
style={{ position: 'absolute', right: 24, top: pos.top, zIndex: 100, pointerEvents: 'auto' }}
|
||||
>
|
||||
<div style={popoverStyle(pos.flipUp)}>
|
||||
<InlineCommentInput
|
||||
initialText={commentText}
|
||||
fileLabel={fileLabel}
|
||||
lineRange={{ start: d.startLine, end: d.endLine }}
|
||||
isEditing={true}
|
||||
onSave={onSaveComment}
|
||||
onCancel={onCancelComment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`saved-${d.id}`}
|
||||
style={{ position: 'absolute', right: 24, top: pos.top, zIndex: 30, pointerEvents: 'auto' }}
|
||||
>
|
||||
<InlineCommentCard
|
||||
draft={d}
|
||||
onEdit={() => onEditDraft(d)}
|
||||
onDelete={() => onDeleteDraft(d)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{lineSelection && !editingDraftId && !isDragging && positions['__new__'] && (
|
||||
<div
|
||||
key="new-comment"
|
||||
style={{ position: 'absolute', right: 24, top: positions['__new__'].top, zIndex: 100, pointerEvents: 'auto' }}
|
||||
>
|
||||
<div style={popoverStyle(positions['__new__'].flipUp)}>
|
||||
<InlineCommentInput
|
||||
initialText={commentText}
|
||||
fileLabel={fileLabel}
|
||||
lineRange={lineSelection}
|
||||
isEditing={false}
|
||||
onSave={onSaveComment}
|
||||
onCancel={onCancelComment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user