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) => (
+
{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) => (
-