| author | |
| committer | |
| log | e8ca3a82806dff84438d5aba66b693468f7ca426 |
| tree | 720c1d38dceaef9b1049052c32e220395fa42a52 |
| parent | f45c6b4a755905485431872f4a4188de3ed05974 |
| signature | Signed by PGP key B5690EEEBB952194 |
26 files changed, 628 insertions(+), 307 deletions(-)
apps/web/src/components/CommandPalette.tsx+3-3| ... | ... | @@ -46,7 +46,6 @@ import { |
| 46 | 46 | FileSearchIcon, |
| 47 | 47 | FolderIcon, |
| 48 | 48 | FolderPlusIcon, |
| 49 | GitPullRequestArrowIcon, | |
| 50 | 49 | LinkIcon, |
| 51 | 50 | MessageSquareIcon, |
| 52 | 51 | PaletteIcon, |
| ... | ... | @@ -181,6 +180,7 @@ import { |
| 181 | 180 | buildSidebarProjectSnapshots, |
| 182 | 181 | } from "../sidebarProjectGrouping"; |
| 183 | 182 | import type { Project } from "../types"; |
| 183 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 184 | 184 | |
| 185 | 185 | const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; |
| 186 | 186 | |
| ... | ... | @@ -1712,7 +1712,7 @@ function OpenCommandPaletteDialog(props: { |
| 1712 | 1712 | value: "action:link-pull-request", |
| 1713 | 1713 | searchTerms: ["link", "pull request", "pr", "attach", "stack"], |
| 1714 | 1714 | title: "Link pull request to thread", |
| 1715 | icon: <GitPullRequestArrowIcon className={ITEM_ICON_CLASS} />, | |
| 1715 | icon: <PullRequestGlyph.link className={ITEM_ICON_CLASS} />, | |
| 1716 | 1716 | run: async () => { |
| 1717 | 1717 | openLinkPullRequestDialog(threadRef); |
| 1718 | 1718 | }, |
| ... | ... | @@ -1724,7 +1724,7 @@ function OpenCommandPaletteDialog(props: { |
| 1724 | 1724 | searchTerms: ["pull requests", "linked", "stack", "prs"], |
| 1725 | 1725 | title: "Show linked pull requests", |
| 1726 | 1726 | disabled: visibleThreadPullRequests(activeThread.pullRequests).length === 0, |
| 1727 | icon: <GitPullRequestArrowIcon className={ITEM_ICON_CLASS} />, | |
| 1727 | icon: <PullRequestGlyph.link className={ITEM_ICON_CLASS} />, | |
| 1728 | 1728 | run: async () => { |
| 1729 | 1729 | useRightPanelStore.getState().open(threadRef, "pull-requests"); |
| 1730 | 1730 | }, |
apps/web/src/components/LegacySidebar.tsx+2-2| ... | ... | @@ -1,5 +1,4 @@ |
| 1 | 1 | import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests"; |
| 2 | import { GitPullRequestIcon } from "lucide-react"; | |
| 3 | 2 | import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests"; |
| 4 | 3 | import { Spinner } from "~/components/ui/spinner"; |
| 5 | 4 | import { |
| ... | ... | @@ -214,6 +213,7 @@ import { |
| 214 | 213 | type SidebarProjectGroupMember, |
| 215 | 214 | type SidebarProjectSnapshot, |
| 216 | 215 | } from "../sidebarProjectGrouping"; |
| 216 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 217 | 217 | const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = { |
| 218 | 218 | updated_at: "Last user message", |
| 219 | 219 | created_at: "Created at", |
| ... | ... | @@ -757,7 +757,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP |
| 757 | 757 | className="text-muted-foreground" |
| 758 | 758 | aria-label={`PR #${currentLinkedPr.number}, status pending`} |
| 759 | 759 | > |
| 760 | <GitPullRequestIcon className="size-3" /> | |
| 760 | <PullRequestGlyph.pullRequest className="size-3" /> | |
| 761 | 761 | </a> |
| 762 | 762 | ) : null} |
| 763 | 763 | {threadStatus && <ThreadStatusLabel status={threadStatus} />} |
apps/web/src/components/RightPanelTabs.tsx+10-10| ... | ... | @@ -21,8 +21,6 @@ import { |
| 21 | 21 | ChevronRight, |
| 22 | 22 | FileDiff, |
| 23 | 23 | Files, |
| 24 | GitPullRequest, | |
| 25 | GitPullRequestArrow, | |
| 26 | 24 | Globe2, |
| 27 | 25 | Plus, |
| 28 | 26 | TerminalSquare, |
| ... | ... | @@ -73,6 +71,7 @@ import { FaviconImage } from "./preview/PreviewFaviconIcon"; |
| 73 | 71 | import { previewBridge } from "./preview/previewBridge"; |
| 74 | 72 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; |
| 75 | 73 | import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation"; |
| 74 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 76 | 75 | |
| 77 | 76 | interface RightPanelTabsProps { |
| 78 | 77 | mode: PreviewPanelMode; |
| ... | ... | @@ -373,7 +372,7 @@ function RightPanelEmptyState(props: { |
| 373 | 372 | }, |
| 374 | 373 | { |
| 375 | 374 | label: "Pull request", |
| 376 | icon: GitPullRequest, | |
| 375 | icon: PullRequestGlyph.pullRequest, | |
| 377 | 376 | shortcut: "P", |
| 378 | 377 | available: props.pullRequestAvailable, |
| 379 | 378 | disabledReason: SURFACE_UNAVAILABLE_HINTS.pullRequest, |
| ... | ... | @@ -382,7 +381,7 @@ function RightPanelEmptyState(props: { |
| 382 | 381 | }, |
| 383 | 382 | { |
| 384 | 383 | label: "Linked pull requests", |
| 385 | icon: GitPullRequestArrow, | |
| 384 | icon: PullRequestGlyph.link, | |
| 386 | 385 | shortcut: "L", |
| 387 | 386 | available: props.pullRequestsAvailable, |
| 388 | 387 | disabledReason: SURFACE_UNAVAILABLE_HINTS.pullRequests, |
| ... | ... | @@ -712,7 +711,7 @@ function SurfaceIcon({ |
| 712 | 711 | /> |
| 713 | 712 | ); |
| 714 | 713 | case "pull-requests": |
| 715 | return <GitPullRequestArrow className="size-3 shrink-0" />; | |
| 714 | return <PullRequestGlyph.link className="size-3 shrink-0" />; | |
| 716 | 715 | case "agents": |
| 717 | 716 | return <Bot className="size-3 shrink-0" />; |
| 718 | 717 | case "device": |
| ... | ... | @@ -801,8 +800,9 @@ function PullRequestSurfaceIcon({ |
| 801 | 800 | }, |
| 802 | 801 | }), |
| 803 | 802 | ).data; |
| 804 | // Only state and draft reach the tab. A list seed cannot know mergeability, so feeding the | |
| 805 | // full detail would flip an open tab to the conflict glyph the moment its read lands. | |
| 803 | // The compact tab intentionally shows lifecycle and draft state only. Conflict warnings have | |
| 804 | // their own presentation on surfaces that have mergeability, while this tab stays stable as | |
| 805 | // detail data arrives. | |
| 806 | 806 | const status = |
| 807 | 807 | linkedSnapshot !== null |
| 808 | 808 | ? linkedSnapshot |
| ... | ... | @@ -810,7 +810,7 @@ function PullRequestSurfaceIcon({ |
| 810 | 810 | ? (seed ?? null) |
| 811 | 811 | : { state: detail.state, isDraft: detail.isDraft }; |
| 812 | 812 | if (status === null) { |
| 813 | return <GitPullRequest className="size-3 shrink-0 text-muted-foreground" />; | |
| 813 | return <PullRequestGlyph.pullRequest className="size-3 shrink-0 text-muted-foreground" />; | |
| 814 | 814 | } |
| 815 | 815 | const presentation = resolvePullRequestState({ state: status.state, isDraft: status.isDraft }); |
| 816 | 816 | return <presentation.Icon className={cn("size-3 shrink-0", presentation.toneClassName)} />; |
| ... | ... | @@ -895,7 +895,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { |
| 895 | 895 | }, |
| 896 | 896 | { |
| 897 | 897 | label: "Pull request", |
| 898 | icon: GitPullRequest, | |
| 898 | icon: PullRequestGlyph.pullRequest, | |
| 899 | 899 | shortcut: "P", |
| 900 | 900 | available: props.pullRequestAvailable, |
| 901 | 901 | disabledReason: SURFACE_DISABLED_REASONS.pullRequest, |
| ... | ... | @@ -903,7 +903,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { |
| 903 | 903 | }, |
| 904 | 904 | { |
| 905 | 905 | label: "Linked pull requests", |
| 906 | icon: GitPullRequestArrow, | |
| 906 | icon: PullRequestGlyph.link, | |
| 907 | 907 | shortcut: "L", |
| 908 | 908 | available: props.pullRequestsAvailable, |
| 909 | 909 | disabledReason: SURFACE_DISABLED_REASONS.pullRequests, |
apps/web/src/components/ThreadStatusIndicators.test.ts+130-22| ... | ... | @@ -1,25 +1,20 @@ |
| 1 | 1 | import { ProjectId, type PullRequestSummary, type VcsStatusResult } from "@t3tools/contracts"; |
| 2 | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | import { | |
| 4 | GitMergeIcon, | |
| 5 | GitPullRequestClosedIcon, | |
| 6 | GitPullRequestDraftIcon, | |
| 7 | GitPullRequestIcon, | |
| 8 | } from "lucide-react"; | |
| 9 | 3 | |
| 10 | 4 | import { |
| 11 | 5 | ChangeRequestStatusIcon, |
| 12 | 6 | prStatusIndicator, |
| 13 | settledPrHoverColorClass, | |
| 7 | resolveThreadPullRequestBadgePresentation, | |
| 14 | 8 | } from "./ThreadStatusIndicators"; |
| 15 | 9 | import { newestPullRequestSummary } from "../state/pullRequests"; |
| 10 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 16 | 11 | |
| 17 | 12 | describe("ChangeRequestStatusIcon", () => { |
| 18 | 13 | it.each([ |
| 19 | ["open", "open", false, GitPullRequestIcon], | |
| 20 | ["draft", "open", true, GitPullRequestDraftIcon], | |
| 21 | ["closed", "closed", false, GitPullRequestClosedIcon], | |
| 22 | ["merged", "merged", false, GitMergeIcon], | |
| 14 | ["open", "open", false, PullRequestGlyph.pullRequest], | |
| 15 | ["draft", "open", true, PullRequestGlyph.draft], | |
| 16 | ["closed", "closed", false, PullRequestGlyph.closed], | |
| 17 | ["merged", "merged", false, PullRequestGlyph.merged], | |
| 23 | 18 | ] as const)("uses the %s pull request glyph", (_label, state, isDraft, expectedIcon) => { |
| 24 | 19 | expect(ChangeRequestStatusIcon({ state, isDraft }).type).toBe(expectedIcon); |
| 25 | 20 | }); |
| ... | ... | @@ -119,18 +114,131 @@ describe("prStatusIndicator", () => { |
| 119 | 114 | }); |
| 120 | 115 | }); |
| 121 | 116 | |
| 122 | describe("settledPrHoverColorClass", () => { | |
| 123 | it.each([ | |
| 124 | ["open", "text-emerald-600"], | |
| 125 | ["merged", "text-violet-600"], | |
| 126 | ["closed", "text-red-600"], | |
| 127 | ] as const)("restores the %s pull request color on row hover", (state, colorClass) => { | |
| 128 | expect(settledPrHoverColorClass(state)).toContain(`group-hover/sidebar-row:${colorClass}`); | |
| 117 | describe("resolveThreadPullRequestBadgePresentation", () => { | |
| 118 | const url = "https://github.com/pingdotgg/t3code/pull/42"; | |
| 119 | ||
| 120 | it("returns the pending pull-request badge when no snapshot is available", () => { | |
| 121 | expect( | |
| 122 | resolveThreadPullRequestBadgePresentation({ | |
| 123 | badge: null, | |
| 124 | number: 42, | |
| 125 | url, | |
| 126 | status: null, | |
| 127 | }), | |
| 128 | ).toEqual({ | |
| 129 | Icon: PullRequestGlyph.pullRequest, | |
| 130 | toneClassName: "text-muted-foreground", | |
| 131 | label: "PR #42, status pending", | |
| 132 | text: 42, | |
| 133 | }); | |
| 129 | 134 | }); |
| 130 | 135 | |
| 131 | it("keeps draft pull requests gray on row hover", () => { | |
| 132 | expect(settledPrHoverColorClass("open", true)).toContain( | |
| 133 | "group-hover/sidebar-row:text-zinc-500", | |
| 134 | ); | |
| 136 | it.each([ | |
| 137 | [ | |
| 138 | "open", | |
| 139 | { state: "open", isDraft: false }, | |
| 140 | PullRequestGlyph.pullRequest, | |
| 141 | "text-emerald-600 dark:text-emerald-300/90", | |
| 142 | "PR #42 - Open: PR branch", | |
| 143 | ], | |
| 144 | [ | |
| 145 | "draft", | |
| 146 | { state: "open", isDraft: true }, | |
| 147 | PullRequestGlyph.draft, | |
| 148 | "text-zinc-500 dark:text-zinc-400/80", | |
| 149 | "PR #42 - Draft: PR branch", | |
| 150 | ], | |
| 151 | [ | |
| 152 | "closed", | |
| 153 | { state: "closed", isDraft: false }, | |
| 154 | PullRequestGlyph.closed, | |
| 155 | "text-red-600 dark:text-red-300/90", | |
| 156 | "PR #42 - Closed: PR branch", | |
| 157 | ], | |
| 158 | [ | |
| 159 | "merged", | |
| 160 | { state: "merged", isDraft: false }, | |
| 161 | PullRequestGlyph.merged, | |
| 162 | "text-violet-600 dark:text-violet-300/90", | |
| 163 | "PR #42 - Merged: PR branch", | |
| 164 | ], | |
| 165 | ] as const)( | |
| 166 | "keeps the %s state for one linked pull request", | |
| 167 | (_state, prOverrides, expectedIcon, expectedToneClassName, expectedLabel) => { | |
| 168 | const fixture = status().pr; | |
| 169 | if (!fixture) throw new Error("Expected pull request fixture"); | |
| 170 | const prStatus = prStatusIndicator({ ...fixture, ...prOverrides }, undefined); | |
| 171 | if (!prStatus) throw new Error("Expected pull request status"); | |
| 172 | ||
| 173 | expect( | |
| 174 | resolveThreadPullRequestBadgePresentation({ | |
| 175 | badge: { kind: "pull-request", others: 0, state: "open" }, | |
| 176 | number: fixture.number, | |
| 177 | url: fixture.url, | |
| 178 | status: prStatus, | |
| 179 | }), | |
| 180 | ).toEqual({ | |
| 181 | Icon: expectedIcon, | |
| 182 | toneClassName: expectedToneClassName, | |
| 183 | label: expectedLabel, | |
| 184 | text: fixture.number, | |
| 185 | }); | |
| 186 | }, | |
| 187 | ); | |
| 188 | ||
| 189 | it.each([ | |
| 190 | ["open", "text-emerald-600 dark:text-emerald-300/90"], | |
| 191 | ["draft", "text-zinc-500 dark:text-zinc-400/80"], | |
| 192 | ["merged", "text-violet-600 dark:text-violet-300/90"], | |
| 193 | ] as const)( | |
| 194 | "uses a layers badge with the %s stack tone without a link identity", | |
| 195 | (state, expectedToneClassName) => { | |
| 196 | expect( | |
| 197 | resolveThreadPullRequestBadgePresentation({ | |
| 198 | badge: { kind: "stack", layers: 3, state }, | |
| 199 | status: null, | |
| 200 | }), | |
| 201 | ).toEqual({ | |
| 202 | Icon: PullRequestGlyph.stack, | |
| 203 | toneClassName: expectedToneClassName, | |
| 204 | label: `Stack of 3 pull requests, ${state}`, | |
| 205 | text: 3, | |
| 206 | }); | |
| 207 | }, | |
| 208 | ); | |
| 209 | ||
| 210 | it.each([ | |
| 211 | ["open", PullRequestGlyph.pullRequest, "text-emerald-600 dark:text-emerald-300/90"], | |
| 212 | ["draft", PullRequestGlyph.draft, "text-zinc-500 dark:text-zinc-400/80"], | |
| 213 | ["merged", PullRequestGlyph.merged, "text-violet-600 dark:text-violet-300/90"], | |
| 214 | ] as const)( | |
| 215 | "draws the count of unrelated linked pull requests with their %s aggregate state", | |
| 216 | (state, expectedIcon, expectedToneClassName) => { | |
| 217 | const fixture = status().pr; | |
| 218 | if (!fixture) throw new Error("Expected pull request fixture"); | |
| 219 | const closedStatus = prStatusIndicator( | |
| 220 | { ...fixture, state: "closed", isDraft: false }, | |
| 221 | undefined, | |
| 222 | ); | |
| 223 | if (!closedStatus) throw new Error("Expected pull request status"); | |
| 224 | ||
| 225 | expect( | |
| 226 | resolveThreadPullRequestBadgePresentation({ | |
| 227 | badge: { kind: "pull-request", others: 2, state }, | |
| 228 | number: fixture.number, | |
| 229 | url: fixture.url, | |
| 230 | status: closedStatus, | |
| 231 | }), | |
| 232 | ).toEqual({ | |
| 233 | Icon: expectedIcon, | |
| 234 | toneClassName: expectedToneClassName, | |
| 235 | label: `PR #42 - Closed: PR branch, and 2 more linked; overall ${state}`, | |
| 236 | text: "+3", | |
| 237 | }); | |
| 238 | }, | |
| 239 | ); | |
| 240 | ||
| 241 | it("omits the control when neither a stack nor a linked identity can be shown", () => { | |
| 242 | expect(resolveThreadPullRequestBadgePresentation({ badge: null, status: null })).toBeNull(); | |
| 135 | 243 | }); |
| 136 | 244 | }); |
apps/web/src/components/ThreadStatusIndicators.tsx+76-99| ... | ... | @@ -14,7 +14,7 @@ import { |
| 14 | 14 | visibleThreadPullRequests, |
| 15 | 15 | type ThreadPullRequestBadge, |
| 16 | 16 | } from "@t3tools/shared/threadPullRequests"; |
| 17 | import { FolderGit2Icon, GitPullRequestArrowIcon, LayersIcon, TerminalIcon } from "lucide-react"; | |
| 17 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; | |
| 18 | 18 | import { useMemo, type MouseEvent } from "react"; |
| 19 | 19 | import { buttonVariants, InlineButton } from "./ui/button"; |
| 20 | 20 | import { cn } from "../lib/utils"; |
| ... | ... | @@ -31,11 +31,17 @@ import type { SidebarThreadSummary } from "../types"; |
| 31 | 31 | import { formatWorktreePathForDisplay } from "../worktreeCleanup"; |
| 32 | 32 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 33 | 33 | import { pullRequestListLines } from "./pullRequest/pullRequestListLines"; |
| 34 | import { | |
| 35 | PULL_REQUEST_STATE_PRESENTATION, | |
| 36 | PullRequestGlyph, | |
| 37 | type PullRequestGlyphIcon, | |
| 38 | } from "./pullRequest/pullRequestIcons"; | |
| 34 | 39 | import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation"; |
| 35 | 40 | |
| 36 | 41 | export interface PrStatusIndicator { |
| 37 | 42 | label: string; |
| 38 | 43 | colorClass: string; |
| 44 | Icon: PullRequestGlyphIcon; | |
| 39 | 45 | tooltip: string; |
| 40 | 46 | tooltipLead: string; |
| 41 | 47 | tooltipTitle: string; |
| ... | ... | @@ -125,16 +131,55 @@ export { |
| 125 | 131 | type ThreadPullRequestBadge, |
| 126 | 132 | } from "@t3tools/shared/threadPullRequests"; |
| 127 | 133 | |
| 128 | /** The glyph a row's badge wears: the layers icon for a stack, the pull-request one otherwise. */ | |
| 129 | function ThreadPullRequestBadgeIcon({ | |
| 130 | icon, | |
| 131 | className, | |
| 134 | export interface ThreadPullRequestBadgePresentation { | |
| 135 | readonly Icon: PullRequestGlyphIcon; | |
| 136 | readonly toneClassName: string; | |
| 137 | readonly label: string; | |
| 138 | readonly text: string | number; | |
| 139 | } | |
| 140 | ||
| 141 | /** Resolve the complete badge appearance before rendering it in the sidebar or composer. */ | |
| 142 | export function resolveThreadPullRequestBadgePresentation({ | |
| 143 | badge, | |
| 144 | number, | |
| 145 | url, | |
| 146 | status, | |
| 132 | 147 | }: { |
| 133 | icon: "stack" | "pull-request"; | |
| 134 | className?: string | undefined; | |
| 135 | }) { | |
| 136 | const Icon = icon === "stack" ? LayersIcon : GitPullRequestArrowIcon; | |
| 137 | return <Icon aria-hidden className={cn("size-3 shrink-0", className)} />; | |
| 148 | readonly badge: ThreadPullRequestBadge | null; | |
| 149 | readonly number?: number | undefined; | |
| 150 | readonly url?: string | undefined; | |
| 151 | readonly status: PrStatusIndicator | null; | |
| 152 | }): ThreadPullRequestBadgePresentation | null { | |
| 153 | // The badge already folds every visible link into one state, draft included, so both the | |
| 154 | // stack and the linked count index the shared table directly rather than the single-PR resolver. | |
| 155 | if (badge?.kind === "stack") { | |
| 156 | const aggregate = PULL_REQUEST_STATE_PRESENTATION[badge.state]; | |
| 157 | return { | |
| 158 | Icon: PullRequestGlyph.stack, | |
| 159 | toneClassName: aggregate.toneClassName, | |
| 160 | label: `Stack of ${badge.layers} pull requests, ${aggregate.label.toLowerCase()}`, | |
| 161 | text: badge.layers, | |
| 162 | }; | |
| 163 | } | |
| 164 | if (number === undefined || url === undefined) return null; | |
| 165 | ||
| 166 | const tooltip = status?.tooltip ?? `PR #${number}, status pending`; | |
| 167 | if (badge?.kind === "pull-request" && badge.others > 0) { | |
| 168 | // Unrelated links fold into one state, so a count of merged PRs reads as merged. | |
| 169 | const aggregate = PULL_REQUEST_STATE_PRESENTATION[badge.state]; | |
| 170 | return { | |
| 171 | Icon: aggregate.Icon, | |
| 172 | toneClassName: aggregate.toneClassName, | |
| 173 | label: `${tooltip}, and ${badge.others} more linked; overall ${aggregate.label.toLowerCase()}`, | |
| 174 | text: `+${badge.others + 1}`, | |
| 175 | }; | |
| 176 | } | |
| 177 | return { | |
| 178 | Icon: status?.Icon ?? PullRequestGlyph.pullRequest, | |
| 179 | toneClassName: status?.colorClass ?? "text-muted-foreground", | |
| 180 | label: tooltip, | |
| 181 | text: number, | |
| 182 | }; | |
| 138 | 183 | } |
| 139 | 184 | |
| 140 | 185 | /** The complete linked-PR control shared by the sidebar and composer footer. */ |
| ... | ... | @@ -155,16 +200,9 @@ export function ThreadPullRequestBadgeControl({ |
| 155 | 200 | onOpenStack: () => void; |
| 156 | 201 | onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void; |
| 157 | 202 | }) { |
| 203 | const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status }); | |
| 204 | if (presentation === null) return null; | |
| 158 | 205 | const isStack = badge?.kind === "stack"; |
| 159 | const linkedCount = badge?.kind === "pull-request" && badge.others > 0 ? badge.others + 1 : null; | |
| 160 | if (!isStack && (number === undefined || url === undefined)) return null; | |
| 161 | const label = isStack | |
| 162 | ? `Stack of ${badge.layers} pull requests, ${badge.state}` | |
| 163 | : `${status?.tooltip ?? `PR #${number}, status pending`}${ | |
| 164 | badge?.kind === "pull-request" && badge.others > 0 | |
| 165 | ? `, and ${badge.others} more linked; overall ${badge.state}` | |
| 166 | : "" | |
| 167 | }`; | |
| 168 | 206 | const className = cn( |
| 169 | 207 | variant === "ghost" |
| 170 | 208 | ? buttonVariants({ variant: "ghost", size: "xs" }) |
| ... | ... | @@ -172,14 +210,12 @@ export function ThreadPullRequestBadgeControl({ |
| 172 | 210 | "text-xs tabular-nums", |
| 173 | 211 | variant === "ghost" && |
| 174 | 212 | "font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]", |
| 175 | badge !== null && (isStack || linkedCount !== null) | |
| 176 | ? PR_STATE_COLOR_CLASS[badge.state] | |
| 177 | : (status?.colorClass ?? "text-muted-foreground"), | |
| 213 | presentation.toneClassName, | |
| 178 | 214 | ); |
| 179 | 215 | const content = ( |
| 180 | 216 | <> |
| 181 | <ThreadPullRequestBadgeIcon icon={badge?.kind ?? "pull-request"} /> | |
| 182 | {isStack ? badge.layers : linkedCount !== null ? `+${linkedCount}` : number} | |
| 217 | <presentation.Icon aria-hidden className="size-3 shrink-0" /> | |
| 218 | {presentation.text} | |
| 183 | 219 | </> |
| 184 | 220 | ); |
| 185 | 221 | return ( |
| ... | ... | @@ -189,7 +225,7 @@ export function ThreadPullRequestBadgeControl({ |
| 189 | 225 | isStack ? ( |
| 190 | 226 | <InlineButton |
| 191 | 227 | className={className} |
| 192 | aria-label={label} | |
| 228 | aria-label={presentation.label} | |
| 193 | 229 | onPointerDown={(event) => event.stopPropagation()} |
| 194 | 230 | onClick={(event) => { |
| 195 | 231 | event.preventDefault(); |
| ... | ... | @@ -203,7 +239,7 @@ export function ThreadPullRequestBadgeControl({ |
| 203 | 239 | target="_blank" |
| 204 | 240 | rel="noopener noreferrer" |
| 205 | 241 | className={className} |
| 206 | aria-label={label} | |
| 242 | aria-label={presentation.label} | |
| 207 | 243 | onPointerDown={(event) => event.stopPropagation()} |
| 208 | 244 | onClick={onOpenPullRequest} |
| 209 | 245 | /> |
| ... | ... | @@ -212,7 +248,7 @@ export function ThreadPullRequestBadgeControl({ |
| 212 | 248 | > |
| 213 | 249 | {content} |
| 214 | 250 | </TooltipTrigger> |
| 215 | <TooltipPopup side="top">{label}</TooltipPopup> | |
| 251 | <TooltipPopup side="top">{presentation.label}</TooltipPopup> | |
| 216 | 252 | </Tooltip> |
| 217 | 253 | ); |
| 218 | 254 | } |
| ... | ... | @@ -254,7 +290,7 @@ export function ThreadPullRequestsMiniList({ |
| 254 | 290 | className={cn("size-3 shrink-0", presentation.toneClassName)} |
| 255 | 291 | /> |
| 256 | 292 | ) : ( |
| 257 | <GitPullRequestArrowIcon | |
| 293 | <PullRequestGlyph.pullRequest | |
| 258 | 294 | aria-hidden |
| 259 | 295 | className="size-3 shrink-0 stroke-muted-foreground" |
| 260 | 296 | /> |
| ... | ... | @@ -275,83 +311,24 @@ export function ThreadPullRequestsMiniList({ |
| 275 | 311 | ); |
| 276 | 312 | } |
| 277 | 313 | |
| 278 | /** The ink each pull-request state wears in the sidebar, shared by the number and stack badges. */ | |
| 279 | const PR_STATE_COLOR_CLASS: Record<ThreadPullRequestBadge["state"], string> = { | |
| 280 | open: "text-emerald-600 dark:text-emerald-300/90", | |
| 281 | merged: "text-violet-600 dark:text-violet-300/90", | |
| 282 | closed: "text-red-600 dark:text-red-300/90", | |
| 283 | draft: "text-zinc-500 dark:text-zinc-400/80", | |
| 284 | }; | |
| 285 | ||
| 286 | export function settledPrHoverColorClass( | |
| 287 | state: NonNullable<ThreadPr>["state"], | |
| 288 | isDraft = false, | |
| 289 | ): string { | |
| 290 | switch (state) { | |
| 291 | case "open": | |
| 292 | if (isDraft) { | |
| 293 | return "group-hover/sidebar-row:text-zinc-500 dark:group-hover/sidebar-row:text-zinc-400/80"; | |
| 294 | } | |
| 295 | return "group-hover/sidebar-row:text-emerald-600 dark:group-hover/sidebar-row:text-emerald-300/90"; | |
| 296 | case "merged": | |
| 297 | return "group-hover/sidebar-row:text-violet-600 dark:group-hover/sidebar-row:text-violet-300/90"; | |
| 298 | case "closed": | |
| 299 | return "group-hover/sidebar-row:text-red-600 dark:group-hover/sidebar-row:text-red-300/90"; | |
| 300 | } | |
| 301 | } | |
| 302 | ||
| 303 | 314 | export function prStatusIndicator( |
| 304 | 315 | pr: ThreadPr, |
| 305 | 316 | provider: VcsStatusResult["sourceControlProvider"] | null | undefined, |
| 306 | 317 | ): PrStatusIndicator | null { |
| 307 | function formatPrState(pr: NonNullable<ThreadPr>): string { | |
| 308 | if (pr.state === "open" && pr.isDraft === true) return "Draft"; | |
| 309 | return pr.state.charAt(0).toUpperCase() + pr.state.slice(1); | |
| 310 | } | |
| 311 | ||
| 312 | function formatPrStatusLead(pr: NonNullable<ThreadPr>, changeRequestShortName: string): string { | |
| 313 | return `${changeRequestShortName} #${pr.number} - ${formatPrState(pr)}`; | |
| 314 | } | |
| 315 | 318 | if (!pr) return null; |
| 316 | 319 | const presentation = resolveChangeRequestPresentation(provider); |
| 320 | const state = resolvePullRequestState({ state: pr.state, isDraft: pr.isDraft === true }); | |
| 317 | 321 | |
| 318 | const tooltipLead = formatPrStatusLead(pr, presentation.shortName); | |
| 319 | const tooltip = `${tooltipLead}: ${pr.title}`; | |
| 320 | ||
| 321 | if (pr.state === "open") { | |
| 322 | const isDraft = pr.isDraft === true; | |
| 323 | return { | |
| 324 | label: `${presentation.shortName} ${isDraft ? "draft" : "open"}`, | |
| 325 | colorClass: isDraft | |
| 326 | ? "text-zinc-500 dark:text-zinc-400/80" | |
| 327 | : "text-emerald-600 dark:text-emerald-300/90", | |
| 328 | tooltip, | |
| 329 | tooltipLead, | |
| 330 | tooltipTitle: pr.title, | |
| 331 | url: pr.url, | |
| 332 | }; | |
| 333 | } | |
| 334 | if (pr.state === "closed") { | |
| 335 | return { | |
| 336 | label: `${presentation.shortName} closed`, | |
| 337 | colorClass: "text-red-600 dark:text-red-300/90", | |
| 338 | tooltip, | |
| 339 | tooltipLead, | |
| 340 | tooltipTitle: pr.title, | |
| 341 | url: pr.url, | |
| 342 | }; | |
| 343 | } | |
| 344 | if (pr.state === "merged") { | |
| 345 | return { | |
| 346 | label: `${presentation.shortName} merged`, | |
| 347 | colorClass: "text-violet-600 dark:text-violet-300/90", | |
| 348 | tooltip, | |
| 349 | tooltipLead, | |
| 350 | tooltipTitle: pr.title, | |
| 351 | url: pr.url, | |
| 352 | }; | |
| 353 | } | |
| 354 | return null; | |
| 322 | const tooltipLead = `${presentation.shortName} #${pr.number} - ${state.label}`; | |
| 323 | return { | |
| 324 | label: `${presentation.shortName} ${state.label.toLowerCase()}`, | |
| 325 | colorClass: state.toneClassName, | |
| 326 | Icon: state.Icon, | |
| 327 | tooltip: `${tooltipLead}: ${pr.title}`, | |
| 328 | tooltipLead, | |
| 329 | tooltipTitle: pr.title, | |
| 330 | url: pr.url, | |
| 331 | }; | |
| 355 | 332 | } |
| 356 | 333 | |
| 357 | 334 | export function ChangeRequestStatusIcon({ |
| ... | ... | @@ -529,7 +506,7 @@ export function ThreadRowLeadingStatus({ thread }: { thread: SidebarThreadSummar |
| 529 | 506 | </Tooltip> |
| 530 | 507 | ) : null} |
| 531 | 508 | {pendingLink ? ( |
| 532 | <GitPullRequestArrowIcon | |
| 509 | <PullRequestGlyph.pullRequest | |
| 533 | 510 | className="size-3 text-muted-foreground" |
| 534 | 511 | aria-label={`PR #${pendingLink.number}, status pending`} |
| 535 | 512 | /> |
apps/web/src/components/chat/MessagesTimeline.tsx+3-3| ... | ... | @@ -112,7 +112,6 @@ import { |
| 112 | 112 | CircleAlertIcon, |
| 113 | 113 | DownloadIcon, |
| 114 | 114 | EyeIcon, |
| 115 | GitPullRequestIcon, | |
| 116 | 115 | GlobeIcon, |
| 117 | 116 | HammerIcon, |
| 118 | 117 | MessageCircleIcon, |
| ... | ... | @@ -250,6 +249,7 @@ import { |
| 250 | 249 | formatReviewCommentFence, |
| 251 | 250 | type ReviewCommentContext, |
| 252 | 251 | } from "../../reviewCommentContext"; |
| 252 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 253 | 253 | |
| 254 | 254 | // --------------------------------------------------------------------------- |
| 255 | 255 | // Context — shared state consumed by every row component via Context. |
| ... | ... | @@ -3326,7 +3326,7 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3326 | 3326 | <UserMessageContextChip |
| 3327 | 3327 | icon={ |
| 3328 | 3328 | isPullRequest ? ( |
| 3329 | <GitPullRequestIcon | |
| 3329 | <PullRequestGlyph.pullRequest | |
| 3330 | 3330 | className={cn( |
| 3331 | 3331 | COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, |
| 3332 | 3332 | CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["pull-request"], |
| ... | ... | @@ -3877,7 +3877,7 @@ function ToolActivityImageIcon(props: { |
| 3877 | 3877 | function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className: string }) { |
| 3878 | 3878 | switch (name) { |
| 3879 | 3879 | case "pull-request": |
| 3880 | return <GitPullRequestIcon className={className} aria-hidden />; | |
| 3880 | return <PullRequestGlyph.pullRequest className={className} aria-hidden />; | |
| 3881 | 3881 | case "bot": |
| 3882 | 3882 | return <BotIcon className={className} aria-hidden />; |
| 3883 | 3883 | case "brain": |
apps/web/src/components/composerContextPresentation.tsx+3-2| ... | ... | @@ -3,7 +3,7 @@ import { ReadOnlySourcePreview } from "./files/AttachmentFilePreview"; |
| 3 | 3 | import type { PreviewAnnotationPayload } from "@t3tools/contracts"; |
| 4 | 4 | import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; |
| 5 | 5 | import { videoMimeType } from "@t3tools/shared/video"; |
| 6 | import { GitPullRequestIcon, MessageCircleIcon, MousePointerClickIcon } from "lucide-react"; | |
| 6 | import { MessageCircleIcon, MousePointerClickIcon } from "lucide-react"; | |
| 7 | 7 | import { createContext, type MouseEvent, type ReactElement, type ReactNode, use } from "react"; |
| 8 | 8 | |
| 9 | 9 | import type { ComposerFileAttachment, ComposerImageAttachment } from "~/composerDraftStore"; |
| ... | ... | @@ -14,6 +14,7 @@ import { |
| 14 | 14 | type AttachmentUploadState, |
| 15 | 15 | } from "~/lib/attachmentUploadState"; |
| 16 | 16 | import { cn } from "~/lib/utils"; |
| 17 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 17 | 18 | import { |
| 18 | 19 | fileContextReference, |
| 19 | 20 | imageContextReference, |
| ... | ... | @@ -392,7 +393,7 @@ const composerContextPresentationRegistry = createContextPresentationRegistry< |
| 392 | 393 | <ContextChip |
| 393 | 394 | icon={ |
| 394 | 395 | isPullRequest ? ( |
| 395 | <GitPullRequestIcon | |
| 396 | <PullRequestGlyph.pullRequest | |
| 396 | 397 | className={cn( |
| 397 | 398 | COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, |
| 398 | 399 | CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["pull-request"], |
apps/web/src/components/contextChipParts.tsx+5-2| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import type { PullRequestContextMetadata } from "@t3tools/contracts"; |
| 2 | import { CircleDashedIcon, FilmIcon, GitPullRequestIcon, ImageIcon } from "lucide-react"; | |
| 2 | import { CircleDashedIcon, FilmIcon, ImageIcon } from "lucide-react"; | |
| 3 | 3 | import { |
| 4 | 4 | useState, |
| 5 | 5 | type ComponentProps, |
| ... | ... | @@ -9,6 +9,7 @@ import { |
| 9 | 9 | } from "react"; |
| 10 | 10 | |
| 11 | 11 | import { cn } from "~/lib/utils"; |
| 12 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 12 | 13 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; |
| 13 | 14 | import { |
| 14 | 15 | COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, |
| ... | ... | @@ -134,7 +135,9 @@ export function PullRequestChip(props: { |
| 134 | 135 | data-markdown-copy={props.copyMarkdown} |
| 135 | 136 | onClick={(event) => props.onOpen(event, props.metadata.url)} |
| 136 | 137 | > |
| 137 | <GitPullRequestIcon className={cn(COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, "size-3.5")} /> | |
| 138 | <PullRequestGlyph.pullRequest | |
| 139 | className={cn(COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, "size-3.5")} | |
| 140 | /> | |
| 138 | 141 | <span className={props.labelClassName}>{props.label}</span> |
| 139 | 142 | </Button> |
| 140 | 143 | } |
apps/web/src/components/pullRequest/PullRequestCommentComposer.tsx+4-9| ... | ... | @@ -1,11 +1,5 @@ |
| 1 | 1 | import type { EnvironmentId, PullRequestDetailView, PullRequestRef } from "@t3tools/contracts"; |
| 2 | import { | |
| 3 | GitPullRequestClosedIcon, | |
| 4 | MessageSquareIcon, | |
| 5 | RotateCcwIcon, | |
| 6 | SendIcon, | |
| 7 | XIcon, | |
| 8 | } from "lucide-react"; | |
| 2 | import { MessageSquareIcon, SendIcon, XIcon } from "lucide-react"; | |
| 9 | 3 | import { useRef, useState } from "react"; |
| 10 | 4 | |
| 11 | 5 | import { useAtomCommand } from "~/state/use-atom-command"; |
| ... | ... | @@ -15,6 +9,7 @@ import { Button } from "../ui/button"; |
| 15 | 9 | import { Popover, PopoverClose, PopoverPopup, PopoverTitle, PopoverTrigger } from "../ui/popover"; |
| 16 | 10 | import { Textarea } from "../ui/textarea"; |
| 17 | 11 | import { toastManager } from "../ui/toast"; |
| 12 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 18 | 13 | |
| 19 | 14 | export function PullRequestCommentComposer({ |
| 20 | 15 | environmentId, |
| ... | ... | @@ -146,9 +141,9 @@ export function PullRequestCommentComposer({ |
| 146 | 141 | onClick={() => void submit(followUpAction)} |
| 147 | 142 | > |
| 148 | 143 | {followUpAction === "close" ? ( |
| 149 | <GitPullRequestClosedIcon className="size-3.5" /> | |
| 144 | <PullRequestGlyph.closed className="size-3.5" /> | |
| 150 | 145 | ) : ( |
| 151 | <RotateCcwIcon className="size-3.5" /> | |
| 146 | <PullRequestGlyph.reopen className="size-3.5" /> | |
| 152 | 147 | )} |
| 153 | 148 | {submitting === followUpAction |
| 154 | 149 | ? followUpAction === "close" |
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+18-22| ... | ... | @@ -29,12 +29,7 @@ import { |
| 29 | 29 | FolderGit2Icon, |
| 30 | 30 | GitBranchIcon, |
| 31 | 31 | GitCommitHorizontalIcon, |
| 32 | GitMergeIcon, | |
| 33 | GitPullRequestClosedIcon, | |
| 34 | GitPullRequestDraftIcon, | |
| 35 | GitPullRequestIcon, | |
| 36 | 32 | HammerIcon, |
| 37 | LayersIcon, | |
| 38 | 33 | MessageCircleQuestionIcon, |
| 39 | 34 | MessageSquareIcon, |
| 40 | 35 | LinkIcon, |
| ... | ... | @@ -176,6 +171,7 @@ import { |
| 176 | 171 | resolvePullRequestState, |
| 177 | 172 | summarizePullRequestChecks, |
| 178 | 173 | } from "./pullRequestPresentation"; |
| 174 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 179 | 175 | |
| 180 | 176 | type DetailTab = "summary" | "timeline" | "code"; |
| 181 | 177 | |
| ... | ... | @@ -455,7 +451,7 @@ function PullRequestBaseFreshnessWarning({ |
| 455 | 451 | disabled={pending} |
| 456 | 452 | onClick={() => onUpdate(method)} |
| 457 | 453 | > |
| 458 | <GitMergeIcon aria-hidden className="size-3" /> | |
| 454 | <PullRequestGlyph.merged aria-hidden className="size-3" /> | |
| 459 | 455 | {method === "rebase" ? "Update with rebase" : "Update branch"} |
| 460 | 456 | </Button> |
| 461 | 457 | ))} |
| ... | ... | @@ -1803,7 +1799,7 @@ export function PullRequestDetailPanel({ |
| 1803 | 1799 | role="img" |
| 1804 | 1800 | aria-label={armedAutoMergeLabel} |
| 1805 | 1801 | > |
| 1806 | <GitMergeIcon aria-hidden className="size-3.5" /> | |
| 1802 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> | |
| 1807 | 1803 | <span className="@max-[30rem]/pr-header:hidden">{armedAutoMergeLabel}</span> |
| 1808 | 1804 | </Badge> |
| 1809 | 1805 | } |
| ... | ... | @@ -1828,7 +1824,7 @@ export function PullRequestDetailPanel({ |
| 1828 | 1824 | handoff === "conflicts" ? "Preparing..." : "Resolve conflicts" |
| 1829 | 1825 | } |
| 1830 | 1826 | > |
| 1831 | <TriangleAlertIcon aria-hidden className="size-3.5" /> | |
| 1827 | <PullRequestGlyph.conflicting aria-hidden className="size-3.5" /> | |
| 1832 | 1828 | <span className="@max-[30rem]/pr-header:hidden"> |
| 1833 | 1829 | {handoff === "conflicts" ? "Preparing..." : "Resolve conflicts"} |
| 1834 | 1830 | </span> |
| ... | ... | @@ -1852,7 +1848,7 @@ export function PullRequestDetailPanel({ |
| 1852 | 1848 | onClick={() => void perform("ready")} |
| 1853 | 1849 | aria-label="Ready for review" |
| 1854 | 1850 | > |
| 1855 | <GitPullRequestIcon aria-hidden className="size-3.5" /> | |
| 1851 | <PullRequestGlyph.pullRequest aria-hidden className="size-3.5" /> | |
| 1856 | 1852 | <span className="@max-[30rem]/pr-header:hidden">Ready for review</span> |
| 1857 | 1853 | </Button> |
| 1858 | 1854 | </span> |
| ... | ... | @@ -1878,7 +1874,7 @@ export function PullRequestDetailPanel({ |
| 1878 | 1874 | : pendingAutoMergeLabel |
| 1879 | 1875 | } |
| 1880 | 1876 | > |
| 1881 | <GitMergeIcon aria-hidden className="size-3.5" /> | |
| 1877 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> | |
| 1882 | 1878 | <span className="@max-[30rem]/pr-header:hidden"> |
| 1883 | 1879 | {pendingAction === "enable-auto-merge" |
| 1884 | 1880 | ? "Enabling..." |
| ... | ... | @@ -1902,7 +1898,7 @@ export function PullRequestDetailPanel({ |
| 1902 | 1898 | role="img" |
| 1903 | 1899 | aria-label={armedAutoMergeLabel} |
| 1904 | 1900 | > |
| 1905 | <GitMergeIcon aria-hidden className="size-3.5" /> | |
| 1901 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> | |
| 1906 | 1902 | <span className="@max-[30rem]/pr-header:hidden">{armedAutoMergeLabel}</span> |
| 1907 | 1903 | </Badge> |
| 1908 | 1904 | } |
| ... | ... | @@ -1926,7 +1922,7 @@ export function PullRequestDetailPanel({ |
| 1926 | 1922 | pendingAction === "merge" ? "Merging..." : selectedMergeMethodLabel |
| 1927 | 1923 | } |
| 1928 | 1924 | > |
| 1929 | <GitMergeIcon aria-hidden className="size-3.5" /> | |
| 1925 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> | |
| 1930 | 1926 | <span className="@max-[30rem]/pr-header:hidden"> |
| 1931 | 1927 | {pendingAction === "merge" ? "Merging..." : selectedMergeMethodLabel} |
| 1932 | 1928 | </span> |
| ... | ... | @@ -2031,9 +2027,9 @@ export function PullRequestDetailPanel({ |
| 2031 | 2027 | onClick={() => void perform(detail.isDraft ? "ready" : "draft")} |
| 2032 | 2028 | > |
| 2033 | 2029 | {detail.isDraft ? ( |
| 2034 | <GitPullRequestIcon className="size-3.5" /> | |
| 2030 | <PullRequestGlyph.pullRequest className="size-3.5" /> | |
| 2035 | 2031 | ) : ( |
| 2036 | <GitPullRequestDraftIcon className="size-3.5" /> | |
| 2032 | <PullRequestGlyph.draft className="size-3.5" /> | |
| 2037 | 2033 | )} |
| 2038 | 2034 | {detail.isDraft ? "Ready for review" : "Convert to draft"} |
| 2039 | 2035 | </MenuItem> |
| ... | ... | @@ -2043,7 +2039,7 @@ export function PullRequestDetailPanel({ |
| 2043 | 2039 | disabled={actionPending} |
| 2044 | 2040 | onClick={() => setConfirmation({ open: true, action: "merge" })} |
| 2045 | 2041 | > |
| 2046 | <GitMergeIcon className="size-3.5" /> | |
| 2042 | <PullRequestGlyph.merged className="size-3.5" /> | |
| 2047 | 2043 | Merge now |
| 2048 | 2044 | </MenuItem> |
| 2049 | 2045 | ) : null} |
| ... | ... | @@ -2055,7 +2051,7 @@ export function PullRequestDetailPanel({ |
| 2055 | 2051 | disabled={actionPending} |
| 2056 | 2052 | onClick={() => void perform("disable-auto-merge")} |
| 2057 | 2053 | > |
| 2058 | <GitMergeIcon className="size-3.5" /> | |
| 2054 | <PullRequestGlyph.merged className="size-3.5" /> | |
| 2059 | 2055 | Disable auto-merge |
| 2060 | 2056 | </MenuItem> |
| 2061 | 2057 | ) : showsAutoMerge ? ( |
| ... | ... | @@ -2065,7 +2061,7 @@ export function PullRequestDetailPanel({ |
| 2065 | 2061 | setConfirmation({ open: true, action: "enable-auto-merge" }) |
| 2066 | 2062 | } |
| 2067 | 2063 | > |
| 2068 | <GitMergeIcon className="size-3.5" /> | |
| 2064 | <PullRequestGlyph.merged className="size-3.5" /> | |
| 2069 | 2065 | Enable auto-merge |
| 2070 | 2066 | </MenuItem> |
| 2071 | 2067 | ) : null} |
| ... | ... | @@ -2100,7 +2096,7 @@ export function PullRequestDetailPanel({ |
| 2100 | 2096 | {/* The radio item lays its children out as one block, so the |
| 2101 | 2097 | icon and the label need their own row to share a line. */} |
| 2102 | 2098 | <span className="flex min-w-0 items-center gap-2"> |
| 2103 | <GitMergeIcon className="size-3.5" /> | |
| 2099 | <PullRequestGlyph.merged className="size-3.5" /> | |
| 2104 | 2100 | <span>{PULL_REQUEST_MERGE_METHOD_LABELS[method]}</span> |
| 2105 | 2101 | </span> |
| 2106 | 2102 | </MenuRadioItem> |
| ... | ... | @@ -2143,7 +2139,7 @@ export function PullRequestDetailPanel({ |
| 2143 | 2139 | disabled={actionPending} |
| 2144 | 2140 | onClick={() => setConfirmation({ open: true, action: "close" })} |
| 2145 | 2141 | > |
| 2146 | <GitPullRequestClosedIcon className="size-3.5" /> | |
| 2142 | <PullRequestGlyph.closed className="size-3.5" /> | |
| 2147 | 2143 | Close pull request |
| 2148 | 2144 | </MenuItem> |
| 2149 | 2145 | </> |
| ... | ... | @@ -2151,7 +2147,7 @@ export function PullRequestDetailPanel({ |
| 2151 | 2147 | <> |
| 2152 | 2148 | <MenuSeparator /> |
| 2153 | 2149 | <MenuItem disabled={actionPending} onClick={() => void perform("reopen")}> |
| 2154 | <GitPullRequestIcon className="size-3.5" /> | |
| 2150 | <PullRequestGlyph.reopen className="size-3.5" /> | |
| 2155 | 2151 | Reopen pull request |
| 2156 | 2152 | </MenuItem> |
| 2157 | 2153 | </> |
| ... | ... | @@ -2220,7 +2216,7 @@ export function PullRequestDetailPanel({ |
| 2220 | 2216 | render={ |
| 2221 | 2217 | <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1"> |
| 2222 | 2218 | {isStackedPullRequest ? ( |
| 2223 | <LayersIcon | |
| 2219 | <PullRequestGlyph.stack | |
| 2224 | 2220 | aria-label="Stacked pull request" |
| 2225 | 2221 | className="size-3 shrink-0" |
| 2226 | 2222 | /> |
| ... | ... | @@ -2404,7 +2400,7 @@ export function PullRequestDetailPanel({ |
| 2404 | 2400 | render={ |
| 2405 | 2401 | <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1"> |
| 2406 | 2402 | {isStackedPullRequest ? ( |
| 2407 | <LayersIcon | |
| 2403 | <PullRequestGlyph.stack | |
| 2408 | 2404 | aria-label="Stacked pull request" |
| 2409 | 2405 | className="size-3 shrink-0" |
| 2410 | 2406 | /> |
apps/web/src/components/pullRequest/PullRequestListFilters.tsx+2-2| ... | ... | @@ -15,7 +15,6 @@ import { |
| 15 | 15 | CircleXIcon, |
| 16 | 16 | EyeOffIcon, |
| 17 | 17 | FolderGit2Icon, |
| 18 | GitPullRequestDraftIcon, | |
| 19 | 18 | LayersIcon, |
| 20 | 19 | ListFilterIcon, |
| 21 | 20 | SearchIcon, |
| ... | ... | @@ -51,6 +50,7 @@ import { |
| 51 | 50 | type PullRequestLabelFacet, |
| 52 | 51 | } from "./pullRequestList.logic"; |
| 53 | 52 | import { PullRequestActorAvatar } from "./pullRequestPresentation"; |
| 53 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 54 | 54 | |
| 55 | 55 | export interface PullRequestFilterOption<Value extends string> { |
| 56 | 56 | readonly value: Value; |
| ... | ... | @@ -143,7 +143,7 @@ export const pullRequestProjectKey = (project: { |
| 143 | 143 | |
| 144 | 144 | const DRAFT_OPTIONS = [ |
| 145 | 145 | { value: UNFILTERED_VALUE, label: "All", Icon: LayersIcon }, |
| 146 | { value: "only", label: "Drafts only", Icon: GitPullRequestDraftIcon }, | |
| 146 | { value: "only", label: "Drafts only", Icon: PullRequestGlyph.draft }, | |
| 147 | 147 | { value: "hide", label: "Hide drafts", Icon: EyeOffIcon }, |
| 148 | 148 | ] as const satisfies ReadonlyArray<PullRequestFilterOption<string>>; |
| 149 | 149 |
apps/web/src/components/pullRequest/PullRequestRow.tsx+18-6| ... | ... | @@ -12,6 +12,7 @@ import { pullRequestLabelColor, type EnvironmentPullRequestEntry } from "./pullR |
| 12 | 12 | import { openOnHostLabel, showPullRequestLinkContextMenu } from "./pullRequestLinkContextMenu"; |
| 13 | 13 | import { |
| 14 | 14 | PullRequestActorLabel, |
| 15 | PullRequestConflictGlyph, | |
| 15 | 16 | PullRequestDiffStat, |
| 16 | 17 | PullRequestMetaLine, |
| 17 | 18 | PullRequestApprovalGlyph, |
| ... | ... | @@ -112,12 +113,23 @@ function PullRequestRowImpl({ |
| 112 | 113 | selected ? "bg-accent" : "hover:bg-accent/60", |
| 113 | 114 | )} |
| 114 | 115 | > |
| 115 | <PullRequestStateGlyph | |
| 116 | state={entry.state} | |
| 117 | isDraft={entry.isDraft} | |
| 118 | mergeability={entry.mergeability} | |
| 119 | baseBranch={entry.baseBranch} | |
| 120 | /> | |
| 116 | {/* The conflict warning rides the corner of the lifecycle glyph, over the arrow's | |
| 117 | merge circle, so the leading slot stays one icon wide and titles line up whether or | |
| 118 | not a row is blocked. The background fill cuts it out of the glyph beneath. */} | |
| 119 | <span className="relative inline-flex shrink-0"> | |
| 120 | <PullRequestStateGlyph state={entry.state} isDraft={entry.isDraft} /> | |
| 121 | {/* The wrapper takes the offset, not the icon, so the tooltip trigger inside keeps the | |
| 122 | badge's size and anchors the popup to it. */} | |
| 123 | <span className="absolute -right-1 -bottom-1 inline-flex"> | |
| 124 | <PullRequestConflictGlyph | |
| 125 | state={entry.state} | |
| 126 | isDraft={entry.isDraft} | |
| 127 | mergeability={entry.mergeability} | |
| 128 | baseBranch={entry.baseBranch} | |
| 129 | className="size-3 fill-background [stroke-width:2.5]" | |
| 130 | /> | |
| 131 | </span> | |
| 132 | </span> | |
| 121 | 133 | <span className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1.5"> |
| 122 | 134 | <span className="col-start-1 row-start-1 block truncate text-sm font-medium text-foreground"> |
| 123 | 135 | {entry.title} |
apps/web/src/components/pullRequest/PullRequestStackMenu.tsx+6-4| ... | ... | @@ -6,7 +6,7 @@ import type { |
| 6 | 6 | PullRequestMergeMethod, |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; |
| 9 | import { GitMergeIcon, LayersIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react"; | |
| 9 | import { RefreshCwIcon, TriangleAlertIcon } from "lucide-react"; | |
| 10 | 10 | import { useState } from "react"; |
| 11 | 11 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 12 | 12 | import { pullRequestEnvironment } from "~/state/pullRequests"; |
| ... | ... | @@ -25,6 +25,7 @@ import { toastManager } from "../ui/toast"; |
| 25 | 25 | import { PullRequestStackLayers } from "./PullRequestStackLayers"; |
| 26 | 26 | import { PullRequestStackHeader } from "./PullRequestStackHeader"; |
| 27 | 27 | import { PullRequestStackLayerContent } from "./PullRequestStackLayerContent"; |
| 28 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 28 | 29 | |
| 29 | 30 | export function PullRequestStackMenu({ |
| 30 | 31 | stack, |
| ... | ... | @@ -133,7 +134,8 @@ export function PullRequestStackMenu({ |
| 133 | 134 | /> |
| 134 | 135 | } |
| 135 | 136 | > |
| 136 | <LayersIcon aria-hidden className="size-3.5" /> {position}/{stack.layers.length} | |
| 137 | <PullRequestGlyph.stack aria-hidden className="size-3.5" /> {position}/ | |
| 138 | {stack.layers.length} | |
| 137 | 139 | {onRetry ? <TriangleAlertIcon aria-hidden className="size-3 text-warning" /> : null} |
| 138 | 140 | </MenuTrigger> |
| 139 | 141 | } |
| ... | ... | @@ -166,7 +168,7 @@ export function PullRequestStackMenu({ |
| 166 | 168 | <MenuSeparator /> |
| 167 | 169 | {canMerge ? ( |
| 168 | 170 | <MenuItem disabled={mergeDisabled} onClick={() => setConfirmation("merge")}> |
| 169 | <GitMergeIcon aria-hidden /> | |
| 171 | <PullRequestGlyph.merged aria-hidden /> | |
| 170 | 172 | Merge stack ({mergeLayers.length}) |
| 171 | 173 | </MenuItem> |
| 172 | 174 | ) : null} |
| ... | ... | @@ -199,7 +201,7 @@ export function PullRequestStackMenu({ |
| 199 | 201 | disabled={mergeDisabled} |
| 200 | 202 | onClick={() => setConfirmation("merge")} |
| 201 | 203 | > |
| 202 | <GitMergeIcon aria-hidden className="size-3.5" /> | |
| 204 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> | |
| 203 | 205 | Merge stack |
| 204 | 206 | </Button> |
| 205 | 207 | </span> |
apps/web/src/components/pullRequest/PullRequestStackPopover.tsx+2-2| ... | ... | @@ -1,11 +1,11 @@ |
| 1 | 1 | import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; |
| 2 | 2 | import type { EnvironmentId, PullRequestRef, PullRequestStackMembership } from "@t3tools/contracts"; |
| 3 | import { LayersIcon } from "lucide-react"; | |
| 4 | 3 | import { useState } from "react"; |
| 5 | 4 | import { usePullRequestStack } from "~/state/usePullRequestStack"; |
| 6 | 5 | import { Menu, MenuTrigger, MenuPopup, MenuGroup, MenuGroupLabel, MenuItem } from "../ui/menu"; |
| 7 | 6 | import { PullRequestStackLayers } from "./PullRequestStackLayers"; |
| 8 | 7 | import { PullRequestStackHeader } from "./PullRequestStackHeader"; |
| 8 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 9 | 9 | |
| 10 | 10 | /** Mounted only while the menu is open, so list rows do not each fetch a stack. */ |
| 11 | 11 | function StackBody({ |
| ... | ... | @@ -74,7 +74,7 @@ export function PullRequestStackPopover({ |
| 74 | 74 | onClick={(event) => event.stopPropagation()} |
| 75 | 75 | onKeyDown={(event) => event.stopPropagation()} |
| 76 | 76 | > |
| 77 | <LayersIcon aria-hidden className="size-3" /> | |
| 77 | <PullRequestGlyph.stack aria-hidden className="size-3" /> | |
| 78 | 78 | {membership.position}/{membership.size} |
| 79 | 79 | </MenuTrigger> |
| 80 | 80 | } |
apps/web/src/components/pullRequest/PullRequestThreadLinks.tsx+4-3| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; |
| 2 | 2 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 3 | 3 | import type { EnvironmentId, PullRequestRef, ScopedThreadRef, ThreadId } from "@t3tools/contracts"; |
| 4 | import { CheckIcon, LinkIcon, MessageSquareIcon, UnlinkIcon } from "lucide-react"; | |
| 4 | import { CheckIcon, MessageSquareIcon } from "lucide-react"; | |
| 5 | 5 | import { useState } from "react"; |
| 6 | 6 | import { threadPullRequestLinkMode } from "@t3tools/client-runtime/thread-pull-request-compatibility"; |
| 7 | 7 | import { usePullRequestLinking } from "~/hooks/usePullRequestLinking"; |
| ... | ... | @@ -18,6 +18,7 @@ import { Command, CommandInput, CommandItem, CommandList } from "../ui/command"; |
| 18 | 18 | import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog"; |
| 19 | 19 | import { MenuItem } from "../ui/menu"; |
| 20 | 20 | import { toastManager } from "../ui/toast"; |
| 21 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 21 | 22 | |
| 22 | 23 | interface PullRequestThreadLinksProps { |
| 23 | 24 | environmentId: EnvironmentId; |
| ... | ... | @@ -144,9 +145,9 @@ function EnabledPullRequestThreadLinks({ |
| 144 | 145 | }} |
| 145 | 146 | > |
| 146 | 147 | {linkedHere ? ( |
| 147 | <UnlinkIcon aria-hidden className="size-3.5" /> | |
| 148 | <PullRequestGlyph.unlink aria-hidden className="size-3.5" /> | |
| 148 | 149 | ) : ( |
| 149 | <LinkIcon aria-hidden className="size-3.5" /> | |
| 150 | <PullRequestGlyph.link aria-hidden className="size-3.5" /> | |
| 150 | 151 | )} |
| 151 | 152 | {linkedHere |
| 152 | 153 | ? "Unlink from this thread" |
apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx+5-7| ... | ... | @@ -11,9 +11,6 @@ import { |
| 11 | 11 | ExternalLinkIcon, |
| 12 | 12 | FileCode2Icon, |
| 13 | 13 | GitCommitHorizontalIcon, |
| 14 | GitMergeIcon, | |
| 15 | GitPullRequestClosedIcon, | |
| 16 | GitPullRequestIcon, | |
| 17 | 14 | MessageSquareIcon, |
| 18 | 15 | PencilIcon, |
| 19 | 16 | } from "lucide-react"; |
| ... | ... | @@ -51,6 +48,7 @@ import { |
| 51 | 48 | pullRequestReviewOutcomeStaleLabel, |
| 52 | 49 | pullRequestReviewOutcomeToneClassName, |
| 53 | 50 | } from "./pullRequestPresentation"; |
| 51 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 54 | 52 | |
| 55 | 53 | /** What every comment on the timeline needs to react; only the subject differs between them. */ |
| 56 | 54 | interface ReactionSurface { |
| ... | ... | @@ -419,16 +417,16 @@ function LifecycleEvent({ event }: { event: PullRequestTimelineEvent }) { |
| 419 | 417 | const presentation = |
| 420 | 418 | event.kind === "opened" |
| 421 | 419 | ? { |
| 422 | icon: <GitPullRequestIcon className="size-3.5" />, | |
| 420 | icon: <PullRequestGlyph.pullRequest className="size-3.5" />, | |
| 423 | 421 | label: "Pull request opened", |
| 424 | 422 | } |
| 425 | 423 | : event.kind === "merged" |
| 426 | 424 | ? { |
| 427 | icon: <GitMergeIcon className="size-3.5" />, | |
| 425 | icon: <PullRequestGlyph.merged className="size-3.5" />, | |
| 428 | 426 | label: "Pull request merged", |
| 429 | 427 | } |
| 430 | 428 | : { |
| 431 | icon: <GitPullRequestClosedIcon className="size-3.5" />, | |
| 429 | icon: <PullRequestGlyph.closed className="size-3.5" />, | |
| 432 | 430 | label: "Pull request closed", |
| 433 | 431 | }; |
| 434 | 432 | |
| ... | ... | @@ -629,7 +627,7 @@ export function PullRequestTimelineTab({ |
| 629 | 627 | |
| 630 | 628 | {events.length === 0 ? ( |
| 631 | 629 | <div className="flex flex-col items-center justify-center py-16 text-center text-muted-foreground"> |
| 632 | <GitPullRequestIcon className="mb-2 size-5" /> | |
| 630 | <PullRequestGlyph.pullRequest className="mb-2 size-5" /> | |
| 633 | 631 | <p className="text-xs">No activity yet.</p> |
| 634 | 632 | </div> |
| 635 | 633 | ) : null} |
apps/web/src/components/pullRequest/PullRequestsUnavailableState.tsx+3-2| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { RefreshIcon } from "~/components/ui/refresh-icon"; |
| 2 | import { ExternalLinkIcon, GitPullRequestIcon } from "lucide-react"; | |
| 2 | import { ExternalLinkIcon } from "lucide-react"; | |
| 3 | 3 | |
| 4 | 4 | import { Button } from "../ui/button"; |
| 5 | 5 | import { |
| ... | ... | @@ -10,6 +10,7 @@ import { |
| 10 | 10 | EmptyMedia, |
| 11 | 11 | EmptyTitle, |
| 12 | 12 | } from "../ui/empty"; |
| 13 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 13 | 14 | |
| 14 | 15 | export function PullRequestsUnavailableState({ |
| 15 | 16 | title = "Could not load pull requests", |
| ... | ... | @@ -27,7 +28,7 @@ export function PullRequestsUnavailableState({ |
| 27 | 28 | return ( |
| 28 | 29 | <Empty className="min-h-0 justify-center-safe overflow-y-auto px-4 py-16 md:px-4 [&>*]:shrink-0"> |
| 29 | 30 | <EmptyMedia variant="icon"> |
| 30 | <GitPullRequestIcon /> | |
| 31 | <PullRequestGlyph.pullRequest /> | |
| 31 | 32 | </EmptyMedia> |
| 32 | 33 | <EmptyHeader> |
| 33 | 34 | <EmptyTitle>{title}</EmptyTitle> |
apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx+24-20| ... | ... | @@ -3,13 +3,7 @@ import { |
| 3 | 3 | resolveThreadPullRequestChains, |
| 4 | 4 | visibleThreadPullRequests, |
| 5 | 5 | } from "@t3tools/shared/threadPullRequests"; |
| 6 | import { | |
| 7 | GitPullRequestArrow, | |
| 8 | LayersIcon, | |
| 9 | LinkIcon, | |
| 10 | MoreHorizontalIcon, | |
| 11 | PlusIcon, | |
| 12 | } from "lucide-react"; | |
| 6 | import { ArrowUpRightIcon, LinkIcon, MoreHorizontalIcon, PlusIcon } from "lucide-react"; | |
| 13 | 7 | import { useCallback, useMemo } from "react"; |
| 14 | 8 | |
| 15 | 9 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; |
| ... | ... | @@ -28,11 +22,13 @@ import { openLinkPullRequestDialog } from "./LinkPullRequestDialog"; |
| 28 | 22 | import { pullRequestListLines, type PullRequestListLine } from "./pullRequestListLines"; |
| 29 | 23 | import { |
| 30 | 24 | PullRequestActorAvatar, |
| 25 | PullRequestConflictGlyph, | |
| 31 | 26 | PullRequestDiffStat, |
| 32 | 27 | PullRequestApprovalGlyph, |
| 33 | 28 | PullRequestStateGlyph, |
| 34 | 29 | pullRequestChecksStatePresentation, |
| 35 | 30 | } from "./pullRequestPresentation"; |
| 31 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 36 | 32 | |
| 37 | 33 | const SOURCE_LABELS: Record<ThreadPullRequestLink["source"], string> = { |
| 38 | 34 | manual: "Linked by you", |
| ... | ... | @@ -84,12 +80,20 @@ function LinkRow({ |
| 84 | 80 | > |
| 85 | 81 | {depth > 0 ? <span aria-hidden className="-ml-2 h-6 w-px shrink-0 bg-border/70" /> : null} |
| 86 | 82 | {snapshot === null ? ( |
| 87 | <GitPullRequestArrow | |
| 83 | <PullRequestGlyph.pullRequest | |
| 88 | 84 | aria-label="Waiting for host state" |
| 89 | 85 | className="size-4 shrink-0 text-muted-foreground" |
| 90 | 86 | /> |
| 91 | 87 | ) : ( |
| 92 | <PullRequestStateGlyph state={snapshot.state} isDraft={snapshot.isDraft} /> | |
| 88 | <span className="flex shrink-0 items-center gap-1"> | |
| 89 | <PullRequestStateGlyph state={snapshot.state} isDraft={snapshot.isDraft} /> | |
| 90 | <PullRequestConflictGlyph | |
| 91 | state={snapshot.state} | |
| 92 | isDraft={snapshot.isDraft} | |
| 93 | baseBranch={snapshot.baseBranch} | |
| 94 | {...(snapshot.mergeability ? { mergeability: snapshot.mergeability } : {})} | |
| 95 | /> | |
| 96 | </span> | |
| 93 | 97 | )} |
| 94 | 98 | <a |
| 95 | 99 | href={link.url} |
| ... | ... | @@ -125,9 +129,6 @@ function LinkRow({ |
| 125 | 129 | <span className="text-amber-600/90 dark:text-amber-400/80">Changes requested</span> |
| 126 | 130 | ) |
| 127 | 131 | ) : null} |
| 128 | {snapshot?.state === "open" && snapshot.mergeability === "conflicting" ? ( | |
| 129 | <span className="text-destructive">Conflicts</span> | |
| 130 | ) : null} | |
| 131 | 132 | {snapshot?.checksState ? <ChecksGlyph state={snapshot.checksState} /> : null} |
| 132 | 133 | <PullRequestDiffStat |
| 133 | 134 | additions={snapshot?.additions ?? 0} |
| ... | ... | @@ -144,11 +145,7 @@ function LinkRow({ |
| 144 | 145 | <span className="inline-flex shrink-0 items-center gap-0.5 text-foreground/70" /> |
| 145 | 146 | } |
| 146 | 147 | > |
| 147 | {stack.kind === "native" ? ( | |
| 148 | <LayersIcon aria-hidden className="size-3" /> | |
| 149 | ) : ( | |
| 150 | <GitPullRequestArrow aria-hidden className="size-3" /> | |
| 151 | )} | |
| 148 | <PullRequestGlyph.stack aria-hidden className="size-3" /> | |
| 152 | 149 | {stack.size} |
| 153 | 150 | </TooltipTrigger> |
| 154 | 151 | <TooltipPopup> |
| ... | ... | @@ -190,9 +187,16 @@ function LinkRow({ |
| 190 | 187 | } |
| 191 | 188 | /> |
| 192 | 189 | <MenuPopup align="end" side="bottom"> |
| 193 | <MenuItem onClick={() => void writeTextToClipboard(link.url, "link")}>Copy link</MenuItem> | |
| 194 | <MenuItem onClick={(event) => openPrLink(event, link.url, threadRef)}>Open</MenuItem> | |
| 190 | <MenuItem onClick={() => void writeTextToClipboard(link.url, "link")}> | |
| 191 | <LinkIcon className="size-3.5" /> | |
| 192 | Copy link | |
| 193 | </MenuItem> | |
| 194 | <MenuItem onClick={(event) => openPrLink(event, link.url, threadRef)}> | |
| 195 | <ArrowUpRightIcon className="size-3.5" /> | |
| 196 | Open | |
| 197 | </MenuItem> | |
| 195 | 198 | <MenuItem onClick={() => onUnlink(link)}> |
| 199 | <PullRequestGlyph.unlink className="size-3.5" /> | |
| 196 | 200 | {link.source === "stack" ? "Dismiss from thread" : "Unlink from thread"} |
| 197 | 201 | </MenuItem> |
| 198 | 202 | </MenuPopup> |
| ... | ... | @@ -250,7 +254,7 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread |
| 250 | 254 | if (links.length === 0) { |
| 251 | 255 | return ( |
| 252 | 256 | <div className="flex h-full flex-col items-center justify-center gap-2 p-6 text-center"> |
| 253 | <LinkIcon aria-hidden className="size-6 text-muted-foreground/60" /> | |
| 257 | <PullRequestGlyph.link aria-hidden className="size-6 text-muted-foreground/60" /> | |
| 254 | 258 | <p className="text-sm font-medium">No linked pull requests</p> |
| 255 | 259 | <p className="max-w-60 text-xs text-muted-foreground"> |
| 256 | 260 | Pull requests the agent opens from this thread land here. Link one yourself from a URL or |
apps/web/src/components/pullRequest/pullRequestIcons.tsx created+54| ... | ... | @@ -0,0 +1,54 @@ |
| 1 | import { | |
| 2 | GitMergeIcon, | |
| 3 | GitPullRequestArrowIcon, | |
| 4 | GitPullRequestClosedIcon, | |
| 5 | GitPullRequestDraftIcon, | |
| 6 | LayersIcon, | |
| 7 | Link2Icon, | |
| 8 | Unlink2Icon, | |
| 9 | TriangleAlertIcon, | |
| 10 | } from "lucide-react"; | |
| 11 | import type { PullRequestState } from "@t3tools/contracts"; | |
| 12 | ||
| 13 | export const PullRequestGlyph = { | |
| 14 | pullRequest: GitPullRequestArrowIcon, | |
| 15 | reopen: GitPullRequestArrowIcon, | |
| 16 | draft: GitPullRequestDraftIcon, | |
| 17 | closed: GitPullRequestClosedIcon, | |
| 18 | merged: GitMergeIcon, | |
| 19 | conflicting: TriangleAlertIcon, | |
| 20 | stack: LayersIcon, | |
| 21 | link: Link2Icon, | |
| 22 | unlink: Unlink2Icon, | |
| 23 | } as const; | |
| 24 | ||
| 25 | export type PullRequestGlyphIcon = (typeof PullRequestGlyph)[keyof typeof PullRequestGlyph]; | |
| 26 | ||
| 27 | export interface PullRequestStatePresentation { | |
| 28 | readonly label: string; | |
| 29 | readonly toneClassName: string; | |
| 30 | readonly Icon: PullRequestGlyphIcon; | |
| 31 | } | |
| 32 | ||
| 33 | export const PULL_REQUEST_STATE_PRESENTATION = { | |
| 34 | open: { | |
| 35 | label: "Open", | |
| 36 | toneClassName: "text-emerald-600 dark:text-emerald-300/90", | |
| 37 | Icon: PullRequestGlyph.pullRequest, | |
| 38 | }, | |
| 39 | draft: { | |
| 40 | label: "Draft", | |
| 41 | toneClassName: "text-zinc-500 dark:text-zinc-400/80", | |
| 42 | Icon: PullRequestGlyph.draft, | |
| 43 | }, | |
| 44 | closed: { | |
| 45 | label: "Closed", | |
| 46 | toneClassName: "text-red-600 dark:text-red-300/90", | |
| 47 | Icon: PullRequestGlyph.closed, | |
| 48 | }, | |
| 49 | merged: { | |
| 50 | label: "Merged", | |
| 51 | toneClassName: "text-violet-600 dark:text-violet-300/90", | |
| 52 | Icon: PullRequestGlyph.merged, | |
| 53 | }, | |
| 54 | } as const satisfies Record<PullRequestState | "draft", PullRequestStatePresentation>; |
apps/web/src/components/pullRequest/pullRequestPresentation.test.ts created+139| ... | ... | @@ -0,0 +1,139 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { resolvePullRequestConflict, resolvePullRequestState } from "./pullRequestPresentation"; | |
| 4 | import { PullRequestGlyph } from "./pullRequestIcons"; | |
| 5 | ||
| 6 | describe("resolvePullRequestState", () => { | |
| 7 | it.each([ | |
| 8 | [ | |
| 9 | "open", | |
| 10 | { state: "open", isDraft: false }, | |
| 11 | PullRequestGlyph.pullRequest, | |
| 12 | "Open", | |
| 13 | "text-emerald-600 dark:text-emerald-300/90", | |
| 14 | ], | |
| 15 | [ | |
| 16 | "draft", | |
| 17 | { state: "open", isDraft: true }, | |
| 18 | PullRequestGlyph.draft, | |
| 19 | "Draft", | |
| 20 | "text-zinc-500 dark:text-zinc-400/80", | |
| 21 | ], | |
| 22 | [ | |
| 23 | "closed", | |
| 24 | { state: "closed", isDraft: false }, | |
| 25 | PullRequestGlyph.closed, | |
| 26 | "Closed", | |
| 27 | "text-red-600 dark:text-red-300/90", | |
| 28 | ], | |
| 29 | [ | |
| 30 | "merged", | |
| 31 | { state: "merged", isDraft: false }, | |
| 32 | PullRequestGlyph.merged, | |
| 33 | "Merged", | |
| 34 | "text-violet-600 dark:text-violet-300/90", | |
| 35 | ], | |
| 36 | ] as const)( | |
| 37 | "resolves the %s lifecycle presentation", | |
| 38 | (_name, input, Icon, label, toneClassName) => { | |
| 39 | expect(resolvePullRequestState(input)).toEqual({ | |
| 40 | Icon, | |
| 41 | label, | |
| 42 | toneClassName, | |
| 43 | }); | |
| 44 | }, | |
| 45 | ); | |
| 46 | ||
| 47 | it("keeps a merged pull request merged when stale draft metadata is also present", () => { | |
| 48 | expect(resolvePullRequestState({ state: "merged", isDraft: true })).toMatchObject({ | |
| 49 | Icon: PullRequestGlyph.merged, | |
| 50 | label: "Merged", | |
| 51 | }); | |
| 52 | }); | |
| 53 | ||
| 54 | it("keeps a closed pull request closed when stale draft metadata is also present", () => { | |
| 55 | expect(resolvePullRequestState({ state: "closed", isDraft: true })).toMatchObject({ | |
| 56 | Icon: PullRequestGlyph.closed, | |
| 57 | label: "Closed", | |
| 58 | }); | |
| 59 | }); | |
| 60 | ||
| 61 | it("keeps lifecycle and conflict presentation independent for an open conflicting pull request", () => { | |
| 62 | const input = { | |
| 63 | state: "open" as const, | |
| 64 | isDraft: false, | |
| 65 | mergeability: "conflicting" as const, | |
| 66 | baseBranch: "main", | |
| 67 | }; | |
| 68 | ||
| 69 | expect(resolvePullRequestState(input)).toMatchObject({ | |
| 70 | Icon: PullRequestGlyph.pullRequest, | |
| 71 | label: "Open", | |
| 72 | }); | |
| 73 | expect(resolvePullRequestConflict(input)).toMatchObject({ | |
| 74 | Icon: PullRequestGlyph.conflicting, | |
| 75 | label: "Conflicts with main", | |
| 76 | toneClassName: "text-destructive", | |
| 77 | }); | |
| 78 | }); | |
| 79 | }); | |
| 80 | ||
| 81 | describe("resolvePullRequestConflict", () => { | |
| 82 | it.each([ | |
| 83 | ["closed", { state: "closed", isDraft: false }], | |
| 84 | ["merged", { state: "merged", isDraft: false }], | |
| 85 | ["closed draft", { state: "closed", isDraft: true }], | |
| 86 | ["merged draft", { state: "merged", isDraft: true }], | |
| 87 | ["open draft", { state: "open", isDraft: true }], | |
| 88 | ] as const)("does not report a conflict for %s", (_name, input) => { | |
| 89 | expect( | |
| 90 | resolvePullRequestConflict({ | |
| 91 | ...input, | |
| 92 | mergeability: "conflicting", | |
| 93 | baseBranch: "main", | |
| 94 | }), | |
| 95 | ).toBeNull(); | |
| 96 | }); | |
| 97 | ||
| 98 | it.each([ | |
| 99 | ["omitted", undefined], | |
| 100 | ["unknown", "unknown"], | |
| 101 | ["mergeable", "mergeable"], | |
| 102 | ] as const)("does not report an open conflict when mergeability is %s", (_name, mergeability) => { | |
| 103 | const input = { | |
| 104 | state: "open" as const, | |
| 105 | isDraft: false, | |
| 106 | ...(mergeability === undefined ? {} : { mergeability }), | |
| 107 | }; | |
| 108 | expect(resolvePullRequestConflict(input)).toBeNull(); | |
| 109 | }); | |
| 110 | ||
| 111 | it("reports a known conflict with its base branch", () => { | |
| 112 | expect( | |
| 113 | resolvePullRequestConflict({ | |
| 114 | state: "open", | |
| 115 | isDraft: false, | |
| 116 | mergeability: "conflicting", | |
| 117 | baseBranch: "main", | |
| 118 | }), | |
| 119 | ).toEqual({ | |
| 120 | Icon: PullRequestGlyph.conflicting, | |
| 121 | label: "Conflicts with main", | |
| 122 | toneClassName: "text-destructive", | |
| 123 | }); | |
| 124 | }); | |
| 125 | ||
| 126 | it("reports a known conflict without inventing a base branch", () => { | |
| 127 | expect( | |
| 128 | resolvePullRequestConflict({ | |
| 129 | state: "open", | |
| 130 | isDraft: false, | |
| 131 | mergeability: "conflicting", | |
| 132 | }), | |
| 133 | ).toEqual({ | |
| 134 | Icon: PullRequestGlyph.conflicting, | |
| 135 | label: "Has conflicts", | |
| 136 | toneClassName: "text-destructive", | |
| 137 | }); | |
| 138 | }); | |
| 139 | }); |
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+60-54| ... | ... | @@ -12,11 +12,6 @@ import { |
| 12 | 12 | CircleDashedIcon, |
| 13 | 13 | CircleDotIcon, |
| 14 | 14 | CircleXIcon, |
| 15 | GitMergeIcon, | |
| 16 | GitPullRequestClosedIcon, | |
| 17 | GitPullRequestDraftIcon, | |
| 18 | GitPullRequestIcon, | |
| 19 | TriangleAlertIcon, | |
| 20 | 15 | UserCheckIcon, |
| 21 | 16 | } from "lucide-react"; |
| 22 | 17 | import { Children, isValidElement, type ReactNode } from "react"; |
| ... | ... | @@ -26,12 +21,12 @@ import { cn } from "~/lib/utils"; |
| 26 | 21 | import { Badge } from "../ui/badge"; |
| 27 | 22 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 28 | 23 | import type { PullRequestReviewOutcome } from "./pullRequestDetail.logic"; |
| 29 | ||
| 30 | interface StatePresentation { | |
| 31 | readonly label: string; | |
| 32 | readonly toneClassName: string; | |
| 33 | readonly Icon: typeof GitPullRequestIcon; | |
| 34 | } | |
| 24 | import { | |
| 25 | PULL_REQUEST_STATE_PRESENTATION, | |
| 26 | PullRequestGlyph, | |
| 27 | type PullRequestStatePresentation, | |
| 28 | type PullRequestGlyphIcon, | |
| 29 | } from "./pullRequestIcons"; | |
| 35 | 30 | |
| 36 | 31 | export function PullRequestApprovalGlyph() { |
| 37 | 32 | return ( |
| ... | ... | @@ -49,57 +44,69 @@ export function PullRequestApprovalGlyph() { |
| 49 | 44 | } |
| 50 | 45 | |
| 51 | 46 | /** |
| 52 | * How a pull request's state reads on this page. Open, closed, merged, and draft use the same | |
| 53 | * ink as the thread badge in `ThreadStatusIndicators`, so one pull request cannot look like two | |
| 54 | * different things in two places. | |
| 47 | * How a pull request's state reads anywhere it appears: the thread badge, the right-panel tab, | |
| 48 | * the list, and the detail header all resolve through here so one pull request cannot look like | |
| 49 | * two different things in two places. | |
| 55 | 50 | * |
| 56 | * Draft outranks conflicts: a draft is not heading for a merge yet, so conflicts only surface | |
| 57 | * once it is real work. | |
| 51 | * Closed and merged take precedence over a stale draft flag. | |
| 58 | 52 | */ |
| 59 | 53 | export function resolvePullRequestState(input: { |
| 60 | 54 | readonly state: PullRequestState; |
| 61 | 55 | readonly isDraft: boolean; |
| 56 | }): PullRequestStatePresentation { | |
| 57 | const key = input.state === "open" && input.isDraft ? "draft" : input.state; | |
| 58 | return PULL_REQUEST_STATE_PRESENTATION[key]; | |
| 59 | } | |
| 60 | ||
| 61 | export interface PullRequestConflictPresentation { | |
| 62 | readonly label: string; | |
| 63 | readonly toneClassName: string; | |
| 64 | readonly Icon: PullRequestGlyphIcon; | |
| 65 | } | |
| 66 | ||
| 67 | export function resolvePullRequestConflict(input: { | |
| 68 | readonly state: PullRequestState; | |
| 69 | readonly isDraft: boolean; | |
| 62 | 70 | readonly mergeability?: PullRequestMergeability; |
| 63 | 71 | readonly baseBranch?: string; |
| 64 | }): StatePresentation { | |
| 65 | if (input.state === "merged") { | |
| 66 | return { | |
| 67 | label: "Merged", | |
| 68 | toneClassName: "text-violet-600 dark:text-violet-300/90", | |
| 69 | Icon: GitMergeIcon, | |
| 70 | }; | |
| 71 | } | |
| 72 | if (input.state === "closed") { | |
| 73 | return { | |
| 74 | label: "Closed", | |
| 75 | toneClassName: "text-red-600 dark:text-red-300/90", | |
| 76 | Icon: GitPullRequestClosedIcon, | |
| 77 | }; | |
| 78 | } | |
| 79 | if (input.isDraft) { | |
| 80 | return { | |
| 81 | label: "Draft", | |
| 82 | toneClassName: "text-zinc-500 dark:text-zinc-400/80", | |
| 83 | Icon: GitPullRequestDraftIcon, | |
| 84 | }; | |
| 85 | } | |
| 86 | if (input.mergeability === "conflicting") { | |
| 87 | return { | |
| 88 | // "Has conflicts" leaves out the one thing a reader wants when the warning triangle catches | |
| 89 | // their eye, so name the branch it collides with wherever the caller knows it. | |
| 90 | label: input.baseBranch ? `Conflicts with ${input.baseBranch}` : "Has conflicts", | |
| 91 | toneClassName: "text-destructive", | |
| 92 | Icon: TriangleAlertIcon, | |
| 93 | }; | |
| 72 | }): PullRequestConflictPresentation | null { | |
| 73 | if (input.state !== "open" || input.isDraft || input.mergeability !== "conflicting") { | |
| 74 | return null; | |
| 94 | 75 | } |
| 95 | 76 | return { |
| 96 | label: "Open", | |
| 97 | toneClassName: "text-emerald-600 dark:text-emerald-300/90", | |
| 98 | Icon: GitPullRequestIcon, | |
| 77 | label: input.baseBranch ? `Conflicts with ${input.baseBranch}` : "Has conflicts", | |
| 78 | toneClassName: "text-destructive", | |
| 79 | Icon: PullRequestGlyph.conflicting, | |
| 99 | 80 | }; |
| 100 | 81 | } |
| 101 | 82 | |
| 102 | 83 | export function PullRequestStateGlyph({ |
| 84 | state, | |
| 85 | isDraft, | |
| 86 | className, | |
| 87 | }: { | |
| 88 | state: PullRequestState; | |
| 89 | isDraft: boolean; | |
| 90 | className?: string; | |
| 91 | }) { | |
| 92 | const presentation = resolvePullRequestState({ state, isDraft }); | |
| 93 | return ( | |
| 94 | <Tooltip> | |
| 95 | {/* The list row is itself a button, so the trigger stays a span: an interactive one would | |
| 96 | nest a control inside that button and steal the row's click target. */} | |
| 97 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> | |
| 98 | <presentation.Icon | |
| 99 | role="img" | |
| 100 | aria-label={presentation.label} | |
| 101 | className={cn("size-4 shrink-0", presentation.toneClassName, className)} | |
| 102 | /> | |
| 103 | </TooltipTrigger> | |
| 104 | <TooltipPopup>{presentation.label}</TooltipPopup> | |
| 105 | </Tooltip> | |
| 106 | ); | |
| 107 | } | |
| 108 | ||
| 109 | export function PullRequestConflictGlyph({ | |
| 103 | 110 | state, |
| 104 | 111 | isDraft, |
| 105 | 112 | mergeability, |
| ... | ... | @@ -112,16 +119,15 @@ export function PullRequestStateGlyph({ |
| 112 | 119 | baseBranch?: string; |
| 113 | 120 | className?: string; |
| 114 | 121 | }) { |
| 115 | const presentation = resolvePullRequestState({ | |
| 122 | const presentation = resolvePullRequestConflict({ | |
| 116 | 123 | state, |
| 117 | 124 | isDraft, |
| 118 | ...(mergeability ? { mergeability } : {}), | |
| 119 | ...(baseBranch ? { baseBranch } : {}), | |
| 125 | ...(mergeability === undefined ? {} : { mergeability }), | |
| 126 | ...(baseBranch === undefined ? {} : { baseBranch }), | |
| 120 | 127 | }); |
| 128 | if (presentation === null) return null; | |
| 121 | 129 | return ( |
| 122 | 130 | <Tooltip> |
| 123 | {/* The list row is itself a button, so the trigger stays a span: an interactive one would | |
| 124 | nest a control inside that button and steal the row's click target. */} | |
| 125 | 131 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> |
| 126 | 132 | <presentation.Icon |
| 127 | 133 | role="img" |
apps/web/src/components/settings/SourceControlSettings.tsx+3-2| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { RefreshIcon } from "~/components/ui/refresh-icon"; |
| 2 | import { ChevronDownIcon, GitPullRequestIcon } from "lucide-react"; | |
| 2 | import { ChevronDownIcon } from "lucide-react"; | |
| 3 | 3 | import * as Duration from "effect/Duration"; |
| 4 | 4 | import * as Option from "effect/Option"; |
| 5 | 5 | import { useEffect, useState, type ReactNode } from "react"; |
| ... | ... | @@ -67,6 +67,7 @@ import { |
| 67 | 67 | useSettingsSearchTargetId, |
| 68 | 68 | } from "./settingsLayout"; |
| 69 | 69 | import { searchableSetting } from "./settingsSearch"; |
| 70 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 70 | 71 | |
| 71 | 72 | const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { |
| 72 | 73 | versionControlSystems: [], |
| ... | ... | @@ -476,7 +477,7 @@ function EmptySourceControlDiscovery({ |
| 476 | 477 | <SettingsSection id={searchableSetting("source-control").id} title="Server environment"> |
| 477 | 478 | <Empty className="min-h-88"> |
| 478 | 479 | <EmptyMedia variant="icon"> |
| 479 | <GitPullRequestIcon /> | |
| 480 | <PullRequestGlyph.pullRequest /> | |
| 480 | 481 | </EmptyMedia> |
| 481 | 482 | <EmptyHeader> |
| 482 | 483 | <EmptyTitle> |
apps/web/src/components/sidebar/SidebarChrome.tsx+3-7| ... | ... | @@ -1,9 +1,4 @@ |
| 1 | import { | |
| 2 | ArrowLeftIcon, | |
| 3 | ChartNoAxesColumnIcon, | |
| 4 | GitPullRequestIcon, | |
| 5 | SettingsIcon, | |
| 6 | } from "lucide-react"; | |
| 1 | import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react"; | |
| 7 | 2 | import type { ReactNode } from "react"; |
| 8 | 3 | import { memo, useCallback } from "react"; |
| 9 | 4 | import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router"; |
| ... | ... | @@ -33,6 +28,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 33 | 28 | import { readPullRequestListPreferences } from "../pullRequest/pullRequestListPreferences"; |
| 34 | 29 | import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill"; |
| 35 | 30 | import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill"; |
| 31 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 36 | 32 | |
| 37 | 33 | export const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 38 | 34 | isElectron, |
| ... | ... | @@ -205,7 +201,7 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { |
| 205 | 201 | /> |
| 206 | 202 | {pullRequestsSupported ? ( |
| 207 | 203 | <SidebarUtilityItem |
| 208 | icon={<GitPullRequestIcon />} | |
| 204 | icon={<PullRequestGlyph.pullRequest />} | |
| 209 | 205 | label="Pull Requests" |
| 210 | 206 | onClick={handlePullRequestsClick} |
| 211 | 207 | /> |
apps/web/src/routes/_chat.pull-requests.tsx+4-6| ... | ... | @@ -21,9 +21,6 @@ import { |
| 21 | 21 | ChevronDownIcon, |
| 22 | 22 | ClockIcon, |
| 23 | 23 | EyeIcon, |
| 24 | GitMergeIcon, | |
| 25 | GitPullRequestClosedIcon, | |
| 26 | GitPullRequestIcon, | |
| 27 | 24 | LayersIcon, |
| 28 | 25 | ListChecksIcon, |
| 29 | 26 | PenLineIcon, |
| ... | ... | @@ -150,6 +147,7 @@ import { useAtomCommand } from "../state/use-atom-command"; |
| 150 | 147 | import { cn } from "~/lib/utils"; |
| 151 | 148 | import { primaryServerKeybindingsAtom } from "~/state/server"; |
| 152 | 149 | import { getSourceControlPresentationForKind } from "~/sourceControlPresentation"; |
| 150 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 153 | 151 | |
| 154 | 152 | function getShortcutContext() { |
| 155 | 153 | return { |
| ... | ... | @@ -196,9 +194,9 @@ const INVOLVEMENT_TABS = [ |
| 196 | 194 | |
| 197 | 195 | const STATE_TABS = [ |
| 198 | 196 | { value: "all", label: "All", Icon: LayersIcon }, |
| 199 | { value: "open", label: "Open", Icon: GitPullRequestIcon }, | |
| 200 | { value: "closed", label: "Closed", Icon: GitPullRequestClosedIcon }, | |
| 201 | { value: "merged", label: "Merged", Icon: GitMergeIcon }, | |
| 197 | { value: "open", label: "Open", Icon: PullRequestGlyph.pullRequest }, | |
| 198 | { value: "closed", label: "Closed", Icon: PullRequestGlyph.closed }, | |
| 199 | { value: "merged", label: "Merged", Icon: PullRequestGlyph.merged }, | |
| 202 | 200 | ] as const satisfies ReadonlyArray<PullRequestFilterOption<PullRequestListState>>; |
| 203 | 201 | |
| 204 | 202 | const SORT_OPTIONS = [ |
apps/web/src/sourceControlPresentation.ts+3-3| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | import { GitPullRequestIcon } from "lucide-react"; | |
| 2 | 1 | import type { ElementType } from "react"; |
| 3 | 2 | import type { SourceControlProviderInfo, SourceControlProviderKind } from "@t3tools/contracts"; |
| 4 | 3 | export { |
| ... | ... | @@ -16,10 +15,11 @@ import { |
| 16 | 15 | import { |
| 17 | 16 | AzureDevOpsIcon, |
| 18 | 17 | BitbucketIcon, |
| 18 | ForgejoIcon, | |
| 19 | 19 | GitHubIcon, |
| 20 | 20 | GitLabIcon, |
| 21 | ForgejoIcon, | |
| 22 | 21 | } from "./components/Icons"; |
| 22 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; | |
| 23 | 23 | |
| 24 | 24 | export interface SourceControlPresentation { |
| 25 | 25 | readonly providerName: string; |
| ... | ... | @@ -66,7 +66,7 @@ export function getSourceControlPresentation( |
| 66 | 66 | return { |
| 67 | 67 | providerName: provider?.name || presentation.providerName, |
| 68 | 68 | terminology: getChangeRequestTerminology(provider), |
| 69 | Icon: GitPullRequestIcon, | |
| 69 | Icon: PullRequestGlyph.pullRequest, | |
| 70 | 70 | }; |
| 71 | 71 | } |
| 72 | 72 | } |
vite.config.ts+44-15| ... | ... | @@ -2,6 +2,43 @@ import "vite-plus/test/config"; |
| 2 | 2 | import { defineConfig } from "vite-plus"; |
| 3 | 3 | import * as NodeURL from "node:url"; |
| 4 | 4 | |
| 5 | /** Import restrictions every file keeps, including the one module exempt from the glyph rule. */ | |
| 6 | const RESTRICTED_IMPORT_PATHS = [ | |
| 7 | { | |
| 8 | name: "@t3tools/client-runtime", | |
| 9 | message: | |
| 10 | "Import from an explicit @t3tools/client-runtime/* subpath. The package has no root export.", | |
| 11 | }, | |
| 12 | { | |
| 13 | name: "@pierre/diffs/react", | |
| 14 | importNames: ["CodeView"], | |
| 15 | message: "Use StyledDiffCodeView so web diff surfaces share styling and virtualized geometry.", | |
| 16 | }, | |
| 17 | ]; | |
| 18 | ||
| 19 | /** Lucide's pull-request glyphs, which only `pullRequestIcons.tsx` may name. */ | |
| 20 | const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = { | |
| 21 | name: "lucide-react", | |
| 22 | importNames: [ | |
| 23 | "GitMerge", | |
| 24 | "GitMergeIcon", | |
| 25 | "GitPullRequest", | |
| 26 | "GitPullRequestIcon", | |
| 27 | "GitPullRequestArrow", | |
| 28 | "GitPullRequestArrowIcon", | |
| 29 | "GitPullRequestClosed", | |
| 30 | "GitPullRequestClosedIcon", | |
| 31 | "GitPullRequestDraft", | |
| 32 | "GitPullRequestDraftIcon", | |
| 33 | "GitPullRequestCreate", | |
| 34 | "GitPullRequestCreateIcon", | |
| 35 | "GitPullRequestCreateArrow", | |
| 36 | "GitPullRequestCreateArrowIcon", | |
| 37 | ], | |
| 38 | message: | |
| 39 | "Pick a glyph by meaning from PullRequestGlyph in apps/web/src/components/pullRequest/pullRequestIcons.tsx so every surface draws the same pull request the same way.", | |
| 40 | }; | |
| 41 | ||
| 5 | 42 | export default defineConfig({ |
| 6 | 43 | resolve: { |
| 7 | 44 | alias: { |
| ... | ... | @@ -103,21 +140,7 @@ export default defineConfig({ |
| 103 | 140 | "typescript/unbound-method": "off", |
| 104 | 141 | "eslint/no-restricted-imports": [ |
| 105 | 142 | "error", |
| 106 | { | |
| 107 | paths: [ | |
| 108 | { | |
| 109 | name: "@t3tools/client-runtime", | |
| 110 | message: | |
| 111 | "Import from an explicit @t3tools/client-runtime/* subpath. The package has no root export.", | |
| 112 | }, | |
| 113 | { | |
| 114 | name: "@pierre/diffs/react", | |
| 115 | importNames: ["CodeView"], | |
| 116 | message: | |
| 117 | "Use StyledDiffCodeView so web diff surfaces share styling and virtualized geometry.", | |
| 118 | }, | |
| 119 | ], | |
| 120 | }, | |
| 143 | { paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS] }, | |
| 121 | 144 | ], |
| 122 | 145 | "t3code/no-global-process-runtime": "error", |
| 123 | 146 | "t3code/no-inline-schema-compile": "warn", |
| ... | ... | @@ -131,6 +154,12 @@ export default defineConfig({ |
| 131 | 154 | files: ["packages/shared/src/hostProcess.ts"], |
| 132 | 155 | rules: { "t3code/no-global-process-runtime": "off" }, |
| 133 | 156 | }, |
| 157 | { | |
| 158 | // The one module allowed to name lucide's pull-request glyphs; everything else picks | |
| 159 | // from its vocabulary. The other import restrictions still apply here. | |
| 160 | files: ["apps/web/src/components/pullRequest/pullRequestIcons.tsx"], | |
| 161 | rules: { "eslint/no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }] }, | |
| 162 | }, | |
| 134 | 163 | { |
| 135 | 164 | files: ["apps/mobile/src/**"], |
| 136 | 165 | rules: { "t3code/no-mobile-uniwind-theme-escape-hatches": "error" }, |