csr tangled client in solid-js
15

Configure Feed

Select the types of activity you want to include in your feed.

fix pr page to look like upstream

dawn (May 18, 2026, 6:47 PM +0300) a5e9fece a8a011b4

+825 -94
+160 -2
src/components/repo.tsx
··· 1 1 import clsx from 'clsx'; 2 2 import DOMPurify from 'dompurify'; 3 - import { ChevronLeft, ChevronRight, FileText } from 'lucide-solid'; 3 + import { 4 + ChevronRight, 5 + Circle, 6 + FileText, 7 + Folder, 8 + Minimize2, 9 + PanelLeftClose, 10 + PanelLeftOpen, 11 + PanelRightClose, 12 + PanelRightOpen, 13 + ChevronLeft, 14 + } from 'lucide-solid'; 4 15 import { marked } from 'marked'; 5 16 import { A } from '@solidjs/router'; 6 - import { For, Show, createMemo, type Component, type JSX } from 'solid-js'; 17 + import { For, Show, createMemo, createSignal, type Component, type JSX } from 'solid-js'; 7 18 import type { TreeEntry } from '../lib/api'; 8 19 import { formatRelativeTime } from '../lib/repo-utils'; 9 20 import { Avatar, SkeletonBlock, cardStyles } from './common'; ··· 569 580 )} 570 581 </For> 571 582 </div> 583 + ); 584 + }; 585 + 586 + const totalDiffStats = (files: DiffFile[]) => 587 + files.reduce( 588 + (acc, file) => ({ 589 + additions: acc.additions + file.additions, 590 + deletions: acc.deletions + file.deletions, 591 + }), 592 + { additions: 0, deletions: 0 }, 593 + ); 594 + 595 + const fileTreeRows = (files: DiffFile[]) => 596 + files.map((file) => { 597 + const parts = file.path.split('/').filter(Boolean); 598 + return { 599 + file, 600 + depth: Math.max(parts.length - 1, 0), 601 + name: parts.at(-1) ?? file.path, 602 + folder: parts.length > 1 ? parts.slice(0, -1).join('/') : '', 603 + }; 604 + }); 605 + 606 + const DiffStatPill: Component<{ additions: number; deletions: number }> = (props) => ( 607 + <div class="untangled-diff-stat-pill"> 608 + <Show when={props.additions > 0}> 609 + <span class="untangled-diff-stat-add">+{props.additions}</span> 610 + </Show> 611 + <Show when={props.deletions > 0}> 612 + <span class="untangled-diff-stat-del">-{props.deletions}</span> 613 + </Show> 614 + </div> 615 + ); 616 + 617 + export const PullDiffView: Component<{ patch: string; roundLabel: string; history: JSX.Element }> = (props) => { 618 + const files = createMemo(() => parseDiffFiles(props.patch)); 619 + const stats = createMemo(() => totalDiffStats(files())); 620 + const rows = createMemo(() => fileTreeRows(files())); 621 + const [expanded, setExpanded] = createSignal(true); 622 + const [filesVisible, setFilesVisible] = createSignal(true); 623 + const [historyVisible, setHistoryVisible] = createSignal(true); 624 + 625 + return ( 626 + <section class="untangled-pr-diff-breakout"> 627 + <div 628 + class={clsx( 629 + 'untangled-pr-diff-toolbar', 630 + !filesVisible() && 'untangled-pr-diff-toolbar-sidebar-collapsed', 631 + !historyVisible() && 'untangled-pr-diff-toolbar-history-collapsed', 632 + )} 633 + > 634 + <div class="untangled-pr-diff-toolbar-main"> 635 + <button 636 + type="button" 637 + class="untangled-pr-sidebar-toggle" 638 + aria-label={filesVisible() ? 'collapse changed files sidebar' : 'expand changed files sidebar'} 639 + aria-pressed={filesVisible()} 640 + onClick={() => setFilesVisible(!filesVisible())} 641 + > 642 + <Show when={filesVisible()} fallback={<PanelLeftOpen class="size-4" />}> 643 + <PanelLeftClose class="size-4" /> 644 + </Show> 645 + </button> 646 + <DiffStatPill additions={stats().additions} deletions={stats().deletions} /> 647 + <span>{files().length} changed files</span> 648 + <span class="hidden h-5 w-px bg-gray-300 dark:bg-gray-700 sm:inline-block" /> 649 + <span class="uppercase tracking-wide">diff</span> 650 + <span class="rounded border border-gray-300 px-2 py-0.5 font-mono text-xs text-gray-700 dark:border-gray-600 dark:text-gray-200"> 651 + {props.roundLabel} 652 + </span> 653 + </div> 654 + <div class="untangled-pr-diff-toolbar-actions"> 655 + <button type="button" class="untangled-pr-tool-button" onClick={() => setExpanded(!expanded())}> 656 + <Minimize2 class="size-4" /> 657 + <span>{expanded() ? 'collapse all' : 'expand all'}</span> 658 + </button> 659 + </div> 660 + <div class="untangled-pr-diff-toolbar-history"> 661 + <button 662 + type="button" 663 + class="untangled-pr-sidebar-toggle" 664 + aria-label={historyVisible() ? 'collapse history panel' : 'expand history panel'} 665 + aria-pressed={historyVisible()} 666 + onClick={() => setHistoryVisible(!historyVisible())} 667 + > 668 + <Show when={historyVisible()} fallback={<PanelRightOpen class="size-4" />}> 669 + <PanelRightClose class="size-4" /> 670 + </Show> 671 + </button> 672 + </div> 673 + </div> 674 + 675 + <div 676 + class={clsx( 677 + 'untangled-pr-diff-grid', 678 + !filesVisible() && 'untangled-pr-diff-grid-sidebar-collapsed', 679 + !historyVisible() && 'untangled-pr-diff-grid-history-collapsed', 680 + )} 681 + > 682 + <Show when={filesVisible()}> 683 + <aside class="untangled-pr-file-tree" aria-label="changed files"> 684 + <For each={rows()}> 685 + {(row) => ( 686 + <a 687 + href={`#diff-${encodeURIComponent(row.file.path)}`} 688 + class="untangled-pr-file-tree-row" 689 + style={{ 'padding-left': `${0.75 + row.depth * 0.85}rem` }} 690 + title={row.file.path} 691 + > 692 + <Show when={row.folder} fallback={<FileText class="size-4 shrink-0" />}> 693 + <Folder class="size-4 shrink-0" /> 694 + </Show> 695 + <span class="truncate">{row.name}</span> 696 + </a> 697 + )} 698 + </For> 699 + </aside> 700 + </Show> 701 + 702 + <div class="untangled-pr-diff-files"> 703 + <For each={files()}> 704 + {(file) => ( 705 + <details id={`diff-${encodeURIComponent(file.path)}`} class="untangled-pr-diff-file" open={expanded()}> 706 + <summary class="untangled-pr-diff-file-header"> 707 + <div class="flex min-w-0 items-center gap-2"> 708 + <ChevronRight class="untangled-pr-diff-chevron size-4 shrink-0" /> 709 + <DiffStatPill additions={file.additions} deletions={file.deletions} /> 710 + <span class="truncate text-base text-gray-100">{file.path}</span> 711 + <Circle class="size-4 shrink-0 text-gray-500" /> 712 + </div> 713 + <label class="flex shrink-0 items-center gap-1 text-xs text-gray-400"> 714 + <Circle class="size-4" /> 715 + reviewed 716 + </label> 717 + </summary> 718 + <div class="untangled-pr-diff-ellipsis">...</div> 719 + <CodeView text={file.text} diff /> 720 + </details> 721 + )} 722 + </For> 723 + </div> 724 + 725 + <Show when={historyVisible()}> 726 + <aside class="untangled-pr-history">{props.history}</aside> 727 + </Show> 728 + </div> 729 + </section> 572 730 ); 573 731 }; 574 732
+500
src/index.css
··· 5 5 font-family: InterVariable, system-ui, sans-serif, ui-sans-serif; 6 6 } 7 7 8 + .untangled-pr-detail { 9 + display: flex; 10 + min-width: 0; 11 + flex-direction: column; 12 + gap: 2rem; 13 + } 14 + 15 + .untangled-pr-header-grid { 16 + display: grid; 17 + grid-template-columns: minmax(0, 1fr) 15rem; 18 + gap: 1rem; 19 + align-items: start; 20 + } 21 + 22 + .untangled-pr-summary-card { 23 + min-width: 0; 24 + border-radius: 0.25rem; 25 + background: rgb(255 255 255); 26 + padding: 1.5rem; 27 + color: rgb(17 24 39); 28 + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 29 + } 30 + 31 + .untangled-pr-meta { 32 + display: flex; 33 + min-width: 0; 34 + flex-direction: column; 35 + gap: 1.75rem; 36 + padding: 0.25rem 0; 37 + color: rgb(107 114 128); 38 + } 39 + 40 + .untangled-pr-meta-section h2 { 41 + margin: 0 0 0.25rem; 42 + font-size: 0.75rem; 43 + font-weight: 700; 44 + text-transform: uppercase; 45 + color: rgb(75 85 99); 46 + } 47 + 48 + .untangled-pr-meta-section h2 span { 49 + display: inline-flex; 50 + min-width: 1.25rem; 51 + justify-content: center; 52 + border-radius: 0.25rem; 53 + background: rgb(229 231 235); 54 + padding: 0 0.25rem; 55 + font-size: 0.75rem; 56 + } 57 + 58 + .untangled-pr-meta-section p { 59 + margin: 0; 60 + font-size: 0.875rem; 61 + } 62 + 63 + .untangled-pr-meta-section code { 64 + font-size: 0.8125rem; 65 + color: rgb(17 24 39); 66 + } 67 + 68 + .untangled-pr-meta-action, 69 + .untangled-pr-icon-button { 70 + display: inline-flex; 71 + align-items: center; 72 + justify-content: center; 73 + border: 1px solid rgb(209 213 219); 74 + border-radius: 0.25rem; 75 + background: transparent; 76 + color: rgb(107 114 128); 77 + } 78 + 79 + .untangled-pr-meta-action { 80 + border: 0; 81 + padding: 0.125rem; 82 + } 83 + 84 + .untangled-pr-icon-button { 85 + width: 2rem; 86 + height: 2rem; 87 + } 88 + 89 + .untangled-pr-diff-breakout { 90 + position: relative; 91 + left: 50%; 92 + width: 100vw; 93 + margin-left: -50vw; 94 + } 95 + 96 + .untangled-pr-diff-toolbar { 97 + position: sticky; 98 + top: 0; 99 + z-index: 30; 100 + display: grid; 101 + grid-template-columns: 11.5rem minmax(0, 1fr) 29.5rem; 102 + gap: 1rem; 103 + align-items: center; 104 + padding: 0.5rem; 105 + background: rgb(249 250 251); 106 + } 107 + 108 + .untangled-pr-diff-toolbar-sidebar-collapsed { 109 + grid-template-columns: minmax(0, 1fr) 29.5rem; 110 + } 111 + 112 + .untangled-pr-diff-toolbar-history-collapsed { 113 + grid-template-columns: 11.5rem minmax(0, 1fr) 2.125rem; 114 + } 115 + 116 + .untangled-pr-diff-toolbar-sidebar-collapsed.untangled-pr-diff-toolbar-history-collapsed { 117 + grid-template-columns: minmax(0, 1fr) 2.125rem; 118 + } 119 + 120 + .untangled-pr-diff-toolbar-main { 121 + grid-column: 1 / 3; 122 + grid-row: 1; 123 + display: flex; 124 + min-width: 0; 125 + flex-wrap: wrap; 126 + align-items: center; 127 + gap: 0.625rem; 128 + font-size: 0.875rem; 129 + color: rgb(107 114 128); 130 + } 131 + 132 + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-main { 133 + grid-column: 1; 134 + } 135 + 136 + .untangled-pr-diff-toolbar-actions { 137 + grid-column: 2; 138 + grid-row: 1; 139 + justify-self: end; 140 + } 141 + 142 + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-actions { 143 + grid-column: 1; 144 + } 145 + 146 + .untangled-pr-diff-toolbar-history { 147 + grid-column: 3; 148 + grid-row: 1; 149 + justify-self: end; 150 + } 151 + 152 + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-history { 153 + grid-column: 2; 154 + } 155 + 156 + .untangled-pr-tool-button, 157 + .untangled-pr-sidebar-toggle { 158 + display: inline-flex; 159 + min-height: 2.125rem; 160 + align-items: center; 161 + justify-content: center; 162 + gap: 0.5rem; 163 + border: 1px solid rgb(209 213 219); 164 + border-radius: 0.25rem; 165 + background: rgb(255 255 255); 166 + padding: 0 0.75rem; 167 + font-size: 0.875rem; 168 + color: rgb(31 41 55); 169 + } 170 + 171 + .untangled-pr-sidebar-toggle { 172 + width: 2.125rem; 173 + padding: 0; 174 + } 175 + 176 + .untangled-diff-stat-pill { 177 + display: inline-flex; 178 + align-items: center; 179 + overflow: hidden; 180 + border-radius: 0.25rem; 181 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 182 + font-size: 0.875rem; 183 + line-height: 1; 184 + } 185 + 186 + .untangled-diff-stat-pill span { 187 + padding: 0.375rem 0.5rem; 188 + } 189 + 190 + .untangled-diff-stat-add { 191 + background: rgb(220 252 231); 192 + color: rgb(21 128 61); 193 + } 194 + 195 + .untangled-diff-stat-del { 196 + background: rgb(254 226 226); 197 + color: rgb(185 28 28); 198 + } 199 + 200 + .untangled-pr-diff-grid { 201 + display: grid; 202 + grid-template-columns: 11.5rem minmax(0, 1fr) 29.5rem; 203 + gap: 1rem; 204 + align-items: start; 205 + } 206 + 207 + .untangled-pr-diff-grid-sidebar-collapsed { 208 + grid-template-columns: minmax(0, 1fr) 29.5rem; 209 + } 210 + 211 + .untangled-pr-diff-grid-history-collapsed { 212 + grid-template-columns: 11.5rem minmax(0, 1fr); 213 + } 214 + 215 + .untangled-pr-diff-grid-sidebar-collapsed.untangled-pr-diff-grid-history-collapsed { 216 + grid-template-columns: minmax(0, 1fr); 217 + } 218 + 219 + .untangled-pr-file-tree { 220 + position: sticky; 221 + top: 3rem; 222 + max-height: calc(100vh - 3rem); 223 + overflow: auto; 224 + min-height: calc(100vh - 3rem); 225 + border: 1px solid rgb(229 231 235); 226 + border-top: 0; 227 + border-radius: 0 0 0.25rem 0.25rem; 228 + background: rgb(255 255 255); 229 + padding: 0.625rem 0 1.5rem; 230 + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 231 + } 232 + 233 + .untangled-pr-file-tree-row { 234 + display: flex; 235 + min-width: 0; 236 + align-items: center; 237 + gap: 0.375rem; 238 + padding-top: 0.25rem; 239 + padding-right: 0.5rem; 240 + padding-bottom: 0.25rem; 241 + color: rgb(55 65 81); 242 + font-size: 0.875rem; 243 + text-decoration: none; 244 + } 245 + 246 + .untangled-pr-file-tree-row:hover { 247 + background: rgb(243 244 246); 248 + text-decoration: none; 249 + } 250 + 251 + .untangled-pr-diff-files { 252 + display: flex; 253 + min-width: 0; 254 + flex-direction: column; 255 + gap: 1rem; 256 + } 257 + 258 + .untangled-pr-diff-file { 259 + overflow: hidden; 260 + border: 1px solid rgb(209 213 219); 261 + border-radius: 0.25rem; 262 + background: rgb(255 255 255); 263 + } 264 + 265 + .untangled-pr-diff-file-header { 266 + display: flex; 267 + min-height: 2.75rem; 268 + cursor: pointer; 269 + list-style: none; 270 + align-items: center; 271 + justify-content: space-between; 272 + gap: 1rem; 273 + padding: 0 1rem; 274 + background: rgb(31 41 55); 275 + } 276 + 277 + .untangled-pr-diff-file-header::-webkit-details-marker { 278 + display: none; 279 + } 280 + 281 + .untangled-pr-diff-file[open] .untangled-pr-diff-chevron { 282 + transform: rotate(90deg); 283 + } 284 + 285 + .untangled-pr-diff-ellipsis { 286 + background: rgb(55 65 81); 287 + padding: 0.125rem 0; 288 + text-align: center; 289 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 290 + color: rgb(209 213 219); 291 + } 292 + 293 + .untangled-pr-history { 294 + position: sticky; 295 + top: 3rem; 296 + max-height: calc(100vh - 3rem); 297 + overflow: auto; 298 + padding-right: 0.5rem; 299 + } 300 + 301 + .untangled-pr-history-card { 302 + overflow: hidden; 303 + border: 1px solid rgb(209 213 219); 304 + border-radius: 0.25rem; 305 + background: rgb(255 255 255); 306 + } 307 + 308 + .untangled-pr-history-card > header { 309 + display: flex; 310 + align-items: center; 311 + justify-content: space-between; 312 + gap: 1rem; 313 + border-bottom: 1px solid rgb(229 231 235); 314 + padding: 0.75rem 1rem; 315 + } 316 + 317 + .untangled-pr-history-card h2 { 318 + margin: 0; 319 + font-size: 1rem; 320 + font-weight: 500; 321 + } 322 + 323 + .untangled-pr-history-card header div { 324 + display: flex; 325 + gap: 1rem; 326 + font-size: 0.875rem; 327 + color: rgb(75 85 99); 328 + } 329 + 330 + .untangled-pr-history-submission { 331 + display: flex; 332 + width: 100%; 333 + gap: 0.75rem; 334 + border: 0; 335 + padding: 1rem; 336 + background: transparent; 337 + color: inherit; 338 + text-align: left; 339 + } 340 + 341 + .untangled-pr-history-submission:hover, 342 + .untangled-pr-history-submission-active { 343 + background: rgb(239 246 255); 344 + } 345 + 346 + .untangled-pr-history-event { 347 + display: flex; 348 + align-items: center; 349 + gap: 0.5rem; 350 + margin: 0 0.125rem 1rem; 351 + border: 1px solid transparent; 352 + border-radius: 0.25rem; 353 + padding: 0.75rem 1rem; 354 + font-weight: 500; 355 + } 356 + 357 + .untangled-pr-history-event-merged { 358 + border-color: rgb(168 85 247); 359 + background: rgb(126 34 206); 360 + color: rgb(250 245 255); 361 + } 362 + 363 + .untangled-pr-history-event-closed { 364 + border-color: rgb(239 68 68); 365 + background: rgb(127 29 29); 366 + color: rgb(254 242 242); 367 + } 368 + 369 + .untangled-pr-history-event-open { 370 + border-color: rgb(34 197 94); 371 + background: rgb(22 101 52); 372 + color: rgb(240 253 244); 373 + } 374 + 375 + .untangled-pr-history-comment { 376 + display: flex; 377 + flex-direction: column; 378 + gap: 0.75rem; 379 + border-top: 1px solid rgb(229 231 235); 380 + padding: 1rem; 381 + } 382 + 383 + @media (prefers-color-scheme: dark) { 384 + .untangled-pr-summary-card, 385 + .untangled-pr-tool-button, 386 + .untangled-pr-sidebar-toggle, 387 + .untangled-pr-file-tree, 388 + .untangled-pr-diff-file, 389 + .untangled-pr-history-card { 390 + background: rgb(31 41 55); 391 + color: rgb(243 244 246); 392 + } 393 + 394 + .untangled-pr-meta, 395 + .untangled-pr-meta-section p { 396 + color: rgb(156 163 175); 397 + } 398 + 399 + .untangled-pr-meta-section h2 { 400 + color: rgb(209 213 219); 401 + } 402 + 403 + .untangled-pr-meta-section h2 span { 404 + background: rgb(75 85 99); 405 + color: rgb(243 244 246); 406 + } 407 + 408 + .untangled-pr-meta-section code { 409 + color: rgb(243 244 246); 410 + } 411 + 412 + .untangled-pr-meta-action, 413 + .untangled-pr-icon-button { 414 + border-color: rgb(55 65 81); 415 + color: rgb(156 163 175); 416 + } 417 + 418 + .untangled-pr-tool-button, 419 + .untangled-pr-sidebar-toggle, 420 + .untangled-pr-diff-file, 421 + .untangled-pr-history-card { 422 + border-color: rgb(55 65 81); 423 + } 424 + 425 + .untangled-pr-file-tree { 426 + border-color: rgb(55 65 81); 427 + } 428 + 429 + .untangled-diff-stat-add { 430 + background: rgb(22 101 52 / 0.5); 431 + color: rgb(74 222 128); 432 + } 433 + 434 + .untangled-diff-stat-del { 435 + background: rgb(127 29 29 / 0.5); 436 + color: rgb(248 113 113); 437 + } 438 + 439 + .untangled-pr-file-tree-row { 440 + color: rgb(229 231 235); 441 + } 442 + 443 + .untangled-pr-file-tree-row:hover { 444 + background: rgb(31 41 55); 445 + } 446 + 447 + .untangled-pr-history-card > header, 448 + .untangled-pr-history-comment { 449 + border-color: rgb(55 65 81); 450 + } 451 + 452 + .untangled-pr-history-card header div { 453 + color: rgb(209 213 219); 454 + } 455 + 456 + .untangled-pr-diff-toolbar { 457 + background: rgb(17 24 39); 458 + } 459 + 460 + .untangled-pr-history-submission:hover, 461 + .untangled-pr-history-submission-active { 462 + background: rgb(30 64 175 / 0.45); 463 + } 464 + } 465 + 466 + @media (max-width: 1023px) { 467 + .untangled-pr-header-grid, 468 + .untangled-pr-diff-grid, 469 + .untangled-pr-diff-grid-sidebar-collapsed, 470 + .untangled-pr-diff-grid-history-collapsed, 471 + .untangled-pr-diff-toolbar, 472 + .untangled-pr-diff-toolbar-sidebar-collapsed, 473 + .untangled-pr-diff-toolbar-history-collapsed { 474 + grid-template-columns: minmax(0, 1fr); 475 + } 476 + 477 + .untangled-pr-diff-toolbar-main, 478 + .untangled-pr-diff-toolbar-actions, 479 + .untangled-pr-diff-toolbar-history, 480 + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-main, 481 + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-actions, 482 + .untangled-pr-diff-toolbar-sidebar-collapsed .untangled-pr-diff-toolbar-history { 483 + grid-column: 1; 484 + } 485 + 486 + .untangled-pr-file-tree, 487 + .untangled-pr-history { 488 + position: static; 489 + max-height: none; 490 + } 491 + 492 + .untangled-pr-file-tree { 493 + display: none; 494 + } 495 + } 496 + 497 + @media (max-width: 767px) { 498 + .untangled-pr-diff-toolbar { 499 + align-items: stretch; 500 + flex-direction: column; 501 + } 502 + 503 + .untangled-pr-summary-card { 504 + padding: 1rem; 505 + } 506 + } 507 + 8 508 html, 9 509 body, 10 510 #root {
+165 -92
src/pages/repo/pulls.tsx
··· 1 1 import clsx from 'clsx'; 2 - import { CirclePlus, LoaderCircle, Search } from 'lucide-solid'; 2 + import { CirclePlus, Copy, GitMerge, GitPullRequest, GitPullRequestClosed, LoaderCircle, MessageSquare, Search } from 'lucide-solid'; 3 3 import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router'; 4 4 import { createQuery, useQueryClient } from '@tanstack/solid-query'; 5 5 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 6 6 import { compareBranches, createPull, createPullComment, fetchPullRoundPatch, getPull, getRepoBranches, listPullsPage, parseAtUri, setPullStatus, type RepoContext } from '../../lib/api'; 7 7 import { useAuth } from '../../lib/auth'; 8 8 import { Avatar, ErrorState, LoadingState, StateBadge, ToggleButton, buttonStyles, cardStyles, inputStyles, textareaStyles } from '../../components/common'; 9 - import { AsideCard, BranchPill, CommentCard, DiffView, MarkdownBlock, PaginationControls, RepoListSkeleton, RepoThreadSkeleton } from '../../components/repo'; 9 + import { BranchPill, CommentCard, DiffView, MarkdownBlock, PaginationControls, PullDiffView, RepoListSkeleton, RepoThreadSkeleton } from '../../components/repo'; 10 10 import { RepoFrame, pullQueryKey, pullsQueryKey, useRepoQuery } from './shared'; 11 11 import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, parseIntegerSearchParam, pullHref, uniqueCommenters } from '../../lib/repo-utils'; 12 12 ··· 454 454 } 455 455 }; 456 456 457 + const participants = createMemo(() => { 458 + const detail = pullQuery.data; 459 + if (!detail) return []; 460 + const seen = new Set<string>(); 461 + const result: Array<{ did: string; handle: string }> = []; 462 + const add = (participant: { did: string; handle: string }) => { 463 + if (seen.has(participant.did)) return; 464 + seen.add(participant.did); 465 + result.push(participant); 466 + }; 467 + add(detail.pull.author); 468 + for (const commenter of uniqueCommenters(detail.comments)) add(commenter); 469 + return result; 470 + }); 471 + 472 + const roundLabel = createMemo(() => `round #${roundIndex()}`); 473 + 474 + const statusEvent = createMemo(() => { 475 + const state = pullQuery.data?.pull.state; 476 + if (state === 'merged') { 477 + return { 478 + icon: <GitMerge class="size-4" />, 479 + text: 'pull request successfully merged', 480 + class: 'untangled-pr-history-event-merged', 481 + }; 482 + } 483 + if (state === 'closed') { 484 + return { 485 + icon: <GitPullRequestClosed class="size-4" />, 486 + text: 'pull request closed', 487 + class: 'untangled-pr-history-event-closed', 488 + }; 489 + } 490 + return { 491 + icon: <GitPullRequest class="size-4" />, 492 + text: 'pull request opened', 493 + class: 'untangled-pr-history-event-open', 494 + }; 495 + }); 496 + 457 497 return ( 458 498 <RepoFrame active="pulls"> 459 499 <Switch> ··· 467 507 </Match> 468 508 <Match when={pullQuery.data}> 469 509 {(detail) => ( 470 - <div class="grid grid-cols-1 gap-4 w-full md:grid-cols-10"> 471 - <div class="col-span-1 flex min-w-0 flex-col gap-4 md:col-span-8"> 472 - <section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white"> 473 - <header class="pb-2"> 510 + <div class="untangled-pr-detail"> 511 + <div class="untangled-pr-header-grid"> 512 + <section class="untangled-pr-summary-card"> 513 + <header> 474 514 <h1 class="m-0 text-2xl"> 475 515 {detail().pull.value.title}{' '} 476 516 <span class="text-gray-500 dark:text-gray-400">#{detail().pull.number}</span> ··· 479 519 480 520 <div class="mt-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400"> 481 521 <StateBadge state={detail().pull.state} kind="pull" /> 482 - <span>opened</span> 522 + <span>opened by</span> 483 523 <Avatar did={detail().pull.author.did} size="size-6" /> 484 524 <span class="text-gray-700 dark:text-gray-200">{detail().pull.author.handle}</span> 485 525 <span class="select-none">·</span> ··· 487 527 <span class="select-none">·</span> 488 528 <span>targeting</span> 489 529 <BranchPill name={detail().pull.value.target.branch} /> 490 - <span>from</span> 491 - <BranchPill name={detail().pull.value.source?.branch ?? detail().pull.value.target.branch} /> 492 530 </div> 493 531 494 532 <Show when={detail().pull.value.body}> 495 - <div class="mt-5 min-w-0"> 533 + <div class="mt-8 min-w-0 text-base leading-7 text-gray-700 dark:text-gray-300"> 496 534 <MarkdownBlock markdown={detail().pull.value.body!} /> 497 535 </div> 498 536 </Show> 499 537 500 - <Show when={auth.agent()}> 501 - <div class="mt-5 flex justify-end"> 502 - <button type="button" class={buttonStyles()} disabled={working()} onClick={() => void toggleState()}> 538 + <div class="mt-4 flex items-center gap-2"> 539 + <Show when={auth.agent()}> 540 + <button type="button" class={clsx(buttonStyles(), 'ml-auto')} disabled={working()} onClick={() => void toggleState()}> 503 541 {detail().pull.state === 'open' ? 'close pull' : 'reopen pull'} 504 542 </button> 505 - </div> 506 - </Show> 543 + </Show> 544 + </div> 507 545 </section> 508 546 509 - <section class={cardStyles('min-w-0 p-6')}> 510 - <div class="mb-4 flex flex-wrap items-center justify-between gap-3"> 511 - <div class="font-medium">patch</div> 512 - <select 513 - value={String(roundIndex())} 514 - onChange={(event) => setRoundIndex(Number(event.currentTarget.value))} 515 - class={inputStyles()} 516 - > 517 - <For each={detail().pull.value.rounds}> 518 - {(round, index) => ( 519 - <option value={String(index())}> 520 - round {index() + 1} · {formatRelativeTime(round.createdAt)} 521 - </option> 522 - )} 547 + <aside class="untangled-pr-meta"> 548 + <div class="untangled-pr-meta-section"> 549 + <h2>Labels</h2> 550 + <p>None yet.</p> 551 + </div> 552 + <div class="untangled-pr-meta-section"> 553 + <h2>Assignee</h2> 554 + <p>None yet.</p> 555 + </div> 556 + <div class="untangled-pr-meta-section"> 557 + <h2> 558 + Participants <span>{participants().length}</span> 559 + </h2> 560 + <div class="mt-3 flex flex-wrap gap-2"> 561 + <For each={participants()}> 562 + {(participant) => <Avatar did={participant.did} size="size-7" />} 523 563 </For> 524 - </select> 564 + </div> 565 + </div> 566 + <div class="untangled-pr-meta-section"> 567 + <h2>AT URI</h2> 568 + <div class="flex min-w-0 items-center gap-2"> 569 + <code class="truncate">{detail().pull.uri}</code> 570 + <button 571 + type="button" 572 + class="untangled-pr-meta-action" 573 + aria-label="copy AT URI" 574 + onClick={() => void navigator.clipboard?.writeText(detail().pull.uri)} 575 + > 576 + <Copy class="size-4" /> 577 + </button> 578 + </div> 525 579 </div> 526 - <Switch> 527 - <Match when={patchQuery.isLoading}> 528 - <LoadingState label="Loading patch…" /> 529 - </Match> 530 - <Match when={patchQuery.error}> 531 - <ErrorState message={getErrorMessage(patchQuery.error)} /> 532 - </Match> 533 - <Match when={patchQuery.data}> 534 - <DiffView patch={patchQuery.data!} maxHeight="40rem" /> 535 - </Match> 536 - </Switch> 537 - </section> 538 - 539 - <div class="flex min-w-0 flex-col gap-4"> 540 - <For each={detail().comments}> 541 - {(item) => ( 542 - <CommentCard 543 - author={item.author.handle} 544 - did={item.author.did} 545 - createdAt={item.value.createdAt} 546 - markdown={item.value.body} 547 - /> 548 - )} 549 - </For> 550 - 551 - <Show when={auth.agent()}> 552 - <form onSubmit={addComment} class="bg-white dark:bg-gray-800 rounded shadow-sm py-4 px-4 relative flex min-w-0 flex-col gap-3"> 553 - <div class="text-sm text-gray-500 dark:text-gray-400">new comment</div> 554 - <textarea 555 - value={comment()} 556 - onInput={(event) => setComment(event.currentTarget.value)} 557 - class={textareaStyles()} 558 - rows="5" 559 - /> 560 - <Show when={error()}> 561 - <p class="text-sm text-red-500">{error()}</p> 562 - </Show> 563 - <div class="flex justify-end"> 564 - <button type="submit" class={clsx(buttonStyles('primary'), 'justify-center min-w-32')} disabled={working()}> 565 - comment 566 - </button> 567 - </div> 568 - </form> 569 - </Show> 570 - </div> 580 + </aside> 571 581 </div> 572 582 573 - <div class="col-span-1 flex min-w-0 flex-col gap-6 md:col-span-2"> 574 - <AsideCard title="participants"> 575 - <div class="flex flex-col gap-3 text-sm"> 576 - <div class="flex items-center gap-2"> 577 - <Avatar did={detail().pull.author.did} size="size-6" /> 578 - <span>{detail().pull.author.handle}</span> 579 - </div> 580 - <For each={uniqueCommenters(detail().comments)}> 581 - {(participant) => ( 582 - <div class="flex items-center gap-2"> 583 - <Avatar did={participant.did} size="size-6" /> 584 - <span>{participant.handle}</span> 585 - </div> 586 - )} 587 - </For> 583 + <Switch> 584 + <Match when={patchQuery.isLoading}> 585 + <div class={cardStyles('p-6')}> 586 + <LoadingState label="Loading patch…" /> 587 + </div> 588 + </Match> 589 + <Match when={patchQuery.error}> 590 + <div class={cardStyles('p-6')}> 591 + <ErrorState message={getErrorMessage(patchQuery.error)} /> 588 592 </div> 589 - </AsideCard> 590 - </div> 593 + </Match> 594 + <Match when={patchQuery.data}> 595 + <PullDiffView 596 + patch={patchQuery.data!} 597 + roundLabel={roundLabel()} 598 + history={ 599 + <div class="untangled-pr-history-card"> 600 + <header> 601 + <h2>History</h2> 602 + <div> 603 + <span>{detail().pull.value.rounds.length} round</span> 604 + <span>{detail().comments.length} comments</span> 605 + </div> 606 + </header> 607 + <For each={detail().pull.value.rounds}> 608 + {(round, index) => ( 609 + <button 610 + type="button" 611 + class={clsx('untangled-pr-history-submission', index() === roundIndex() && 'untangled-pr-history-submission-active')} 612 + onClick={() => setRoundIndex(index())} 613 + > 614 + <Avatar did={detail().pull.author.did} size="size-8" /> 615 + <span class="min-w-0"> 616 + <span class="flex flex-wrap items-center gap-2 text-sm"> 617 + <span class="font-medium">{detail().pull.author.handle}</span> 618 + <span>submitted</span> 619 + <span class="rounded bg-blue-600 px-2 py-0.5 font-mono text-xs text-white">#{index()}</span> 620 + <span>·</span> 621 + <span>{formatRelativeTime(round.createdAt)}</span> 622 + </span> 623 + </span> 624 + </button> 625 + )} 626 + </For> 627 + <div class={clsx('untangled-pr-history-event', statusEvent().class)}> 628 + {statusEvent().icon} 629 + <span>{statusEvent().text}</span> 630 + </div> 631 + <For each={detail().comments}> 632 + {(item) => ( 633 + <CommentCard 634 + author={item.author.handle} 635 + did={item.author.did} 636 + createdAt={item.value.createdAt} 637 + markdown={item.value.body} 638 + /> 639 + )} 640 + </For> 641 + <Show when={auth.agent()}> 642 + <form onSubmit={addComment} class="untangled-pr-history-comment"> 643 + <textarea 644 + value={comment()} 645 + onInput={(event) => setComment(event.currentTarget.value)} 646 + class={textareaStyles()} 647 + rows="4" 648 + placeholder="new comment" 649 + /> 650 + <Show when={error()}> 651 + <p class="text-sm text-red-500">{error()}</p> 652 + </Show> 653 + <button type="submit" class={clsx(buttonStyles('primary'), 'justify-center')} disabled={working()}> 654 + <MessageSquare class="size-4" /> 655 + comment 656 + </button> 657 + </form> 658 + </Show> 659 + </div> 660 + } 661 + /> 662 + </Match> 663 + </Switch> 591 664 </div> 592 665 )} 593 666 </Match>