Skip to content

Commit d651118

Browse files
authored
fix: Address feedback nits and bug reports (see desc) (#1092)
1. Rename "Branch here" to "New branch" across git interaction UI 2. Freeze-frame GIF thumbnails in attachment bar to prevent animation 3. Fix project selector dropdown highlighting first item instead of selected project 4. Add fontFamily: inherit to project selector button for correct font 5. Make the image preview canvas trim to fit the size of the image + center it
1 parent 5271711 commit d651118

11 files changed

Lines changed: 232 additions & 72 deletions

File tree

apps/twig/src/main/services/archive/service.integration.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ async function withTestContext(
174174
const result =
175175
method === "detached"
176176
? await manager.createDetachedWorktreeAtCommit("HEAD", "test-wt")
177-
: await manager.createWorktreeForExistingBranch(branchName!, "test-wt");
177+
: await manager.createWorktreeForExistingBranch(
178+
branchName ?? "",
179+
"test-wt",
180+
);
178181
foldersStore.set("taskAssociations", [
179182
worktreeAssociation(result.worktreeName),
180183
]);
@@ -251,7 +254,7 @@ describe("ArchiveService integration", () => {
251254
const repoName = path.basename(ctx.repoPath);
252255
const newWorktreePath = path.join(
253256
ctx.worktreeBasePath,
254-
result.worktreeName!,
257+
result.worktreeName ?? "",
255258
repoName,
256259
);
257260
expect(await pathExists(newWorktreePath)).toBe(true);
@@ -315,7 +318,7 @@ describe("ArchiveService integration", () => {
315318
const repoName = path.basename(ctx.repoPath);
316319
const newWorktreePath = path.join(
317320
ctx.worktreeBasePath,
318-
result.worktreeName!,
321+
result.worktreeName ?? "",
319322
repoName,
320323
);
321324

apps/twig/src/renderer/features/git-interaction/components/GitInteractionDialogs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ export function GitBranchDialog({
554554
open={open}
555555
onOpenChange={onOpenChange}
556556
icon={<GitFork size={ICON_SIZE} />}
557-
title="Branch here"
557+
title="New branch"
558558
error={error}
559559
buttonLabel="Create"
560560
buttonDisabled={!branchName.trim()}

apps/twig/src/renderer/features/git-interaction/state/gitInteractionLogic.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export function computeGitInteractionState(input: GitState): GitComputed {
171171
const detachedHead = isDetachedHead(input);
172172

173173
if (detachedHead) {
174-
const branchAction = makeAction("branch-here", "Branch here", repoReason);
174+
const branchAction = makeAction("branch-here", "New branch", repoReason);
175175
return {
176176
actions: [branchAction],
177177
primaryAction: branchAction,
@@ -188,7 +188,7 @@ export function computeGitInteractionState(input: GitState): GitComputed {
188188
const onDefaultBranch = isOnDefaultBranch(input);
189189

190190
if (onDefaultBranch && input.hasChanges) {
191-
const branchAction = makeAction("branch-here", "Branch here", repoReason);
191+
const branchAction = makeAction("branch-here", "New branch", repoReason);
192192
const commitAction = getCommitAction(input, repoReason);
193193
return {
194194
actions: [branchAction, commitAction],

apps/twig/src/renderer/features/git-interaction/state/gitInteractionStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export function getGitInteractionActionLabel(
161161
case "view-pr":
162162
return "View PR";
163163
case "branch-here":
164-
return "Branch here";
164+
return "New branch";
165165
default:
166166
return "Git Action";
167167
}

apps/twig/src/renderer/features/message-editor/components/AttachmentsBar.tsx

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
import { File, X } from "@phosphor-icons/react";
22
import { Dialog, Flex, IconButton, Text } from "@radix-ui/themes";
33
import { trpcReact } from "@renderer/trpc/client";
4+
import { useEffect, useRef } from "react";
45
import type { FileAttachment } from "../utils/content";
5-
import { isImageFile } from "../utils/imageUtils";
6+
import { isGifFile, isImageFile } from "../utils/imageUtils";
7+
8+
function FrozenGifThumbnail({ src, alt }: { src: string; alt: string }) {
9+
const canvasRef = useRef<HTMLCanvasElement>(null);
10+
11+
useEffect(() => {
12+
const img = new Image();
13+
img.onload = () => {
14+
const canvas = canvasRef.current;
15+
if (!canvas) return;
16+
const size = 56;
17+
canvas.width = size;
18+
canvas.height = size;
19+
const ctx = canvas.getContext("2d");
20+
if (!ctx) return;
21+
const min = Math.min(img.naturalWidth, img.naturalHeight);
22+
const sx = (img.naturalWidth - min) / 2;
23+
const sy = (img.naturalHeight - min) / 2;
24+
ctx.drawImage(img, sx, sy, min, min, 0, 0, size, size);
25+
};
26+
img.src = src;
27+
}, [src]);
28+
29+
return (
30+
<canvas ref={canvasRef} aria-label={alt} className="size-3.5 rounded-sm" />
31+
);
32+
}
633

734
function ImageThumbnail({
835
attachment,
@@ -16,6 +43,8 @@ function ImageThumbnail({
1643
{ staleTime: Infinity },
1744
);
1845

46+
const isGif = isGifFile(attachment.label);
47+
1948
return (
2049
<Dialog.Root>
2150
<div className="group relative flex-shrink-0">
@@ -25,11 +54,15 @@ function ImageThumbnail({
2554
className="inline-flex items-center gap-1 rounded-[var(--radius-1)] bg-[var(--gray-a3)] p-1 font-medium text-[10px] text-[var(--gray-11)] leading-tight hover:bg-[var(--gray-a4)]"
2655
>
2756
{dataUrl ? (
28-
<img
29-
src={dataUrl}
30-
alt={attachment.label}
31-
className="size-3.5 rounded-sm object-cover"
32-
/>
57+
isGif ? (
58+
<FrozenGifThumbnail src={dataUrl} alt={attachment.label} />
59+
) : (
60+
<img
61+
src={dataUrl}
62+
alt={attachment.label}
63+
className="size-3.5 rounded-sm object-cover"
64+
/>
65+
)
3366
) : (
3467
<span className="size-3.5 rounded-sm bg-[var(--gray-a5)]" />
3568
)}
@@ -49,7 +82,10 @@ function ImageThumbnail({
4982
<X size={8} weight="bold" />
5083
</IconButton>
5184
</div>
52-
<Dialog.Content maxWidth="90vw" style={{ padding: 16 }}>
85+
<Dialog.Content
86+
maxWidth="85vw"
87+
style={{ padding: 16, width: "fit-content" }}
88+
>
5389
<Dialog.Title size="2" mb="2">
5490
{attachment.label}
5591
</Dialog.Title>
@@ -58,9 +94,11 @@ function ImageThumbnail({
5894
src={dataUrl}
5995
alt={attachment.label}
6096
style={{
61-
maxWidth: "85vw",
97+
maxWidth: "80vw",
6298
maxHeight: "75vh",
6399
objectFit: "contain",
100+
display: "block",
101+
margin: "0 auto",
64102
}}
65103
/>
66104
) : (

apps/twig/src/renderer/features/message-editor/utils/imageUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ export function isImageFile(filename: string): boolean {
1515
const ext = filename.split(".").pop()?.toLowerCase();
1616
return !!ext && IMAGE_EXTENSIONS.has(ext);
1717
}
18+
19+
export function isGifFile(filename: string): boolean {
20+
return filename.split(".").pop()?.toLowerCase() === "gif";
21+
}

apps/twig/src/renderer/features/onboarding/components/GitIntegrationStep.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,21 @@ export function GitIntegrationStep({
106106
</Text>
107107

108108
{selectedProject && (
109-
<ProjectSelect
110-
projectId={selectedProject.id}
111-
projectName={selectedProject.name}
112-
projects={projects.map((p) => ({ id: p.id, name: p.name }))}
113-
onProjectChange={setManuallySelectedProjectId}
114-
disabled={isLoading}
115-
/>
109+
<Flex direction="column" gap="1">
110+
<Text
111+
size="1"
112+
style={{ color: "var(--cave-charcoal)", opacity: 0.5 }}
113+
>
114+
{selectedProject.organization.name}
115+
</Text>
116+
<ProjectSelect
117+
projectId={selectedProject.id}
118+
projectName={selectedProject.name}
119+
projects={projects.map((p) => ({ id: p.id, name: p.name }))}
120+
onProjectChange={setManuallySelectedProjectId}
121+
disabled={isLoading}
122+
/>
123+
</Flex>
116124
)}
117125
</Flex>
118126

apps/twig/src/renderer/features/onboarding/components/ProjectSelect.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ export function ProjectSelect({
2020
disabled = false,
2121
}: ProjectSelectProps) {
2222
const [open, setOpen] = useState(false);
23+
const currentProject = projects.find((p) => p.id === projectId);
24+
const defaultValue = currentProject
25+
? `${currentProject.name} ${currentProject.id}`
26+
: undefined;
27+
const [highlightedValue, setHighlightedValue] = useState(defaultValue);
2328

2429
if (projects.length <= 1) {
25-
return null;
30+
return (
31+
<Text size="2" style={{ color: "var(--cave-charcoal)", opacity: 0.5 }}>
32+
{projectName}
33+
</Text>
34+
);
2635
}
2736

2837
return (
@@ -31,7 +40,15 @@ export function ProjectSelect({
3140
{projectName}
3241
{" · "}
3342
</span>
34-
<Popover.Root open={open} onOpenChange={setOpen}>
43+
<Popover.Root
44+
open={open}
45+
onOpenChange={(nextOpen) => {
46+
setOpen(nextOpen);
47+
if (nextOpen) {
48+
setHighlightedValue(defaultValue);
49+
}
50+
}}
51+
>
3552
<Popover.Trigger>
3653
<button
3754
type="button"
@@ -42,6 +59,7 @@ export function ProjectSelect({
4259
padding: 0,
4360
color: "var(--accent-9)",
4461
cursor: disabled ? "not-allowed" : "pointer",
62+
fontFamily: "inherit",
4563
fontWeight: 500,
4664
fontSize: "inherit",
4765
opacity: disabled ? 0.5 : 1,
@@ -57,7 +75,12 @@ export function ProjectSelect({
5775
align="start"
5876
sideOffset={8}
5977
>
60-
<Command.Root shouldFilter={true} label="Project picker">
78+
<Command.Root
79+
shouldFilter={true}
80+
label="Project picker"
81+
value={highlightedValue}
82+
onValueChange={setHighlightedValue}
83+
>
6184
<Command.Input placeholder="Search projects..." autoFocus={true} />
6285
<Command.List>
6386
<Command.Empty>No projects found.</Command.Empty>

apps/twig/src/renderer/features/sidebar/components/ProjectSwitcher.tsx

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -310,35 +310,82 @@ export function ProjectSwitcher() {
310310
</Popover.Content>
311311
</Popover.Root>
312312

313-
<Dialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
314-
<Dialog.Content
315-
className="project-picker-dialog"
316-
style={{ maxWidth: 600, padding: 0 }}
317-
>
318-
<Command.Root shouldFilter={true} label="Project picker">
319-
<Command.Input placeholder="Search projects..." autoFocus={true} />
320-
<Command.List>
321-
<Command.Empty>No projects found.</Command.Empty>
322-
{groupedProjects.map((group) =>
323-
group.projects.map((project) => (
324-
<Command.Item
325-
key={project.id}
326-
value={`${project.name} ${project.id}`}
327-
onSelect={() => handleProjectSelect(project.id)}
328-
>
329-
<Flex align="center" justify="between" width="100%">
330-
<Text size="1">{project.name}</Text>
331-
{project.id === currentProjectId && (
332-
<Check size={14} className="text-accent-11" />
333-
)}
334-
</Flex>
335-
</Command.Item>
336-
)),
337-
)}
338-
</Command.List>
339-
</Command.Root>
340-
</Dialog.Content>
341-
</Dialog.Root>
313+
<ProjectPickerDialogInner
314+
dialogOpen={dialogOpen}
315+
setDialogOpen={setDialogOpen}
316+
groupedProjects={groupedProjects}
317+
currentProjectId={currentProjectId}
318+
currentProject={currentProject}
319+
handleProjectSelect={handleProjectSelect}
320+
/>
342321
</>
343322
);
344323
}
324+
325+
interface ProjectPickerDialogInnerProps {
326+
dialogOpen: boolean;
327+
setDialogOpen: (open: boolean) => void;
328+
groupedProjects: ReturnType<typeof useProjects>["groupedProjects"];
329+
currentProjectId: number | null;
330+
currentProject: { id: number; name: string } | undefined;
331+
handleProjectSelect: (projectId: number) => void;
332+
}
333+
334+
function ProjectPickerDialogInner({
335+
dialogOpen,
336+
setDialogOpen,
337+
groupedProjects,
338+
currentProjectId,
339+
currentProject,
340+
handleProjectSelect,
341+
}: ProjectPickerDialogInnerProps) {
342+
const defaultValue = currentProject
343+
? `${currentProject.name} ${currentProject.id}`
344+
: undefined;
345+
const [highlightedValue, setHighlightedValue] = useState(defaultValue);
346+
347+
return (
348+
<Dialog.Root
349+
open={dialogOpen}
350+
onOpenChange={(open) => {
351+
setDialogOpen(open);
352+
if (open) {
353+
setHighlightedValue(defaultValue);
354+
}
355+
}}
356+
>
357+
<Dialog.Content
358+
className="project-picker-dialog"
359+
style={{ maxWidth: 600, padding: 0 }}
360+
>
361+
<Command.Root
362+
shouldFilter={true}
363+
label="Project picker"
364+
value={highlightedValue}
365+
onValueChange={setHighlightedValue}
366+
>
367+
<Command.Input placeholder="Search projects..." autoFocus={true} />
368+
<Command.List>
369+
<Command.Empty>No projects found.</Command.Empty>
370+
{groupedProjects.map((group) =>
371+
group.projects.map((project) => (
372+
<Command.Item
373+
key={project.id}
374+
value={`${project.name} ${project.id}`}
375+
onSelect={() => handleProjectSelect(project.id)}
376+
>
377+
<Flex align="center" justify="between" width="100%">
378+
<Text size="1">{project.name}</Text>
379+
{project.id === currentProjectId && (
380+
<Check size={14} className="text-accent-11" />
381+
)}
382+
</Flex>
383+
</Command.Item>
384+
)),
385+
)}
386+
</Command.List>
387+
</Command.Root>
388+
</Dialog.Content>
389+
</Dialog.Root>
390+
);
391+
}

0 commit comments

Comments
 (0)