| author | |
| committer | |
| log | 0ee02ebf228769beec4b240db6254ba2bad565af |
| tree | e9e83aa3ab7085db62516f26b450247ff31f6078 |
| parent | 0fa4a859f6992be80302e2a7508e28b9b64e4991 |
| signature | Signed by PGP key B5690EEEBB952194 |
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() { |
| 87 | 87 | ||
| 88 | function EmptyMobileClients() { | 88 | function 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 Live | 97 | 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 environments | 242 | <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({ |
| 131 | 131 | ||
| 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 search | 148 | 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 | } |
| 160 | 160 | ||
| 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"; |
| 2 | import { ExternalLinkIcon } from "lucide-react"; | 2 | import { ExternalLinkIcon } from "lucide-react"; |
| 3 | 3 | ||
| 4 | import { Button } from "../ui/button"; | 4 | import { Button } from "../ui/button"; |
| 5 | import { | 5 | import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty"; |
| 6 | Empty, | ||
| 7 | EmptyContent, | ||
| 8 | EmptyDescription, | ||
| 9 | EmptyHeader, | ||
| 10 | EmptyMedia, | ||
| 11 | EmptyTitle, | ||
| 12 | } from "../ui/empty"; | ||
| 13 | import { PullRequestGlyph } from "./pullRequestIcons"; | 6 | import { PullRequestGlyph } from "./pullRequestIcons"; |
| 14 | 7 | ||
| 15 | export function PullRequestsUnavailableState({ | 8 | export 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 | <Button | 35 | <Button |
| 43 | size="sm" | 36 | size="sm" |
| ... | @@ -60,7 +53,7 @@ export function PullRequestsUnavailableState({ | ... | @@ -60,7 +53,7 @@ export function PullRequestsUnavailableState({ |
| 60 | Open on GitHub | 53 | 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({ |
| 476 | 476 | ||
| 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"; |
| 2 | 2 | ||
| 3 | import { cn } from "~/lib/utils"; | 3 | import { cn } from "~/lib/utils"; |
| 4 | 4 | ||
| 5 | function 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. | ||
| 7 | const 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 | |||
| 14 | function Empty({ | ||
| 15 | className, | ||
| 16 | size = "default", | ||
| 17 | ...props | ||
| 18 | }: React.ComponentProps<"div"> & { size?: keyof typeof emptySizeClassName }) { | ||
| 6 | return ( | 19 | return ( |
| 7 | <div | 20 | <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 the | 8 | // 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 you | 9 | // 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. |
| 11 | export const RESTYLE_CEILING = 766; | 11 | export const RESTYLE_CEILING = 737; |
| 12 | 12 | ||
| 13 | const RULE = "shadcn(no-restyle)"; | 13 | const RULE = "shadcn(no-restyle)"; |
| 14 | 14 |