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() {...@@ -12,10 +12,10 @@ export function NoProjectsHero() {
12 return (12 return (
13 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none">13 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none">
14 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">14 <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">
16 <div className="w-full max-w-lg px-8 py-12">16 <div className="w-full max-w-lg px-8 py-12">
17 <EmptyHeader className="max-w-none">17 <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>
19 <EmptyDescription>Add a project to start your first thread.</EmptyDescription>19 <EmptyDescription>Add a project to start your first thread.</EmptyDescription>
20 <div className="mt-6 flex justify-center">20 <div className="mt-6 flex justify-center">
21 <Button size="sm" onClick={openAddProject}>21 <Button size="sm" onClick={openAddProject}>
apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx+7-5
...@@ -87,13 +87,13 @@ function MobileClientsSkeleton() {...@@ -87,13 +87,13 @@ function MobileClientsSkeleton() {
8787
88function EmptyMobileClients() {88function EmptyMobileClients() {
89 return (89 return (
90 <Empty className="min-h-64 gap-4 border-t px-6 py-10 md:p-10">90 <Empty size="compact">
91 <EmptyMedia className="mb-0" variant="icon">91 <EmptyMedia variant="icon">
92 <SmartphoneIcon />92 <SmartphoneIcon />
93 </EmptyMedia>93 </EmptyMedia>
94 <EmptyHeader>94 <EmptyHeader>
95 <EmptyTitle className="text-[1.0625rem] leading-6">No mobile clients</EmptyTitle>95 <EmptyTitle>No mobile clients</EmptyTitle>
96 <EmptyDescription className="text-[0.8125rem] leading-[1.125rem]">96 <EmptyDescription>
97 Sign in to T3 Code on your iPhone to register it for push notifications and Live97 Sign in to T3 Code on your iPhone to register it for push notifications and Live
98 Activities.98 Activities.
99 </EmptyDescription>99 </EmptyDescription>
...@@ -147,7 +147,9 @@ export function MobileClientsUserProfilePage() {...@@ -147,7 +147,9 @@ export function MobileClientsUserProfilePage() {
147 ))}147 ))}
148 </ul>148 </ul>
149 ) : (149 ) : (
150 <EmptyMobileClients />150 <div className="border-t">
151 <EmptyMobileClients />
152 </div>
151 )}153 )}
152 </div>154 </div>
153 </ClerkUserProfilePage>155 </ClerkUserProfilePage>
apps/web/src/components/clerk/T3ConnectUserProfilePage.tsx+14-13
...@@ -233,19 +233,20 @@ export function T3ConnectUserProfilePage() {...@@ -233,19 +233,20 @@ export function T3ConnectUserProfilePage() {
233 ))}233 ))}
234 </ul>234 </ul>
235 ) : environmentsState.error ? null : (235 ) : environmentsState.error ? null : (
236 <Empty className="min-h-64 gap-4 border-t px-6 py-10 md:p-10">236 <div className="border-t">
237 <EmptyMedia className="mb-0" variant="icon">237 <Empty size="compact">
238 <ServerIcon />238 <EmptyMedia variant="icon">
239 </EmptyMedia>239 <ServerIcon />
240 <EmptyHeader>240 </EmptyMedia>
241 <EmptyTitle className="text-[1.0625rem] leading-6">241 <EmptyHeader>
242 No T3 Connect environments242 <EmptyTitle>No T3 Connect environments</EmptyTitle>
243 </EmptyTitle>243 <EmptyDescription>
244 <EmptyDescription className="text-[0.8125rem] leading-[1.125rem]">244 Link an environment from its local Settings to make it available through T3
245 Link an environment from its local Settings to make it available through T3 Connect.245 Connect.
246 </EmptyDescription>246 </EmptyDescription>
247 </EmptyHeader>247 </EmptyHeader>
248 </Empty>248 </Empty>
249 </div>
249 )}250 )}
250 </div>251 </div>
251 </ClerkUserProfilePage>252 </ClerkUserProfilePage>
apps/web/src/components/pullRequest/PullRequestListEmptyState.tsx+7-7
...@@ -100,7 +100,7 @@ export function PullRequestListEmptyState({...@@ -100,7 +100,7 @@ export function PullRequestListEmptyState({
100 // Ahead of the search and the filters, because neither can produce a row until a project does.100 // Ahead of the search and the filters, because neither can produce a row until a project does.
101 if (!hasProjects) {101 if (!hasProjects) {
102 return (102 return (
103 <Empty className="py-16">103 <Empty>
104 <BranchMark joined={false} />104 <BranchMark joined={false} />
105 <EmptyHeader>105 <EmptyHeader>
106 <EmptyTitle>No projects in this workspace</EmptyTitle>106 <EmptyTitle>No projects in this workspace</EmptyTitle>
...@@ -131,7 +131,7 @@ export function PullRequestListEmptyState({...@@ -131,7 +131,7 @@ export function PullRequestListEmptyState({
131131
132 if (query.length > 0) {132 if (query.length > 0) {
133 return (133 return (
134 <Empty className="py-16">134 <Empty>
135 <BranchMark joined={false} />135 <BranchMark joined={false} />
136 <EmptyHeader>136 <EmptyHeader>
137 {/* A pasted paragraph is still a search, but it is not a title. */}137 {/* A pasted paragraph is still a search, but it is not a title. */}
...@@ -142,7 +142,7 @@ export function PullRequestListEmptyState({...@@ -142,7 +142,7 @@ export function PullRequestListEmptyState({
142 The hosts were searched for it. Try fewer words, or search by number, author or branch.142 The hosts were searched for it. Try fewer words, or search by number, author or branch.
143 </EmptyDescription>143 </EmptyDescription>
144 </EmptyHeader>144 </EmptyHeader>
145 <EmptyContent className="flex-row flex-wrap justify-center gap-2">145 <div className="flex flex-wrap justify-center gap-2">
146 <Button size="sm" variant="outline" onClick={onClearQuery}>146 <Button size="sm" variant="outline" onClick={onClearQuery}>
147 <SearchIcon className="size-3.5" />147 <SearchIcon className="size-3.5" />
148 Clear search148 Clear search
...@@ -153,13 +153,13 @@ export function PullRequestListEmptyState({...@@ -153,13 +153,13 @@ export function PullRequestListEmptyState({
153 <RefreshIcon size="sm" refreshing={refreshing} />153 <RefreshIcon size="sm" refreshing={refreshing} />
154 {refreshing ? "Checking..." : "Check again"}154 {refreshing ? "Checking..." : "Check again"}
155 </Button>155 </Button>
156 </EmptyContent>156 </div>
157 </Empty>157 </Empty>
158 );158 );
159 }159 }
160160
161 return (161 return (
162 <Empty className="py-16">162 <Empty>
163 <BranchMark joined={false} />163 <BranchMark joined={false} />
164 <EmptyHeader>164 <EmptyHeader>
165 <EmptyTitle>{filtered ? "Nothing under these filters" : "No pull requests"}</EmptyTitle>165 <EmptyTitle>{filtered ? "Nothing under these filters" : "No pull requests"}</EmptyTitle>
...@@ -169,7 +169,7 @@ export function PullRequestListEmptyState({...@@ -169,7 +169,7 @@ export function PullRequestListEmptyState({
169 : "Pull requests from every project in this workspace appear here."}169 : "Pull requests from every project in this workspace appear here."}
170 </EmptyDescription>170 </EmptyDescription>
171 </EmptyHeader>171 </EmptyHeader>
172 <EmptyContent className="flex-row flex-wrap justify-center gap-2">172 <div className="flex flex-wrap justify-center gap-2">
173 {canLoadMore ? (173 {canLoadMore ? (
174 <Button size="sm" variant="outline" disabled={loadingMore} onClick={onLoadMore}>174 <Button size="sm" variant="outline" disabled={loadingMore} onClick={onLoadMore}>
175 {loadingMore ? "Loading..." : "Load more pull requests"}175 {loadingMore ? "Loading..." : "Load more pull requests"}
...@@ -179,7 +179,7 @@ export function PullRequestListEmptyState({...@@ -179,7 +179,7 @@ export function PullRequestListEmptyState({
179 <RefreshIcon size="sm" refreshing={refreshing} />179 <RefreshIcon size="sm" refreshing={refreshing} />
180 {refreshing ? "Checking..." : "Check again"}180 {refreshing ? "Checking..." : "Check again"}
181 </Button>181 </Button>
182 </EmptyContent>182 </div>
183 </Empty>183 </Empty>
184 );184 );
185}185}
apps/web/src/components/pullRequest/PullRequestsUnavailableState.tsx+4-11
...@@ -2,14 +2,7 @@ import { RefreshIcon } from "~/components/ui/refresh-icon";...@@ -2,14 +2,7 @@ import { RefreshIcon } from "~/components/ui/refresh-icon";
2import { ExternalLinkIcon } from "lucide-react";2import { ExternalLinkIcon } from "lucide-react";
33
4import { Button } from "../ui/button";4import { Button } from "../ui/button";
5import {5import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
6 Empty,
7 EmptyContent,
8 EmptyDescription,
9 EmptyHeader,
10 EmptyMedia,
11 EmptyTitle,
12} from "../ui/empty";
13import { PullRequestGlyph } from "./pullRequestIcons";6import { PullRequestGlyph } from "./pullRequestIcons";
147
15export function PullRequestsUnavailableState({8export function PullRequestsUnavailableState({
...@@ -26,7 +19,7 @@ export function PullRequestsUnavailableState({...@@ -26,7 +19,7 @@ export function PullRequestsUnavailableState({
26 gitHubUrl?: string;19 gitHubUrl?: string;
27}) {20}) {
28 return (21 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">
30 <EmptyMedia variant="icon">23 <EmptyMedia variant="icon">
31 <PullRequestGlyph.pullRequest />24 <PullRequestGlyph.pullRequest />
32 </EmptyMedia>25 </EmptyMedia>
...@@ -37,7 +30,7 @@ export function PullRequestsUnavailableState({...@@ -37,7 +30,7 @@ export function PullRequestsUnavailableState({
37 <EmptyDescription>{error}</EmptyDescription>30 <EmptyDescription>{error}</EmptyDescription>
38 </EmptyHeader>31 </EmptyHeader>
39 {onRetry || gitHubUrl ? (32 {onRetry || gitHubUrl ? (
40 <EmptyContent className="flex-row flex-wrap justify-center gap-2">33 <div className="flex flex-wrap justify-center gap-2">
41 {onRetry ? (34 {onRetry ? (
42 <Button35 <Button
43 size="sm"36 size="sm"
...@@ -60,7 +53,7 @@ export function PullRequestsUnavailableState({...@@ -60,7 +53,7 @@ export function PullRequestsUnavailableState({
60 Open on GitHub53 Open on GitHub
61 </Button>54 </Button>
62 ) : null}55 ) : null}
63 </EmptyContent>56 </div>
64 ) : null}57 ) : null}
65 </Empty>58 </Empty>
66 );59 );
apps/web/src/components/settings/ProviderSettingsPanel.tsx+1-1
...@@ -203,7 +203,7 @@ function ProviderSettingsPlaceholder({...@@ -203,7 +203,7 @@ function ProviderSettingsPlaceholder({
203 divided={false}203 divided={false}
204 className={cn(providerCardHeightClassName, "flex overflow-x-hidden overflow-y-auto")}204 className={cn(providerCardHeightClassName, "flex overflow-x-hidden overflow-y-auto")}
205 >205 >
206 <Empty className="min-h-88">206 <Empty>
207 <EmptyMedia variant="icon">{icon}</EmptyMedia>207 <EmptyMedia variant="icon">{icon}</EmptyMedia>
208 <EmptyHeader>208 <EmptyHeader>
209 <EmptyTitle>{title}</EmptyTitle>209 <EmptyTitle>{title}</EmptyTitle>
apps/web/src/components/settings/SourceControlSettings.tsx+1-1
...@@ -476,7 +476,7 @@ function EmptySourceControlDiscovery({...@@ -476,7 +476,7 @@ function EmptySourceControlDiscovery({
476476
477 return (477 return (
478 <SettingsSection id={searchableSetting("source-control").id} title="Server environment">478 <SettingsSection id={searchableSetting("source-control").id} title="Server environment">
479 <Empty className="min-h-88">479 <Empty>
480 <EmptyMedia variant="icon">480 <EmptyMedia variant="icon">
481 <PullRequestGlyph.pullRequest />481 <PullRequestGlyph.pullRequest />
482 </EmptyMedia>482 </EmptyMedia>
apps/web/src/components/ui/empty.tsx+16-2
...@@ -2,11 +2,25 @@ import { cva, type VariantProps } from "class-variance-authority";...@@ -2,11 +2,25 @@ import { cva, type VariantProps } from "class-variance-authority";
22
3import { cn } from "~/lib/utils";3import { 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 }) {
6 return (19 return (
7 <div20 <div
8 className={cn(21 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],
10 className,24 className,
11 )}25 )}
12 data-slot="empty"26 data-slot="empty"
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 = 766;11export const RESTYLE_CEILING = 737;
1212
13const RULE = "shadcn(no-restyle)";13const RULE = "shadcn(no-restyle)";
1414