From a5e9fece980778fe944f7412958090a491a32052 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 18 May 2026 18:47:04 +0300 Subject: [PATCH] fix pr page to look like upstream --- src/components/repo.tsx | 162 ++++++++++++- src/index.css | 500 +++++++++++++++++++++++++++++++++++++++ src/pages/repo/pulls.tsx | 257 +++++++++++++------- 3 files changed, 825 insertions(+), 94 deletions(-) diff --git a/src/components/repo.tsx b/src/components/repo.tsx index b04d757..056134d 100644 --- a/src/components/repo.tsx +++ b/src/components/repo.tsx @@ -1,9 +1,20 @@ import clsx from 'clsx'; import DOMPurify from 'dompurify'; -import { ChevronLeft, ChevronRight, FileText } from 'lucide-solid'; +import { + ChevronRight, + Circle, + FileText, + Folder, + Minimize2, + PanelLeftClose, + PanelLeftOpen, + PanelRightClose, + PanelRightOpen, + ChevronLeft, +} from 'lucide-solid'; import { marked } from 'marked'; import { A } from '@solidjs/router'; -import { For, Show, createMemo, type Component, type JSX } from 'solid-js'; +import { For, Show, createMemo, createSignal, type Component, type JSX } from 'solid-js'; import type { TreeEntry } from '../lib/api'; import { formatRelativeTime } from '../lib/repo-utils'; import { Avatar, SkeletonBlock, cardStyles } from './common'; @@ -572,6 +583,153 @@ export const DiffView: Component<{ patch: string; maxHeight?: string; wrap?: boo ); }; +const totalDiffStats = (files: DiffFile[]) => + files.reduce( + (acc, file) => ({ + additions: acc.additions + file.additions, + deletions: acc.deletions + file.deletions, + }), + { additions: 0, deletions: 0 }, + ); + +const fileTreeRows = (files: DiffFile[]) => + files.map((file) => { + const parts = file.path.split('/').filter(Boolean); + return { + file, + depth: Math.max(parts.length - 1, 0), + name: parts.at(-1) ?? file.path, + folder: parts.length > 1 ? parts.slice(0, -1).join('/') : '', + }; + }); + +const DiffStatPill: Component<{ additions: number; deletions: number }> = (props) => ( +
+ 0}> + +{props.additions} + + 0}> + -{props.deletions} + +
+); + +export const PullDiffView: Component<{ patch: string; roundLabel: string; history: JSX.Element }> = (props) => { + const files = createMemo(() => parseDiffFiles(props.patch)); + const stats = createMemo(() => totalDiffStats(files())); + const rows = createMemo(() => fileTreeRows(files())); + const [expanded, setExpanded] = createSignal(true); + const [filesVisible, setFilesVisible] = createSignal(true); + const [historyVisible, setHistoryVisible] = createSignal(true); + + return ( +
+
+
+ + + {files().length} changed files +
+
+ +
+
+ +
+
+ +
+ + + + +
+ + {(file) => ( +
+ +
+ + + {file.path} + +
+ +
+
...
+ +
+ )} +
+
+ + + + +
+
+ ); +}; + export const AsideCard: Component<{ title: string; children: JSX.Element }> = (props) => (
{props.title}
diff --git a/src/index.css b/src/index.css index 710908d..e5df282 100644 --- a/src/index.css +++ b/src/index.css @@ -5,6 +5,506 @@ font-family: InterVariable, system-ui, sans-serif, ui-sans-serif; } +.untangled-pr-detail { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2rem; +} + +.untangled-pr-header-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 15rem; + gap: 1rem; + align-items: start; +} + +.untangled-pr-summary-card { + min-width: 0; + border-radius: 0.25rem; + background: rgb(255 255 255); + padding: 1.5rem; + color: rgb(17 24 39); + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.untangled-pr-meta { + display: flex; + min-width: 0; + flex-direction: column; + gap: 1.75rem; + padding: 0.25rem 0; + color: rgb(107 114 128); +} + +.untangled-pr-meta-section h2 { + margin: 0 0 0.25rem; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + color: rgb(75 85 99); +} + +.untangled-pr-meta-section h2 span { + display: inline-flex; + min-width: 1.25rem; + justify-content: center; + border-radius: 0.25rem; + background: rgb(229 231 235); + padding: 0 0.25rem; + font-size: 0.75rem; +} + +.untangled-pr-meta-section p { + margin: 0; + font-size: 0.875rem; +} + +.untangled-pr-meta-section code { + font-size: 0.8125rem; + color: rgb(17 24 39); +} + +.untangled-pr-meta-action, +.untangled-pr-icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgb(209 213 219); + border-radius: 0.25rem; + background: transparent; + color: rgb(107 114 128); +} + +.untangled-pr-meta-action { + border: 0; + padding: 0.125rem; +} + +.untangled-pr-icon-button { + width: 2rem; + height: 2rem; +} + +.untangled-pr-diff-breakout { + position: relative; + left: 50%; + width: 100vw; + margin-left: -50vw; +} + +.untangled-pr-diff-toolbar { + position: sticky; + top: 0; + z-index: 30; + display: grid; + grid-template-columns: 11.5rem minmax(0, 1fr) 29.5rem; + gap: 1rem; + align-items: center; + padding: 0.5rem; + background: rgb(249 250 251); +} + +.untangled-pr-diff-toolbar-sidebar-collapsed { + grid-template-columns: minmax(0, 1fr) 29.5rem; +} + +.untangled-pr-diff-toolbar-history-collapsed { + grid-template-columns: 11.5rem minmax(0, 1fr) 2.125rem; +} + +.untangled-pr-diff-toolbar-sidebar-collapsed.untangled-pr-diff-toolbar-history-collapsed { + grid-template-columns: minmax(0, 1fr) 2.125rem; +} + +.untangled-pr-diff-toolbar-main { + grid-column: 1 / 3; + grid-row: 1; + display: flex; + min-width: 0; + flex-wrap: wrap; + align-items: center; + gap: 0.625rem; + font-size: 0.875rem; + color: rgb(107 114 128); +} + +.untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-main { + grid-column: 1; +} + +.untangled-pr-diff-toolbar-actions { + grid-column: 2; + grid-row: 1; + justify-self: end; +} + +.untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-actions { + grid-column: 1; +} + +.untangled-pr-diff-toolbar-history { + grid-column: 3; + grid-row: 1; + justify-self: end; +} + +.untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-history { + grid-column: 2; +} + +.untangled-pr-tool-button, +.untangled-pr-sidebar-toggle { + display: inline-flex; + min-height: 2.125rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + border: 1px solid rgb(209 213 219); + border-radius: 0.25rem; + background: rgb(255 255 255); + padding: 0 0.75rem; + font-size: 0.875rem; + color: rgb(31 41 55); +} + +.untangled-pr-sidebar-toggle { + width: 2.125rem; + padding: 0; +} + +.untangled-diff-stat-pill { + display: inline-flex; + align-items: center; + overflow: hidden; + border-radius: 0.25rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 0.875rem; + line-height: 1; +} + +.untangled-diff-stat-pill span { + padding: 0.375rem 0.5rem; +} + +.untangled-diff-stat-add { + background: rgb(220 252 231); + color: rgb(21 128 61); +} + +.untangled-diff-stat-del { + background: rgb(254 226 226); + color: rgb(185 28 28); +} + +.untangled-pr-diff-grid { + display: grid; + grid-template-columns: 11.5rem minmax(0, 1fr) 29.5rem; + gap: 1rem; + align-items: start; +} + +.untangled-pr-diff-grid-sidebar-collapsed { + grid-template-columns: minmax(0, 1fr) 29.5rem; +} + +.untangled-pr-diff-grid-history-collapsed { + grid-template-columns: 11.5rem minmax(0, 1fr); +} + +.untangled-pr-diff-grid-sidebar-collapsed.untangled-pr-diff-grid-history-collapsed { + grid-template-columns: minmax(0, 1fr); +} + +.untangled-pr-file-tree { + position: sticky; + top: 3rem; + max-height: calc(100vh - 3rem); + overflow: auto; + min-height: calc(100vh - 3rem); + border: 1px solid rgb(229 231 235); + border-top: 0; + border-radius: 0 0 0.25rem 0.25rem; + background: rgb(255 255 255); + padding: 0.625rem 0 1.5rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.untangled-pr-file-tree-row { + display: flex; + min-width: 0; + align-items: center; + gap: 0.375rem; + padding-top: 0.25rem; + padding-right: 0.5rem; + padding-bottom: 0.25rem; + color: rgb(55 65 81); + font-size: 0.875rem; + text-decoration: none; +} + +.untangled-pr-file-tree-row:hover { + background: rgb(243 244 246); + text-decoration: none; +} + +.untangled-pr-diff-files { + display: flex; + min-width: 0; + flex-direction: column; + gap: 1rem; +} + +.untangled-pr-diff-file { + overflow: hidden; + border: 1px solid rgb(209 213 219); + border-radius: 0.25rem; + background: rgb(255 255 255); +} + +.untangled-pr-diff-file-header { + display: flex; + min-height: 2.75rem; + cursor: pointer; + list-style: none; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0 1rem; + background: rgb(31 41 55); +} + +.untangled-pr-diff-file-header::-webkit-details-marker { + display: none; +} + +.untangled-pr-diff-file[open] .untangled-pr-diff-chevron { + transform: rotate(90deg); +} + +.untangled-pr-diff-ellipsis { + background: rgb(55 65 81); + padding: 0.125rem 0; + text-align: center; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + color: rgb(209 213 219); +} + +.untangled-pr-history { + position: sticky; + top: 3rem; + max-height: calc(100vh - 3rem); + overflow: auto; + padding-right: 0.5rem; +} + +.untangled-pr-history-card { + overflow: hidden; + border: 1px solid rgb(209 213 219); + border-radius: 0.25rem; + background: rgb(255 255 255); +} + +.untangled-pr-history-card > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-bottom: 1px solid rgb(229 231 235); + padding: 0.75rem 1rem; +} + +.untangled-pr-history-card h2 { + margin: 0; + font-size: 1rem; + font-weight: 500; +} + +.untangled-pr-history-card header div { + display: flex; + gap: 1rem; + font-size: 0.875rem; + color: rgb(75 85 99); +} + +.untangled-pr-history-submission { + display: flex; + width: 100%; + gap: 0.75rem; + border: 0; + padding: 1rem; + background: transparent; + color: inherit; + text-align: left; +} + +.untangled-pr-history-submission:hover, +.untangled-pr-history-submission-active { + background: rgb(239 246 255); +} + +.untangled-pr-history-event { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0.125rem 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; + padding: 0.75rem 1rem; + font-weight: 500; +} + +.untangled-pr-history-event-merged { + border-color: rgb(168 85 247); + background: rgb(126 34 206); + color: rgb(250 245 255); +} + +.untangled-pr-history-event-closed { + border-color: rgb(239 68 68); + background: rgb(127 29 29); + color: rgb(254 242 242); +} + +.untangled-pr-history-event-open { + border-color: rgb(34 197 94); + background: rgb(22 101 52); + color: rgb(240 253 244); +} + +.untangled-pr-history-comment { + display: flex; + flex-direction: column; + gap: 0.75rem; + border-top: 1px solid rgb(229 231 235); + padding: 1rem; +} + +@media (prefers-color-scheme: dark) { + .untangled-pr-summary-card, + .untangled-pr-tool-button, + .untangled-pr-sidebar-toggle, + .untangled-pr-file-tree, + .untangled-pr-diff-file, + .untangled-pr-history-card { + background: rgb(31 41 55); + color: rgb(243 244 246); + } + + .untangled-pr-meta, + .untangled-pr-meta-section p { + color: rgb(156 163 175); + } + + .untangled-pr-meta-section h2 { + color: rgb(209 213 219); + } + + .untangled-pr-meta-section h2 span { + background: rgb(75 85 99); + color: rgb(243 244 246); + } + + .untangled-pr-meta-section code { + color: rgb(243 244 246); + } + + .untangled-pr-meta-action, + .untangled-pr-icon-button { + border-color: rgb(55 65 81); + color: rgb(156 163 175); + } + + .untangled-pr-tool-button, + .untangled-pr-sidebar-toggle, + .untangled-pr-diff-file, + .untangled-pr-history-card { + border-color: rgb(55 65 81); + } + + .untangled-pr-file-tree { + border-color: rgb(55 65 81); + } + + .untangled-diff-stat-add { + background: rgb(22 101 52 / 0.5); + color: rgb(74 222 128); + } + + .untangled-diff-stat-del { + background: rgb(127 29 29 / 0.5); + color: rgb(248 113 113); + } + + .untangled-pr-file-tree-row { + color: rgb(229 231 235); + } + + .untangled-pr-file-tree-row:hover { + background: rgb(31 41 55); + } + + .untangled-pr-history-card > header, + .untangled-pr-history-comment { + border-color: rgb(55 65 81); + } + + .untangled-pr-history-card header div { + color: rgb(209 213 219); + } + + .untangled-pr-diff-toolbar { + background: rgb(17 24 39); + } + + .untangled-pr-history-submission:hover, + .untangled-pr-history-submission-active { + background: rgb(30 64 175 / 0.45); + } +} + +@media (max-width: 1023px) { + .untangled-pr-header-grid, + .untangled-pr-diff-grid, + .untangled-pr-diff-grid-sidebar-collapsed, + .untangled-pr-diff-grid-history-collapsed, + .untangled-pr-diff-toolbar, + .untangled-pr-diff-toolbar-sidebar-collapsed, + .untangled-pr-diff-toolbar-history-collapsed { + grid-template-columns: minmax(0, 1fr); + } + + .untangled-pr-diff-toolbar-main, + .untangled-pr-diff-toolbar-actions, + .untangled-pr-diff-toolbar-history, + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-main, + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-actions, + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-history { + grid-column: 1; + } + + .untangled-pr-file-tree, + .untangled-pr-history { + position: static; + max-height: none; + } + + .untangled-pr-file-tree { + display: none; + } +} + +@media (max-width: 767px) { + .untangled-pr-diff-toolbar { + align-items: stretch; + flex-direction: column; + } + + .untangled-pr-summary-card { + padding: 1rem; + } +} + html, body, #root { diff --git a/src/pages/repo/pulls.tsx b/src/pages/repo/pulls.tsx index 04ef677..7753f3e 100644 --- a/src/pages/repo/pulls.tsx +++ b/src/pages/repo/pulls.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx'; -import { CirclePlus, LoaderCircle, Search } from 'lucide-solid'; +import { CirclePlus, Copy, GitMerge, GitPullRequest, GitPullRequestClosed, LoaderCircle, MessageSquare, Search } from 'lucide-solid'; import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router'; import { createQuery, useQueryClient } from '@tanstack/solid-query'; import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; import { compareBranches, createPull, createPullComment, fetchPullRoundPatch, getPull, getRepoBranches, listPullsPage, parseAtUri, setPullStatus, type RepoContext } from '../../lib/api'; import { useAuth } from '../../lib/auth'; import { Avatar, ErrorState, LoadingState, StateBadge, ToggleButton, buttonStyles, cardStyles, inputStyles, textareaStyles } from '../../components/common'; -import { AsideCard, BranchPill, CommentCard, DiffView, MarkdownBlock, PaginationControls, RepoListSkeleton, RepoThreadSkeleton } from '../../components/repo'; +import { BranchPill, CommentCard, DiffView, MarkdownBlock, PaginationControls, PullDiffView, RepoListSkeleton, RepoThreadSkeleton } from '../../components/repo'; import { RepoFrame, pullQueryKey, pullsQueryKey, useRepoQuery } from './shared'; import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, parseIntegerSearchParam, pullHref, uniqueCommenters } from '../../lib/repo-utils'; @@ -454,6 +454,46 @@ export const PullPage: Component = () => { } }; + const participants = createMemo(() => { + const detail = pullQuery.data; + if (!detail) return []; + const seen = new Set(); + const result: Array<{ did: string; handle: string }> = []; + const add = (participant: { did: string; handle: string }) => { + if (seen.has(participant.did)) return; + seen.add(participant.did); + result.push(participant); + }; + add(detail.pull.author); + for (const commenter of uniqueCommenters(detail.comments)) add(commenter); + return result; + }); + + const roundLabel = createMemo(() => `round #${roundIndex()}`); + + const statusEvent = createMemo(() => { + const state = pullQuery.data?.pull.state; + if (state === 'merged') { + return { + icon: , + text: 'pull request successfully merged', + class: 'untangled-pr-history-event-merged', + }; + } + if (state === 'closed') { + return { + icon: , + text: 'pull request closed', + class: 'untangled-pr-history-event-closed', + }; + } + return { + icon: , + text: 'pull request opened', + class: 'untangled-pr-history-event-open', + }; + }); + return ( @@ -467,10 +507,10 @@ export const PullPage: Component = () => { {(detail) => ( -
-
-
-
+
+
+
+

{detail().pull.value.title}{' '} #{detail().pull.number} @@ -479,7 +519,7 @@ export const PullPage: Component = () => {
- opened + opened by {detail().pull.author.handle} · @@ -487,107 +527,140 @@ export const PullPage: Component = () => { · targeting - from -
-
+
- -
- -
-
+ +

-
-
-
patch
- +
- - - - - - - - - - - -
- -
- - {(item) => ( - - )} - - - -
-
new comment
-