Compare commits

..

1 Commits

Author SHA1 Message Date
shadcn
ecbace99d9 feat: shadcn info preset code 2026-04-27 11:05:25 +04:00
1679 changed files with 3315 additions and 46168 deletions

View File

@@ -9,6 +9,5 @@
"WebFetch(domain:github.com)"
],
"deny": []
},
"outputStyle": "Explanatory"
}
}

View File

@@ -1,41 +0,0 @@
{
"name": "shadcn",
"displayName": "shadcn/ui",
"version": "1.0.0",
"description": "UI component and design system framework. Search registries, install components as source code, and audit your project.",
"author": {
"name": "shadcn"
},
"homepage": "https://ui.shadcn.com",
"repository": "https://github.com/shadcn-ui/ui",
"license": "MIT",
"logo": "skills/shadcn/assets/shadcn.png",
"keywords": [
"shadcn",
"shadcn-ui",
"ui",
"components",
"tailwind",
"tailwindcss",
"radix",
"react",
"design-system",
"registry",
"mcp"
],
"category": "developer-tools",
"tags": [
"ui",
"components",
"design-system",
"react",
"tailwind"
],
"skills": "./skills/",
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}

View File

@@ -3,7 +3,7 @@ name: Write Beta Release comment
on:
workflow_run:
workflows: ["Release"]
workflows: ["Release - Beta"]
types:
- completed
@@ -11,13 +11,12 @@ jobs:
comment:
if: |
github.repository_owner == 'shadcn-ui' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
name: Write comment to the PR
steps:
- name: "Comment on PR"
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -54,7 +53,7 @@ jobs:
```
- name: "Remove the autorelease label once published"
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

64
.github/workflows/prerelease.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
# Adapted from create-t3-app.
name: Release - Beta
on:
pull_request:
types: [labeled]
branches:
- main
permissions:
id-token: write
contents: read
jobs:
prerelease:
if: |
github.repository_owner == 'shadcn-ui' &&
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
name: Build & Publish a beta release to NPM
runs-on: ubuntu-latest
environment: Preview
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v4
with:
version: 9.0.6
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Publish Beta to NPM
run: pnpm pub:beta
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
path: packages/shadcn
- name: Upload packaged artifact
uses: actions/upload-artifact@v4
with:
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/shadcn/dist/index.js

View File

@@ -2,81 +2,24 @@
name: Release
run-name: ${{ github.event_name == 'pull_request' && format('Release Beta - PR {0}', github.event.number) || 'Release Stable' }}
on:
pull_request:
types: [labeled]
branches:
- main
push:
branches:
- main
permissions:
id-token: write
contents: write
pull-requests: write
jobs:
prerelease:
if: ${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && contains(github.event.pull_request.labels.*.name, '🚀 autorelease') }}
name: Publish Beta to NPM
runs-on: ubuntu-latest
environment: Preview
permissions:
id-token: write
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v4
with:
version: 9.0.6
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Publish Beta to NPM
run: pnpm pub:beta
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
path: packages/shadcn
- name: Upload packaged artifact
uses: actions/upload-artifact@v4
with:
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/shadcn/dist/index.js
release:
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
name: Create Version PR or Publish Stable Release
if: ${{ github.repository_owner == 'shadcn-ui' }}
name: Create a PR for release workflow
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
steps:
- name: Checkout Repo
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -104,19 +47,10 @@ jobs:
- name: Build the package
run: pnpm shadcn:build
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
git_tag_gpgsign: true
- name: Create Version PR or Publish to NPM
id: changesets
uses: changesets/action@v1
with:
setupGitUser: false
commit: "chore(release): version packages"
title: "chore(release): version packages"
version: node .github/changeset-version.js

View File

@@ -1,75 +0,0 @@
name: Signed commits
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions:
pull-requests: write
jobs:
signed-commits:
if: github.repository_owner == 'shadcn-ui'
runs-on: ubuntu-latest
name: Signed commits
steps:
- name: Check PR commits
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = "Can you sign the commits please? See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits. Thank you."
const { owner, repo } = context.repo
const pullNumber = context.payload.pull_request.number
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner,
repo,
pull_number: pullNumber,
per_page: 100,
})
const unsignedCommits = commits.filter((commit) => {
return commit.commit.verification?.reason === "unsigned"
})
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pullNumber,
per_page: 100,
})
const existingComments = comments.filter((comment) => {
return comment.user.type === "Bot" && comment.body.trim() === body
})
if (unsignedCommits.length > 0) {
core.info(`Found ${unsignedCommits.length} unsigned commits.`)
if (existingComments.length === 0) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body,
})
}
return
}
core.info("All commits are signed.")
for (const comment of existingComments) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id,
})
}

View File

@@ -39,9 +39,6 @@ jobs:
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: pnpm install

View File

@@ -3,12 +3,15 @@ import Image from "next/image"
import Link from "next/link"
import { Announcement } from "@/components/announcement"
import { ExamplesNav } from "@/components/examples-nav"
import {
PageActions,
PageHeader,
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header"
import { PageNav } from "@/components/page-nav"
import { ThemeSelector } from "@/components/theme-selector"
import { Button } from "@/registry/new-york-v4/ui/button"
import { RootComponents } from "./components"

View File

@@ -1,216 +0,0 @@
"use client"
import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/styles/base-sera/ui/badge"
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-sera/ui/card"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/styles/base-sera/ui/input-group"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
} from "@/styles/base-sera/ui/pagination"
import { Progress, ProgressValue } from "@/styles/base-sera/ui/progress"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/styles/base-sera/ui/table"
const ARTICLE_ROWS = [
{
title: "The Future of Sustainable Architecture",
wordProgress: "1.4k / 2.6k words",
author: "Elena Rostova",
issue: "Summer 2024",
status: "in-revision",
statusLabel: "In revision",
progress: 45,
},
{
title: "Brutalism's Second Act",
wordProgress: "2.1k / 2.5k words",
author: "Marcus Chen",
issue: "Summer 2024",
status: "final-edit",
statusLabel: "Final edit",
progress: 90,
},
{
title: "The Typography of Public Spaces",
wordProgress: "0.5k / 1.5k words",
author: "Sarah Jenkins",
issue: "Autumn 2024",
status: "drafting",
statusLabel: "Drafting",
progress: 20,
},
{
title: "Rethinking Urban Canopies",
wordProgress: "1.8k / 1.8k words",
author: "David O'Connor",
issue: "Summer 2024",
status: "published",
statusLabel: "Published",
progress: 100,
},
{
title: "Light, Glass, and the Modern Museum",
wordProgress: "1.2k / 2.0k words",
author: "Amara Osei",
issue: "Autumn 2024",
status: "in-revision",
statusLabel: "In revision",
progress: 55,
},
{
title: "Concrete Utopias: Housing in the 21st Century",
wordProgress: "3.0k / 3.0k words",
author: "Tomás Herrera",
issue: "Summer 2024",
status: "published",
statusLabel: "Published",
progress: 100,
},
{
title: "Designing for Silence",
wordProgress: "0.8k / 2.2k words",
author: "Ingrid Solberg",
issue: "Winter 2024",
status: "drafting",
statusLabel: "Drafting",
progress: 30,
},
{
title: "The Invisible Infrastructure of Cities",
wordProgress: "2.4k / 2.8k words",
author: "James Whitfield",
issue: "Autumn 2024",
status: "final-edit",
statusLabel: "Final edit",
progress: 85,
},
] as const
const STATUS_BADGE_VARIANT = {
"in-revision": "outline",
"final-edit": "default",
drafting: "ghost",
published: "secondary",
} as const
const STATUS_DOT_CLASSNAME = {
"in-revision": "bg-amber-600/80",
"final-edit": "bg-foreground/90",
drafting: "bg-muted-foreground/60",
published: "bg-emerald-600/80",
}
export function ArticleDirectory() {
return (
<Card>
<CardHeader>
<InputGroup>
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput type="search" placeholder="Search articles..." />
</InputGroup>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Title</TableHead>
<TableHead className="w-[170px]">Author</TableHead>
<TableHead className="w-[150px]">Issue</TableHead>
<TableHead className="w-[180px]">Status</TableHead>
<TableHead className="w-[140px]">Progress</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ARTICLE_ROWS.map((row) => (
<TableRow key={row.title}>
<TableCell>
<div className="flex flex-col gap-1">
<p className="font-heading text-xl tracking-tight text-foreground">
{row.title}
</p>
<p className="text-xs text-muted-foreground">
{row.wordProgress}
</p>
</div>
</TableCell>
<TableCell>{row.author}</TableCell>
<TableCell>{row.issue}</TableCell>
<TableCell>
<Badge variant={STATUS_BADGE_VARIANT[row.status]}>
<span
className={cn(
"size-1.5 rounded-full",
STATUS_DOT_CLASSNAME[row.status]
)}
/>
{row.statusLabel}
</Badge>
</TableCell>
<TableCell>
<Progress
value={row.progress}
aria-label={`${row.progress}% complete`}
className="flex flex-row-reverse items-center **:data-[slot=progress-track]:w-16"
>
<ProgressValue>
{(formattedValue) => `${formattedValue}`}
</ProgressValue>
</Progress>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationLink
href="#"
size="icon-sm"
aria-label="Previous page"
>
<ChevronLeftIcon className="cn-rtl-flip" />
</PaginationLink>
</PaginationItem>
{[1, 2, 3].map((page) => (
<PaginationItem key={page}>
<PaginationLink href="#" size="icon-sm" isActive={page === 1}>
{page}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationLink href="#" size="icon-sm" aria-label="Next page">
<ChevronRightIcon className="cn-rtl-flip" />
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
)
}

View File

@@ -1,47 +0,0 @@
import { ArrowLeftIcon, PlusIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/styles/base-sera/ui/breadcrumb"
import { Button } from "@/styles/base-sera/ui/button"
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
export function PreviewHeader() {
return (
<header>
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<Breadcrumb>
<BreadcrumbList className="justify-center md:justify-start">
<BreadcrumbItem>
<BreadcrumbLink
href="#"
className="inline-flex items-center gap-1.5"
>
<ArrowLeftIcon className="size-3" />
Editorial Dashboard
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
Article Directory
</h1>
</div>
<div>
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
<Button>
<PlusIcon data-icon="inline-start" />
New Article
</Button>
</ButtonGroup>
</div>
</div>
</header>
)
}

View File

@@ -1,16 +0,0 @@
import { Separator } from "@/styles/base-sera/ui/separator"
import { ArticleDirectory as ArticleDirectoryList } from "./components/article-directory"
import { PreviewHeader } from "./components/preview-header"
export function ArticleDirectory() {
return (
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
<PreviewHeader />
<Separator className="hidden sm:block" />
<div className="container py-(--gap)">
<ArticleDirectoryList />
</div>
</div>
)
}

View File

@@ -1,56 +0,0 @@
"use client"
import * as React from "react"
import { MoveRightIcon } from "lucide-react"
import { Button } from "@/styles/base-sera/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/styles/base-sera/ui/progress"
const DEMOGRAPHIC_DATA = [
{ age: "18 - 24", percentage: 22 },
{ age: "25 - 34", percentage: 64 },
{ age: "35 - 44", percentage: 12 },
{ age: "45+", percentage: 5 },
]
export function Demographics({ ...props }: React.ComponentProps<typeof Card>) {
return (
<Card {...props}>
<CardHeader>
<CardTitle className="text-2xl">Demographics</CardTitle>
<CardDescription>Reader Profile</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-10">
{DEMOGRAPHIC_DATA.map((item) => (
<Progress
key={item.age}
value={item.percentage}
aria-label={item.age}
>
<ProgressLabel>{item.age}</ProgressLabel>
<ProgressValue>
{(formattedValue) => `${formattedValue}`}
</ProgressValue>
</Progress>
))}
</CardContent>
<CardFooter>
<Button variant="link" className="w-full">
View all source <MoveRightIcon data-icon="inline-end" />
</Button>
</CardFooter>
</Card>
)
}

View File

@@ -1,93 +0,0 @@
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
type Metric = {
label: string
value: string
comparison: string
change: string
trend: "up" | "down"
}
const METRIC_CARDS: Metric[] = [
{
label: "Total visitors",
value: "248.5k",
comparison: "12.4%",
change: "vs last period",
trend: "up",
},
{
label: "Unique readers",
value: "182.1k",
comparison: "8.7%",
change: "vs last period",
trend: "up",
},
{
label: "Avg. time on page",
value: "3m 42s",
comparison: "1.2%",
change: "vs last period",
trend: "down",
},
{
label: "Bounce rate",
value: "42.8%",
comparison: "3.5%",
change: "vs last period",
trend: "down",
},
]
export function MetricsGrid() {
return (
<>
{METRIC_CARDS.map((metric) => (
<MetricCard
key={metric.label}
metric={metric}
className="col-span-full md:col-span-6 lg:col-span-3"
/>
))}
</>
)
}
function MetricCard({
metric,
className,
}: {
metric: Metric
className: string
}) {
return (
<Card className={cn("gap-0", className)}>
<CardContent className="flex flex-col gap-2">
<CardDescription className="text-xs uppercase">
{metric.label}
</CardDescription>
<CardTitle className="text-5xl tracking-tight lowercase">
{metric.value}
</CardTitle>
<CardDescription>
{metric.trend === "up" ? (
<TrendingUpIcon className="inline-block size-2.5 text-muted-foreground" />
) : (
<TrendingDownIcon className="inline-block size-2.5 text-muted-foreground" />
)}{" "}
<span className="text-foreground">{metric.comparison}</span>{" "}
<span>{metric.change}</span>
</CardDescription>
</CardContent>
</Card>
)
}

View File

@@ -1,103 +0,0 @@
"use client"
import * as React from "react"
import { ChevronDownIcon, DownloadIcon } from "lucide-react"
import { Button } from "@/styles/base-sera/ui/button"
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/styles/base-sera/ui/dropdown-menu"
const EXPORT_DATE_OPTIONS = [
{
label: "Last 7 days",
value: "last-7-days",
},
{
label: "Last 30 days",
value: "last-30-days",
},
{
label: "This month",
value: "this-month",
},
{
label: "Last month",
value: "last-month",
},
]
export function PreviewHeader() {
const [selectedDateRange, setSelectedDateRange] =
React.useState("last-30-days")
const selectedDateRangeLabel = React.useMemo(() => {
const selectedOption = EXPORT_DATE_OPTIONS.find(
(option) => option.value === selectedDateRange
)
if (!selectedOption) {
return "Last 30 days"
}
return selectedOption.label
}, [selectedDateRange])
return (
<header>
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
Audience Analytics
</h1>
<div className="line-clamp-1 text-sm font-medium tracking-wider text-muted-foreground uppercase">
Editorial Performance Dashboard
</div>
</div>
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
className="bg-background hover:bg-background/80 data-popup-open:bg-background"
/>
}
>
{selectedDateRangeLabel}{" "}
<ChevronDownIcon data-icon="inline-end" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuRadioGroup
value={selectedDateRange}
onValueChange={setSelectedDateRange}
>
{EXPORT_DATE_OPTIONS.map((option) => (
<DropdownMenuRadioItem
key={option.value}
value={option.value}
>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<Button>
<DownloadIcon data-icon="inline-start" />
<span className="lg:hidden">Export</span>
<span className="hidden lg:inline">Export Report</span>
</Button>
</ButtonGroup>
</div>
</header>
)
}

View File

@@ -1,257 +0,0 @@
"use client"
import * as React from "react"
import { ArrowDownIcon, MoreHorizontalIcon } from "lucide-react"
import { Button } from "@/styles/base-sera/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/base-sera/ui/dropdown-menu"
import { Spinner } from "@/styles/base-sera/ui/spinner"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/styles/base-sera/ui/table"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/styles/base-sera/ui/toggle-group"
type EditorialMetric = "views" | "time" | "shares"
type EditorialRow = {
rank: number
title: string
author: string
published: string
pageviews: string
avgTime: string
}
const METRIC_LABEL: Record<EditorialMetric, string> = {
views: "VIEWS",
time: "TIME",
shares: "SHARES",
}
const EDITORIAL_ROWS: EditorialRow[] = [
{
rank: 1,
title: "The New Vanguard of Minimalist Architecture",
author: "Elena Rostova",
published: "Oct 12",
pageviews: "45.2k",
avgTime: "04:15",
},
{
rank: 2,
title: "Autumn Sartorial Code: Deconstructed Classics",
author: "Julian Vance",
published: "Oct 05",
pageviews: "38.9k",
avgTime: "03:42",
},
{
rank: 3,
title: "Interview: Director Sofia Coppola on The Aesthetics of Isolation",
author: "Marcus Trent",
published: "Sep 28",
pageviews: "31.4k",
avgTime: "06:20",
},
{
rank: 4,
title: "Sourcing Ceramics from Kyoto's Oldest Kilns",
author: "Sarah Lin",
published: "Oct 18",
pageviews: "22.1k",
avgTime: "02:55",
},
{
rank: 5,
title: "Field Notes from Copenhagen Design Week",
author: "Noah Bennett",
published: "Oct 21",
pageviews: "19.7k",
avgTime: "03:18",
},
{
rank: 6,
title: "A Studio Visit with Milan's Most Elusive Lighting Designer",
author: "Claire Duval",
published: "Oct 09",
pageviews: "17.4k",
avgTime: "04:02",
},
{
rank: 7,
title: "Collecting the New Avant-Garde in Contemporary Furniture",
author: "Tommy Rhodes",
published: "Sep 30",
pageviews: "15.9k",
avgTime: "03:36",
},
{
rank: 8,
title: "Inside Lisbon's Quiet Culinary Renaissance",
author: "Amara Iqbal",
published: "Oct 14",
pageviews: "14.2k",
avgTime: "05:08",
},
{
rank: 9,
title: "Why Slow Interiors Are Defining the Next Luxury Wave",
author: "Henry Vale",
published: "Oct 03",
pageviews: "12.7k",
avgTime: "03:11",
},
{
rank: 10,
title: "The Return of Print: Independent Magazine Covers to Watch",
author: "Mina Okafor",
published: "Sep 26",
pageviews: "11.3k",
avgTime: "02:49",
},
]
type TopEditorialProps = React.ComponentProps<typeof Card> & {
selectedMetric?: EditorialMetric
}
export function TopEditorial({
selectedMetric = "views",
...props
}: TopEditorialProps) {
const [visibleCount, setVisibleCount] = React.useState(5)
const [isLoadingMore, setIsLoadingMore] = React.useState(false)
const hasMoreRows = visibleCount < EDITORIAL_ROWS.length
const visibleRows = EDITORIAL_ROWS.slice(0, visibleCount)
const handleLoadMore = React.useCallback(() => {
if (!hasMoreRows || isLoadingMore) {
return
}
setIsLoadingMore(true)
window.setTimeout(() => {
setVisibleCount(EDITORIAL_ROWS.length)
setIsLoadingMore(false)
}, 2000)
}, [hasMoreRows, isLoadingMore])
return (
<Card {...props}>
<CardHeader>
<div className="flex flex-col gap-(--gap) sm:flex-row">
<div className="flex flex-col gap-1.5">
<CardTitle className="text-2xl">Top Editorials</CardTitle>
<CardDescription>Ranked by engagement</CardDescription>
</div>
<ToggleGroup
aria-label="Top editorials metric selector"
value={[selectedMetric]}
variant="outline"
className="w-full sm:ml-auto sm:w-fit"
>
{(["views", "time", "shares"] as const).map((metric) => {
return (
<ToggleGroupItem key={metric} value={metric} className="flex-1">
{METRIC_LABEL[metric]}
</ToggleGroupItem>
)
})}
</ToggleGroup>
</div>
</CardHeader>
<CardContent className="flex-1 **:data-[slot=table-container]:no-scrollbar **:data-[slot=table-container]:overflow-y-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Title</TableHead>
<TableHead>Published</TableHead>
<TableHead>Page Views</TableHead>
<TableHead>Read Time</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visibleRows.map((row) => (
<TableRow key={row.rank}>
<TableCell className="translate-y-1 align-text-top">
{row.rank}
</TableCell>
<TableCell>
<div className="flex flex-col gap-2">
<p className="font-heading text-xl tracking-tight text-foreground">
{row.title}
</p>
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
By {row.author}
</p>
</div>
</TableCell>
<TableCell>{row.published}</TableCell>
<TableCell>{row.pageviews}</TableCell>
<TableCell>{row.avgTime}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="ghost" size="icon-xs" />}
aria-label={`Open actions for ${row.title}`}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Publish</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter className="justify-center">
{hasMoreRows ? (
<Button
type="button"
variant="outline"
onClick={handleLoadMore}
disabled={isLoadingMore}
>
Load more content{" "}
{isLoadingMore ? (
<Spinner data-icon="inline-end" />
) : (
<ArrowDownIcon data-icon="inline-end" />
)}
</Button>
) : null}
</CardFooter>
</Card>
)
}

View File

@@ -1,57 +0,0 @@
"use client"
import * as React from "react"
import dynamic from "next/dynamic"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
const TrafficOverviewContent = dynamic(
() => import("./traffic-overview").then((mod) => mod.TrafficOverview),
{
ssr: false,
loading: () => <TrafficOverviewFallback />,
}
)
export function TrafficOverviewDeferred({
className,
...props
}: React.ComponentProps<typeof Card>) {
return (
<div className={className}>
<TrafficOverviewContent {...props} />
</div>
)
}
function TrafficOverviewFallback() {
return (
<Card>
<CardHeader>
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
<CardDescription>
Traffic for the last 30 days has increased by 12.4% compared to the
previous period.
</CardDescription>
</CardHeader>
<CardContent>
<div
aria-hidden="true"
className="flex h-82 w-full flex-col justify-end gap-6 overflow-hidden bg-muted/40 p-5"
>
<div className="h-px w-full bg-border" />
<div className="h-px w-full bg-border" />
<div className="h-px w-full bg-border" />
<div className="h-px w-full bg-border" />
<div className="h-px w-full bg-border" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,155 +0,0 @@
"use client"
import { TrendingUpIcon } from "lucide-react"
import {
CartesianGrid,
Line,
LineChart,
ReferenceDot,
XAxis,
YAxis,
} from "recharts"
import { Badge } from "@/styles/base-sera/ui/badge"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/styles/base-sera/ui/chart"
const TRAFFIC_OVERVIEW_DATA = [
{ date: "2025-10-01", views: 2600, unique: 1600 },
{ date: "2025-10-04", views: 4500, unique: 3000 },
{ date: "2025-10-08", views: 3500, unique: 2500 },
{ date: "2025-10-10", views: 6400, unique: 4500 },
{ date: "2025-10-13", views: 5400, unique: 4000 },
{ date: "2025-10-15", views: 8300, unique: 6500 },
{ date: "2025-10-17", views: 7400, unique: 6000 },
{ date: "2025-10-18", views: 9240, unique: 7105 },
{ date: "2025-10-22", views: 7700, unique: 6400 },
{ date: "2025-10-26", views: 8800, unique: 7000 },
{ date: "2025-10-29", views: 9800, unique: 8400 },
]
const TRAFFIC_CHART_CONFIG = {
views: {
label: "Views",
theme: {
light: "var(--chart-5)",
dark: "var(--chart-1)",
},
},
unique: {
label: "Unique",
theme: {
light: "var(--chart-1)",
dark: "var(--chart-2)",
},
},
} satisfies ChartConfig
const X_AXIS_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
timeZone: "UTC",
})
function formatYAxisTick(value: number) {
if (value === 0) {
return "0"
}
if (value % 1000 === 0) {
return `${value / 1000}k`
}
return `${value / 1000}k`
}
function formatXAxisTick(value: string) {
const date = new Date(`${value}T00:00:00Z`)
if (Number.isNaN(date.getTime())) {
return value
}
return X_AXIS_DATE_FORMATTER.format(date)
}
export function TrafficOverview({
...props
}: React.ComponentProps<typeof Card>) {
return (
<Card {...props}>
<CardHeader>
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
<CardDescription>
Traffic for the last 30 days has increased by 12.4% compared to the
previous period.
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={TRAFFIC_CHART_CONFIG} className="h-82 w-full">
<LineChart data={TRAFFIC_OVERVIEW_DATA}>
<CartesianGrid
vertical={false}
strokeDasharray="3 6"
stroke="var(--border)"
/>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
tickMargin={10}
tickFormatter={formatXAxisTick}
/>
<YAxis
tickLine={false}
axisLine={false}
width={44}
domain={[0, 10000]}
ticks={[0, 2500, 5000, 7500, 10000]}
tickFormatter={formatYAxisTick}
hide
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Line
type="linear"
dataKey="views"
stroke="var(--color-views)"
strokeWidth={2.2}
dot={false}
activeDot={{ r: 3.5, fill: "var(--color-views)" }}
/>
<Line
type="linear"
dataKey="unique"
stroke="var(--color-unique)"
strokeWidth={2}
strokeDasharray="4 6"
dot={false}
activeDot={false}
/>
<ReferenceDot
x="2025-10-18"
y={9240}
r={2.5}
fill="var(--color-views)"
stroke="var(--color-views)"
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
)
}

View File

@@ -1,22 +0,0 @@
import { Separator } from "@/styles/base-sera/ui/separator"
import { Demographics } from "./components/demographics"
import { MetricsGrid } from "./components/metrics-grid"
import { PreviewHeader } from "./components/preview-header"
import { TopEditorial } from "./components/top-editorial"
import { TrafficOverviewDeferred } from "./components/traffic-overview-deferred"
export function AudienceAnalytics() {
return (
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
<PreviewHeader />
<Separator className="hidden sm:block" />
<div className="container grid grid-cols-12 gap-(--gap) py-(--gap)">
<MetricsGrid />
<TrafficOverviewDeferred className="col-span-full md:col-span-6 lg:col-span-8" />
<Demographics className="col-span-full md:col-span-6 lg:col-span-4" />
<TopEditorial className="col-span-full" />
</div>
</div>
)
}

View File

@@ -1,46 +0,0 @@
import Image from "next/image"
import { cn } from "@/lib/utils"
export function ImagePreview() {
return (
<div className="mt-8 flex flex-col overflow-hidden md:hidden">
<ImagePreviewItem name="sera-01" />
<ImagePreviewItem name="sera-03" />
<ImagePreviewItem name="sera-02" />
<ImagePreviewItem name="sera-06" />
</div>
)
}
function ImagePreviewItem({
name,
className,
}: {
name: string
className?: string
}) {
return (
<div
className={cn(
"theme-taupe overflow-hidden bg-muted px-4 py-2 first:pt-4 last:pb-4",
className
)}
>
<Image
src={`/images/${name}-light.png`}
alt={name}
width={1440}
height={900}
className="dark:hidden"
/>
<Image
src={`/images/${name}-dark.png`}
alt={name}
width={1440}
height={900}
className="hidden dark:block"
/>
</div>
)
}

View File

@@ -1,148 +0,0 @@
"use client"
import * as React from "react"
import dynamic from "next/dynamic"
type LazyPreviewName =
| "articleDirectory"
| "emptyState"
| "editArticle"
| "mediaLibrary"
| "mediaLibraryTable"
const PREVIEW_MIN_HEIGHTS: Record<LazyPreviewName, number> = {
articleDirectory: 760,
emptyState: 560,
editArticle: 980,
mediaLibrary: 880,
mediaLibraryTable: 980,
}
const ArticleDirectoryPreview = dynamic(
() => import("../article-directory").then((mod) => mod.ArticleDirectory),
{
ssr: false,
loading: () => (
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.articleDirectory} />
),
}
)
const EmptyStatePreview = dynamic(
() => import("../empty-state").then((mod) => mod.EmptyState),
{
ssr: false,
loading: () => (
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.emptyState} />
),
}
)
const EditArticlePreview = dynamic(
() => import("../edit-article").then((mod) => mod.EditArticle),
{
ssr: false,
loading: () => (
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.editArticle} />
),
}
)
const MediaLibraryPreview = dynamic(
() => import("../media-library").then((mod) => mod.MediaLibrary),
{
ssr: false,
loading: () => (
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.mediaLibrary} />
),
}
)
const MediaLibraryTablePreview = dynamic(
() => import("../media-library-table").then((mod) => mod.MediaLibraryTable),
{
ssr: false,
loading: () => (
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS.mediaLibraryTable} />
),
}
)
const PREVIEW_COMPONENTS: Record<LazyPreviewName, React.ComponentType> = {
articleDirectory: ArticleDirectoryPreview,
emptyState: EmptyStatePreview,
editArticle: EditArticlePreview,
mediaLibrary: MediaLibraryPreview,
mediaLibraryTable: MediaLibraryTablePreview,
}
export function LazyPreview({ name }: { name: LazyPreviewName }) {
const containerRef = React.useRef<HTMLDivElement>(null)
const [shouldRender, setShouldRender] = React.useState(false)
const PreviewComponent = PREVIEW_COMPONENTS[name]
React.useEffect(() => {
if (shouldRender) {
return
}
const container = containerRef.current
if (!container || !("IntersectionObserver" in window)) {
setShouldRender(true)
return
}
const observer = new IntersectionObserver(
(entries) => {
if (!entries.some((entry) => entry.isIntersecting)) {
return
}
setShouldRender(true)
observer.disconnect()
},
{
rootMargin: "800px 0px",
}
)
observer.observe(container)
return () => observer.disconnect()
}, [shouldRender])
return (
<div ref={containerRef}>
{shouldRender ? (
<PreviewComponent />
) : (
<PreviewPlaceholder minHeight={PREVIEW_MIN_HEIGHTS[name]} />
)}
</div>
)
}
function PreviewPlaceholder({ minHeight }: { minHeight: number }) {
return (
<div
aria-hidden="true"
className="preview theme-taupe @container/preview w-full flex-1 bg-muted p-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:p-6 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)]"
style={{ minHeight }}
>
<div className="container flex flex-col gap-(--gap) py-(--gap)">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-3">
<div className="h-5 w-44 bg-background/80" />
<div className="h-3 w-56 max-w-full bg-background/60" />
</div>
<div className="hidden h-8 w-28 bg-background/70 sm:block" />
</div>
<div className="grid grid-cols-1 gap-(--gap) md:grid-cols-3">
<div className="min-h-48 bg-background/70 md:col-span-2" />
<div className="min-h-48 bg-background/70" />
</div>
</div>
</div>
)
}

View File

@@ -1,59 +0,0 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const THEME_OPTIONS = [
{ label: "Taupe", value: "theme-taupe" },
{ label: "Neutral", value: "theme-neutral" },
{ label: "Stone", value: "theme-stone" },
{ label: "Zinc", value: "theme-zinc" },
{ label: "Mauve", value: "theme-mauve" },
{ label: "Olive", value: "theme-olive" },
{ label: "Mist", value: "theme-mist" },
] as const
const DEFAULT_THEME = "theme-taupe"
function applyThemeToPreviews(theme: string) {
const previewElements = document.querySelectorAll<HTMLElement>(".preview")
previewElements.forEach((element) => {
THEME_OPTIONS.forEach((option) => {
element.classList.remove(option.value)
})
element.classList.add(theme)
})
}
export function ThemeSwitcher() {
const [theme, setTheme] = React.useState<string>(DEFAULT_THEME)
React.useEffect(() => {
applyThemeToPreviews(theme)
}, [theme])
return (
<div className="fixed inset-x-0 bottom-8 z-50 flex justify-center px-4">
<div className="w-full max-w-[60vw] rounded-full border-0 bg-neutral-950/50 p-1.5 shadow-xl backdrop-blur-xl sm:max-w-fit">
<div className="no-scrollbar flex snap-x snap-mandatory items-center overflow-x-auto">
{THEME_OPTIONS.map((option) => (
<button
data-active={theme === option.value}
key={option.value}
type="button"
onClick={() => {
setTheme(option.value)
}}
className="shrink-0 snap-center rounded-full px-3 py-1.5 text-sm font-medium text-neutral-300 outline-hidden transition-colors select-none hover:text-neutral-100 data-active:bg-neutral-500 data-active:text-neutral-100"
>
{option.label}
</button>
))}
</div>
</div>
</div>
)
}

View File

@@ -1,337 +0,0 @@
"use client"
import {
AlignCenterIcon,
AlignLeftIcon,
AlignRightIcon,
BoldIcon,
ChevronDownIcon,
Code2Icon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
ImageIcon,
ItalicIcon,
LinkIcon,
ListIcon,
ListOrderedIcon,
RedoIcon,
StrikethroughIcon,
TypeIcon,
UnderlineIcon,
UndoIcon,
} from "lucide-react"
import { Button } from "@/styles/base-sera/ui/button"
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@/styles/base-sera/ui/button-group"
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/base-sera/ui/dropdown-menu"
import {
Field,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/styles/base-sera/ui/field"
import { Input } from "@/styles/base-sera/ui/input"
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/styles/base-sera/ui/progress"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/base-sera/ui/select"
import { Textarea } from "@/styles/base-sera/ui/textarea"
type Milestone = {
name: string
complete: boolean
note?: string
}
const MILESTONES: Milestone[] = [
{
name: "Outline & Commissioning",
complete: true,
},
{
name: "First Draft Submitted",
complete: true,
},
{
name: "Review & Revisions",
complete: false,
note: "Waiting on editor",
},
{
name: "Final Copy Edit",
complete: false,
},
{
name: "Art Direction & Layout",
complete: false,
},
]
const ISSUES = [
{ label: "Spring Issue 2024", value: "spring-2024" },
{ label: "Summer Issue 2024", value: "summer-2024" },
{ label: "Autumn Issue 2024", value: "autumn-2024" },
{ label: "Winter Issue 2024", value: "winter-2024" },
]
export function EditorWorkspace() {
return (
<div className="grid grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_300px]">
<section className="flex flex-col border border-border/70 bg-background">
<div className="flex border-b p-2">
<ButtonGroup>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="sm">
Normal Text
<ChevronDownIcon data-icon="inline-end" />
</Button>
}
/>
<DropdownMenuContent>
<DropdownMenuItem>
<TypeIcon />
Normal Text
</DropdownMenuItem>
<DropdownMenuItem>
<Heading1Icon />
Heading 1
</DropdownMenuItem>
<DropdownMenuItem>
<Heading2Icon />
Heading 2
</DropdownMenuItem>
<DropdownMenuItem>
<Heading3Icon />
Heading 3
</DropdownMenuItem>
<DropdownMenuItem>
<ListIcon />
Bullet List
</DropdownMenuItem>
<DropdownMenuItem>
<ListOrderedIcon />
Numbered List
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ButtonGroupSeparator className="mx-2 data-vertical:h-4 data-vertical:self-center" />
<Button variant="ghost" size="icon-sm" aria-label="Bold">
<BoldIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Italic">
<ItalicIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Underline">
<UnderlineIcon />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Strikethrough"
className="hidden md:flex"
>
<StrikethroughIcon />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Code"
className="hidden md:flex"
>
<Code2Icon />
</Button>
<ButtonGroupSeparator className="mx-2 hidden md:flex data-vertical:h-4 data-vertical:self-center" />
<Button
variant="ghost"
size="icon-sm"
aria-label="Align Left"
className="hidden md:flex"
>
<AlignLeftIcon />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Align Center"
className="hidden md:flex"
>
<AlignCenterIcon />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Align Right"
className="hidden md:flex"
>
<AlignRightIcon />
</Button>
<ButtonGroupSeparator className="mx-2 hidden md:flex data-vertical:h-4 data-vertical:self-center" />
<Button
variant="ghost"
size="icon-sm"
aria-label="Link"
className="hidden md:flex"
>
<LinkIcon />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Image"
className="hidden md:flex"
>
<ImageIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="ml-auto">
<Button variant="ghost" size="icon-sm" aria-label="Undo">
<UndoIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Redo">
<RedoIcon />
</Button>
</ButtonGroup>
</div>
<div className="mx-auto flex max-w-2xl flex-1 flex-col gap-8 px-10 py-10 leading-loose md:px-14 lg:py-18">
<h1 className="font-heading text-4xl leading-12 font-medium tracking-wide uppercase">
The Future of Sustainable Architecture
</h1>
<p>
As cities continue to expand at an unprecedented rate, the
architectural paradigm is shifting from mere expansion to
sustainable integration. The concrete jungles of the 20th century
are making way for structures that breathe, adapt, and give back to
their environments.
</p>
<p>
Historically, urban development has been a zero-sum game with
nature.
</p>
<h2 className="font-heading text-2xl tracking-wide uppercase">
The Living Building Challenge
</h2>
<p>
Sterling&apos;s latest project in downtown Seattle is a testament to
this new philosophy. &quot;We are no longer designing static
structures,&quot; Sterling explained during a recent site visit.
&quot;We are engineering localized ecosystems.&quot;
</p>
<p>
The building features a facade of responsive biomaterials that
adjust their porosity based on humidity and temperature,
significantly reducing the need for artificial climate control.
Rainwater is not merely channeled away but captured, filtered
through a series of integrated rooftop wetlands, and reused within
the building&apos;s greywater system.
</p>
<p className="text-sm text-muted-foreground">
This shift requires more than just innovative materials; it demands
a fundamental change in how we value space. Check with engineering
team for specific stats.
</p>
</div>
</section>
<aside className="grid grid-cols-12 gap-(--gap) xl:flex xl:flex-col">
<Card className="col-span-full md:col-span-6 lg:col-span-4">
<CardHeader>
<CardTitle>Article Details</CardTitle>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel>Issue</FieldLabel>
<Select items={ISSUES} defaultValue="summer-2024">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{ISSUES.map((issue) => (
<SelectItem key={issue.value} value={issue.value}>
{issue.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel>Author</FieldLabel>
<Input defaultValue="Elena Rostova" />
</Field>
</FieldGroup>
</CardContent>
</Card>
<Card className="col-span-full md:col-span-6 lg:col-span-4">
<CardHeader>
<CardTitle>Publication Flow</CardTitle>
</CardHeader>
<CardContent>
<FieldGroup>
<FieldSet>
<FieldLegend>Required Milestones</FieldLegend>
<Field>
{MILESTONES.map((milestone) => (
<Field key={milestone.name} orientation="horizontal">
<Checkbox
defaultChecked={milestone.complete}
name={milestone.name}
id={milestone.name}
/>
<FieldLabel htmlFor={milestone.name}>
{milestone.name}
</FieldLabel>
</Field>
))}
</Field>
</FieldSet>
<Field>
<FieldLabel>Add note for editor</FieldLabel>
<Textarea placeholder="This article needs to be revised for clarity and accuracy." />
</Field>
</FieldGroup>
</CardContent>
</Card>
<Card className="col-span-full lg:col-span-4">
<CardHeader>
<CardTitle>Word Count</CardTitle>
</CardHeader>
<CardContent>
<Progress value={70}>
<ProgressLabel>1,402 / 2,000 words</ProgressLabel>
<ProgressValue />
</Progress>
</CardContent>
</Card>
</aside>
</div>
)
}

View File

@@ -1,45 +0,0 @@
import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react"
import { Badge } from "@/styles/base-sera/ui/badge"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
} from "@/styles/base-sera/ui/breadcrumb"
import { Button } from "@/styles/base-sera/ui/button"
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
export function PreviewHeader() {
return (
<header>
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
<ArrowLeftIcon className="size-3.5" />
Back to articles
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
EDIT ARTICLE
</h1>
</div>
<ButtonGroup className="gap-2 md:gap-4">
<Badge title="2 minutes ago">Autosaved</Badge>
<ButtonGroup className="gap-2 md:gap-4">
<Button variant="link">
Preview
<ExternalLinkIcon data-icon="inline-end" />
</Button>
<Button>Submit Draft</Button>
</ButtonGroup>
</ButtonGroup>
</div>
</header>
)
}

View File

@@ -1,16 +0,0 @@
import { Separator } from "@/styles/base-sera/ui/separator"
import { EditorWorkspace } from "./components/editor-workspace"
import { PreviewHeader } from "./components/preview-header"
export function EditArticle() {
return (
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
<PreviewHeader />
<Separator className="hidden sm:block" />
<div className="container py-(--gap)">
<EditorWorkspace />
</div>
</div>
)
}

View File

@@ -1,95 +0,0 @@
import { FileTextIcon, PlusIcon } from "lucide-react"
import { Badge } from "@/styles/base-sera/ui/badge"
import { Button } from "@/styles/base-sera/ui/button"
import { Card, CardContent } from "@/styles/base-sera/ui/card"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/base-sera/ui/empty"
import { Separator } from "@/styles/base-sera/ui/separator"
type Stage = {
id: string
label: string
description: string
dotClassName: string
}
const STAGES: Stage[] = [
{
id: "drafting",
label: "Drafting",
description:
"Start the writing process. Articles here are works in progress, visible only to editors and authors.",
dotClassName: "bg-amber-600",
},
{
id: "in-revision",
label: "In Revision",
description:
"Content undergoing editorial review. Track changes and word counts as pieces take shape.",
dotClassName: "bg-orange-700",
},
{
id: "final-edit",
label: "Final Edit",
description:
"The final polish before publication. Ensure all styling and factual checks are complete.",
dotClassName: "bg-foreground",
},
]
export function EmptyDirectory() {
return (
<Card className="py-24">
<CardContent className="flex flex-col items-center gap-10">
<Empty className="min-h-96">
<EmptyHeader>
<EmptyMedia
variant="icon"
className="size-14 rounded-full bg-muted/70 text-muted-foreground"
>
<FileTextIcon className="size-5" />
</EmptyMedia>
<EmptyTitle className="font-heading text-2xl tracking-normal normal-case">
A Blank Canvas
</EmptyTitle>
<EmptyDescription>
Your editorial directory is currently empty. Start building your
publication&apos;s next issue by drafting the first piece.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>
<PlusIcon data-icon="inline-start" />
Create first article
</Button>
</EmptyContent>
</Empty>
<Separator className="max-w-2xl" />
<div className="grid w-full max-w-2xl grid-cols-1 gap-8 sm:grid-cols-3">
{STAGES.map((stage) => (
<div key={stage.id} className="flex flex-col gap-2">
<Badge>
<span
aria-hidden
className={`size-1.5 rounded-full ${stage.dotClassName}`}
/>
{stage.label}
</Badge>
<p className="text-xs leading-relaxed text-muted-foreground">
{stage.description}
</p>
</div>
))}
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,37 +0,0 @@
import { ArrowLeftIcon, PlusIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
} from "@/styles/base-sera/ui/breadcrumb"
import { Button } from "@/styles/base-sera/ui/button"
export function PreviewHeader() {
return (
<header>
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
<ArrowLeftIcon className="size-3.5" />
Editorial Dashboard
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
Article Directory
</h1>
</div>
<Button className="sm:ml-auto">
<PlusIcon data-icon="inline-start" />
New Article
</Button>
</div>
</header>
)
}

View File

@@ -1,16 +0,0 @@
import { Separator } from "@/styles/base-sera/ui/separator"
import { EmptyDirectory } from "./components/empty-directory"
import { PreviewHeader } from "./components/preview-header"
export function EmptyState() {
return (
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
<PreviewHeader />
<Separator className="hidden sm:block" />
<div className="container py-(--gap)">
<EmptyDirectory />
</div>
</div>
)
}

View File

@@ -1,211 +0,0 @@
"use client"
import * as React from "react"
import {
FileTextIcon,
ImageIcon,
MoreVerticalIcon,
SearchIcon,
VideoIcon,
} from "lucide-react"
import { Badge } from "@/styles/base-sera/ui/badge"
import { Button } from "@/styles/base-sera/ui/button"
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-sera/ui/card"
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/base-sera/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/styles/base-sera/ui/input-group"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/styles/base-sera/ui/pagination"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/styles/base-sera/ui/table"
import { ASSETS, type AssetType } from "../../media-library/data"
function AssetTypeIcon({
type,
className,
}: {
type: AssetType
className?: string
}) {
if (type === "MP4") {
return <VideoIcon className={className} />
}
if (type === "PDF") {
return <FileTextIcon className={className} />
}
return <ImageIcon className={className} />
}
export function AssetTable() {
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(
new Set(["1"])
)
const toggleSelection = React.useCallback((id: string) => {
setSelectedIds((previous) => {
const next = new Set(previous)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}, [])
return (
<Card>
<CardHeader>
<InputGroup className="w-full">
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search files, tags, or metadata..." />
</InputGroup>
</CardHeader>
<CardContent className="px-0 py-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-10 pl-6" aria-label="Select" />
<TableHead className="w-20" aria-label="Preview" />
<TableHead>Filename</TableHead>
<TableHead>Type</TableHead>
<TableHead>Dimensions</TableHead>
<TableHead>Size</TableHead>
<TableHead>Uploaded By</TableHead>
<TableHead>Date</TableHead>
<TableHead className="w-10 pr-6" aria-label="Actions" />
</TableRow>
</TableHeader>
<TableBody>
{ASSETS.map((asset) => {
const isSelected = selectedIds.has(asset.id)
return (
<TableRow
key={asset.id}
data-state={isSelected ? "selected" : undefined}
className="cursor-pointer"
onClick={() => toggleSelection(asset.id)}
>
<TableCell className="pl-6">
<Checkbox
checked={isSelected}
aria-label={`Select ${asset.name}`}
onClick={(event) => event.stopPropagation()}
onCheckedChange={() => toggleSelection(asset.id)}
/>
</TableCell>
<TableCell>
<div className="relative flex aspect-4/3 w-16 items-center justify-center bg-muted/60 ring-1 ring-border/70 ring-inset">
{asset.duration ? (
<span className="absolute right-1 bottom-1 bg-foreground/90 px-1 text-[0.5rem] font-semibold tracking-wider text-background">
{asset.duration}
</span>
) : null}
<AssetTypeIcon
type={asset.type}
className="size-4 text-muted-foreground/60"
/>
</div>
</TableCell>
<TableCell className="text-sm font-medium text-foreground">
{asset.name}
</TableCell>
<TableCell>
<Badge
variant="outline"
className="border px-2 py-0.5 text-[0.625rem]"
>
{asset.type}
</Badge>
</TableCell>
<TableCell className="text-sm">{asset.dimensions}</TableCell>
<TableCell className="text-sm">{asset.size}</TableCell>
<TableCell>{asset.uploadedBy}</TableCell>
<TableCell className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
{asset.date}
</TableCell>
<TableCell className="pr-6 text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="ghost" size="icon-xs" />}
aria-label={`Open actions for ${asset.name}`}
onClick={(event) => event.stopPropagation()}
>
<MoreVerticalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Preview</DropdownMenuItem>
<DropdownMenuItem>Download</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</CardContent>
<CardFooter className="justify-center py-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" text="" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
1
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" text="" />
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
)
}

View File

@@ -1,233 +0,0 @@
"use client"
import * as React from "react"
import { addDays, format } from "date-fns"
import { CalendarIcon, FilterIcon, XIcon } from "lucide-react"
import { type DateRange } from "react-day-picker"
import { Button } from "@/styles/base-sera/ui/button"
import { Calendar } from "@/styles/base-sera/ui/calendar"
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-sera/ui/card"
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from "@/styles/base-sera/ui/combobox"
import {
Field,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/styles/base-sera/ui/field"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/base-sera/ui/popover"
import { RadioGroup, RadioGroupItem } from "@/styles/base-sera/ui/radio-group"
import { Slider } from "@/styles/base-sera/ui/slider"
const FILE_TYPES = [
{
id: "images",
label: "Images (JPEG, PNG, WEBP)",
defaultChecked: true,
},
{
id: "video",
label: "Video (MP4, MOV)",
defaultChecked: false,
},
{
id: "documents",
label: "Documents (PDF)",
defaultChecked: false,
},
{
id: "audio",
label: "Audio (MP3, WAV)",
defaultChecked: false,
},
]
const DATE_OPTIONS = [
{ value: "any", label: "Any time" },
{ value: "24h", label: "Past 24 hours" },
{ value: "week", label: "Past week" },
{ value: "month", label: "Past month" },
]
const TAGS = [
"architecture",
"brutalism",
"ceramics",
"design-week",
"editorial",
"exterior",
"film",
"food",
"furniture",
"interior",
"kyoto",
"minimalism",
"print",
"sustainability",
"summer-issue",
"video",
] as const
export function FilterLibrary() {
const tagAnchor = useComboboxAnchor()
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
to: addDays(
new Date(new Date().getFullYear(), new Date().getMonth(), 1),
21
),
})
return (
<Card>
<CardHeader className="border-b">
<CardTitle>Filter Library</CardTitle>
</CardHeader>
<CardContent>
<FieldGroup>
<FieldSet>
<FieldLegend>File Type</FieldLegend>
<Field>
{FILE_TYPES.map((type) => (
<Field key={type.id} orientation="horizontal">
<Checkbox id={type.id} defaultChecked={type.defaultChecked} />
<FieldLabel htmlFor={type.id}>{type.label}</FieldLabel>
</Field>
))}
</Field>
</FieldSet>
<FieldSet>
<FieldLegend>Date Uploaded</FieldLegend>
<RadioGroup defaultValue="any">
{DATE_OPTIONS.map((option) => (
<Field key={option.value} orientation="horizontal">
<RadioGroupItem
value={option.value}
id={`date-${option.value}`}
/>
<FieldLabel htmlFor={`date-${option.value}`}>
{option.label}
</FieldLabel>
</Field>
))}
</RadioGroup>
</FieldSet>
<Field>
<FieldLabel htmlFor="custom-range">Custom Range</FieldLabel>
<Popover>
<PopoverTrigger
render={
<Button
variant="outline"
id="custom-range"
className="w-full"
/>
}
>
<CalendarIcon data-icon="inline-start" />
{dateRange?.from ? (
dateRange.to ? (
<>
{format(dateRange.from, "LLL dd, y")} {" "}
{format(dateRange.to, "LLL dd, y")}
</>
) : (
format(dateRange.from, "LLL dd, y")
)
) : (
<span>Pick a date range</span>
)}
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
mode="range"
defaultMonth={dateRange?.from}
selected={dateRange}
onSelect={setDateRange}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</Field>
<FieldSet>
<FieldLegend>File Size</FieldLegend>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between text-xs font-medium tracking-wider text-muted-foreground uppercase">
<span>0 MB</span>
<span>500+ MB</span>
</div>
<Slider defaultValue={[0, 60]} max={100} step={1} />
<div className="flex items-center justify-between text-xs font-medium">
<span>Min: 0 MB</span>
<span>Max: 300 MB</span>
</div>
</div>
</FieldSet>
<FieldSet>
<FieldLegend>Tags</FieldLegend>
<Field>
<Combobox
multiple
autoHighlight
items={TAGS}
defaultValue={["architecture", "brutalism"]}
>
<ComboboxChips ref={tagAnchor}>
<ComboboxValue>
{(values) => (
<React.Fragment>
{values.map((value: string) => (
<ComboboxChip key={value}>{value}</ComboboxChip>
))}
<ComboboxChipsInput placeholder="Filter by tag..." />
</React.Fragment>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={tagAnchor}>
<ComboboxEmpty>No tags found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
</FieldSet>
</FieldGroup>
</CardContent>
<CardFooter className="flex flex-col gap-2 border-t">
<Button className="w-full">Apply Filters</Button>
<Button variant="ghost" className="w-full">
Reset
</Button>
</CardFooter>
</Card>
)
}

View File

@@ -1,47 +0,0 @@
import { ArrowLeftIcon, SlidersHorizontalIcon, UploadIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
} from "@/styles/base-sera/ui/breadcrumb"
import { Button } from "@/styles/base-sera/ui/button"
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
export function PreviewHeader() {
return (
<header>
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
<ArrowLeftIcon className="size-3.5" />
Asset management
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
Media Library
</h1>
</div>
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
<Button
variant="outline"
className="bg-background hover:bg-background/80"
>
<SlidersHorizontalIcon data-icon="inline-start" />
Filters
</Button>
<Button>
<UploadIcon data-icon="inline-start" />
Upload Assets
</Button>
</ButtonGroup>
</div>
</header>
)
}

View File

@@ -1,18 +0,0 @@
import { Separator } from "@/styles/base-sera/ui/separator"
import { AssetTable } from "./components/asset-table"
import { FilterLibrary } from "./components/filter-library"
import { PreviewHeader } from "./components/preview-header"
export function MediaLibraryTable() {
return (
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
<PreviewHeader />
<Separator className="hidden sm:block" />
<div className="container grid grid-cols-1 items-start gap-(--gap) py-(--gap) xl:grid-cols-[minmax(0,1fr)_320px]">
<AssetTable />
<FilterLibrary />
</div>
</div>
)
}

View File

@@ -1,110 +0,0 @@
import {
DownloadIcon,
FileTextIcon,
ImageIcon,
PlusIcon,
VideoIcon,
} from "lucide-react"
import { Badge } from "@/styles/base-sera/ui/badge"
import { Button } from "@/styles/base-sera/ui/button"
import { Card, CardContent, CardFooter } from "@/styles/base-sera/ui/card"
import {
Item,
ItemContent,
ItemDescription,
ItemTitle,
} from "@/styles/base-sera/ui/item"
import { Separator } from "@/styles/base-sera/ui/separator"
import { type Asset, type AssetType } from "../data"
const TYPE_LABEL: Record<AssetType, string> = {
JPEG: "Image / JPEG",
PNG: "Image / PNG",
WEBP: "Image / WEBP",
MP4: "Video / MP4",
PDF: "Document / PDF",
}
export function AssetDetails({ asset }: { asset: Asset }) {
return (
<Card className="gap-0">
<CardContent className="flex flex-col gap-6">
<div className="flex aspect-5/4 items-center justify-center bg-muted/60 text-muted-foreground/60 ring-1 ring-border/70 ring-inset">
{asset.type === "MP4" ? (
<VideoIcon className="size-8" />
) : asset.type === "PDF" ? (
<FileTextIcon className="size-8" />
) : (
<ImageIcon className="size-8" />
)}
</div>
<h2 className="line-clamp-2 font-heading text-xl tracking-wide">
{asset.name}
</h2>
<Separator />
<dl className="flex flex-col gap-5 text-sm">
<div className="flex flex-col gap-1.5">
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
Asset Type
</dt>
<dd>{TYPE_LABEL[asset.type]}</dd>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
Dimensions
</dt>
<dd>{asset.dimensions}</dd>
</div>
<div className="flex flex-col gap-1.5">
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
File Size
</dt>
<dd>{asset.size}</dd>
</div>
</div>
</dl>
<Separator />
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
Tags
</h3>
<Button variant="ghost" size="icon-xs" aria-label="Add tag">
<PlusIcon />
</Button>
</div>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{asset.tags.map((tag) => (
<Badge key={tag}>{tag}</Badge>
))}
</div>
</div>
<Separator />
<div className="flex flex-col gap-3">
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
Used In
</h3>
<div className="flex flex-col gap-2">
{asset.usedIn.map((usage) => (
<Item key={usage.title} variant="outline">
<ItemContent>
<ItemTitle>{usage.title}</ItemTitle>
<ItemDescription>{usage.role}</ItemDescription>
</ItemContent>
</Item>
))}
</div>
</div>
</CardContent>
<CardFooter className="mt-6 border-t pt-6">
<Button className="w-full">
<DownloadIcon data-icon="inline-start" />
Download
</Button>
</CardFooter>
</Card>
)
}

View File

@@ -1,160 +0,0 @@
"use client"
import {
CheckIcon,
FileTextIcon,
ImageIcon,
SearchIcon,
VideoIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/styles/base-sera/ui/badge"
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-sera/ui/card"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/styles/base-sera/ui/input-group"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/styles/base-sera/ui/pagination"
import { ASSETS, type Asset, type AssetType } from "../data"
function AssetTypeIcon({
type,
className,
}: {
type: AssetType
className?: string
}) {
if (type === "MP4") {
return <VideoIcon className={className} />
}
if (type === "PDF") {
return <FileTextIcon className={className} />
}
return <ImageIcon className={className} />
}
export function AssetGrid({
selectedId,
onSelect,
}: {
selectedId: string
onSelect: (id: string) => void
}) {
return (
<Card>
<CardHeader>
<InputGroup className="w-full">
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search files, tags, or metadata..." />
</InputGroup>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{ASSETS.map((asset) => (
<AssetGridItem
key={asset.id}
asset={asset}
selected={asset.id === selectedId}
onSelect={() => onSelect(asset.id)}
/>
))}
</div>
</CardContent>
<CardFooter className="justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
1
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
)
}
function AssetGridItem({
asset,
selected,
onSelect,
}: {
asset: Asset
selected: boolean
onSelect: () => void
}) {
return (
<button
type="button"
onClick={onSelect}
aria-pressed={selected}
className="group flex flex-col gap-2.5 text-left outline-none focus-visible:[&>div:first-child]:ring-2 focus-visible:[&>div:first-child]:ring-ring"
>
<div
className={cn(
"relative flex aspect-4/3 items-center justify-center bg-muted/60 ring-1 ring-border/70 transition-shadow ring-inset group-hover:ring-foreground/40",
selected && "ring-2 ring-foreground group-hover:ring-foreground"
)}
>
{selected ? (
<div className="absolute top-2 left-2 flex size-5 items-center justify-center bg-foreground text-background">
<CheckIcon className="size-3" />
</div>
) : null}
<Badge
variant="outline"
className="absolute top-2 right-2 border bg-background px-2 py-1 text-[0.625rem]"
>
{asset.type}
</Badge>
{asset.duration ? (
<Badge className="absolute bottom-2 left-2 bg-foreground px-2 py-1 text-background">
{asset.duration}
</Badge>
) : null}
<AssetTypeIcon
type={asset.type}
className="size-7 text-muted-foreground/60"
/>
</div>
<div className="flex flex-col gap-0.5 px-0.5">
<p className="line-clamp-1 text-sm font-medium">{asset.name}</p>
<p className="text-[0.625rem] font-semibold tracking-wider text-muted-foreground uppercase">
{asset.date} · {asset.size}
</p>
</div>
</button>
)
}

View File

@@ -1,47 +0,0 @@
import { ArrowLeftIcon, SlidersHorizontalIcon, UploadIcon } from "lucide-react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
} from "@/styles/base-sera/ui/breadcrumb"
import { Button } from "@/styles/base-sera/ui/button"
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
export function PreviewHeader() {
return (
<header>
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2 text-center sm:text-left">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
<ArrowLeftIcon className="size-3.5" />
Asset management
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
Media Library
</h1>
</div>
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
<Button
variant="outline"
className="bg-background hover:bg-background/80"
>
<SlidersHorizontalIcon data-icon="inline-start" />
Filters
</Button>
<Button>
<UploadIcon data-icon="inline-start" />
Upload Assets
</Button>
</ButtonGroup>
</div>
</header>
)
}

View File

@@ -1,188 +0,0 @@
export type AssetType = "JPEG" | "PNG" | "WEBP" | "MP4" | "PDF"
export type Asset = {
id: string
name: string
date: string
size: string
type: AssetType
dimensions: string
duration?: string
uploadedBy: string
uploadedByInitials: string
uploadedOn: string
tags: string[]
usedIn: { title: string; role: string }[]
}
export const ASSETS: Asset[] = [
{
id: "1",
name: "brutalism-facade-01.jpg",
date: "Oct 24",
size: "4.2 MB",
type: "JPEG",
dimensions: "4000 × 3000",
uploadedBy: "Marcus Chen",
uploadedByInitials: "MC",
uploadedOn: "Oct 24, 2024",
tags: ["architecture", "brutalism", "exterior", "summer-issue"],
usedIn: [
{ title: "Brutalism's Second Act", role: "Cover Image" },
{ title: "Autumn Sartorial Code", role: "Inline Gallery" },
],
},
{
id: "2",
name: "brutalism-interior-raw.jpg",
date: "Oct 24",
size: "3.8 MB",
type: "JPEG",
dimensions: "3800 × 2850",
uploadedBy: "Marcus Chen",
uploadedByInitials: "MC",
uploadedOn: "Oct 24, 2024",
tags: ["architecture", "brutalism", "interior"],
usedIn: [{ title: "Brutalism's Second Act", role: "Inline Gallery" }],
},
{
id: "3",
name: "seattle-living-building-diagram.png",
date: "Oct 22",
size: "1.1 MB",
type: "PNG",
dimensions: "2000 × 1500",
uploadedBy: "Sarah Jenkins",
uploadedByInitials: "SJ",
uploadedOn: "Oct 22, 2024",
tags: ["diagram", "sustainability", "seattle"],
usedIn: [
{ title: "The Future of Sustainable Architecture", role: "Diagram" },
],
},
{
id: "4",
name: "interview-sofia-coppola-clip1.mp4",
date: "Oct 18",
size: "45.0 MB",
type: "MP4",
dimensions: "1920 × 1080",
duration: "0:45",
uploadedBy: "Emma Ross",
uploadedByInitials: "ER",
uploadedOn: "Oct 18, 2024",
tags: ["video", "interview", "film"],
usedIn: [{ title: "The Aesthetics of Isolation", role: "Featured Video" }],
},
{
id: "5",
name: "kyoto-kilns-pottery-detail.jpg",
date: "Oct 15",
size: "5.6 MB",
type: "JPEG",
dimensions: "4500 × 3000",
uploadedBy: "Marcus Chen",
uploadedByInitials: "MC",
uploadedOn: "Oct 15, 2024",
tags: ["ceramics", "kyoto", "craft"],
usedIn: [{ title: "Kyoto's Oldest Kilns", role: "Hero Image" }],
},
{
id: "6",
name: "copenhagen-design-week-street.jpg",
date: "Oct 12",
size: "3.2 MB",
type: "JPEG",
dimensions: "3600 × 2400",
uploadedBy: "Noah Bennett",
uploadedByInitials: "NB",
uploadedOn: "Oct 12, 2024",
tags: ["copenhagen", "design-week", "street"],
usedIn: [{ title: "Field Notes from Copenhagen", role: "Inline Gallery" }],
},
{
id: "7",
name: "minimalist-chair-render.webp",
date: "Oct 10",
size: "0.8 MB",
type: "WEBP",
dimensions: "2400 × 1600",
uploadedBy: "Claire Duval",
uploadedByInitials: "CD",
uploadedOn: "Oct 10, 2024",
tags: ["furniture", "minimalism", "render"],
usedIn: [{ title: "The New Vanguard", role: "Product Shot" }],
},
{
id: "8",
name: "autumn-issue-style-guide.pdf",
date: "Oct 05",
size: "12.4 MB",
type: "PDF",
dimensions: "N/A",
uploadedBy: "Emma Ross",
uploadedByInitials: "ER",
uploadedOn: "Oct 05, 2024",
tags: ["guidelines", "internal", "autumn"],
usedIn: [{ title: "Autumn Issue 2024", role: "Reference" }],
},
{
id: "9",
name: "milan-lighting-studio-visit.jpg",
date: "Oct 09",
size: "6.1 MB",
type: "JPEG",
dimensions: "5200 × 3466",
uploadedBy: "Claire Duval",
uploadedByInitials: "CD",
uploadedOn: "Oct 09, 2024",
tags: ["milan", "lighting", "studio"],
usedIn: [
{ title: "Milan's Most Elusive Lighting Designer", role: "Hero Image" },
],
},
{
id: "10",
name: "lisbon-culinary-scene-raw.webp",
date: "Oct 14",
size: "2.4 MB",
type: "WEBP",
dimensions: "3000 × 2000",
uploadedBy: "Amara Iqbal",
uploadedByInitials: "AI",
uploadedOn: "Oct 14, 2024",
tags: ["lisbon", "food", "editorial"],
usedIn: [
{ title: "Lisbon's Quiet Culinary Renaissance", role: "Inline Gallery" },
],
},
{
id: "11",
name: "print-magazine-covers-mo...",
date: "Sep 26",
size: "8.9 MB",
type: "PNG",
dimensions: "3200 × 2400",
uploadedBy: "Mina Okafor",
uploadedByInitials: "MO",
uploadedOn: "Sep 26, 2024",
tags: ["print", "magazine", "covers"],
usedIn: [{ title: "The Return of Print", role: "Cover Image" }],
},
{
id: "12",
name: "avant-garde-furniture-trailer.mp4",
date: "Sep 30",
size: "78.2 MB",
type: "MP4",
dimensions: "3840 × 2160",
duration: "1:12",
uploadedBy: "Tommy Rhodes",
uploadedByInitials: "TR",
uploadedOn: "Sep 30, 2024",
tags: ["video", "furniture", "trailer"],
usedIn: [
{ title: "Collecting the New Avant-Garde", role: "Featured Video" },
],
},
]

View File

@@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import { Separator } from "@/styles/base-sera/ui/separator"
import { AssetDetails } from "./components/asset-details"
import { AssetGrid } from "./components/asset-grid"
import { PreviewHeader } from "./components/preview-header"
import { ASSETS } from "./data"
export function MediaLibrary() {
const [selectedId, setSelectedId] = React.useState<string>(ASSETS[0].id)
const selectedAsset = React.useMemo(
() => ASSETS.find((asset) => asset.id === selectedId) ?? ASSETS[0],
[selectedId]
)
return (
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
<PreviewHeader />
<Separator className="hidden sm:block" />
<div className="container grid grid-cols-1 items-start gap-(--gap) py-(--gap) xl:grid-cols-[minmax(0,1fr)_320px]">
<AssetGrid selectedId={selectedId} onSelect={setSelectedId} />
<AssetDetails asset={selectedAsset} />
</div>
</div>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -1,72 +0,0 @@
import { type Metadata } from "next"
import Link from "next/link"
import {
PageActions,
PageHeader,
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header"
import { Button } from "@/styles/radix-sera/ui/button"
import { AudienceAnalytics } from "./audience-analytics"
import { LazyPreview } from "./components/lazy-preview"
import "./style.css"
import { ArrowRightIcon } from "lucide-react"
import { ImagePreview } from "./components/image-preview"
const title = "Introducing Sera"
const description =
"Minimal. Editorial. Typographic. Underline Controls and Uppercase Headings. Shaped by Print Design Principles."
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
},
twitter: {
card: "summary_large_image",
title,
description,
},
}
export default function SeraPage() {
return (
<>
<PageHeader>
<PageHeaderHeading className="font-(family-name:--font-playfair-display) text-[2.875rem] tracking-tight!">
{title}
</PageHeaderHeading>
<PageHeaderDescription className="max-w-2xl text-pretty md:text-balance">
{description}
</PageHeaderDescription>
<PageActions className="**:[.container]:justify-start">
<Button asChild size="sm">
<Link href="/create?preset=b4xFeBLg4O">
Open in shadcn/create
<ArrowRightIcon data-icon="inline-end" />
</Link>
</Button>
</PageActions>
</PageHeader>
<ImagePreview />
<div className="container-wrapper hidden flex-1 flex-col section-soft px-0 md:flex md:px-2 md:py-12">
<div className="container flex flex-1 flex-col gap-10 px-0 3xl:max-w-[2000px] md:px-6">
<AudienceAnalytics />
<LazyPreview name="articleDirectory" />
<LazyPreview name="emptyState" />
<LazyPreview name="editArticle" />
<LazyPreview name="mediaLibrary" />
<LazyPreview name="mediaLibraryTable" />
</div>
</div>
{/* <ThemeSwitcher /> */}
</>
)
}

View File

@@ -1,495 +0,0 @@
@layer base {
.preview {
--font-sans: var(--font-noto-sans);
--font-heading: var(--font-playfair-display);
contain-intrinsic-size: auto 900px;
content-visibility: auto;
}
.theme-taupe {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.3);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.3);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.3);
--primary: oklch(0.214 0.009 43.1);
--primary-foreground: oklch(0.986 0.002 67.8);
--secondary: oklch(0.96 0.002 17.2);
--secondary-foreground: oklch(0.214 0.009 43.1);
--muted: oklch(0.96 0.002 17.2);
--muted-foreground: oklch(0.547 0.021 43.1);
--accent: oklch(0.96 0.002 17.2);
--accent-foreground: oklch(0.214 0.009 43.1);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0.005 34.3);
--input: oklch(0.922 0.005 34.3);
--ring: oklch(0.714 0.014 41.2);
--chart-1: oklch(0.868 0.007 39.5);
--chart-2: oklch(0.547 0.021 43.1);
--chart-3: oklch(0.438 0.017 39.3);
--chart-4: oklch(0.367 0.016 35.7);
--chart-5: oklch(0.268 0.011 36.5);
--sidebar: oklch(0.986 0.002 67.8);
--sidebar-foreground: oklch(0.147 0.004 49.3);
--sidebar-primary: oklch(0.214 0.009 43.1);
--sidebar-primary-foreground: oklch(0.986 0.002 67.8);
--sidebar-accent: oklch(0.96 0.002 17.2);
--sidebar-accent-foreground: oklch(0.214 0.009 43.1);
--sidebar-border: oklch(0.922 0.005 34.3);
--sidebar-ring: oklch(0.714 0.014 41.2);
.dark & {
--background: oklch(0.147 0.004 49.3);
--foreground: oklch(0.986 0.002 67.8);
--card: oklch(0.214 0.009 43.1);
--card-foreground: oklch(0.986 0.002 67.8);
--popover: oklch(0.214 0.009 43.1);
--popover-foreground: oklch(0.986 0.002 67.8);
--primary: oklch(0.922 0.005 34.3);
--primary-foreground: oklch(0.214 0.009 43.1);
--secondary: oklch(0.268 0.011 36.5);
--secondary-foreground: oklch(0.986 0.002 67.8);
--muted: oklch(0.268 0.011 36.5);
--muted-foreground: oklch(0.714 0.014 41.2);
--accent: oklch(0.268 0.011 36.5);
--accent-foreground: oklch(0.986 0.002 67.8);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.547 0.021 43.1);
--chart-1: oklch(0.868 0.007 39.5);
--chart-2: oklch(0.547 0.021 43.1);
--chart-3: oklch(0.438 0.017 39.3);
--chart-4: oklch(0.367 0.016 35.7);
--chart-5: oklch(0.268 0.011 36.5);
--sidebar: oklch(0.214 0.009 43.1);
--sidebar-foreground: oklch(0.986 0.002 67.8);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.986 0.002 67.8);
--sidebar-accent: oklch(0.268 0.011 36.5);
--sidebar-accent-foreground: oklch(0.986 0.002 67.8);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.547 0.021 43.1);
}
}
.theme-neutral {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
.dark & {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
}
.theme-stone {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.709 0.01 56.259);
--chart-1: oklch(0.869 0.005 56.366);
--chart-2: oklch(0.553 0.013 58.071);
--chart-3: oklch(0.444 0.011 73.639);
--chart-4: oklch(0.374 0.01 67.558);
--chart-5: oklch(0.268 0.007 34.298);
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.216 0.006 56.043);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.709 0.01 56.259);
.dark & {
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.216 0.006 56.043);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.553 0.013 58.071);
--chart-1: oklch(0.869 0.005 56.366);
--chart-2: oklch(0.553 0.013 58.071);
--chart-3: oklch(0.444 0.011 73.639);
--chart-4: oklch(0.374 0.01 67.558);
--chart-5: oklch(0.268 0.007 34.298);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.553 0.013 58.071);
}
}
.theme-zinc {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
--chart-4: oklch(0.37 0.013 285.805);
--chart-5: oklch(0.274 0.006 286.033);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
.dark & {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
--chart-4: oklch(0.37 0.013 285.805);
--chart-5: oklch(0.274 0.006 286.033);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
}
.theme-mauve {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0.008 326);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0.008 326);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0.008 326);
--primary: oklch(0.212 0.019 322.12);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.96 0.003 325.6);
--secondary-foreground: oklch(0.212 0.019 322.12);
--muted: oklch(0.96 0.003 325.6);
--muted-foreground: oklch(0.542 0.034 322.5);
--accent: oklch(0.96 0.003 325.6);
--accent-foreground: oklch(0.212 0.019 322.12);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0.005 325.62);
--input: oklch(0.922 0.005 325.62);
--ring: oklch(0.711 0.019 323.02);
--chart-1: oklch(0.865 0.012 325.68);
--chart-2: oklch(0.542 0.034 322.5);
--chart-3: oklch(0.435 0.029 321.78);
--chart-4: oklch(0.364 0.029 323.89);
--chart-5: oklch(0.263 0.024 320.12);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0.008 326);
--sidebar-primary: oklch(0.212 0.019 322.12);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.96 0.003 325.6);
--sidebar-accent-foreground: oklch(0.212 0.019 322.12);
--sidebar-border: oklch(0.922 0.005 325.62);
--sidebar-ring: oklch(0.711 0.019 323.02);
.dark & {
--background: oklch(0.145 0.008 326);
--foreground: oklch(0.985 0 0);
--card: oklch(0.212 0.019 322.12);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.212 0.019 322.12);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0.005 325.62);
--primary-foreground: oklch(0.212 0.019 322.12);
--secondary: oklch(0.263 0.024 320.12);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.263 0.024 320.12);
--muted-foreground: oklch(0.711 0.019 323.02);
--accent: oklch(0.263 0.024 320.12);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.542 0.034 322.5);
--chart-1: oklch(0.865 0.012 325.68);
--chart-2: oklch(0.542 0.034 322.5);
--chart-3: oklch(0.435 0.029 321.78);
--chart-4: oklch(0.364 0.029 323.89);
--chart-5: oklch(0.263 0.024 320.12);
--sidebar: oklch(0.212 0.019 322.12);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.263 0.024 320.12);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.542 0.034 322.5);
}
}
.theme-olive {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.153 0.006 107.1);
--card: oklch(1 0 0);
--card-foreground: oklch(0.153 0.006 107.1);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.153 0.006 107.1);
--primary: oklch(0.228 0.013 107.4);
--primary-foreground: oklch(0.988 0.003 106.5);
--secondary: oklch(0.966 0.005 106.5);
--secondary-foreground: oklch(0.228 0.013 107.4);
--muted: oklch(0.966 0.005 106.5);
--muted-foreground: oklch(0.58 0.031 107.3);
--accent: oklch(0.966 0.005 106.5);
--accent-foreground: oklch(0.228 0.013 107.4);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.93 0.007 106.5);
--input: oklch(0.93 0.007 106.5);
--ring: oklch(0.737 0.021 106.9);
--chart-1: oklch(0.88 0.011 106.6);
--chart-2: oklch(0.58 0.031 107.3);
--chart-3: oklch(0.466 0.025 107.3);
--chart-4: oklch(0.394 0.023 107.4);
--chart-5: oklch(0.286 0.016 107.4);
--sidebar: oklch(0.988 0.003 106.5);
--sidebar-foreground: oklch(0.153 0.006 107.1);
--sidebar-primary: oklch(0.228 0.013 107.4);
--sidebar-primary-foreground: oklch(0.988 0.003 106.5);
--sidebar-accent: oklch(0.966 0.005 106.5);
--sidebar-accent-foreground: oklch(0.228 0.013 107.4);
--sidebar-border: oklch(0.93 0.007 106.5);
--sidebar-ring: oklch(0.737 0.021 106.9);
.dark & {
--background: oklch(0.153 0.006 107.1);
--foreground: oklch(0.988 0.003 106.5);
--card: oklch(0.228 0.013 107.4);
--card-foreground: oklch(0.988 0.003 106.5);
--popover: oklch(0.228 0.013 107.4);
--popover-foreground: oklch(0.988 0.003 106.5);
--primary: oklch(0.93 0.007 106.5);
--primary-foreground: oklch(0.228 0.013 107.4);
--secondary: oklch(0.286 0.016 107.4);
--secondary-foreground: oklch(0.988 0.003 106.5);
--muted: oklch(0.286 0.016 107.4);
--muted-foreground: oklch(0.737 0.021 106.9);
--accent: oklch(0.286 0.016 107.4);
--accent-foreground: oklch(0.988 0.003 106.5);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.58 0.031 107.3);
--chart-1: oklch(0.88 0.011 106.6);
--chart-2: oklch(0.58 0.031 107.3);
--chart-3: oklch(0.466 0.025 107.3);
--chart-4: oklch(0.394 0.023 107.4);
--chart-5: oklch(0.286 0.016 107.4);
--sidebar: oklch(0.228 0.013 107.4);
--sidebar-foreground: oklch(0.988 0.003 106.5);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.988 0.003 106.5);
--sidebar-accent: oklch(0.286 0.016 107.4);
--sidebar-accent-foreground: oklch(0.988 0.003 106.5);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.58 0.031 107.3);
}
}
.theme-mist {
--radius: 0;
--background: oklch(1 0 0);
--foreground: oklch(0.148 0.004 228.8);
--card: oklch(1 0 0);
--card-foreground: oklch(0.148 0.004 228.8);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.148 0.004 228.8);
--primary: oklch(0.218 0.008 223.9);
--primary-foreground: oklch(0.987 0.002 197.1);
--secondary: oklch(0.963 0.002 197.1);
--secondary-foreground: oklch(0.218 0.008 223.9);
--muted: oklch(0.963 0.002 197.1);
--muted-foreground: oklch(0.56 0.021 213.5);
--accent: oklch(0.963 0.002 197.1);
--accent-foreground: oklch(0.218 0.008 223.9);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.925 0.005 214.3);
--input: oklch(0.925 0.005 214.3);
--ring: oklch(0.723 0.014 214.4);
--chart-1: oklch(0.872 0.007 219.6);
--chart-2: oklch(0.56 0.021 213.5);
--chart-3: oklch(0.45 0.017 213.2);
--chart-4: oklch(0.378 0.015 216);
--chart-5: oklch(0.275 0.011 216.9);
--sidebar: oklch(0.987 0.002 197.1);
--sidebar-foreground: oklch(0.148 0.004 228.8);
--sidebar-primary: oklch(0.218 0.008 223.9);
--sidebar-primary-foreground: oklch(0.987 0.002 197.1);
--sidebar-accent: oklch(0.963 0.002 197.1);
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
--sidebar-border: oklch(0.925 0.005 214.3);
--sidebar-ring: oklch(0.723 0.014 214.4);
.dark & {
--background: oklch(0.148 0.004 228.8);
--foreground: oklch(0.987 0.002 197.1);
--card: oklch(0.218 0.008 223.9);
--card-foreground: oklch(0.987 0.002 197.1);
--popover: oklch(0.218 0.008 223.9);
--popover-foreground: oklch(0.987 0.002 197.1);
--primary: oklch(0.925 0.005 214.3);
--primary-foreground: oklch(0.218 0.008 223.9);
--secondary: oklch(0.275 0.011 216.9);
--secondary-foreground: oklch(0.987 0.002 197.1);
--muted: oklch(0.275 0.011 216.9);
--muted-foreground: oklch(0.723 0.014 214.4);
--accent: oklch(0.275 0.011 216.9);
--accent-foreground: oklch(0.987 0.002 197.1);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.56 0.021 213.5);
--chart-1: oklch(0.872 0.007 219.6);
--chart-2: oklch(0.56 0.021 213.5);
--chart-3: oklch(0.45 0.017 213.2);
--chart-4: oklch(0.378 0.015 216);
--chart-5: oklch(0.275 0.011 216.9);
--sidebar: oklch(0.218 0.008 223.9);
--sidebar-foreground: oklch(0.987 0.002 197.1);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.987 0.002 197.1);
--sidebar-accent: oklch(0.275 0.011 216.9);
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.56 0.021 213.5);
}
}
}
@utility font-heading {
font-family: var(--font-serif);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -5,7 +5,6 @@ import * as React from "react"
import {
buildRegistryTheme,
DEFAULT_CONFIG,
POINTER_CURSOR_SELECTOR,
type DesignSystemConfig,
} from "@/registry/config"
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
@@ -17,12 +16,6 @@ import {
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
const POINTER_CURSOR_CSS = `@layer base {
${POINTER_CURSOR_SELECTOR} {
cursor: pointer;
}
}
`
type RegistryThemeCssVars = NonNullable<
ReturnType<typeof buildRegistryTheme>["cssVars"]
@@ -51,17 +44,14 @@ function buildCssRule(selector: string, cssVars?: Record<string, string>) {
return `${selector} {\n${declarations}\n}\n`
}
function buildThemeCssText(cssVars: RegistryThemeCssVars, pointer: boolean) {
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
return [
buildCssRule(":root", {
...(cssVars.theme ?? {}),
...(cssVars.light ?? {}),
}),
buildCssRule(".dark", cssVars.dark),
pointer ? POINTER_CURSOR_CSS : "",
]
.filter(Boolean)
.join("\n")
].join("\n")
}
export function DesignSystemProvider({
@@ -83,7 +73,6 @@ export function DesignSystemProvider({
chartColor,
menuAccent,
menuColor,
pointer,
radius,
} = searchParams
const effectiveRadius = style === "lyra" ? "none" : radius
@@ -141,7 +130,7 @@ export function DesignSystemProvider({
useIframeMessageListener("design-system-params", handleDesignSystemMessage)
React.useEffect(() => {
if (style === "lyra" || (style === "sera" && radius !== "none")) {
if (style === "lyra" && radius !== "none") {
setSearchParams({ radius: "none" })
}
}, [style, radius, setSearchParams])
@@ -219,8 +208,8 @@ export function DesignSystemProvider({
document.head.appendChild(styleElement)
}
styleElement.textContent = buildThemeCssText(registryTheme.cssVars, pointer)
}, [registryTheme, pointer])
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
}, [registryTheme])
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.

View File

@@ -1,12 +1,7 @@
"use client"
import * as React from "react"
import {
Copy01Icon,
Globe02Icon,
HandPointingRight04Icon,
Tick02Icon,
} from "@hugeicons/core-free-icons"
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
@@ -90,8 +85,7 @@ export function ProjectForm({
const templateFlag = ` --template ${framework}`
const monorepoFlag = isMonorepo ? " --monorepo" : ""
const rtlFlag = params.rtl ? " --rtl" : ""
const pointerFlag = params.pointer ? " --pointer" : ""
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}${pointerFlag}`
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
return IS_LOCAL_DEV
? {
@@ -106,14 +100,7 @@ export function ProjectForm({
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
}
}, [
framework,
isMonorepo,
params.base,
params.pointer,
params.rtl,
presetCode,
])
}, [framework, isMonorepo, params.base, params.rtl, presetCode])
const command = commands[packageManager]
@@ -167,23 +154,6 @@ export function ProjectForm({
<FieldLegend variant="label" className="sr-only">
Options
</FieldLegend>
<Field orientation="horizontal">
<FieldLabel htmlFor="pointer">
<HugeiconsIcon
icon={HandPointingRight04Icon}
className="size-4 -rotate-90"
/>
Use pointer on buttons
</FieldLabel>
<Switch
id="pointer"
checked={params.pointer}
onCheckedChange={(checked) =>
setParams({ pointer: checked === true })
}
/>
</Field>
<FieldSeparator className="-mx-6" />
<Field
orientation="horizontal"
data-disabled={hasMonorepo ? undefined : "true"}

View File

@@ -23,7 +23,7 @@ export function RadiusPicker({
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const isRadiusLocked = params.style === "lyra" || params.style === "sera"
const isRadiusLocked = params.style === "lyra"
const selectedRadiusName = isRadiusLocked ? "none" : params.radius
const currentRadius = RADII.find(

View File

@@ -16,17 +16,8 @@ export function ShareButton() {
const shareUrl = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
const searchParams = new URLSearchParams({
preset: presetCode,
item: params.item,
})
if (params.pointer) {
searchParams.set("pointer", "true")
}
return `${origin}/create?${searchParams.toString()}`
}, [params.item, params.pointer, presetCode])
return `${origin}/create?preset=${presetCode}&item=${params.item}`
}, [presetCode, params.item])
React.useEffect(() => {
if (hasCopied) {

View File

@@ -2,7 +2,7 @@
import * as React from "react"
import { PRESETS, type Style, type StyleName } from "@/registry/config"
import { type Style, type StyleName } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import {
Picker,
@@ -53,24 +53,7 @@ export function StylePicker({
<PickerRadioGroup
value={currentStyle?.name}
onValueChange={(value) => {
const styleName = value as StyleName
const preset = PRESETS.find(
(p) => p.base === params.base && p.style === styleName
)
setParams({
style: styleName,
...(preset && {
baseColor: preset.baseColor,
theme: preset.theme,
chartColor: preset.chartColor,
iconLibrary: preset.iconLibrary,
font: preset.font,
fontHeading: preset.fontHeading,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
}),
})
setParams({ style: value as StyleName })
}}
>
<PickerGroup>

View File

@@ -1,12 +1,10 @@
import {
DM_Sans,
EB_Garamond,
Figtree,
Geist,
Geist_Mono,
IBM_Plex_Sans,
Instrument_Sans,
Instrument_Serif,
Inter,
JetBrains_Mono,
Lora,
@@ -151,17 +149,6 @@ const playfairDisplay = Playfair_Display({
variable: "--font-playfair-display",
})
const ebGaramond = EB_Garamond({
subsets: ["latin"],
variable: "--font-eb-garamond",
})
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
weight: "400",
variable: "--font-instrument-serif",
})
const PREVIEW_FONTS = {
geist: geistSans,
inter,
@@ -187,8 +174,6 @@ const PREVIEW_FONTS = {
merriweather,
lora,
"playfair-display": playfairDisplay,
"eb-garamond": ebGaramond,
"instrument-serif": instrumentSerif,
} satisfies Record<FontName, PreviewFont>
function createFontOption(name: FontName) {
@@ -231,8 +216,6 @@ export const FONTS = [
createFontOption("merriweather"),
createFontOption("lora"),
createFontOption("playfair-display"),
createFontOption("eb-garamond"),
createFontOption("instrument-serif"),
] as const
export type Font = (typeof FONTS)[number]

View File

@@ -91,7 +91,6 @@ const designSystemSearchParams = {
"laravel",
] as const).withDefault("next"),
rtl: parseAsBoolean.withDefault(false),
pointer: parseAsBoolean.withDefault(false),
size: parseAsInteger.withDefault(100),
custom: parseAsBoolean.withDefault(false),
}
@@ -127,7 +126,6 @@ const NON_DESIGN_SYSTEM_KEYS = [
"preset",
"template",
"rtl",
"pointer",
"size",
"custom",
] as const
@@ -226,7 +224,6 @@ function resolvePresetParams(
preset: rawParams.preset,
template: rawParams.template,
rtl: rawParams.rtl,
pointer: rawParams.pointer,
size: rawParams.size,
custom: rawParams.custom,
})

View File

@@ -10,7 +10,7 @@ import { Button } from "@/styles/radix-nova/ui/button"
export const revalidate = false
export const dynamic = "force-static"
const NUMBER_OF_LATEST_PAGES = 5
const NUMBER_OF_LATEST_PAGES = 2
export function generateMetadata() {
return {

View File

@@ -3,51 +3,6 @@ import { describe, expect, it } from "vitest"
import { parseDesignSystemConfig } from "./parse-config"
describe("parseDesignSystemConfig", () => {
it("defaults pointer to false when omitted", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.pointer).toBe(false)
})
it("parses pointer=true", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&pointer=true&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.pointer).toBe(true)
})
it("defaults missing chartColor from the selected theme", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.chartColor).toBe("taupe")
})
it("honors explicit fontHeading and chartColor overrides when a preset is present", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
@@ -63,17 +18,4 @@ describe("parseDesignSystemConfig", () => {
expect(result.data.fontHeading).toBe("playfair-display")
expect(result.data.chartColor).toBe("emerald")
})
it("keeps pointer outside preset decoding", () => {
const result = parseDesignSystemConfig(
new URLSearchParams("preset=a0&pointer=true")
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.pointer).toBe(true)
})
})

View File

@@ -23,7 +23,6 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
base: searchParams.get("base") ?? "radix",
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
pointer: searchParams.get("pointer") === "true",
}
} else {
configInput = {
@@ -40,7 +39,6 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
radius: searchParams.get("radius"),
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
pointer: searchParams.get("pointer") === "true",
}
}

View File

@@ -1,74 +0,0 @@
import { describe, expect, it } from "vitest"
import {
buildRegistryBase,
DEFAULT_CONFIG,
POINTER_CURSOR_SELECTOR,
} from "@/registry/config"
import { GET } from "./route"
function createRequest(search = "") {
const searchParams = new URLSearchParams(
Object.entries(DEFAULT_CONFIG).map(([key, value]) => [key, String(value)])
)
const url = new URL(`http://localhost:4000/init${search}`)
for (const [key, value] of url.searchParams) {
searchParams.set(key, value)
}
return {
nextUrl: new URL(`http://localhost:4000/init?${searchParams}`),
} as Parameters<typeof GET>[0]
}
describe("GET /init", () => {
it("returns the full registry base when only is omitted", async () => {
const response = await GET(createRequest())
const json = await response.json()
expect(response.status).toBe(200)
expect(json).toEqual(buildRegistryBase(DEFAULT_CONFIG))
expect(json.css["@layer base"][POINTER_CURSOR_SELECTOR]).toBeUndefined()
})
it("returns pointer cursor css when pointer is enabled", async () => {
const response = await GET(createRequest("?pointer=true"))
const json = await response.json()
expect(response.status).toBe(200)
expect(json.css["@layer base"][POINTER_CURSOR_SELECTOR]).toEqual({
cursor: "pointer",
})
})
it("returns a sparse registry base when only is provided", async () => {
const response = await GET(createRequest("?only=theme"))
const json = await response.json()
expect(response.status).toBe(200)
expect(json.type).toBe("registry:base")
expect(json.config).toEqual({
menuColor: "default",
menuAccent: "subtle",
tailwind: {
baseColor: "neutral",
},
})
expect(json.cssVars.light).toBeDefined()
expect(json.cssVars.light.radius).toBe("0.625rem")
expect(json.dependencies).toBeUndefined()
expect(json.registryDependencies).toBeUndefined()
})
it("rejects unsupported only values", async () => {
const response = await GET(createRequest("?only=icon"))
const json = await response.json()
expect(response.status).toBe(400)
expect(json.error).toBe(
"Invalid only value. Use one or more of: theme, font"
)
})
})

View File

@@ -3,11 +3,7 @@ import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
import { registryItemSchema } from "shadcn/schema"
import {
buildPartialRegistryBase,
buildRegistryBase,
parseRegistryBaseParts,
} from "@/registry/config"
import { buildRegistryBase } from "@/registry/config"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
@@ -20,20 +16,13 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
const onlyResult = parseRegistryBaseParts(searchParams.get("only"))
if (!onlyResult.success) {
return NextResponse.json({ error: onlyResult.error }, { status: 400 })
}
const rawPreset = searchParams.get("preset")
const presetCode =
rawPreset && isPresetCode(rawPreset)
? rawPreset
: getPresetCode(result.data)
const registryBase = onlyResult.parts
? buildPartialRegistryBase(result.data, onlyResult.parts)
: buildRegistryBase(result.data)
const registryBase = buildRegistryBase(result.data)
const parseResult = registryItemSchema.safeParse(registryBase)
if (!parseResult.success) {

View File

@@ -9,7 +9,6 @@
@import "../registry/styles/style-maia.css" layer(base);
@import "../registry/styles/style-mira.css" layer(base);
@import "../registry/styles/style-luma.css" layer(base);
@import "../registry/styles/style-sera.css" layer(base);
@custom-variant style-vega (&:where(.style-vega *));
@custom-variant style-nova (&:where(.style-nova *));
@@ -17,7 +16,6 @@
@custom-variant style-maia (&:where(.style-maia *));
@custom-variant style-mira (&:where(.style-mira *));
@custom-variant style-luma (&:where(.style-luma *));
@custom-variant style-sera (&:where(.style-sera *));
@custom-variant dark (&:is(.dark *));
@custom-variant fixed (&:is(.layout-fixed *));

View File

@@ -7,7 +7,7 @@ export function Announcement() {
return (
<Badge asChild variant="secondary" className="bg-muted">
<Link href="/docs/changelog">
New preset commands <ArrowRightIcon />
Introducing Luma <ArrowRightIcon />
</Link>
</Badge>
)

View File

@@ -1,4 +1,4 @@
const SHOW = true
const SHOW = false
export function TailwindIndicator({
forceMount = false,

View File

@@ -39,8 +39,6 @@ Options:
--no-monorepo skip the monorepo prompt.
--rtl enable RTL support.
--no-rtl disable RTL support.
--pointer enable pointer cursor for buttons.
--no-pointer disable pointer cursor for buttons.
--reinstall re-install existing UI components.
--no-reinstall do not re-install existing UI components.
-h, --help display help for command
@@ -92,17 +90,9 @@ Options:
Use the `apply` command to apply a preset to an existing project.
```bash
npx shadcn@latest apply a2r6bw
npx shadcn@latest apply --preset a2r6bw
```
You can apply only the theme or fonts from a preset without reinstalling UI components:
```bash
npx shadcn@latest apply a2r6bw --only theme
```
Supported values for `--only` are `theme` and `font`.
**Options**
```bash
@@ -115,7 +105,6 @@ Arguments:
Options:
--preset <preset> preset configuration to apply
--only [parts] apply only parts of a preset: theme, font
-y, --yes skip confirmation prompt. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-s, --silent mute output. (default: false)
@@ -124,110 +113,6 @@ Options:
---
## preset
Use the `preset` command to inspect preset codes and resolve the preset for an existing project.
```bash
npx shadcn@latest preset decode a2r6bw
```
### preset decode
Use `preset decode` to decode a preset code.
```bash
npx shadcn@latest preset decode a2r6bw
```
**Options**
```bash
Usage: shadcn preset decode [options] <code>
decode a preset code
Arguments:
code the preset code to decode
Options:
--json output as JSON. (default: false)
-h, --help display help for command
```
### preset resolve
Use `preset resolve` to resolve the preset from the current project.
```bash
npx shadcn@latest preset resolve
```
The `preset info` command is an alias for `preset resolve`:
```bash
npx shadcn@latest preset info
```
**Options**
```bash
Usage: shadcn preset resolve|info [options]
resolve a preset from your project
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
--json output as JSON. (default: false)
-h, --help display help for command
```
### preset url
Use `preset url` to print the create URL for a preset code.
```bash
npx shadcn@latest preset url a2r6bw
```
**Options**
```bash
Usage: shadcn preset url [options] <code>
get the create URL for a preset code
Arguments:
code the preset code
Options:
-h, --help display help for command
```
### preset open
Use `preset open` to open a preset code in the browser.
```bash
npx shadcn@latest preset open a2r6bw
```
**Options**
```bash
Usage: shadcn preset open [options] <code>
open a preset code in the browser
Arguments:
code the preset code
Options:
-h, --help display help for command
```
---
## view
Use the `view` command to view items from the registry before installing them.

View File

@@ -10,6 +10,7 @@ description: Every component recreated in Figma. With customizable props, typogr
## Free
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted shadcn/ui kit, MIT licensed, maintained by team of designers, with free design to code plugin
- [shadcn/ui components](https://www.figma.com/community/file/1342715840824755935) by [Sitsiilia Bergmann](https://x.com/sitsiilia) - A well-structured component library aligned with the shadcn component system, regularly maintained.
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
@@ -19,4 +20,3 @@ description: Every component recreated in Figma. With customizable props, typogr
- [shadcncraft Design System](https://shadcncraft.com) - Production-ready shadcn/ui kit with Pro React blocks and 1:1 Figma alignment. Design and ship faster with tweakcn theming, AI-assisted workflows, and Figma to React export, built for real product UIs.
- [shadcn/studio UI Kit](https://shadcnstudio.com/figma) - Accelerate design & development with a shadcn/ui compatible Figma kit with updated components, 550+ blocks, 10+ templates, 20+ themes, and an AI tool that converts designs into shadcn/ui code.
- [Shadcnblocks.com](https://www.shadcnblocks.com) - A Premium Shadcn Figma UI Kit with components, 500+ pro blocks, shadcn theme variables, light/dark mode and Figma MCP ready.
- [Obra shadcn/ui Pro](https://shadcn.obra.studio/products/obra-shadcn-ui-pro) by [Obra Studio](https://obra.studio/) - Focused on designers who need to get work done — the best designer experience for shadcn/ui within Figma. variable consistency with shadcn, plus custom components, Pro blocks, and a design-to-code plugin.

View File

@@ -116,7 +116,7 @@ npx shadcn@latest docs combobox
combobox
- docs https://ui.shadcn.com/docs/components/radix/combobox
- examples https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/combobox-example.tsx
- examples https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/combobox-example.tsx
- api https://base-ui.com/react/components/combobox
```

View File

@@ -1,35 +0,0 @@
---
title: April 2026 - Partial Preset Apply
description: Apply only the theme or fonts from a preset while keeping your existing components.
date: 2026-04-22
---
You can now selectively apply a preset.
Say someone shares a preset with you and you already have your own components, but you like the theme or the fonts. Now you can apply just that.
Keep your components. Apply only what you want.
```bash
# Apply the full preset.
npx shadcn@latest apply --preset b2D0vQ7G4
# Apply only the theme.
npx shadcn@latest apply --preset b2D0vQ7G4 --only theme
# Apply only the fonts.
npx shadcn@latest apply --preset b2D0vQ7G4 --only font
# Apply theme and fonts.
npx shadcn@latest apply --preset b2D0vQ7G4 --only theme,font
```
The default behavior is unchanged. Running `shadcn apply --preset <preset>` still applies the full preset.
Partial preset apply currently supports `theme` and `font`.
<Button asChild size="sm">
<Link href="/create" className="mt-6 no-underline!">
Try a Preset
</Link>
</Button>

View File

@@ -1,24 +0,0 @@
---
title: April 2026 - Pointer Cursor
description: Add cursor pointer behavior for buttons during project setup.
date: 2026-04-25
---
You can now enable `cursor: pointer` for buttons when initializing a project.
```bash
npx shadcn@latest init --pointer
```
This adds the following CSS to your global CSS file:
```css title="globals.css"
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
```
The `--pointer` option is not part of preset codes. It is applied as a project setup option, similar to `--rtl`.

View File

@@ -1,93 +0,0 @@
---
title: April 2026 - shadcn preset
description: Decode, share, open, and resolve preset codes from the shadcn CLI.
date: 2026-04-28
---
We added `shadcn preset` commands for working with preset codes.
## Decode a preset
You can decode a preset code to see exactly what it contains:
```bash
npx shadcn@latest preset decode b5owWMfJ8l
```
```txt
Preset
code b5owWMfJ8l
version b
style mira
baseColor mauve
theme mauve
chartColor amber
iconLibrary hugeicons
font inter
fontHeading oxanium
radius large
menuAccent subtle
menuColor inverted-translucent
url https://ui.shadcn.com/create?preset=b5owWMfJ8l
```
## Resolve from a project
Use `preset resolve` in an existing project to see the preset that matches your current configuration.
```bash
npx shadcn@latest preset resolve
```
```txt
Preset
code b5Kc6P0Vc
version b
style luma
baseColor olive
theme lime
chartColor sky
iconLibrary hugeicons
font geist
fontHeading inherit
radius default
menuAccent subtle
menuColor default
url https://ui.shadcn.com/create?preset=b5Kc6P0Vc
```
It works with monorepos too:
```bash
npx shadcn@latest preset resolve -c apps/web
```
## Share or open
Use `preset url` when you need a shareable link:
```bash
npx shadcn@latest preset url b5owWMfJ8l
```
```txt
https://ui.shadcn.com/create?preset=b5owWMfJ8l
```
Use `preset open` to open the preset on shadcn/create for customization:
```bash
npx shadcn@latest preset open b5owWMfJ8l
```
```txt
Opening https://ui.shadcn.com/create?preset=b5owWMfJ8l in your browser.
```
This makes presets easier to inspect, share, and hand off to coding agents without manually decoding codes or building URLs.
<Button asChild size="sm">
<Link href="/create" className="mt-6 no-underline!">
Try a Preset
</Link>
</Button>

View File

@@ -1,37 +0,0 @@
---
title: April 2026 - Introducing Sera
description: Minimal. Editorial. Typographic. Underline Controls and Uppercase Headings. Shaped by Print Design Principles.
date: 2026-04-16
---
Introducing Sera, a new shadcn/ui style. Minimal. Editorial. Typographic. Underline Controls and Uppercase Headings. Shaped by Print Design Principles.
<a href="/create?preset=b4xFeBLg4O">
<Image
src="/images/sera-01-light.png"
width="2160"
height="1832"
alt="Sera style preview"
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
/>
<Image
src="/images/sera-01-dark.png"
width="2160"
height="1832"
alt="Sera style preview"
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
/>
<span className="sr-only">Try Sera in shadcn/create</span>
</a>
Sera is a typography-first style built on print design principles. It pairs serif headings with sans-serif body text, uses square corners, uppercase tracking, and underlined controls to create an editorial feel for your app.
Like the other new styles, Sera goes beyond theming. It changes the geometry, spacing, and feel of the components so your app starts from a different visual baseline.
Available now in [shadcn/create](/create) for both Radix and Base UI.
<Button asChild size="sm">
<Link href="/create?preset=b4xFeBLg4O" className="mt-6 no-underline!">
Try Sera
</Link>
</Button>

View File

@@ -66,8 +66,6 @@ Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-th
If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file:
You can also enable this during project setup with `npx shadcn@latest init --pointer`.
```css showLineNumbers title="globals.css"
@layer base {
button:not(:disabled),

View File

@@ -66,8 +66,6 @@ Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-th
If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file:
You can also enable this during project setup with `npx shadcn@latest init --pointer`.
```css showLineNumbers title="globals.css"
@layer base {
button:not(:disabled),

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
{
"title": "Dark mode",
"pages": ["index", "next", "vite", "astro", "remix", "tanstack-start"]
"pages": ["index", "next", "vite", "astro", "remix"]
}

View File

@@ -1,191 +0,0 @@
---
title: TanStack Start
description: Adding dark mode to your TanStack Start app.
---
<Steps>
### Create a theme provider
TanStack Start uses `ScriptOnce` from `@tanstack/react-router` to inject a script that runs before React hydrates, preventing flash of unstyled content (FOUC).
```tsx title="components/theme-provider.tsx" showLineNumbers
import { createContext, useContext, useEffect, useState } from "react"
import { ScriptOnce } from "@tanstack/react-router"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
function getThemeScript(storageKey: string, defaultTheme: Theme) {
const key = JSON.stringify(storageKey)
const fallback = JSON.stringify(defaultTheme)
return `(function(){try{var t=localStorage.getItem(${key});if(t!=='light'&&t!=='dark'&&t!=='system'){t=${fallback}}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();`
}
const ThemeProviderContext = createContext<ThemeProviderState>({
theme: "system",
setTheme: () => {},
})
function applyTheme(theme: Theme) {
const root = document.documentElement
root.classList.remove("light", "dark")
const resolved =
theme === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: theme
root.classList.add(resolved)
root.style.colorScheme = resolved
}
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
}: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>(defaultTheme)
const [mounted, setMounted] = useState(false)
useEffect(() => {
const stored = localStorage.getItem(storageKey)
setThemeState(
stored === "light" || stored === "dark" || stored === "system"
? stored
: defaultTheme
)
setMounted(true)
}, [defaultTheme, storageKey])
useEffect(() => {
if (!mounted) return
applyTheme(theme)
}, [theme, mounted])
useEffect(() => {
if (!mounted || theme !== "system") return
const media = window.matchMedia("(prefers-color-scheme: dark)")
const onChange = () => applyTheme("system")
media.addEventListener("change", onChange)
return () => media.removeEventListener("change", onChange)
}, [theme, mounted])
const setTheme = (next: Theme) => {
localStorage.setItem(storageKey, next)
setThemeState(next)
}
return (
<ThemeProviderContext value={{ theme, setTheme }}>
<ScriptOnce>{getThemeScript(storageKey, defaultTheme)}</ScriptOnce>
{children}
</ThemeProviderContext>
)
}
export function useTheme() {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}
```
### Wrap your root layout
Add the `ThemeProvider` to your root layout and add the `suppressHydrationWarning` prop to the `html` tag.
```tsx {8,19,24-26} title="src/routes/__root.tsx" showLineNumbers
import {
createRootRoute,
HeadContent,
Outlet,
Scripts,
} from "@tanstack/react-router"
import { ThemeProvider } from "@/components/theme-provider"
export const Route = createRootRoute({
head: () => ({
// ...
}),
component: RootComponent,
})
function RootComponent() {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body>
<ThemeProvider defaultTheme="system" storageKey="theme">
<Outlet />
</ThemeProvider>
<Scripts />
</body>
</html>
)
}
```
### Add a mode toggle
Place a mode toggle on your site to toggle between light and dark mode.
```tsx title="components/mode-toggle.tsx" showLineNumbers
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/components/theme-provider"
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
```
</Steps>

View File

@@ -144,106 +144,6 @@ npm run dev
Your files will now be served at `http://localhost:3000/r/[NAME].json` eg. `http://localhost:3000/r/hello-world.json`.
## Content negotiation
The `shadcn` CLI supports **HTTP Content Negotiation**. This allows you to host your registry at any endpoint — including the root of your domain — and serve different content depending on who is asking.
From a single URL, you can serve:
- **HTML** to browsers — a landing page, documentation, or marketing site.
- **JSON** to the `shadcn` CLI — an installable registry item.
- **Markdown** to AI agents and LLMs — a machine-readable version of your content.
The client signals its preference using the `Accept` request header, and your server decides what to return.
### Request headers
When the CLI makes a request to a registry, it sends the following headers:
- **User-Agent**: `shadcn`
- **Accept**: `application/vnd.shadcn.v1+json, application/json;q=0.9`
### Root hosting
By checking these headers on your server, you can route CLI traffic to an installable registry item while keeping browser traffic flowing to your documentation or homepage.
The examples below assume your built registry item is served at `/r/index.json`. Adjust the path to match your output.
In Next.js, express this as a rewrite in `next.config.ts`. This keeps the negotiation in the routing layer and avoids a Proxy function for this static rewrite:
```typescript title="next.config.ts" showLineNumbers
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
async rewrites() {
return {
beforeFiles: [
{
source: "/",
has: [
{
type: "header",
key: "accept",
value: "(.*)application/vnd\\.shadcn\\.v1\\+json(.*)",
},
],
destination: "/r/index.json",
},
{
source: "/",
has: [
{
type: "header",
key: "user-agent",
value: "shadcn",
},
],
destination: "/r/index.json",
},
],
}
},
async headers() {
return [
{
source: "/",
headers: [{ key: "Vary", value: "Accept, User-Agent" }],
},
]
},
}
export default nextConfig
```
Or, in an Express.js server:
```javascript title="server.js" showLineNumbers
app.get("/", (req, res) => {
res.vary("Accept")
res.vary("User-Agent")
// Check if the client prefers the shadcn vendor type.
if (req.accepts("application/vnd.shadcn.v1+json")) {
return res.json(registryData)
}
// Optional: Secondary check for the User-Agent.
if (req.get("User-Agent") === "shadcn") {
return res.json(registryData)
}
// Otherwise, serve your documentation or homepage.
res.send(htmlContent)
})
```
This enables:
- **Branded Registry URLs**: `shadcn add https://ui.example.com`
- **Shorter URLs**: Users type your domain root, not `/r/` or `/registry/` sub-paths.
- **Easy Mnemonics**: Easier for users to remember and share your registry.
## Publish your registry
To make your registry available to other developers, you can publish it by deploying your project to a public URL.

View File

@@ -301,31 +301,6 @@ export const FONT_DEFINITIONS = [
dependency: "@fontsource-variable/playfair-display",
subsets: ["latin"],
},
{
name: "eb-garamond",
title: "EB Garamond",
type: "serif",
family: "'EB Garamond Variable', serif",
registryVariable: "--font-serif",
previewVariable: "--font-eb-garamond",
provider: "google",
import: "EB_Garamond",
dependency: "@fontsource-variable/eb-garamond",
subsets: ["latin"],
},
{
name: "instrument-serif",
title: "Instrument Serif",
type: "serif",
family: "'Instrument Serif', serif",
registryVariable: "--font-serif",
previewVariable: "--font-instrument-serif",
provider: "google",
import: "Instrument_Serif",
dependency: "@fontsource/instrument-serif",
subsets: ["latin"],
weight: ["400"],
},
] as const satisfies readonly FontDefinition[]
export type FontName = (typeof FONT_DEFINITIONS)[number]["name"]

View File

@@ -1,13 +1,9 @@
import {
Lora as FontLora,
Geist_Mono as FontMono,
Noto_Sans as FontNotoSans,
Noto_Sans_Arabic as FontNotoSansArabic,
Noto_Sans_Hebrew as FontNotoSansHebrew,
Noto_Serif as FontNotoSerif,
Geist as FontSans,
Inter,
Playfair_Display,
} from "next/font/google"
import { cn } from "@/lib/utils"
@@ -28,16 +24,6 @@ const fontInter = Inter({
variable: "--font-inter",
})
const fontNotoSans = FontNotoSans({
subsets: ["latin"],
variable: "--font-noto-sans",
})
const fontNotoSerif = FontNotoSerif({
subsets: ["latin"],
variable: "--font-noto-serif",
})
const fontNotoSansArabic = FontNotoSansArabic({
subsets: ["latin"],
variable: "--font-ar",
@@ -48,24 +34,10 @@ const fontNotoSansHebrew = FontNotoSansHebrew({
variable: "--font-he",
})
const fontLora = FontLora({
subsets: ["latin"],
variable: "--font-lora",
})
const fontPlayfairDisplay = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair-display",
})
export const fontVariables = cn(
fontSans.variable,
fontMono.variable,
fontInter.variable,
fontNotoSans.variable,
fontNotoSerif.variable,
fontNotoSansArabic.variable,
fontNotoSansHebrew.variable,
fontPlayfairDisplay.variable,
fontLora.variable
fontNotoSansHebrew.variable
)

View File

@@ -132,12 +132,6 @@ const nextConfig = {
destination: "/create",
permanent: true,
},
{
source: "/code/:path*",
destination:
"https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/:path*",
permanent: false,
},
]
},
rewrites() {

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm icons:dev & next dev --turbopack --port 4000",
"dev": "pnpm registry:build && pnpm icons:dev & next dev --turbopack --port 4000",
"build": "pnpm registry:build && next build",
"start": "next start --port 4000",
"preview": "pnpm registry:build && next build && next start --port 4000",
@@ -76,7 +76,7 @@
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"server-only": "^0.0.1",
"shadcn": "4.6.0",
"shadcn": "4.2.0",
"shiki": "^1.10.1",
"sonner": "^2.0.0",
"swr": "^2.3.6",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -215,42 +215,6 @@
"menuAccent": "subtle",
"menuColor": "default",
"radius": "default"
},
{
"name": "radix-sera",
"title": "Sera (Radix)",
"description": "Sera / Lucide / Noto Sans + Playfair Display",
"base": "radix",
"style": "sera",
"baseColor": "taupe",
"theme": "taupe",
"chartColor": "taupe",
"iconLibrary": "lucide",
"font": "noto-sans",
"fontHeading": "playfair-display",
"item": "Item",
"rtl": false,
"menuAccent": "subtle",
"menuColor": "default",
"radius": "default"
},
{
"name": "base-sera",
"title": "Sera (Base)",
"description": "Sera / Lucide / Noto Sans + Playfair Display",
"base": "base",
"style": "sera",
"baseColor": "taupe",
"theme": "taupe",
"chartColor": "taupe",
"iconLibrary": "lucide",
"font": "noto-sans",
"fontHeading": "playfair-display",
"item": "Item",
"rtl": false,
"menuAccent": "subtle",
"menuColor": "default",
"radius": "default"
}
]
}

View File

@@ -12,12 +12,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/accordion",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/accordion-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/accordion-example.tsx",
"api": "https://www.radix-ui.com/primitives/docs/components/accordion.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/accordion",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/accordion-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/accordion-example.tsx",
"api": "https://base-ui.com/react/components/accordion.md"
}
}
@@ -36,11 +36,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/alert",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/alert-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/alert-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/alert",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/alert-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/alert-example.tsx"
}
}
}
@@ -59,12 +59,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/alert-dialog",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/alert-dialog-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/alert-dialog-example.tsx",
"api": "https://www.radix-ui.com/primitives/docs/components/alert-dialog.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/alert-dialog",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/alert-dialog-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/alert-dialog-example.tsx",
"api": "https://base-ui.com/react/components/alert-dialog.md"
}
}
@@ -83,12 +83,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/aspect-ratio",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/aspect-ratio-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/aspect-ratio-example.tsx",
"api": "https://www.radix-ui.com/primitives/docs/components/aspect-ratio.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/aspect-ratio",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/aspect-ratio-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/aspect-ratio-example.tsx"
}
}
}
@@ -106,12 +106,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/avatar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/avatar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/avatar-example.tsx",
"api": "https://www.radix-ui.com/primitives/docs/components/avatar.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/avatar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/avatar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/avatar-example.tsx",
"api": "https://base-ui.com/react/components/avatar.md"
}
}
@@ -130,11 +130,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/badge",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/badge-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/badge-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/badge",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/badge-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/badge-example.tsx"
}
}
}
@@ -152,11 +152,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/breadcrumb",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/breadcrumb-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/breadcrumb-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/breadcrumb",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/breadcrumb-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/breadcrumb-example.tsx"
}
}
}
@@ -174,11 +174,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/button",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/button-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/button-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/button",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/button-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/button-example.tsx"
}
}
}
@@ -197,11 +197,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/button-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/button-group-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/button-group-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/button-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/button-group-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/button-group-example.tsx"
}
}
}
@@ -221,12 +221,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/calendar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/calendar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/calendar-example.tsx",
"api": "https://react-day-picker.js.org"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/calendar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/calendar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/calendar-example.tsx",
"api": "https://react-day-picker.js.org"
}
}
@@ -245,11 +245,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/card",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/card-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/card-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/card",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/card-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/card-example.tsx"
}
}
}
@@ -269,12 +269,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/carousel",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/carousel-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/carousel-example.tsx",
"api": "https://www.embla-carousel.com/get-started/react"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/carousel",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/carousel-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/carousel-example.tsx",
"api": "https://www.embla-carousel.com/get-started/react"
}
}
@@ -294,11 +294,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/chart",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/chart-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/chart-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/chart",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/chart-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/chart-example.tsx"
}
}
}
@@ -316,12 +316,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/checkbox",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/checkbox-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/checkbox-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/checkbox.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/checkbox",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/checkbox-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/checkbox-example.tsx",
"api": "https://base-ui.com/react/components/checkbox.md"
}
}
@@ -340,12 +340,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/collapsible",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/collapsible-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/collapsible-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/collapsible.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/collapsible",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/collapsible-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/collapsible-example.tsx",
"api": "https://base-ui.com/react/components/collapsible.md"
}
}
@@ -366,12 +366,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/combobox",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/combobox-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/combobox-example.tsx",
"api": "https://base-ui.com/react/components/combobox"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/combobox",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/combobox-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/combobox-example.tsx",
"api": "https://base-ui.com/react/components/combobox.md"
}
}
@@ -392,12 +392,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/command",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/command-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/command-example.tsx",
"api": "https://github.com/dip/cmdk"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/command",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/command-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/command-example.tsx",
"api": "https://github.com/dip/cmdk"
}
}
@@ -416,12 +416,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/context-menu",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/context-menu-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/context-menu-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/context-menu.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/context-menu",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/context-menu-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/context-menu-example.tsx",
"api": "https://base-ui.com/react/components/context-menu.md"
}
}
@@ -441,12 +441,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/dialog",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/dialog-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/dialog-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/dialog.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/dialog",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/dialog-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/dialog-example.tsx",
"api": "https://base-ui.com/react/components/dialog.md"
}
}
@@ -488,12 +488,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/drawer",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/drawer-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/drawer-example.tsx",
"api": "https://vaul.emilkowal.ski/getting-started"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/drawer",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/drawer-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/drawer-example.tsx",
"api": "https://vaul.emilkowal.ski/getting-started"
}
}
@@ -512,12 +512,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/dropdown-menu",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/dropdown-menu-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/dropdown-menu-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/dropdown-menu.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/dropdown-menu",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/dropdown-menu-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/dropdown-menu-example.tsx",
"api": "https://base-ui.com/react/components/menu.md"
}
}
@@ -536,11 +536,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/empty",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/empty-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/empty-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/empty",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/empty-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/empty-example.tsx"
}
}
}
@@ -559,11 +559,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/field",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/field-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/field-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/field",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/field-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/field-example.tsx"
}
}
}
@@ -585,12 +585,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/hover-card",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/hover-card-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/hover-card-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/hover-card.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/hover-card",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/hover-card-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/hover-card-example.tsx",
"api": "https://base-ui.com/react/components/hover-card.md"
}
}
@@ -609,11 +609,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/input",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/input-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/input-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/input",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/input-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/input-example.tsx"
}
}
}
@@ -632,11 +632,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/input-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/input-group-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/input-group-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/input-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/input-group-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/input-group-example.tsx"
}
}
}
@@ -655,12 +655,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/input-otp",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/input-otp-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/input-otp-example.tsx",
"api": "https://input-otp.rodz.dev"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/input-otp",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/input-otp-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/input-otp-example.tsx",
"api": "https://input-otp.rodz.dev"
}
}
@@ -680,11 +680,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/item",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/item-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/item-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/item",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/item-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/item-example.tsx"
}
}
}
@@ -702,11 +702,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/kbd",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/kbd-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/kbd-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/kbd",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/kbd-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/kbd-example.tsx"
}
}
}
@@ -724,12 +724,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/label",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/label-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/label-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/label.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/label",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/label-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/label-example.tsx",
"api": "https://base-ui.com/react/components/label.md"
}
}
@@ -748,12 +748,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/menubar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/menubar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/menubar-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/menubar.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/menubar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/menubar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/menubar-example.tsx",
"api": "https://base-ui.com/react/components/menubar.md"
}
}
@@ -772,11 +772,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/native-select",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/native-select-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/native-select-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/native-select",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/native-select-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/native-select-example.tsx"
}
}
}
@@ -794,12 +794,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/navigation-menu",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/navigation-menu-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/navigation-menu-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/navigation-menu.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/navigation-menu",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/navigation-menu-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/navigation-menu-example.tsx",
"api": "https://base-ui.com/react/components/navigation-menu.md"
}
}
@@ -819,11 +819,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/pagination",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/pagination-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/pagination-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/pagination",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/pagination-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/pagination-example.tsx"
}
}
}
@@ -841,12 +841,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/popover",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/popover-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/popover-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/popover.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/popover",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/popover-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/popover-example.tsx",
"api": "https://base-ui.com/react/components/popover.md"
}
}
@@ -865,12 +865,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/progress",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/progress-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/progress-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/progress.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/progress",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/progress-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/progress-example.tsx",
"api": "https://base-ui.com/react/components/progress.md"
}
}
@@ -889,12 +889,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/radio-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/radio-group-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/radio-group-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/radio-group.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/radio-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/radio-group-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/radio-group-example.tsx",
"api": "https://base-ui.com/react/components/radio-group.md"
}
}
@@ -914,12 +914,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/resizable",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/resizable-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/resizable-example.tsx",
"api": "https://github.com/bvaughn/react-resizable-panels"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/resizable",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/resizable-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/resizable-example.tsx",
"api": "https://github.com/bvaughn/react-resizable-panels"
}
}
@@ -938,12 +938,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/scroll-area",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/scroll-area-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/scroll-area-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/scroll-area.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/scroll-area",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/scroll-area-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/scroll-area-example.tsx",
"api": "https://base-ui.com/react/components/scroll-area.md"
}
}
@@ -962,12 +962,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/select",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/select-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/select-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/select.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/select",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/select-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/select-example.tsx",
"api": "https://base-ui.com/react/components/select.md"
}
}
@@ -986,12 +986,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/separator",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/separator-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/separator-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/separator.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/separator",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/separator-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/separator-example.tsx",
"api": "https://base-ui.com/react/components/separator.md"
}
}
@@ -1011,12 +1011,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/sheet",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/sheet-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/sheet-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/dialog.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/sheet",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/sheet-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/sheet-example.tsx",
"api": "https://base-ui.com/react/components/dialog.md"
}
}
@@ -1044,11 +1044,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/sidebar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/sidebar-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/sidebar-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/sidebar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/sidebar-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/sidebar-example.tsx"
}
}
}
@@ -1066,11 +1066,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/skeleton",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/skeleton-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/skeleton-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/skeleton",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/skeleton-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/skeleton-example.tsx"
}
}
}
@@ -1088,12 +1088,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/slider",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/slider-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/slider-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/slider.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/slider",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/slider-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/slider-example.tsx",
"api": "https://base-ui.com/react/components/slider.md"
}
}
@@ -1113,12 +1113,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/sonner",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/sonner-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/sonner-example.tsx",
"api": "https://sonner.emilkowal.ski"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/sonner",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/sonner-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/sonner-example.tsx",
"api": "https://sonner.emilkowal.ski"
}
}
@@ -1137,11 +1137,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/spinner",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/spinner-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/spinner-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/spinner",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/spinner-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/spinner-example.tsx"
}
}
}
@@ -1159,12 +1159,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/switch",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/switch-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/switch-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/switch.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/switch",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/switch-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/switch-example.tsx",
"api": "https://base-ui.com/react/components/switch.md"
}
}
@@ -1183,11 +1183,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/table",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/table-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/table-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/table",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/table-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/table-example.tsx"
}
}
}
@@ -1205,12 +1205,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/tabs",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/tabs-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/tabs-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/tabs.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/tabs",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/tabs-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/tabs-example.tsx",
"api": "https://base-ui.com/react/components/tabs.md"
}
}
@@ -1229,11 +1229,11 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/textarea",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/textarea-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/textarea-example.tsx"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/textarea",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/textarea-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/textarea-example.tsx"
}
}
}
@@ -1251,12 +1251,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/toggle",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/toggle-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/toggle-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/toggle.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/toggle",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/toggle-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/toggle-example.tsx",
"api": "https://base-ui.com/react/components/toggle.md"
}
}
@@ -1276,12 +1276,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/toggle-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/toggle-group-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/toggle-group-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/toggle-group.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/toggle-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/toggle-group-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/toggle-group-example.tsx",
"api": "https://base-ui.com/react/components/toggle-group.md"
}
}
@@ -1301,12 +1301,12 @@
"links": {
"radix": {
"docs": "https://ui.shadcn.com/docs/components/radix/tooltip",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/tooltip-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/radix/examples/tooltip-example.tsx",
"api": "https://www.radix-ui.com/docs/primitives/components/tooltip.md"
},
"base": {
"docs": "https://ui.shadcn.com/docs/components/base/tooltip",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/tooltip-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/tooltip-example.tsx",
"api": "https://base-ui.com/react/components/tooltip.md"
}
}

View File

@@ -233,12 +233,6 @@
"url": "https://eldoraui.site/r/{name}.json",
"description": "An open-source, modern UI component library for React, built with TypeScript, Tailwind CSS, and Framer Motion. Eldora UI offers beautifully crafted, reusable components designed for performance and elegance."
},
{
"name": "@evilcharts",
"homepage": "https://evilcharts.com",
"url": "https://evilcharts.com/r/{name}.json",
"description": "EvilCharts is an open-source chart UI website built with shadcn and Recharts, beautifully designed and handcrafted."
},
{
"name": "@formcn",
"homepage": "https://formcn.dev",
@@ -275,12 +269,6 @@
"url": "https://glass-ui.crenspire.com/r/{name}.json",
"description": "A shadcn-ui compatible registry distributing 40+ glassmorphic React/TypeScript components with Apple-inspired design. Components include enhanced visual effects (glow, shimmer, ripple), theme support, and customizable glassmorphism styling."
},
{
"name": "@glasscn",
"homepage": "https://glasscn.vercel.app/",
"url": "https://glasscn-components.vercel.app/r/{name}.json",
"description": "A shadcn-compatible registry of glassmorphism components inspired by Apple"
},
{
"name": "@ha-components",
"homepage": "https://hacomponents.keshuac.com",
@@ -359,12 +347,6 @@
"url": "https://limeplay.winoffrg.dev/r/{name}.json",
"description": "Modern UI Library for building media players in React. Powered by Shaka Player."
},
{
"name": "@loading-ui",
"homepage": "https://loading-ui.com",
"url": "https://loading-ui.com/r/{name}.json",
"description": "Spinners, loaders, and loading animations for modern web apps. Free and open-source."
},
{
"name": "@lmscn",
"homepage": "https://lmscn.vercel.app",
@@ -609,7 +591,7 @@
"name": "@shadcnblocks",
"homepage": "https://shadcnblocks.com",
"url": "https://shadcnblocks.com/r/{name}.json",
"description": "A shadcn/ui registry with 1429 blocks, 1189 component variants, 14 templates, themes, and admin dashboard patterns."
"description": "A registry with hundreds of extra blocks for shadcn ui."
},
{
"name": "@shadcndesign",
@@ -845,12 +827,6 @@
"url": "https://darshitdev.in/r/{name}.json",
"description": "Magic 3D Tabs component featuring mouse-interactive 3D rotation, floating particles background effect, and smooth spring animations."
},
{
"name": "@devl",
"homepage": "https://devl.dev",
"url": "https://devl.dev/r/{name}.json",
"description": "Hand-crafted layouts and UI primitives for shipping fast."
},
{
"name": "@beste-ui",
"homepage": "https://ui.beste.co",
@@ -1054,29 +1030,5 @@
"homepage": "https://www.remocn.dev/",
"url": "https://www.remocn.dev/r/{name}.json",
"description": "Production-ready components for Remotion - text animations, backgrounds, transitions, UI blocks, and full scene compositions"
},
{
"name": "@exabase",
"homepage": "https://exawizards.com/exabase/design/",
"url": "https://exawizards.com/exabase/design/registry/{name}.json",
"description": "A collection of UI components based on the exaBase Design System, built with React and Tailwind CSS."
},
{
"name": "@aicanvas",
"homepage": "https://aicanvas.me",
"url": "https://aicanvas.me/r/{name}.json",
"description": "54 animated React components with AI reproduction prompts for Claude Code, Lovable, and v0. Free and open source."
},
{
"name": "@xcn",
"homepage": "https://ui.radiumcoders.com",
"url": "https://ui.radiumcoders.com/r/xcn/{name}.json",
"description": "Hand-crafted, beautiful, and minimal UI components built with Tailwind CSS and Motion."
},
{
"name": "@dotmatrix",
"homepage": "https://dotmatrix.zzzzshawn.cloud",
"url": "https://dotmatrix.zzzzshawn.cloud/r/{name}.json",
"description": "Production-ready dot-matrix loading components for React, featuring square, circular, and triangle animations with polished motion."
}
]

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/accordion",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/accordion-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/accordion-example.tsx",
"api": "https://base-ui.com/react/components/accordion.md"
}
},

View File

@@ -14,7 +14,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/alert-dialog",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/alert-dialog-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/alert-dialog-example.tsx",
"api": "https://base-ui.com/react/components/alert-dialog.md"
}
},

View File

@@ -4,14 +4,14 @@
"files": [
{
"path": "registry/base-luma/ui/alert.tsx",
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-luma/lib/utils\"\n\nconst alertVariants = cva(\n \"group/alert relative grid w-full gap-0.5 rounded-2xl border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n destructive:\n \"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Alert({\n className,\n variant,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n return (\n <div\n data-slot=\"alert\"\n role=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\n \"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-action\"\n className={cn(\"absolute top-2.5 right-3\", className)}\n {...props}\n />\n )\n}\n\nexport { Alert, AlertTitle, AlertDescription, AlertAction }\n",
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-luma/lib/utils\"\n\nconst alertVariants = cva(\n \"group/alert relative grid w-full gap-0.5 rounded-2xl border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n destructive:\n \"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Alert({\n className,\n variant,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n return (\n <div\n data-slot=\"alert\"\n role=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\n \"cn-font-heading font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-action\"\n className={cn(\"absolute top-2.5 right-3\", className)}\n {...props}\n />\n )\n}\n\nexport { Alert, AlertTitle, AlertDescription, AlertAction }\n",
"type": "registry:ui"
}
],
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/alert",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/alert-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/alert-example.tsx"
}
},
"type": "registry:ui"

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/aspect-ratio",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/aspect-ratio-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/aspect-ratio-example.tsx"
}
},
"type": "registry:ui"

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/avatar",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/avatar-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/avatar-example.tsx",
"api": "https://base-ui.com/react/components/avatar.md"
}
},

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/badge",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/badge-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/badge-example.tsx"
}
},
"type": "registry:ui"

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/breadcrumb",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/breadcrumb-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/breadcrumb-example.tsx"
}
},
"type": "registry:ui"

View File

@@ -14,7 +14,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/button-group",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/button-group-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/button-group-example.tsx"
}
},
"type": "registry:ui"

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/button",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/button-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/button-example.tsx"
}
},
"type": "registry:ui"

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/card",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/card-example.tsx"
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/card-example.tsx"
}
},
"type": "registry:ui"

View File

@@ -17,7 +17,7 @@
"meta": {
"links": {
"docs": "https://ui.shadcn.com/docs/components/base/carousel",
"examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/carousel-example.tsx",
"examples": "https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases/base/examples/carousel-example.tsx",
"api": "https://www.embla-carousel.com/get-started/react"
}
},

Some files were not shown because too many files have changed in this diff Show More