authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 12:22:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 12:22:19-07:00
log0ee02ebf228769beec4b240db6254ba2bad565af
treee9e83aa3ab7085db62516f26b450247ff31f6078
parent0fa4a859f6992be80302e2a7508e28b9b64e4991
signature Signed by PGP key B5690EEEBB952194

refactor(web): Empty has three sizes (#13040)

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

9 files changed, 53 insertions(+), 43 deletions(-)

apps/web/src/components/NoProjectsHero.tsx+2-2
......@@ -12,10 +12,10 @@ export function NoProjectsHero() {
1212 return (
1313 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none">
1414 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
15 <Empty className="flex-1">
15 <Empty size="hero" className="flex-1">
1616 <div className="w-full max-w-lg px-8 py-12">
1717 <EmptyHeader className="max-w-none">
18 <EmptyTitle className="text-2xl sm:text-3xl">What should we work on?</EmptyTitle>
18 <EmptyTitle>What should we work on?</EmptyTitle>
1919 <EmptyDescription>Add a project to start your first thread.</EmptyDescription>
2020 <div className="mt-6 flex justify-center">
2121 <Button size="sm" onClick={openAddProject}>
apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx+7-5
......@@ -87,13 +87,13 @@ function MobileClientsSkeleton() {
8787
8888function EmptyMobileClients() {
8989 return (
90 <Empty className="min-h-64 gap-4 border-t px-6 py-10 md:p-10">
91 <EmptyMedia className="mb-0" variant="icon">
90 <Empty size="compact">
91 <EmptyMedia variant="icon">
9292 <SmartphoneIcon />
9393 </EmptyMedia>
9494 <EmptyHeader>
95 <EmptyTitle className="text-[1.0625rem] leading-6">No mobile clients</EmptyTitle>
96 <EmptyDescription className="text-[0.8125rem] leading-[1.125rem]">
95 <EmptyTitle>No mobile clients</EmptyTitle>
96 <EmptyDescription>
9797 Sign in to T3 Code on your iPhone to register it for push notifications and Live
9898 Activities.
9999 </EmptyDescription>
......@@ -147,7 +147,9 @@ export function MobileClientsUserProfilePage() {
147147 ))}
148148 </ul>
149149 ) : (
150 <EmptyMobileClients />
150 <div className="border-t">
151 <EmptyMobileClients />
152 </div>
151153 )}
152154 </div>
153155 </ClerkUserProfilePage>
apps/web/src/components/clerk/T3ConnectUserProfilePage.tsx+14-13
......@@ -233,19 +233,20 @@ export function T3ConnectUserProfilePage() {
233233 ))}
234234 </ul>
235235 ) : environmentsState.error ? null : (
236 <Empty className="min-h-64 gap-4 border-t px-6 py-10 md:p-10">
237 <EmptyMedia className="mb-0" variant="icon">
238 <ServerIcon />
239 </EmptyMedia>
240 <EmptyHeader>
241 <EmptyTitle className="text-[1.0625rem] leading-6">
242 No T3 Connect environments
243 </EmptyTitle>
244 <EmptyDescription className="text-[0.8125rem] leading-[1.125rem]">
245 Link an environment from its local Settings to make it available through T3 Connect.
246 </EmptyDescription>
247 </EmptyHeader>
248 </Empty>
236 <div className="border-t">
237 <Empty size="compact">
238 <EmptyMedia variant="icon">
239 <ServerIcon />
240 </EmptyMedia>
241 <EmptyHeader>
242 <EmptyTitle>No T3 Connect environments</EmptyTitle>
243 <EmptyDescription>
244 Link an environment from its local Settings to make it available through T3
245 Connect.
246 </EmptyDescription>
247 </EmptyHeader>
248 </Empty>
249 </div>
249250 )}
250251 </div>
251252 </ClerkUserProfilePage>
apps/web/src/components/pullRequest/PullRequestListEmptyState.tsx+7-7
......@@ -100,7 +100,7 @@ export function PullRequestListEmptyState({
100100 // Ahead of the search and the filters, because neither can produce a row until a project does.
101101 if (!hasProjects) {
102102 return (
103 <Empty className="py-16">
103 <Empty>
104104 <BranchMark joined={false} />
105105 <EmptyHeader>
106106 <EmptyTitle>No projects in this workspace</EmptyTitle>
......@@ -131,7 +131,7 @@ export function PullRequestListEmptyState({
131131
132132 if (query.length > 0) {
133133 return (
134 <Empty className="py-16">
134 <Empty>
135135 <BranchMark joined={false} />
136136 <EmptyHeader>
137137 {/* A pasted paragraph is still a search, but it is not a title. */}
......@@ -142,7 +142,7 @@ export function PullRequestListEmptyState({
142142 The hosts were searched for it. Try fewer words, or search by number, author or branch.
143143 </EmptyDescription>
144144 </EmptyHeader>
145 <EmptyContent className="flex-row flex-wrap justify-center gap-2">
145 <div className="flex flex-wrap justify-center gap-2">
146146 <Button size="sm" variant="outline" onClick={onClearQuery}>
147147 <SearchIcon className="size-3.5" />
148148 Clear search
......@@ -153,13 +153,13 @@ export function PullRequestListEmptyState({
153153 <RefreshIcon size="sm" refreshing={refreshing} />
154154 {refreshing ? "Checking..." : "Check again"}
155155 </Button>
156 </EmptyContent>
156 </div>
157157 </Empty>
158158 );
159159 }
160160
161161 return (
162 <Empty className="py-16">
162 <Empty>
163163 <BranchMark joined={false} />
164164 <EmptyHeader>
165165 <EmptyTitle>{filtered ? "Nothing under these filters" : "No pull requests"}</EmptyTitle>
......@@ -169,7 +169,7 @@ export function PullRequestListEmptyState({
169169 : "Pull requests from every project in this workspace appear here."}
170170 </EmptyDescription>
171171 </EmptyHeader>
172 <EmptyContent className="flex-row flex-wrap justify-center gap-2">
172 <div className="flex flex-wrap justify-center gap-2">
173173 {canLoadMore ? (
174174 <Button size="sm" variant="outline" disabled={loadingMore} onClick={onLoadMore}>
175175 {loadingMore ? "Loading..." : "Load more pull requests"}
......@@ -179,7 +179,7 @@ export function PullRequestListEmptyState({
179179 <RefreshIcon size="sm" refreshing={refreshing} />
180180 {refreshing ? "Checking..." : "Check again"}
181181 </Button>
182 </EmptyContent>
182 </div>
183183 </Empty>
184184 );
185185}
apps/web/src/components/pullRequest/PullRequestsUnavailableState.tsx+4-11
......@@ -2,14 +2,7 @@ import { RefreshIcon } from "~/components/ui/refresh-icon";
22import { ExternalLinkIcon } from "lucide-react";
33
44import { Button } from "../ui/button";
5import {
6 Empty,
7 EmptyContent,
8 EmptyDescription,
9 EmptyHeader,
10 EmptyMedia,
11 EmptyTitle,
12} from "../ui/empty";
5import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
136import { PullRequestGlyph } from "./pullRequestIcons";
147
158export function PullRequestsUnavailableState({
......@@ -26,7 +19,7 @@ export function PullRequestsUnavailableState({
2619 gitHubUrl?: string;
2720}) {
2821 return (
29 <Empty className="min-h-0 justify-center-safe overflow-y-auto px-4 py-16 md:px-4 [&>*]:shrink-0">
22 <Empty className="min-h-0 justify-center-safe overflow-y-auto [&>*]:shrink-0">
3023 <EmptyMedia variant="icon">
3124 <PullRequestGlyph.pullRequest />
3225 </EmptyMedia>
......@@ -37,7 +30,7 @@ export function PullRequestsUnavailableState({
3730 <EmptyDescription>{error}</EmptyDescription>
3831 </EmptyHeader>
3932 {onRetry || gitHubUrl ? (
40 <EmptyContent className="flex-row flex-wrap justify-center gap-2">
33 <div className="flex flex-wrap justify-center gap-2">
4134 {onRetry ? (
4235 <Button
4336 size="sm"
......@@ -60,7 +53,7 @@ export function PullRequestsUnavailableState({
6053 Open on GitHub
6154 </Button>
6255 ) : null}
63 </EmptyContent>
56 </div>
6457 ) : null}
6558 </Empty>
6659 );
apps/web/src/components/settings/ProviderSettingsPanel.tsx+1-1
......@@ -203,7 +203,7 @@ function ProviderSettingsPlaceholder({
203203 divided={false}
204204 className={cn(providerCardHeightClassName, "flex overflow-x-hidden overflow-y-auto")}
205205 >
206 <Empty className="min-h-88">
206 <Empty>
207207 <EmptyMedia variant="icon">{icon}</EmptyMedia>
208208 <EmptyHeader>
209209 <EmptyTitle>{title}</EmptyTitle>
apps/web/src/components/settings/SourceControlSettings.tsx+1-1
......@@ -476,7 +476,7 @@ function EmptySourceControlDiscovery({
476476
477477 return (
478478 <SettingsSection id={searchableSetting("source-control").id} title="Server environment">
479 <Empty className="min-h-88">
479 <Empty>
480480 <EmptyMedia variant="icon">
481481 <PullRequestGlyph.pullRequest />
482482 </EmptyMedia>
apps/web/src/components/ui/empty.tsx+16-2
......@@ -2,11 +2,25 @@ import { cva, type VariantProps } from "class-variance-authority";
22
33import { cn } from "~/lib/utils";
44
5function Empty({ className, ...props }: React.ComponentProps<"div">) {
5// Size sets how much room the state claims and how large its title reads:
6// "compact" is a card-sized notice, "hero" fills a whole route.
7const emptySizeClassName = {
8 compact:
9 "min-h-64 gap-4 p-6 md:p-10 [&_[data-slot=empty-media]]:mb-0 [&_[data-slot=empty-title]]:text-[1.0625rem] [&_[data-slot=empty-title]]:leading-6 [&_[data-slot=empty-description]]:text-[0.8125rem] [&_[data-slot=empty-description]]:leading-[1.125rem]",
10 default: "gap-6 p-6 md:p-12",
11 hero: "gap-6 p-6 md:p-12 [&_[data-slot=empty-title]]:text-2xl sm:[&_[data-slot=empty-title]]:text-3xl",
12} as const;
13
14function Empty({
15 className,
16 size = "default",
17 ...props
18}: React.ComponentProps<"div"> & { size?: keyof typeof emptySizeClassName }) {
619 return (
720 <div
821 className={cn(
9 "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance p-6 text-center md:p-12",
22 "flex min-w-0 flex-1 flex-col items-center justify-center text-balance text-center",
23 emptySizeClassName[size],
1024 className,
1125 )}
1226 data-slot="empty"
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 = 766;
11export const RESTYLE_CEILING = 737;
1212
1313const RULE = "shadcn(no-restyle)";
1414