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({...@@ -844,32 +844,33 @@ function MarkdownDetails({
844 const content = childNodes.filter((_, index) => index !== summaryIndex);844 const content = childNodes.filter((_, index) => index !== summaryIndex);
845845
846 return (846 return (
847 <Collapsible847 <div className="my-2 border-y border-border/60">
848 defaultOpen={open}848 <Collapsible
849 onOpenChange={setIsOpen}849 defaultOpen={open}
850 className="chat-markdown-details my-2 border-y border-border/60"850 onOpenChange={setIsOpen}
851 data-markdown-details=""851 data-markdown-details=""
852 data-markdown-details-open={isOpen ? "true" : "false"}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=""
857 >853 >
858 <ChevronRightIcon854 <CollapsibleTrigger
859 className="size-4 shrink-0 text-muted-foreground transition-transform"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"
860 aria-hidden856 data-markdown-details-summary=""
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 >857 >
869 {content}858 <ChevronRightIcon
870 </div>859 className="size-4 shrink-0 text-muted-foreground transition-transform"
871 </CollapsiblePanel>860 aria-hidden
872 </Collapsible>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>
873 );874 );
874}875}
875876
apps/web/src/components/onboarding/WelcomeWizard.tsx+89-93
...@@ -350,40 +350,38 @@ function ConnectionStep({...@@ -350,40 +350,38 @@ function ConnectionStep({
350 onToggleEnvironment={onToggleEnvironment}350 onToggleEnvironment={onToggleEnvironment}
351 />351 />
352 ) : null}352 ) : null}
353 <Collapsible353 <div className="rounded-lg border border-border bg-background">
354 open={pairingOpen}354 <Collapsible open={pairingOpen} onOpenChange={setPairingOpen}>
355 onOpenChange={setPairingOpen}355 <CollapsibleTrigger
356 className="rounded-lg border border-border bg-background"356 disabled={isPairing}
357 >357 render={
358 <CollapsibleTrigger358 <Button
359 disabled={isPairing}359 variant="ghost"
360 render={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 <Button361 />
362 variant="ghost"362 }
363 className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto"363 >
364 />364 <LinkIcon className="size-4 text-muted-foreground" />
365 }365 <span className="flex-1">Add a computer</span>
366 >366 <ChevronRightIcon
367 <LinkIcon className="size-4 text-muted-foreground" />367 className={cn("size-4 text-muted-foreground", pairingOpen && "rotate-90")}
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 }}
383 />368 />
384 </div>369 </CollapsibleTrigger>
385 </CollapsiblePanel>370 <CollapsiblePanel>
386 </Collapsible>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>
387 </div>385 </div>
388 <div className="mt-6 flex items-center justify-end gap-3">386 <div className="mt-6 flex items-center justify-end gap-3">
389 <Button387 <Button
...@@ -419,69 +417,67 @@ function ConnectAccountOption({...@@ -419,69 +417,67 @@ function ConnectAccountOption({
419 const onDiscoveryReady = useCallback(() => setDiscoveryReady(true), []);417 const onDiscoveryReady = useCallback(() => setDiscoveryReady(true), []);
420418
421 return (419 return (
422 <Collapsible420 <div className="rounded-lg border border-border bg-background">
423 open={expanded && !!isSignedIn && discoveryReady}421 <Collapsible open={expanded && !!isSignedIn && discoveryReady} onOpenChange={setExpanded}>
424 onOpenChange={setExpanded}422 <CollapsibleTrigger
425 className="rounded-lg border border-border bg-background"423 disabled={disabled || !isLoaded}
426 >424 onClick={(event) => {
427 <CollapsibleTrigger425 if (!isSignedIn) {
428 disabled={disabled || !isLoaded}426 event.preventDefault();
429 onClick={(event) => {427 setExpanded(true);
430 if (!isSignedIn) {428 openAuthPrompt();
431 event.preventDefault();429 }
432 setExpanded(true);430 }}
433 openAuthPrompt();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 />
434 }436 }
435 }}437 >
436 render={438 <CloudIcon className="size-4 text-muted-foreground" />
437 <Button439 <span className="flex-1">T3 Connect</span>
438 variant="ghost"440 <span className="text-xs text-muted-foreground">
439 className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto"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")}
440 />451 />
441 }452 </CollapsibleTrigger>
442 >453 <CollapsiblePanel keepMounted>
443 <CloudIcon className="size-4 text-muted-foreground" />454 <div className="px-3 pb-3">
444 <span className="flex-1">T3 Connect</span>455 <div className="mb-3 space-y-1.5">
445 <span className="text-xs text-muted-foreground">456 {isSignedIn ? (
446 {!isLoaded457 <CloudEnvironmentConnectRows
447 ? "Loading sign-in…"458 primaryEnvironmentId={null}
448 : !isSignedIn459 savedEnvironments={environments}
449 ? "Sign in"460 showSavedEnvironments
450 : !discoveryReady461 onDiscoveryReady={onDiscoveryReady}
451 ? "Loading computers…"462 selection={{ selectedIds, onChange: onToggleEnvironment, autoSelectedComputers }}
452 : null}463 refreshWhileEmpty
453 </span>464 empty={
454 <ChevronRightIcon465 <p className="py-3 text-sm text-muted-foreground">No computers linked yet.</p>
455 className={cn("size-4 text-muted-foreground", expanded && isSignedIn && "rotate-90")}466 }
456 />467 />
457 </CollapsibleTrigger>468 ) : null}
458 <CollapsiblePanel keepMounted>469 </div>
459 <div className="px-3 pb-3">470 <p className="text-sm text-muted-foreground">
460 <div className="mb-3 space-y-1.5">471 Run this on each computer you want to connect.
461 {isSignedIn ? (472 </p>
462 <CloudEnvironmentConnectRows473 <CommandBlock command="npx t3 connect" className="mt-3" />
463 primaryEnvironmentId={null}474 <p className="mt-3 text-xs text-muted-foreground">
464 savedEnvironments={environments}475 Keep T3 Code running. Select the computers you want to set up above.
465 showSavedEnvironments476 </p>
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}
474 </div>477 </div>
475 <p className="text-sm text-muted-foreground">478 </CollapsiblePanel>
476 Run this on each computer you want to connect.479 </Collapsible>
477 </p>480 </div>
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>
485 );481 );
486}482}
487483
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+53-53
...@@ -1406,63 +1406,63 @@ function PullRequestCodeTab({...@@ -1406,63 +1406,63 @@ function PullRequestCodeTab({
1406 {/* Above the code, closed, and counted: these belong to the change rather than to any1406 {/* Above the code, closed, and counted: these belong to the change rather than to any
1407 line of it, and in the stream they read as cards dropped into the patch. */}1407 line of it, and in the stream they read as cards dropped into the patch. */}
1408 {orphanFiles.size > 0 ? (1408 {orphanFiles.size > 0 ? (
1409 <Collapsible1409 <div className="shrink-0 border-b border-border/60">
1410 className="shrink-0 border-b border-border/60"1410 <Collapsible open={orphansOpen} onOpenChange={setOrphansOpen}>
1411 open={orphansOpen}1411 {/* Still a heading, so the section keeps its place in a screen reader's outline;
1412 onOpenChange={setOrphansOpen}
1413 >
1414 {/* Still a heading, so the section keeps its place in a screen reader's outline;
1415 the count is spelled out there rather than left as a bare number. */}1412 the count is spelled out there rather than left as a bare number. */}
1416 <h2>1413 <h2>
1417 <CollapsibleTrigger className="flex w-full items-center gap-1.5 px-4 py-2 text-left text-xs text-muted-foreground">1414 <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 file1415 {/* While slices are still arriving a conversation may simply belong to a file
1419 that has not landed yet, which is not the same as being off the diff. */}1416 that has not landed yet, which is not the same as being off the diff. */}
1420 <span>1417 <span>
1421 {nextCursor === null1418 {nextCursor === null
1422 ? "Conversations not on the current diff"1419 ? "Conversations not on the current diff"
1423 : "Conversations not on the diff loaded so far"}1420 : "Conversations not on the diff loaded so far"}
1424 </span>1421 </span>
1425 <ChevronRightIcon1422 <ChevronRightIcon
1426 aria-hidden1423 aria-hidden
1427 className={cn("size-3.5 transition-transform", orphansOpen && "rotate-90")}1424 className={cn("size-3.5 transition-transform", orphansOpen && "rotate-90")}
1428 />1425 />
1429 <span aria-hidden className="tabular-nums">1426 <span aria-hidden className="tabular-nums">
1430 {orphanThreads.length}1427 {orphanThreads.length}
1431 </span>1428 </span>
1432 <span className="sr-only">1429 <span className="sr-only">
1433 {orphanThreads.length === 11430 {orphanThreads.length === 1
1434 ? "1 conversation"1431 ? "1 conversation"
1435 : `${orphanThreads.length} conversations`}1432 : `${orphanThreads.length} conversations`}
1436 </span>1433 </span>
1437 </CollapsibleTrigger>1434 </CollapsibleTrigger>
1438 </h2>1435 </h2>
1439 <CollapsiblePanel>1436 <CollapsiblePanel>
1440 {/* Capped: opened on a change with dozens of them, this would otherwise leave no1437 {/* Capped: opened on a change with dozens of them, this would otherwise leave no
1441 room for the diff it sits above. */}1438 room for the diff it sits above. */}
1442 <div className="max-h-64 space-y-3 overflow-auto px-4 pb-3">1439 <div className="max-h-64 space-y-3 overflow-auto px-4 pb-3">
1443 {[...orphanFiles].map(([path, threads]) => (1440 {[...orphanFiles].map(([path, threads]) => (
1444 <div key={path}>1441 <div key={path}>
1445 <Tooltip>1442 <Tooltip>
1446 <TooltipTrigger1443 <TooltipTrigger
1447 render={<p className="truncate px-3 text-xs text-muted-foreground">{path}</p>}1444 render={
1448 />1445 <p className="truncate px-3 text-xs text-muted-foreground">{path}</p>
1449 <TooltipPopup side="top">{path}</TooltipPopup>1446 }
1450 </Tooltip>1447 />
1451 <div className="mt-1 space-y-2">1448 <TooltipPopup side="top">{path}</TooltipPopup>
1452 {threads.map((thread) => (1449 </Tooltip>
1453 <div key={thread.id}>1450 <div className="mt-1 space-y-2">
1454 {thread.line === null ? null : (1451 {threads.map((thread) => (
1455 <p className="px-3 text-xs text-muted-foreground">Line {thread.line}</p>1452 <div key={thread.id}>
1456 )}1453 {thread.line === null ? null : (
1457 {renderThreadCard(thread)}1454 <p className="px-3 text-xs text-muted-foreground">Line {thread.line}</p>
1458 </div>1455 )}
1459 ))}1456 {renderThreadCard(thread)}
1457 </div>
1458 ))}
1459 </div>
1460 </div>1460 </div>
1461 </div>1461 ))}
1462 ))}1462 </div>
1463 </div>1463 </CollapsiblePanel>
1464 </CollapsiblePanel>1464 </Collapsible>
1465 </Collapsible>1465 </div>
1466 ) : null}1466 ) : null}
1467 <div className="flex min-h-0 flex-1 overflow-hidden">1467 <div className="flex min-h-0 flex-1 overflow-hidden">
1468 {/* Relative wrapper so the review overlay floats over the diff rather than pushing it1468 {/* 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({...@@ -378,74 +378,73 @@ function CommentGroup({
378 null,378 null,
379 );379 );
380 return (380 return (
381 <Collapsible381 <div className="overflow-hidden rounded-lg border border-border bg-background">
382 className="overflow-hidden rounded-lg border border-border/70 bg-muted/20"382 <Collapsible onOpenChange={onOpenChange}>
383 onOpenChange={onOpenChange}383 <div className="flex items-center gap-3 pl-3">
384 >384 <div className="flex shrink-0 -space-x-1.5">
385 <div className="flex items-center gap-3 pl-3">385 {authors.slice(0, 3).map((actor) => (
386 <div className="flex shrink-0 -space-x-1.5">386 <PullRequestActorLabel
387 {authors.slice(0, 3).map((actor) => (387 key={actor?.login ?? "ghost"}
388 <PullRequestActorLabel388 actor={actor}
389 key={actor?.login ?? "ghost"}389 profileUrl={
390 actor={actor}390 detail.provider === "github" && actor
391 profileUrl={391 ? new URL(
392 detail.provider === "github" && actor392 actor.isBot || actor.login.endsWith("[bot]")
393 ? new URL(393 ? `/apps/${encodeURIComponent(actor.login.replace(/\[bot\]$/, ""))}`
394 actor.isBot || actor.login.endsWith("[bot]")394 : `/${encodeURIComponent(actor.login)}`,
395 ? `/apps/${encodeURIComponent(actor.login.replace(/\[bot\]$/, ""))}`395 detail.url,
396 : `/${encodeURIComponent(actor.login)}`,396 ).toString()
397 detail.url,397 : null
398 ).toString()398 }
399 : null399 labelClassName="sr-only"
400 }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 labelClassName="sr-only"401 />
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"402 ))}
403 />403 {authors.length > 3 ? (
404 ))}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 ? (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"}
420 </span>406 </span>
421 {fileCount > 0 ? (407 ) : null}
422 <span>408 </div>
423 · {fileCount} {fileCount === 1 ? "file" : "files"}409 <CollapsibleTrigger
424 </span>410 aria-label={label}
425 ) : null}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"
426 {latest ? (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">
427 <span>416 <span>
428 · Latest{" "}417 {authors.length} {authors.length === 1 ? "author" : "authors"}
429 <Tooltip>
430 <TooltipTrigger render={<time dateTime={latest} />}>
431 {formatRelativeTimeLabel(latest)}
432 </TooltipTrigger>
433 <TooltipPopup>{new Date(latest).toLocaleString()}</TooltipPopup>
434 </Tooltip>
435 </span>418 </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>
437 </span>436 </span>
438 </span>437 <ChevronRightIcon
439 <ChevronRightIcon438 aria-hidden
440 aria-hidden439 className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-panel-open:rotate-90"
441 className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-panel-open:rotate-90"440 />
442 />441 </CollapsibleTrigger>
443 </CollapsibleTrigger>442 </div>
444 </div>443 <CollapsiblePanel keepMounted>
445 <CollapsiblePanel keepMounted>444 <div className="border-t border-border/60 px-3 pb-3">{children}</div>
446 <div className="border-t border-border/60 px-3 pb-3">{children}</div>445 </CollapsiblePanel>
447 </CollapsiblePanel>446 </Collapsible>
448 </Collapsible>447 </div>
449 );448 );
450}449}
451450
scripts/lint-restyle-ceiling.ts+1-1
...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
10// migrate a file, and delete this script when the rule becomes an error.10// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 793;11export const RESTYLE_CEILING = 776;
1212
13const RULE = "shadcn(no-restyle)";13const RULE = "shadcn(no-restyle)";
1414