authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 12:22:18-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 12:22:18-07:00
loge420ef98333c97205f836201cfeb94682a41893c
treeca9dbc54a12fa1b5c91ce29892c3f81affdbb2ef
parent016cdb9622359967afe80a27c538056ef1e7b857
signature Signed by PGP key B5690EEEBB952194

refactor(web): Collapsible stays a plain container (#13038)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

5 files changed, 231 insertions(+), 235 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+25-24
......@@ -844,32 +844,33 @@ function MarkdownDetails({
844844 const content = childNodes.filter((_, index) => index !== summaryIndex);
845845
846846 return (
847 <Collapsible
848 defaultOpen={open}
849 onOpenChange={setIsOpen}
850 className="chat-markdown-details my-2 border-y border-border/60"
851 data-markdown-details=""
852 data-markdown-details-open={isOpen ? "true" : "false"}
853 >
854 <CollapsibleTrigger
855 className="flex w-full items-center gap-2 py-2 text-left text-sm font-medium text-foreground data-panel-open:[&_svg]:rotate-90"
856 data-markdown-details-summary=""
847 <div className="my-2 border-y border-border/60">
848 <Collapsible
849 defaultOpen={open}
850 onOpenChange={setIsOpen}
851 data-markdown-details=""
852 data-markdown-details-open={isOpen ? "true" : "false"}
857853 >
858 <ChevronRightIcon
859 className="size-4 shrink-0 text-muted-foreground transition-transform"
860 aria-hidden
861 />
862 <span>{summary}</span>
863 </CollapsibleTrigger>
864 <CollapsiblePanel>
865 <div
866 className="pb-3 ps-6 text-foreground/[calc(80%+var(--appearance-contrast-boost)/5)]"
867 data-markdown-details-content=""
854 <CollapsibleTrigger
855 className="flex w-full items-center gap-2 py-2 text-left text-sm font-medium text-foreground data-panel-open:[&_svg]:rotate-90"
856 data-markdown-details-summary=""
868857 >
869 {content}
870 </div>
871 </CollapsiblePanel>
872 </Collapsible>
858 <ChevronRightIcon
859 className="size-4 shrink-0 text-muted-foreground transition-transform"
860 aria-hidden
861 />
862 <span>{summary}</span>
863 </CollapsibleTrigger>
864 <CollapsiblePanel>
865 <div
866 className="pb-3 ps-6 text-foreground/[calc(80%+var(--appearance-contrast-boost)/5)]"
867 data-markdown-details-content=""
868 >
869 {content}
870 </div>
871 </CollapsiblePanel>
872 </Collapsible>
873 </div>
873874 );
874875}
875876
apps/web/src/components/onboarding/WelcomeWizard.tsx+89-93
......@@ -350,40 +350,38 @@ function ConnectionStep({
350350 onToggleEnvironment={onToggleEnvironment}
351351 />
352352 ) : null}
353 <Collapsible
354 open={pairingOpen}
355 onOpenChange={setPairingOpen}
356 className="rounded-lg border border-border bg-background"
357 >
358 <CollapsibleTrigger
359 disabled={isPairing}
360 render={
361 <Button
362 variant="ghost"
363 className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto"
364 />
365 }
366 >
367 <LinkIcon className="size-4 text-muted-foreground" />
368 <span className="flex-1">Add a computer</span>
369 <ChevronRightIcon
370 className={cn("size-4 text-muted-foreground", pairingOpen && "rotate-90")}
371 />
372 </CollapsibleTrigger>
373 <CollapsiblePanel>
374 <div className="px-3 pb-3">
375 <PairingForm
376 isPairing={isPairing}
377 setIsPairing={setIsPairing}
378 onPaired={(environmentId) => {
379 setPairingOpen(false);
380 onPaired(environmentId);
381 requestAnimationFrame(() => continueRef.current?.focus());
382 }}
353 <div className="rounded-lg border border-border bg-background">
354 <Collapsible open={pairingOpen} onOpenChange={setPairingOpen}>
355 <CollapsibleTrigger
356 disabled={isPairing}
357 render={
358 <Button
359 variant="ghost"
360 className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto"
361 />
362 }
363 >
364 <LinkIcon className="size-4 text-muted-foreground" />
365 <span className="flex-1">Add a computer</span>
366 <ChevronRightIcon
367 className={cn("size-4 text-muted-foreground", pairingOpen && "rotate-90")}
383368 />
384 </div>
385 </CollapsiblePanel>
386 </Collapsible>
369 </CollapsibleTrigger>
370 <CollapsiblePanel>
371 <div className="px-3 pb-3">
372 <PairingForm
373 isPairing={isPairing}
374 setIsPairing={setIsPairing}
375 onPaired={(environmentId) => {
376 setPairingOpen(false);
377 onPaired(environmentId);
378 requestAnimationFrame(() => continueRef.current?.focus());
379 }}
380 />
381 </div>
382 </CollapsiblePanel>
383 </Collapsible>
384 </div>
387385 </div>
388386 <div className="mt-6 flex items-center justify-end gap-3">
389387 <Button
......@@ -419,69 +417,67 @@ function ConnectAccountOption({
419417 const onDiscoveryReady = useCallback(() => setDiscoveryReady(true), []);
420418
421419 return (
422 <Collapsible
423 open={expanded && !!isSignedIn && discoveryReady}
424 onOpenChange={setExpanded}
425 className="rounded-lg border border-border bg-background"
426 >
427 <CollapsibleTrigger
428 disabled={disabled || !isLoaded}
429 onClick={(event) => {
430 if (!isSignedIn) {
431 event.preventDefault();
432 setExpanded(true);
433 openAuthPrompt();
420 <div className="rounded-lg border border-border bg-background">
421 <Collapsible open={expanded && !!isSignedIn && discoveryReady} onOpenChange={setExpanded}>
422 <CollapsibleTrigger
423 disabled={disabled || !isLoaded}
424 onClick={(event) => {
425 if (!isSignedIn) {
426 event.preventDefault();
427 setExpanded(true);
428 openAuthPrompt();
429 }
430 }}
431 render={
432 <Button
433 variant="ghost"
434 className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto"
435 />
434436 }
435 }}
436 render={
437 <Button
438 variant="ghost"
439 className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto"
437 >
438 <CloudIcon className="size-4 text-muted-foreground" />
439 <span className="flex-1">T3 Connect</span>
440 <span className="text-xs text-muted-foreground">
441 {!isLoaded
442 ? "Loading sign-in…"
443 : !isSignedIn
444 ? "Sign in"
445 : !discoveryReady
446 ? "Loading computers…"
447 : null}
448 </span>
449 <ChevronRightIcon
450 className={cn("size-4 text-muted-foreground", expanded && isSignedIn && "rotate-90")}
440451 />
441 }
442 >
443 <CloudIcon className="size-4 text-muted-foreground" />
444 <span className="flex-1">T3 Connect</span>
445 <span className="text-xs text-muted-foreground">
446 {!isLoaded
447 ? "Loading sign-in…"
448 : !isSignedIn
449 ? "Sign in"
450 : !discoveryReady
451 ? "Loading computers…"
452 : null}
453 </span>
454 <ChevronRightIcon
455 className={cn("size-4 text-muted-foreground", expanded && isSignedIn && "rotate-90")}
456 />
457 </CollapsibleTrigger>
458 <CollapsiblePanel keepMounted>
459 <div className="px-3 pb-3">
460 <div className="mb-3 space-y-1.5">
461 {isSignedIn ? (
462 <CloudEnvironmentConnectRows
463 primaryEnvironmentId={null}
464 savedEnvironments={environments}
465 showSavedEnvironments
466 onDiscoveryReady={onDiscoveryReady}
467 selection={{ selectedIds, onChange: onToggleEnvironment, autoSelectedComputers }}
468 refreshWhileEmpty
469 empty={
470 <p className="py-3 text-sm text-muted-foreground">No computers linked yet.</p>
471 }
472 />
473 ) : null}
452 </CollapsibleTrigger>
453 <CollapsiblePanel keepMounted>
454 <div className="px-3 pb-3">
455 <div className="mb-3 space-y-1.5">
456 {isSignedIn ? (
457 <CloudEnvironmentConnectRows
458 primaryEnvironmentId={null}
459 savedEnvironments={environments}
460 showSavedEnvironments
461 onDiscoveryReady={onDiscoveryReady}
462 selection={{ selectedIds, onChange: onToggleEnvironment, autoSelectedComputers }}
463 refreshWhileEmpty
464 empty={
465 <p className="py-3 text-sm text-muted-foreground">No computers linked yet.</p>
466 }
467 />
468 ) : null}
469 </div>
470 <p className="text-sm text-muted-foreground">
471 Run this on each computer you want to connect.
472 </p>
473 <CommandBlock command="npx t3 connect" className="mt-3" />
474 <p className="mt-3 text-xs text-muted-foreground">
475 Keep T3 Code running. Select the computers you want to set up above.
476 </p>
474477 </div>
475 <p className="text-sm text-muted-foreground">
476 Run this on each computer you want to connect.
477 </p>
478 <CommandBlock command="npx t3 connect" className="mt-3" />
479 <p className="mt-3 text-xs text-muted-foreground">
480 Keep T3 Code running. Select the computers you want to set up above.
481 </p>
482 </div>
483 </CollapsiblePanel>
484 </Collapsible>
478 </CollapsiblePanel>
479 </Collapsible>
480 </div>
485481 );
486482}
487483
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+53-53
......@@ -1406,63 +1406,63 @@ function PullRequestCodeTab({
14061406 {/* Above the code, closed, and counted: these belong to the change rather than to any
14071407 line of it, and in the stream they read as cards dropped into the patch. */}
14081408 {orphanFiles.size > 0 ? (
1409 <Collapsible
1410 className="shrink-0 border-b border-border/60"
1411 open={orphansOpen}
1412 onOpenChange={setOrphansOpen}
1413 >
1414 {/* Still a heading, so the section keeps its place in a screen reader's outline;
1409 <div className="shrink-0 border-b border-border/60">
1410 <Collapsible open={orphansOpen} onOpenChange={setOrphansOpen}>
1411 {/* Still a heading, so the section keeps its place in a screen reader's outline;
14151412 the count is spelled out there rather than left as a bare number. */}
1416 <h2>
1417 <CollapsibleTrigger className="flex w-full items-center gap-1.5 px-4 py-2 text-left text-xs text-muted-foreground">
1418 {/* While slices are still arriving a conversation may simply belong to a file
1413 <h2>
1414 <CollapsibleTrigger className="flex w-full items-center gap-1.5 px-4 py-2 text-left text-xs text-muted-foreground">
1415 {/* While slices are still arriving a conversation may simply belong to a file
14191416 that has not landed yet, which is not the same as being off the diff. */}
1420 <span>
1421 {nextCursor === null
1422 ? "Conversations not on the current diff"
1423 : "Conversations not on the diff loaded so far"}
1424 </span>
1425 <ChevronRightIcon
1426 aria-hidden
1427 className={cn("size-3.5 transition-transform", orphansOpen && "rotate-90")}
1428 />
1429 <span aria-hidden className="tabular-nums">
1430 {orphanThreads.length}
1431 </span>
1432 <span className="sr-only">
1433 {orphanThreads.length === 1
1434 ? "1 conversation"
1435 : `${orphanThreads.length} conversations`}
1436 </span>
1437 </CollapsibleTrigger>
1438 </h2>
1439 <CollapsiblePanel>
1440 {/* Capped: opened on a change with dozens of them, this would otherwise leave no
1417 <span>
1418 {nextCursor === null
1419 ? "Conversations not on the current diff"
1420 : "Conversations not on the diff loaded so far"}
1421 </span>
1422 <ChevronRightIcon
1423 aria-hidden
1424 className={cn("size-3.5 transition-transform", orphansOpen && "rotate-90")}
1425 />
1426 <span aria-hidden className="tabular-nums">
1427 {orphanThreads.length}
1428 </span>
1429 <span className="sr-only">
1430 {orphanThreads.length === 1
1431 ? "1 conversation"
1432 : `${orphanThreads.length} conversations`}
1433 </span>
1434 </CollapsibleTrigger>
1435 </h2>
1436 <CollapsiblePanel>
1437 {/* Capped: opened on a change with dozens of them, this would otherwise leave no
14411438 room for the diff it sits above. */}
1442 <div className="max-h-64 space-y-3 overflow-auto px-4 pb-3">
1443 {[...orphanFiles].map(([path, threads]) => (
1444 <div key={path}>
1445 <Tooltip>
1446 <TooltipTrigger
1447 render={<p className="truncate px-3 text-xs text-muted-foreground">{path}</p>}
1448 />
1449 <TooltipPopup side="top">{path}</TooltipPopup>
1450 </Tooltip>
1451 <div className="mt-1 space-y-2">
1452 {threads.map((thread) => (
1453 <div key={thread.id}>
1454 {thread.line === null ? null : (
1455 <p className="px-3 text-xs text-muted-foreground">Line {thread.line}</p>
1456 )}
1457 {renderThreadCard(thread)}
1458 </div>
1459 ))}
1439 <div className="max-h-64 space-y-3 overflow-auto px-4 pb-3">
1440 {[...orphanFiles].map(([path, threads]) => (
1441 <div key={path}>
1442 <Tooltip>
1443 <TooltipTrigger
1444 render={
1445 <p className="truncate px-3 text-xs text-muted-foreground">{path}</p>
1446 }
1447 />
1448 <TooltipPopup side="top">{path}</TooltipPopup>
1449 </Tooltip>
1450 <div className="mt-1 space-y-2">
1451 {threads.map((thread) => (
1452 <div key={thread.id}>
1453 {thread.line === null ? null : (
1454 <p className="px-3 text-xs text-muted-foreground">Line {thread.line}</p>
1455 )}
1456 {renderThreadCard(thread)}
1457 </div>
1458 ))}
1459 </div>
14601460 </div>
1461 </div>
1462 ))}
1463 </div>
1464 </CollapsiblePanel>
1465 </Collapsible>
1461 ))}
1462 </div>
1463 </CollapsiblePanel>
1464 </Collapsible>
1465 </div>
14661466 ) : null}
14671467 <div className="flex min-h-0 flex-1 overflow-hidden">
14681468 {/* Relative wrapper so the review overlay floats over the diff rather than pushing it
apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx+63-64
......@@ -378,74 +378,73 @@ function CommentGroup({
378378 null,
379379 );
380380 return (
381 <Collapsible
382 className="overflow-hidden rounded-lg border border-border/70 bg-muted/20"
383 onOpenChange={onOpenChange}
384 >
385 <div className="flex items-center gap-3 pl-3">
386 <div className="flex shrink-0 -space-x-1.5">
387 {authors.slice(0, 3).map((actor) => (
388 <PullRequestActorLabel
389 key={actor?.login ?? "ghost"}
390 actor={actor}
391 profileUrl={
392 detail.provider === "github" && actor
393 ? new URL(
394 actor.isBot || actor.login.endsWith("[bot]")
395 ? `/apps/${encodeURIComponent(actor.login.replace(/\[bot\]$/, ""))}`
396 : `/${encodeURIComponent(actor.login)}`,
397 detail.url,
398 ).toString()
399 : null
400 }
401 labelClassName="sr-only"
402 className="relative rounded-full bg-background ring-2 ring-background hover:z-10 focus-visible:z-10 [&>img]:size-6 [&>span:first-child]:size-6"
403 />
404 ))}
405 {authors.length > 3 ? (
406 <span className="relative flex size-6 items-center justify-center rounded-full bg-muted text-[10px] text-muted-foreground ring-2 ring-background">
407 +{authors.length - 3}
408 </span>
409 ) : null}
410 </div>
411 <CollapsibleTrigger
412 aria-label={label}
413 className="group flex min-w-0 flex-1 items-center gap-3 rounded-md py-3 pr-3 text-left hover:bg-muted/30"
414 >
415 <span className="min-w-0 flex-1 space-y-1">
416 <span className="block text-xs font-medium text-foreground/90">{label}</span>
417 <span className="flex flex-wrap gap-x-1.5 text-[11px] text-muted-foreground">
418 <span>
419 {authors.length} {authors.length === 1 ? "author" : "authors"}
381 <div className="overflow-hidden rounded-lg border border-border bg-background">
382 <Collapsible onOpenChange={onOpenChange}>
383 <div className="flex items-center gap-3 pl-3">
384 <div className="flex shrink-0 -space-x-1.5">
385 {authors.slice(0, 3).map((actor) => (
386 <PullRequestActorLabel
387 key={actor?.login ?? "ghost"}
388 actor={actor}
389 profileUrl={
390 detail.provider === "github" && actor
391 ? new URL(
392 actor.isBot || actor.login.endsWith("[bot]")
393 ? `/apps/${encodeURIComponent(actor.login.replace(/\[bot\]$/, ""))}`
394 : `/${encodeURIComponent(actor.login)}`,
395 detail.url,
396 ).toString()
397 : null
398 }
399 labelClassName="sr-only"
400 className="relative rounded-full bg-background ring-2 ring-background hover:z-10 focus-visible:z-10 [&>img]:size-6 [&>span:first-child]:size-6"
401 />
402 ))}
403 {authors.length > 3 ? (
404 <span className="relative flex size-6 items-center justify-center rounded-full bg-muted text-[10px] text-muted-foreground ring-2 ring-background">
405 +{authors.length - 3}
420406 </span>
421 {fileCount > 0 ? (
422 <span>
423 · {fileCount} {fileCount === 1 ? "file" : "files"}
424 </span>
425 ) : null}
426 {latest ? (
407 ) : null}
408 </div>
409 <CollapsibleTrigger
410 aria-label={label}
411 className="group flex min-w-0 flex-1 items-center gap-3 rounded-md py-3 pr-3 text-left hover:bg-muted/30"
412 >
413 <span className="min-w-0 flex-1 space-y-1">
414 <span className="block text-xs font-medium text-foreground/90">{label}</span>
415 <span className="flex flex-wrap gap-x-1.5 text-[11px] text-muted-foreground">
427416 <span>
428 · Latest{" "}
429 <Tooltip>
430 <TooltipTrigger render={<time dateTime={latest} />}>
431 {formatRelativeTimeLabel(latest)}
432 </TooltipTrigger>
433 <TooltipPopup>{new Date(latest).toLocaleString()}</TooltipPopup>
434 </Tooltip>
417 {authors.length} {authors.length === 1 ? "author" : "authors"}
435418 </span>
436 ) : null}
419 {fileCount > 0 ? (
420 <span>
421 · {fileCount} {fileCount === 1 ? "file" : "files"}
422 </span>
423 ) : null}
424 {latest ? (
425 <span>
426 · Latest{" "}
427 <Tooltip>
428 <TooltipTrigger render={<time dateTime={latest} />}>
429 {formatRelativeTimeLabel(latest)}
430 </TooltipTrigger>
431 <TooltipPopup>{new Date(latest).toLocaleString()}</TooltipPopup>
432 </Tooltip>
433 </span>
434 ) : null}
435 </span>
437436 </span>
438 </span>
439 <ChevronRightIcon
440 aria-hidden
441 className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-panel-open:rotate-90"
442 />
443 </CollapsibleTrigger>
444 </div>
445 <CollapsiblePanel keepMounted>
446 <div className="border-t border-border/60 px-3 pb-3">{children}</div>
447 </CollapsiblePanel>
448 </Collapsible>
437 <ChevronRightIcon
438 aria-hidden
439 className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-panel-open:rotate-90"
440 />
441 </CollapsibleTrigger>
442 </div>
443 <CollapsiblePanel keepMounted>
444 <div className="border-t border-border/60 px-3 pb-3">{children}</div>
445 </CollapsiblePanel>
446 </Collapsible>
447 </div>
449448 );
450449}
451450
scripts/lint-restyle-ceiling.ts+1-1
......@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
88// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
99// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
1010// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 793;
11export const RESTYLE_CEILING = 776;
1212
1313const RULE = "shadcn(no-restyle)";
1414