mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-30 16:14:13 +00:00
feat: blocks (#3094)
This commit is contained in:
27
.github/ISSUE_TEMPLATE/block_request.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/block_request.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Request a block"
|
||||
description: "Request a new block for shadcn/ui."
|
||||
title: "[blocks]: "
|
||||
labels: ["area: blocks", "area: request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to create a block request! Please search open/closed requests before submitting, as the block or a similar one may have already been requested.
|
||||
|
||||
- type: textarea
|
||||
id: block-description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Tell us about your block request
|
||||
placeholder: "A dashboard for an e-commerce website showing sales, orders, and customers..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: block-example-url
|
||||
attributes:
|
||||
label: Example
|
||||
description: Link to an example of the block
|
||||
placeholder: ex. https://example.com
|
||||
validations:
|
||||
required: false
|
||||
File diff suppressed because it is too large
Load Diff
50
apps/www/actions/edit-in-v0.ts
Normal file
50
apps/www/actions/edit-in-v0.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
"use server"
|
||||
|
||||
import { track } from "@vercel/analytics/server"
|
||||
|
||||
const EDIT_IN_V0_SOURCE = "ui.shadcn.com"
|
||||
|
||||
export async function editInV0({
|
||||
name,
|
||||
description,
|
||||
style,
|
||||
code,
|
||||
}: {
|
||||
name: string
|
||||
description: string
|
||||
style: string
|
||||
code: string
|
||||
}) {
|
||||
try {
|
||||
await track("edit_in_v0", {
|
||||
name,
|
||||
description,
|
||||
style,
|
||||
})
|
||||
|
||||
const response = await fetch(`${process.env.V0_URL}/api/edit`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ description, code, source: EDIT_IN_V0_SOURCE }),
|
||||
headers: {
|
||||
"x-v0-edit-secret": process.env.V0_EDIT_SECRET!,
|
||||
"x-vercel-protection-bypass":
|
||||
process.env.DEPLOYMENT_PROTECTION_BYPASS || "not-set",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error("Unauthorized")
|
||||
}
|
||||
|
||||
throw new Error("Something went wrong. Please try again later.")
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return { error: error.message }
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/www/app/(app)/blocks/layout.tsx
Normal file
52
apps/www/app/(app)/blocks/layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Metadata } from "next"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import {
|
||||
PageActions,
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Building Blocks.",
|
||||
description:
|
||||
"Beautifully designed. Copy and paste into your apps. Open Source.",
|
||||
}
|
||||
|
||||
export default function BlocksLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="container relative">
|
||||
<PageHeader className="max-w-3xl">
|
||||
<Announcement />
|
||||
<PageHeaderHeading className="text-balance">
|
||||
Building Blocks for the Web
|
||||
</PageHeaderHeading>
|
||||
<PageHeaderDescription>
|
||||
Beautifully designed. Copy and paste into your apps. Open Source.
|
||||
</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild>
|
||||
<a href="#blocks">Browse</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<a
|
||||
href="https://github.com/shadcn-ui/ui/issues/new/choose"
|
||||
target="_blank"
|
||||
>
|
||||
Request a block
|
||||
</a>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<section id="blocks" className="grid scroll-mt-24 gap-24 lg:gap-48">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
apps/www/app/(app)/blocks/page.tsx
Normal file
10
apps/www/app/(app)/blocks/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getAllBlockIds } from "@/lib/blocks"
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
|
||||
export default async function BlocksPage() {
|
||||
const blocks = await getAllBlockIds()
|
||||
|
||||
return blocks.map((name, index) => (
|
||||
<BlockDisplay key={`${name}-${index}`} name={name} />
|
||||
))
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/registry/new-york/ui/button"
|
||||
import { UserAuthForm } from "@/app/examples/authentication/components/user-auth-form"
|
||||
import { UserAuthForm } from "@/app/(app)/examples/authentication/components/user-auth-form"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Authentication",
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york/ui/tabs"
|
||||
import { CalendarDateRangePicker } from "@/app/examples/dashboard/components/date-range-picker"
|
||||
import { MainNav } from "@/app/examples/dashboard/components/main-nav"
|
||||
import { Overview } from "@/app/examples/dashboard/components/overview"
|
||||
import { RecentSales } from "@/app/examples/dashboard/components/recent-sales"
|
||||
import { Search } from "@/app/examples/dashboard/components/search"
|
||||
import TeamSwitcher from "@/app/examples/dashboard/components/team-switcher"
|
||||
import { UserNav } from "@/app/examples/dashboard/components/user-nav"
|
||||
import { CalendarDateRangePicker } from "@/app/(app)/examples/dashboard/components/date-range-picker"
|
||||
import { MainNav } from "@/app/(app)/examples/dashboard/components/main-nav"
|
||||
import { Overview } from "@/app/(app)/examples/dashboard/components/overview"
|
||||
import { RecentSales } from "@/app/(app)/examples/dashboard/components/recent-sales"
|
||||
import { Search } from "@/app/(app)/examples/dashboard/components/search"
|
||||
import TeamSwitcher from "@/app/(app)/examples/dashboard/components/team-switcher"
|
||||
import { UserNav } from "@/app/(app)/examples/dashboard/components/user-nav"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashboard",
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { AccountForm } from "@/app/examples/forms/account/account-form"
|
||||
import { AccountForm } from "@/app/(app)/examples/forms/account/account-form"
|
||||
|
||||
export default function SettingsAccountPage() {
|
||||
return (
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { AppearanceForm } from "@/app/examples/forms/appearance/appearance-form"
|
||||
import { AppearanceForm } from "@/app/(app)/examples/forms/appearance/appearance-form"
|
||||
|
||||
export default function SettingsAppearancePage() {
|
||||
return (
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { DisplayForm } from "@/app/examples/forms/display/display-form"
|
||||
import { DisplayForm } from "@/app/(app)/examples/forms/display/display-form"
|
||||
|
||||
export default function SettingsDisplayPage() {
|
||||
return (
|
||||
@@ -2,7 +2,7 @@ import { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { SidebarNav } from "@/app/examples/forms/components/sidebar-nav"
|
||||
import { SidebarNav } from "@/app/(app)/examples/forms/components/sidebar-nav"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Forms",
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { NotificationsForm } from "@/app/examples/forms/notifications/notifications-form"
|
||||
import { NotificationsForm } from "@/app/(app)/examples/forms/notifications/notifications-form"
|
||||
|
||||
export default function SettingsNotificationsPage() {
|
||||
return (
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { ProfileForm } from "@/app/examples/forms/profile-form"
|
||||
import { ProfileForm } from "@/app/(app)/examples/forms/profile-form"
|
||||
|
||||
export default function SettingsProfilePage() {
|
||||
return (
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york/ui/tooltip"
|
||||
import { Mail } from "@/app/examples/mail/data"
|
||||
import { Mail } from "@/app/(app)/examples/mail/data"
|
||||
|
||||
interface MailDisplayProps {
|
||||
mail: Mail | null
|
||||
@@ -5,8 +5,8 @@ import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/registry/new-york/ui/badge"
|
||||
import { ScrollArea } from "@/registry/new-york/ui/scroll-area"
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { Mail } from "@/app/examples/mail/data"
|
||||
import { useMail } from "@/app/examples/mail/use-mail"
|
||||
import { Mail } from "@/app/(app)/examples/mail/data"
|
||||
import { useMail } from "@/app/(app)/examples/mail/use-mail"
|
||||
|
||||
interface MailListProps {
|
||||
items: Mail[]
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
File,
|
||||
Inbox,
|
||||
MessagesSquare,
|
||||
PenBox,
|
||||
Search,
|
||||
Send,
|
||||
ShoppingCart,
|
||||
@@ -15,15 +15,14 @@ import {
|
||||
Users2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { AccountSwitcher } from "@/app/examples/mail/components/account-switcher"
|
||||
import { MailDisplay } from "@/app/examples/mail/components/mail-display"
|
||||
import { MailList } from "@/app/examples/mail/components/mail-list"
|
||||
import { Nav } from "@/app/examples/mail/components/nav"
|
||||
import { Mail } from "@/app/examples/mail/data"
|
||||
import { useMail } from "@/app/examples/mail/use-mail"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import { Input } from "@/registry/new-york/ui/input"
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/registry/new-york/ui/resizable"
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@@ -31,7 +30,12 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york/ui/tabs"
|
||||
import { TooltipProvider } from "@/registry/new-york/ui/tooltip"
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/registry/new-york/ui/resizable"
|
||||
import { AccountSwitcher } from "@/app/(app)/examples/mail/components/account-switcher"
|
||||
import { MailDisplay } from "@/app/(app)/examples/mail/components/mail-display"
|
||||
import { MailList } from "@/app/(app)/examples/mail/components/mail-list"
|
||||
import { Nav } from "@/app/(app)/examples/mail/components/nav"
|
||||
import { type Mail } from "@/app/(app)/examples/mail/data"
|
||||
import { useMail } from "@/app/(app)/examples/mail/use-mail"
|
||||
|
||||
interface MailProps {
|
||||
accounts: {
|
||||
@@ -78,9 +82,17 @@ export function Mail({
|
||||
collapsed
|
||||
)}`
|
||||
}}
|
||||
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
|
||||
className={cn(
|
||||
isCollapsed &&
|
||||
"min-w-[50px] transition-all duration-300 ease-in-out"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex h-[52px] items-center justify-center", isCollapsed ? 'h-[52px]': 'px-2')}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[52px] items-center justify-center",
|
||||
isCollapsed ? "h-[52px]" : "px-2"
|
||||
)}
|
||||
>
|
||||
<AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} />
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -168,8 +180,18 @@ export function Mail({
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<h1 className="text-xl font-bold">Inbox</h1>
|
||||
<TabsList className="ml-auto">
|
||||
<TabsTrigger value="all" className="text-zinc-600 dark:text-zinc-200">All mail</TabsTrigger>
|
||||
<TabsTrigger value="unread" className="text-zinc-600 dark:text-zinc-200">Unread</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="text-zinc-600 dark:text-zinc-200"
|
||||
>
|
||||
All mail
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="unread"
|
||||
className="text-zinc-600 dark:text-zinc-200"
|
||||
>
|
||||
Unread
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cookies } from "next/headers"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Mail } from "@/app/examples/mail/components/mail"
|
||||
import { accounts, mails } from "@/app/examples/mail/data"
|
||||
import { Mail } from "@/app/(app)/examples/mail/components/mail"
|
||||
import { accounts, mails } from "@/app/(app)/examples/mail/data"
|
||||
|
||||
export default function MailPage() {
|
||||
const layout = cookies().get("react-resizable-panels:layout")
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom, useAtom } from "jotai"
|
||||
|
||||
import { Mail, mails } from "@/app/examples/mail/data"
|
||||
import { Mail, mails } from "@/app/(app)/examples/mail/data"
|
||||
|
||||
type Config = {
|
||||
selected: Mail["id"] | null
|
||||
@@ -5,7 +5,7 @@ import { Table } from "@tanstack/react-table"
|
||||
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import { Input } from "@/registry/new-york/ui/input"
|
||||
import { DataTableViewOptions } from "@/app/examples/tasks/components/data-table-view-options"
|
||||
import { DataTableViewOptions } from "@/app/(app)/examples/tasks/components/data-table-view-options"
|
||||
|
||||
import { priorities, statuses } from "../data/data"
|
||||
import { DataTableFacetedFilter } from "./data-table-faceted-filter"
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
TableRow,
|
||||
} from "@/registry/new-york/ui/table"
|
||||
|
||||
import { DataTablePagination } from "../components/data-table-pagination"
|
||||
import { DataTableToolbar } from "../components/data-table-toolbar"
|
||||
import { DataTablePagination } from "./data-table-pagination"
|
||||
import { DataTableToolbar } from "./data-table-toolbar"
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
@@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
||||
// Simulate a database read for tasks.
|
||||
async function getTasks() {
|
||||
const data = await fs.readFile(
|
||||
path.join(process.cwd(), "app/examples/tasks/data/tasks.json")
|
||||
path.join(process.cwd(), "app/(app)/examples/tasks/data/tasks.json")
|
||||
)
|
||||
|
||||
const tasks = JSON.parse(data.toString())
|
||||
16
apps/www/app/(app)/layout.tsx
Normal file
16
apps/www/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SiteFooter } from "@/components/site-footer"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function AppLayout({ children }: AppLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
<main className="flex-1">{children}</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { buttonVariants } from "@/registry/new-york/ui/button"
|
||||
import MailPage from "@/app/examples/mail/page"
|
||||
import MailPage from "@/app/(app)/examples/mail/page"
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@/components/page-header"
|
||||
import { ThemeCustomizer } from "@/components/theme-customizer"
|
||||
import { ThemeWrapper } from "@/components/theme-wrapper"
|
||||
import { ThemesTabs } from "@/app/themes/tabs"
|
||||
import { ThemesTabs } from "@/app/(app)/themes/tabs"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Themes",
|
||||
86
apps/www/app/(blocks)/blocks/[style]/[name]/page.tsx
Normal file
86
apps/www/app/(blocks)/blocks/[style]/[name]/page.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { getAllBlockIds, getBlock } from "@/lib/blocks"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { Style, styles } from "@/registry/styles"
|
||||
|
||||
import "@/styles/mdx.css"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
style: Style["name"]
|
||||
name: string
|
||||
}
|
||||
}): Promise<Metadata> {
|
||||
const { name, style } = params
|
||||
const block = await getBlock(name, style)
|
||||
|
||||
if (!block) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
title: block.name,
|
||||
description: block.description,
|
||||
openGraph: {
|
||||
title: block.name,
|
||||
description: block.description,
|
||||
type: "article",
|
||||
url: absoluteUrl(`/blocks/${block.name}`),
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: block.name,
|
||||
description: block.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@shadcn",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const blockIds = await getAllBlockIds()
|
||||
return styles
|
||||
.map((style) =>
|
||||
blockIds.map((name) => ({
|
||||
style: style.name,
|
||||
name,
|
||||
}))
|
||||
)
|
||||
.flat()
|
||||
}
|
||||
|
||||
export default async function BlockPage({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
style: Style["name"]
|
||||
name: string
|
||||
}
|
||||
}) {
|
||||
const { name, style } = params
|
||||
const block = await getBlock(name, style)
|
||||
|
||||
if (!block) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const Component = block.component
|
||||
|
||||
return (
|
||||
<div className={block.container?.className || ""}>
|
||||
<Component />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import { fontSans } from "@/lib/fonts"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Analytics } from "@/components/analytics"
|
||||
import { ThemeProvider } from "@/components/providers"
|
||||
import { SiteFooter } from "@/components/site-footer"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher"
|
||||
import { Toaster as DefaultToaster } from "@/registry/default/ui/toaster"
|
||||
@@ -85,7 +83,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.className
|
||||
fontSans.variable
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
@@ -96,9 +94,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
>
|
||||
<div vaul-drawer-wrapper="">
|
||||
<div className="relative flex min-h-screen flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<main className="flex-1">{children}</main>
|
||||
<SiteFooter />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<TailwindIndicator />
|
||||
|
||||
@@ -10,10 +10,8 @@ export function Announcement() {
|
||||
className="inline-flex items-center rounded-lg bg-muted px-3 py-1 text-sm font-medium"
|
||||
>
|
||||
🎉 <Separator className="mx-2 h-4" orientation="vertical" />{" "}
|
||||
<span className="sm:hidden">New components and more.</span>
|
||||
<span className="hidden sm:inline">
|
||||
New components, breadcrumb and input otp.
|
||||
</span>
|
||||
<span className="sm:hidden">Introducing Blocks</span>
|
||||
<span className="hidden sm:inline">Introducing Blocks</span>
|
||||
<ArrowRightIcon className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
)
|
||||
|
||||
54
apps/www/components/block-copy-code-button.tsx
Normal file
54
apps/www/components/block-copy-code-button.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ClipboardIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { trackEvent } from "@/lib/events"
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york/ui/tooltip"
|
||||
|
||||
export function BlockCopyCodeButton({
|
||||
name,
|
||||
code,
|
||||
}: {
|
||||
name: string
|
||||
code: string
|
||||
}) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setHasCopied(false)
|
||||
}, 2000)
|
||||
}, [hasCopied])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-7 w-7 [&_svg]:size-3.5"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(code)
|
||||
trackEvent({
|
||||
name: "copy_block_code",
|
||||
properties: {
|
||||
name,
|
||||
},
|
||||
})
|
||||
setHasCopied(true)
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy code</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
24
apps/www/components/block-display.tsx
Normal file
24
apps/www/components/block-display.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getBlock } from "@/lib/blocks"
|
||||
import { BlockPreview } from "@/components/block-preview"
|
||||
import { styles } from "@/registry/styles"
|
||||
|
||||
export async function BlockDisplay({ name }: { name: string }) {
|
||||
const blocks = await Promise.all(
|
||||
styles.map(async (style) => {
|
||||
const block = await getBlock(name, style.name)
|
||||
|
||||
// Cannot (and don't need to) pass component to the client.
|
||||
delete block?.component
|
||||
|
||||
return block
|
||||
})
|
||||
)
|
||||
|
||||
if (!blocks?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return blocks.map((block) => (
|
||||
<BlockPreview key={`${block.style}-${block.name}`} block={block} />
|
||||
))
|
||||
}
|
||||
210
apps/www/components/block-preview.tsx
Normal file
210
apps/www/components/block-preview.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
CircleHelp,
|
||||
Info,
|
||||
Monitor,
|
||||
Phone,
|
||||
Smartphone,
|
||||
Tablet,
|
||||
} from "lucide-react"
|
||||
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { BlockCopyCodeButton } from "@/components/block-copy-code-button"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { StyleSwitcher } from "@/components/style-switcher"
|
||||
import { V0Button } from "@/components/v0-button"
|
||||
import { Badge } from "@/registry/new-york/ui/badge"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york/ui/popover"
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/registry/new-york/ui/resizable"
|
||||
import { Separator } from "@/registry/new-york/ui/separator"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york/ui/tabs"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york/ui/toggle-group"
|
||||
import { Block } from "@/registry/schema"
|
||||
|
||||
export function BlockPreview({ block }: { block: Block }) {
|
||||
const [config] = useConfig()
|
||||
const [isLoading, setIsLoading] = React.useState(true)
|
||||
const ref = React.useRef<ImperativePanelHandle>(null)
|
||||
|
||||
if (config.style !== block.style) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
id={block.name}
|
||||
defaultValue="preview"
|
||||
className="relative grid w-full scroll-m-20 gap-4"
|
||||
style={
|
||||
{
|
||||
"--container-height": block.container?.height,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4 sm:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<TabsList className="hidden sm:flex">
|
||||
<TabsTrigger value="preview">Preview</TabsTrigger>
|
||||
<TabsTrigger value="code">Code</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden h-4 md:flex"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href={`#${block.name}`}>
|
||||
<Badge variant="outline">{block.name}</Badge>
|
||||
</a>
|
||||
<Popover>
|
||||
<PopoverTrigger className="hidden text-muted-foreground hover:text-foreground sm:flex">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Block description</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
sideOffset={10}
|
||||
className="text-sm"
|
||||
>
|
||||
{block.description}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{block.code && (
|
||||
<div className="flex items-center gap-2 pr-[14px] sm:ml-auto">
|
||||
<div className="hidden h-[28px] items-center gap-1.5 rounded-md border p-[2px] shadow-sm md:flex">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="100"
|
||||
onValueChange={(value) => {
|
||||
if (ref.current) {
|
||||
ref.current.resize(parseInt(value))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="100"
|
||||
className="h-[22px] w-[22px] rounded-sm p-0"
|
||||
>
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="60"
|
||||
className="h-[22px] w-[22px] rounded-sm p-0"
|
||||
>
|
||||
<Tablet className="h-3.5 w-3.5" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="25"
|
||||
className="h-[22px] w-[22px] rounded-sm p-0"
|
||||
>
|
||||
<Smartphone className="h-3.5 w-3.5" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden h-4 md:flex"
|
||||
/>
|
||||
<StyleSwitcher className="h-7" />
|
||||
<Popover>
|
||||
<PopoverTrigger className="hidden text-muted-foreground hover:text-foreground sm:flex">
|
||||
<CircleHelp className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Block description</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
sideOffset={20}
|
||||
className="space-y-3 rounded-[0.5rem] text-sm"
|
||||
>
|
||||
<p className="font-medium">
|
||||
What is the difference between the New York and Default style?
|
||||
</p>
|
||||
<p>
|
||||
A style comes with its own set of components, animations,
|
||||
icons and more.
|
||||
</p>
|
||||
<p>
|
||||
The <span className="font-medium">Default</span> style has
|
||||
larger inputs, uses lucide-react for icons and
|
||||
tailwindcss-animate for animations.
|
||||
</p>
|
||||
<p>
|
||||
The <span className="font-medium">New York</span> style ships
|
||||
with smaller buttons and inputs. It also uses shadows on cards
|
||||
and buttons.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<BlockCopyCodeButton name={block.name} code={block.code} />
|
||||
<V0Button
|
||||
name={block.name}
|
||||
description={block.description || "Edit in v0"}
|
||||
code={block.code}
|
||||
style={block.style}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TabsContent
|
||||
value="preview"
|
||||
className="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted"
|
||||
>
|
||||
<ResizablePanelGroup direction="horizontal" className="relative z-10">
|
||||
<ResizablePanel
|
||||
ref={ref}
|
||||
className="relative rounded-lg border bg-background transition-all"
|
||||
defaultSize={100}
|
||||
minSize={25}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="absolute inset-0 z-10 flex h-[--container-height] w-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Icons.spinner className="h-4 w-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : null}
|
||||
<iframe
|
||||
src={`/blocks/${block.style}/${block.name}`}
|
||||
height={block.container?.height}
|
||||
className="relative z-20 w-full bg-background"
|
||||
onLoad={() => {
|
||||
setIsLoading(false)
|
||||
}}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle className="relative hidden w-3 bg-transparent p-0 after:absolute after:right-0 after:top-1/2 after:h-8 after:w-[6px] after:-translate-y-1/2 after:translate-x-[-1px] after:rounded-full after:bg-border after:transition-all after:hover:h-10 sm:block" />
|
||||
<ResizablePanel defaultSize={0} minSize={0} />
|
||||
</ResizablePanelGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="code">
|
||||
<div
|
||||
data-rehype-pretty-code-fragment
|
||||
dangerouslySetInnerHTML={{ __html: block.highlightedCode }}
|
||||
className="w-full overflow-hidden rounded-md [&_pre]:my-0 [&_pre]:h-[--container-height] [&_pre]:overflow-auto [&_pre]:whitespace-break-spaces [&_pre]:p-6 [&_pre]:font-mono [&_pre]:text-sm [&_pre]:leading-relaxed"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -11,42 +11,42 @@ const examples = [
|
||||
{
|
||||
name: "Mail",
|
||||
href: "/examples/mail",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/mail",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/mail",
|
||||
},
|
||||
{
|
||||
name: "Dashboard",
|
||||
href: "/examples/dashboard",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/dashboard",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Cards",
|
||||
href: "/examples/cards",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/cards",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/cards",
|
||||
},
|
||||
{
|
||||
name: "Tasks",
|
||||
href: "/examples/tasks",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/tasks",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/tasks",
|
||||
},
|
||||
{
|
||||
name: "Playground",
|
||||
href: "/examples/playground",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/playground",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/playground",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/examples/forms",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/forms",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/forms",
|
||||
},
|
||||
{
|
||||
name: "Music",
|
||||
href: "/examples/music",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/music",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/music",
|
||||
},
|
||||
{
|
||||
name: "Authentication",
|
||||
href: "/examples/authentication",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/examples/authentication",
|
||||
code: "https://github.com/shadcn/ui/tree/main/apps/www/app/(app)/examples/authentication",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function MainNav() {
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-6 text-sm">
|
||||
<nav className="flex items-center gap-4 text-sm lg:gap-6">
|
||||
<Link
|
||||
href="/docs"
|
||||
className={cn(
|
||||
@@ -63,6 +63,17 @@ export function MainNav() {
|
||||
>
|
||||
Examples
|
||||
</Link>
|
||||
<Link
|
||||
href="/blocks"
|
||||
className={cn(
|
||||
"transition-colors hover:text-foreground/80",
|
||||
pathname?.startsWith("/blocks")
|
||||
? "text-foreground"
|
||||
: "text-foreground/60"
|
||||
)}
|
||||
>
|
||||
Blocks
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.github}
|
||||
className={cn(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TooltipProvider } from "@/registry/new-york/ui/tooltip"
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
|
||||
117
apps/www/components/v0-button.tsx
Normal file
117
apps/www/components/v0-button.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { editInV0 } from "@/actions/edit-in-v0"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { useFormStatus } from "react-dom"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import { Style } from "@/registry/styles"
|
||||
|
||||
export function V0Button({
|
||||
name,
|
||||
description,
|
||||
code,
|
||||
style,
|
||||
}: {
|
||||
name: string
|
||||
description: string
|
||||
code: string
|
||||
style: Style["name"]
|
||||
}) {
|
||||
if (style === "new-york") {
|
||||
return (
|
||||
<Button
|
||||
aria-label="Edit in v0"
|
||||
className="h-7 gap-1"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
toast("New York not available.", {
|
||||
description: (
|
||||
<div className="flex items-center">
|
||||
Only the Default style is available in{" "}
|
||||
<V0Logo className="ml-1 text-foreground" aria-label="v0" />.
|
||||
</div>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit in <V0Logo />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<form
|
||||
action={async () => {
|
||||
try {
|
||||
const result = await editInV0({
|
||||
name,
|
||||
description,
|
||||
code,
|
||||
style,
|
||||
})
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
if (result?.url) {
|
||||
const popupOpened = window.open(result.url, "_blank")
|
||||
if (!popupOpened) {
|
||||
toast.warning("Pop-up window blocked.", {
|
||||
description:
|
||||
"Click the pop-up button in your browser to continue.",
|
||||
duration: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function Form() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Edit in v0"
|
||||
className="h-7 gap-1"
|
||||
size="sm"
|
||||
disabled={pending}
|
||||
>
|
||||
{pending && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
Edit in <V0Logo />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function V0Logo({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 40 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn("h-5 w-5 text-current", className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M23.3919 0H32.9188C36.7819 0 39.9136 3.13165 39.9136 6.99475V16.0805H36.0006V6.99475C36.0006 6.90167 35.9969 6.80925 35.9898 6.71766L26.4628 16.079C26.4949 16.08 26.5272 16.0805 26.5595 16.0805H36.0006V19.7762H26.5595C22.6964 19.7762 19.4788 16.6139 19.4788 12.7508V3.68923H23.3919V12.7508C23.3919 12.9253 23.4054 13.0977 23.4316 13.2668L33.1682 3.6995C33.0861 3.6927 33.003 3.68923 32.9188 3.68923H23.3919V0Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M13.7688 19.0956L0 3.68759H5.53933L13.6231 12.7337V3.68759H17.7535V17.5746C17.7535 19.6705 15.1654 20.6584 13.7688 19.0956Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -24,18 +24,8 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/examples",
|
||||
},
|
||||
{
|
||||
title: "Figma",
|
||||
href: "/docs/figma",
|
||||
},
|
||||
{
|
||||
title: "GitHub",
|
||||
href: "https://github.com/shadcn/ui",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: "Twitter",
|
||||
href: "https://twitter.com/shadcn",
|
||||
external: true,
|
||||
title: "Blocks",
|
||||
href: "/blocks",
|
||||
},
|
||||
],
|
||||
sidebarNav: [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user