Compare commits

..

1 Commits

Author SHA1 Message Date
shadcn
84eb464ae1 ci: add request workflow 2025-10-31 11:05:18 +04:00
26 changed files with 1163 additions and 1030 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
Fix utils import transform when workspace alias does not start with @

131
.github/workflows/request.yml vendored Normal file
View File

@@ -0,0 +1,131 @@
name: "Convert requests to discussions"
on:
workflow_dispatch:
schedule:
# This runs every hour: https://crontab.guru/#0_*_*_*_*
- cron: "0 * * * *"
jobs:
convert:
runs-on: ubuntu-latest
if: github.repository_owner == 'shadcn-ui'
steps:
- name: "Convert issues to discussions"
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Fetch issues with "area: request" label (limit 20).
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'area: request',
state: 'open',
per_page: 20,
sort: 'created',
direction: 'asc',
});
console.log(`Found ${issues.length} issues with "area: request" label`);
if (issues.length === 0) {
console.log('No issues to convert');
return;
}
// Get the repository node ID for GraphQL queries.
const repoQuery = `
query getRepo($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
id
discussionCategories(first: 20) {
nodes {
id
name
slug
}
}
}
}
`;
const repoResult = await github.graphql(repoQuery, {
owner: context.repo.owner,
repo: context.repo.repo,
});
const requestsCategory = repoResult.repository.discussionCategories.nodes.find(
cat => cat.name.toLowerCase() === 'requests' || cat.slug.toLowerCase() === 'requests'
);
if (!requestsCategory) {
throw new Error('Requests category not found in discussions. Available categories: ' +
repoResult.repository.discussionCategories.nodes.map(c => c.name).join(', '));
}
// Convert each issue to a discussion.
for (const issue of issues) {
try {
// Create discussion from issue using GraphQL.
const discussionTitle = issue.title;
const discussionBody = `**Converted from issue #${issue.number}**
${issue.body || ''}
---
_This discussion was automatically created from [issue #${issue.number}](${issue.html_url})_`;
const createDiscussionMutation = `
mutation createDiscussion($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
createDiscussion(input: {
repositoryId: $repositoryId
categoryId: $categoryId
title: $title
body: $body
}) {
discussion {
id
number
url
}
}
}
`;
const discussionResult = await github.graphql(createDiscussionMutation, {
repositoryId: repoResult.repository.id,
categoryId: requestsCategory.id,
title: discussionTitle,
body: discussionBody,
});
const discussion = discussionResult.createDiscussion.discussion;
console.log(`Created discussion #${discussion.number} from issue #${issue.number}`);
// Add a comment to the original issue linking to the discussion.
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `This issue has been converted to a discussion: [#${discussion.number}](${discussion.url})`,
});
// Close the original issue.
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
});
console.log(`Closed issue #${issue.number}`);
} catch (error) {
console.error(`Failed to convert issue #${issue.number}:`, error);
// Continue with next issue instead of failing the entire workflow.
}
}
console.log(`Completed processing ${issues.length} issues`);

View File

@@ -1,5 +1,4 @@
import type { Metadata } from "next"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
import { fontVariables } from "@/lib/fonts"
@@ -85,20 +84,18 @@ export default function RootLayout({
</head>
<body
className={cn(
"group/body overscroll-none antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
"group/body theme-blue overscroll-none antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
fontVariables
)}
>
<ThemeProvider>
<LayoutProvider>
<NuqsAdapter>
<ActiveThemeProvider>
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</NuqsAdapter>
<ActiveThemeProvider initialTheme="blue">
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</LayoutProvider>
</ThemeProvider>
</body>

View File

@@ -8,7 +8,7 @@ import {
useState,
} from "react"
const DEFAULT_THEME = "default"
const DEFAULT_THEME = "blue"
type ThemeContextType = {
activeTheme: string

View File

@@ -1,11 +1,8 @@
"use client"
import * as React from "react"
import { IconArrowUpRight } from "@tabler/icons-react"
import { useSearchRegistry } from "@/hooks/use-search-registry"
import { DirectoryAddButton } from "@/components/directory-add-button"
import globalRegistries from "@/registry/directory.json"
import registries from "@/registry/directory.json"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
@@ -19,72 +16,55 @@ import {
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
import { SearchDirectory } from "./search-directory"
function getHomepageUrl(homepage: string) {
const url = new URL(homepage)
url.searchParams.set("utm_source", "ui.shadcn.com")
url.searchParams.set("utm_medium", "referral")
url.searchParams.set("utm_campaign", "directory")
return url.toString()
}
export function DirectoryList() {
const { registries } = useSearchRegistry()
return (
<div className="mt-6">
<SearchDirectory />
<ItemGroup className="my-8">
{registries.map((registry, index) => (
<React.Fragment key={index}>
<Item className="group/item relative gap-6 px-0">
<ItemMedia
variant="image"
dangerouslySetInnerHTML={{ __html: registry.logo }}
className="*:[svg]:fill-foreground grayscale *:[svg]:size-8"
/>
<ItemContent>
<ItemTitle>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
>
{registry.name}
</a>
</ItemTitle>
{registry.description && (
<ItemDescription className="text-pretty">
{registry.description}
</ItemDescription>
)}
</ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex">
<Button size="sm" variant="outline" asChild>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
>
View <IconArrowUpRight />
</a>
</Button>
<DirectoryAddButton registry={registry} />
</ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden">
<Button size="sm" variant="outline">
<ItemGroup className="my-8">
{registries.map((registry, index) => (
<React.Fragment key={index}>
<Item className="group/item relative gap-6 px-0 sm:px-4">
<ItemMedia
variant="image"
dangerouslySetInnerHTML={{ __html: registry.logo }}
className="*:[svg]:fill-foreground grayscale *:[svg]:size-8"
/>
<ItemContent>
<ItemTitle>
<a
href={registry.homepage}
target="_blank"
rel="noopener noreferrer"
>
{registry.name}
</a>
</ItemTitle>
{registry.description && (
<ItemDescription className="text-pretty">
{registry.description}
</ItemDescription>
)}
</ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex">
<Button size="sm" variant="outline" asChild>
<a
href={registry.homepage}
target="_blank"
rel="noopener noreferrer"
>
View <IconArrowUpRight />
</Button>
<DirectoryAddButton registry={registry} />
</ItemFooter>
</Item>
{index < globalRegistries.length - 1 && (
<ItemSeparator className="my-1" />
)}
</React.Fragment>
))}
</ItemGroup>
</div>
</a>
</Button>
<DirectoryAddButton registry={registry} />
</ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden">
<Button size="sm" variant="outline">
View <IconArrowUpRight />
</Button>
<DirectoryAddButton registry={registry} />
</ItemFooter>
</Item>
{index < registries.length - 1 && <ItemSeparator className="my-1" />}
</React.Fragment>
))}
</ItemGroup>
)
}

View File

@@ -21,20 +21,15 @@ export function GitHubLink() {
export async function StarsCount() {
const data = await fetch("https://api.github.com/repos/shadcn-ui/ui", {
next: { revalidate: 86400 },
next: { revalidate: 86400 }, // Cache for 1 day (86400 seconds)
})
const json = await data.json()
const formattedCount =
json.stargazers_count >= 1000
? json.stargazers_count % 1000 === 0
? `${Math.floor(json.stargazers_count / 1000)}k`
: `${(json.stargazers_count / 1000).toFixed(1)}k`
: json.stargazers_count.toLocaleString()
return (
<span className="text-muted-foreground w-fit text-xs tabular-nums">
{formattedCount.replace(".0k", "k")}
<span className="text-muted-foreground w-8 text-xs tabular-nums">
{json.stargazers_count >= 1000
? `${(json.stargazers_count / 1000).toFixed(1)}k`
: json.stargazers_count.toLocaleString()}
</span>
)
}

View File

@@ -1,49 +0,0 @@
import * as React from "react"
import { Search, X } from "lucide-react"
import { useSearchRegistry } from "@/hooks/use-search-registry"
import { Field } from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
export const SearchDirectory = () => {
const { query, setQuery } = useSearchRegistry()
const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setQuery(value)
}
return (
<Field>
<InputGroup>
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupInput
placeholder="Search"
value={query}
onChange={onQueryChange}
/>
<InputGroupAddon
align="inline-end"
data-disabled={!query.length}
className="data-[disabled=true]:hidden"
>
<InputGroupButton
aria-label="Clear"
title="Clear"
size="icon-xs"
onClick={() => setQuery(null)}
>
<X />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
)
}

View File

@@ -124,7 +124,9 @@ export function CopyCodeButton({
</DrawerTrigger>
<DrawerContent className="h-auto">
<DrawerHeader>
<DrawerTitle className="capitalize">{activeThemeName}</DrawerTitle>
<DrawerTitle className="capitalize">
{activeThemeName === "neutral" ? "Default" : activeThemeName}
</DrawerTitle>
<DrawerDescription>
Copy and paste the following code into your CSS file.
</DrawerDescription>
@@ -147,7 +149,9 @@ export function CopyCodeButton({
</DialogTrigger>
<DialogContent className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 outline-none md:max-w-2xl dark:bg-neutral-800 dark:ring-neutral-900">
<DialogHeader>
<DialogTitle className="capitalize">{activeThemeName}</DialogTitle>
<DialogTitle className="capitalize">
{activeThemeName === "neutral" ? "Default" : activeThemeName}
</DialogTitle>
<DialogDescription>
Copy and paste the following code into your CSS file.
</DialogDescription>
@@ -161,7 +165,7 @@ export function CopyCodeButton({
function CustomizerCode({ themeName }: { themeName: string }) {
const [hasCopied, setHasCopied] = React.useState(false)
const [tailwindVersion, setTailwindVersion] = React.useState("v4-oklch")
const [tailwindVersion, setTailwindVersion] = React.useState("v4")
const activeTheme = React.useMemo(
() => baseColors.find((theme) => theme.name === themeName),
[themeName]
@@ -187,11 +191,10 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="min-w-0 px-4 pb-4 md:p-0"
>
<TabsList>
<TabsTrigger value="v4-oklch">OKLCH</TabsTrigger>
<TabsTrigger value="v4-hsl">HSL</TabsTrigger>
<TabsTrigger value="v4">Tailwind v4</TabsTrigger>
<TabsTrigger value="v3">Tailwind v3</TabsTrigger>
</TabsList>
<TabsContent value="v4-oklch">
<TabsContent value="v4">
<figure
data-rehype-pretty-code-figure
className="!mx-0 mt-0 rounded-lg"
@@ -213,12 +216,14 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="bg-code text-code-foreground absolute top-3 right-2 z-10 size-7 shadow-none hover:opacity-100 focus-visible:opacity-100"
onClick={() => {
copyToClipboardWithMeta(
getThemeCodeOKLCH(activeThemeOKLCH, 0.65),
tailwindVersion === "v3"
? getThemeCode(activeTheme, 0.65)
: getThemeCodeOKLCH(activeThemeOKLCH, 0.65),
{
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.65,
radius: 0.5,
},
}
)
@@ -241,8 +246,7 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}: <ColorIndicator color={value} />{" "}
{value};
&nbsp;&nbsp;&nbsp;--{key}: {value};
</span>
))}
<span data-line className="line text-code-foreground">
@@ -260,8 +264,7 @@ function CustomizerCode({ themeName }: { themeName: string }) {
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}: <ColorIndicator color={value} />{" "}
{value};
&nbsp;&nbsp;&nbsp;--{key}: {value};
</span>
))}
<span data-line className="line text-code-foreground">
@@ -271,90 +274,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</pre>
</figure>
</TabsContent>
<TabsContent value="v4-hsl">
<figure
data-rehype-pretty-code-figure
className="!mx-0 mt-0 rounded-lg"
>
<figcaption
className="text-code-foreground [&_svg]:text-code-foreground flex items-center gap-2 [&_svg]:size-4 [&_svg]:opacity-70"
data-rehype-pretty-code-title=""
data-language="css"
data-theme="github-dark github-light-default"
>
<Icons.css className="fill-foreground" />
app/globals.css
</figcaption>
<pre className="no-scrollbar max-h-[300px] min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0 md:max-h-[450px]">
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="bg-code text-code-foreground absolute top-3 right-2 z-10 size-7 shadow-none hover:opacity-100 focus-visible:opacity-100"
onClick={() => {
copyToClipboardWithMeta(
getThemeCodeHSLV4(activeTheme, 0.65),
{
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.65,
},
}
)
setHasCopied(true)
}}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
<code data-line-numbers data-language="css">
<span data-line className="line text-code-foreground">
&nbsp;:root &#123;
</span>
<span data-line className="line text-code-foreground">
&nbsp;&nbsp;&nbsp;--radius: 0.65rem;
</span>
{Object.entries(activeTheme?.cssVars.light || {}).map(
([key, value]) => (
<span
data-line
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}:{" "}
<ColorIndicator color={`hsl(${value})`} /> hsl({value});
</span>
)
)}
<span data-line className="line text-code-foreground">
&nbsp;&#125;
</span>
<span data-line className="line text-code-foreground">
&nbsp;
</span>
<span data-line className="line text-code-foreground">
&nbsp;.dark &#123;
</span>
{Object.entries(activeTheme?.cssVars.dark || {}).map(
([key, value]) => (
<span
data-line
className="line text-code-foreground"
key={key}
>
&nbsp;&nbsp;&nbsp;--{key}:{" "}
<ColorIndicator color={`hsl(${value})`} /> hsl({value});
</span>
)
)}
<span data-line className="line text-code-foreground">
&nbsp;&#125;
</span>
</code>
</pre>
</figure>
</TabsContent>
<TabsContent value="v3">
<figure
data-rehype-pretty-code-figure
@@ -376,13 +295,18 @@ function CustomizerCode({ themeName }: { themeName: string }) {
variant="ghost"
className="bg-code text-code-foreground absolute top-3 right-2 z-10 size-7 shadow-none hover:opacity-100 focus-visible:opacity-100"
onClick={() => {
copyToClipboardWithMeta(getThemeCode(activeTheme, 0.5), {
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.5,
},
})
copyToClipboardWithMeta(
tailwindVersion === "v3"
? getThemeCode(activeTheme, 0.65)
: getThemeCodeOKLCH(activeThemeOKLCH, 0.65),
{
name: "copy_theme_code",
properties: {
theme: themeName,
radius: 0.5,
},
}
)
setHasCopied(true)
}}
>
@@ -398,16 +322,10 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--background:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["background"]})`}
/>{" "}
{activeTheme?.cssVars.light["background"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--foreground:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["foreground"]})`}
/>{" "}
{activeTheme?.cssVars.light["foreground"]};
</span>
{[
@@ -422,13 +340,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
]
})`}
/>{" "}
{
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
@@ -438,13 +349,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}-foreground:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.light[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.light
]
})`}
/>{" "}
{
activeTheme?.cssVars.light[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.light
@@ -456,23 +360,14 @@ function CustomizerCode({ themeName }: { themeName: string }) {
))}
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--border:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["border"]})`}
/>{" "}
{activeTheme?.cssVars.light["border"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--input:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["input"]})`}
/>{" "}
{activeTheme?.cssVars.light["input"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--ring:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.light["ring"]})`}
/>{" "}
{activeTheme?.cssVars.light["ring"]};
</span>
<span data-line className="line">
@@ -483,13 +378,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
]
})`}
/>{" "}
{
activeTheme?.cssVars.light[
prefix as keyof typeof activeTheme.cssVars.light
@@ -511,16 +399,10 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--background:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["background"]})`}
/>{" "}
{activeTheme?.cssVars.dark["background"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--foreground:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["foreground"]})`}
/>{" "}
{activeTheme?.cssVars.dark["foreground"]};
</span>
{[
@@ -535,13 +417,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
]
})`}
/>{" "}
{
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
@@ -551,13 +426,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}-foreground:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.dark[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.dark
]
})`}
/>{" "}
{
activeTheme?.cssVars.dark[
`${prefix}-foreground` as keyof typeof activeTheme.cssVars.dark
@@ -569,23 +437,14 @@ function CustomizerCode({ themeName }: { themeName: string }) {
))}
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--border:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["border"]})`}
/>{" "}
{activeTheme?.cssVars.dark["border"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--input:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["input"]})`}
/>{" "}
{activeTheme?.cssVars.dark["input"]};
</span>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--ring:{" "}
<ColorIndicator
color={`hsl(${activeTheme?.cssVars.dark["ring"]})`}
/>{" "}
{activeTheme?.cssVars.dark["ring"]};
</span>
{["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"].map(
@@ -593,13 +452,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
<React.Fragment key={prefix}>
<span data-line className="line">
&nbsp;&nbsp;&nbsp;&nbsp;--{prefix}:{" "}
<ColorIndicator
color={`hsl(${
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
]
})`}
/>{" "}
{
activeTheme?.cssVars.dark[
prefix as keyof typeof activeTheme.cssVars.dark
@@ -625,15 +477,6 @@ function CustomizerCode({ themeName }: { themeName: string }) {
)
}
function ColorIndicator({ color }: { color: string }) {
return (
<span
className="border-border/50 inline-block size-3 border"
style={{ backgroundColor: color }}
/>
)
}
function getThemeCodeOKLCH(theme: BaseColorOKLCH | undefined, radius: number) {
if (!theme) {
return ""
@@ -666,27 +509,6 @@ function getThemeCode(theme: BaseColor | undefined, radius: number) {
})
}
function getThemeCodeHSLV4(theme: BaseColor | undefined, radius: number) {
if (!theme) {
return ""
}
const rootSection =
":root {\n --radius: " +
radius +
"rem;\n" +
Object.entries(theme.cssVars.light)
.map((entry) => " --" + entry[0] + ": hsl(" + entry[1] + ");")
.join("\n") +
"\n}\n\n.dark {\n" +
Object.entries(theme.cssVars.dark)
.map((entry) => " --" + entry[0] + ": hsl(" + entry[1] + ");")
.join("\n") +
"\n}\n"
return rootSection
}
const BASE_STYLES_WITH_VARIABLES = `
@layer base {
:root {

View File

@@ -17,14 +17,12 @@ import { CopyCodeButton } from "./theme-customizer"
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
const { activeTheme, setActiveTheme } = useThemeConfig()
const value = activeTheme === "default" ? "neutral" : activeTheme
return (
<div className={cn("flex items-center gap-2", className)}>
<Label htmlFor="theme-selector" className="sr-only">
Theme
</Label>
<Select value={value} onValueChange={setActiveTheme}>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
id="theme-selector"
size="sm"
@@ -40,7 +38,7 @@ export function ThemeSelector({ className }: React.ComponentProps<"div">) {
value={theme.name}
className="data-[state=checked]:opacity-50"
>
{theme.label}
{theme.label === "Neutral" ? "Default" : theme.label}
</SelectItem>
))}
</SelectContent>

View File

@@ -24,7 +24,7 @@ To create a new monorepo project, run the `init` command. You will be prompted
to select the type of project you are creating.
```bash
npx shadcn@latest init
npx shadcn@canary init
```
Select the `Next.js (Monorepo)` option.
@@ -51,15 +51,15 @@ cd apps/web
```
```bash
npx shadcn@latest add [COMPONENT]
npx shadcn@canary add [COMPONENT]
```
The CLI will figure out what type of component you are adding and install the
correct files to the correct path.
For example, if you run `npx shadcn@latest add button`, the CLI will install the button component under `packages/ui` and update the import path for components in `apps/web`.
For example, if you run `npx shadcn@canary add button`, the CLI will install the button component under `packages/ui` and update the import path for components in `apps/web`.
If you run `npx shadcn@latest add login-01`, the CLI will install the `button`, `label`, `input` and `card` components under `packages/ui` and the `login-form` component under `apps/web/components`.
If you run `npx shadcn@canary add login-01`, the CLI will install the `button`, `label`, `input` and `card` components under `packages/ui` and the `login-form` component under `apps/web/components`.
### Importing components

View File

@@ -193,7 +193,7 @@ You can now build your chart using Recharts components.
<Callout className="mt-4 bg-amber-50 border-amber-200 dark:bg-amber-950/50 dark:border-amber-950">
**Important:** Remember to set a `min-h-[VALUE]` on the `ChartContainer` component. This is required for the chart to be responsive.
**Important:** Remember to set a `min-h-[VALUE]` on the `ChartContainer` component. This is required for the chart be responsive.
</Callout>
@@ -370,7 +370,7 @@ The chart config is where you define the labels, icons and colors for a chart.
It is intentionally decoupled from chart data.
This allows you to share config and color tokens between charts. It can also work independently for cases where your data or color tokens live remotely or in a different format.
This allows you to share config and color tokens between charts. It can also works independently for cases where your data or color tokens live remotely or in a different format.
```tsx showLineNumbers /ChartConfig/
import { Monitor } from "lucide-react"
@@ -394,7 +394,7 @@ const chartConfig = {
## Theming
Charts have built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl or oklch.
Charts has built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl or oklch.
### CSS Variables

View File

@@ -1,6 +1,6 @@
---
title: Empty
description: Use the Empty component to display an empty state.
description: Use the Empty component to display a empty state.
component: true
---
@@ -70,7 +70,7 @@ import {
### Outline
Use the `border` utility class to create an outline empty state.
Use the `border` utility class to create a outline empty state.
<ComponentPreview
name="empty-outline"

View File

@@ -18,7 +18,7 @@ npx create-tsrouter-app@latest my-app --template file-router --tailwind --add-on
You can now start adding components to your project.
```bash
npx shadcn@latest add button
npx shadcn@canary add button
```
The command above will add the `Button` component to your project. You can then import it like this:

View File

@@ -7,18 +7,109 @@ description: Install and configure shadcn/ui for TanStack Start.
### Create project
Run the following command to create a new TanStack Start project with shadcn/ui:
Start by creating a new TanStack Start project by following the [Build a Project from Scratch](https://tanstack.com/start/latest/docs/framework/react/build-from-scratch) guide on the TanStack Start website.
**Do not add Tailwind yet. We'll install Tailwind v4 in the next step.**
### Add Tailwind
Install `tailwindcss` and its dependencies.
```bash
npm create @tanstack/start@latest --tailwind --add-ons shadcn
npm install tailwindcss @tailwindcss/postcss postcss
```
### Add Components
### Create postcss.config.ts
Create a `postcss.config.ts` file at the root of your project.
```ts title="postcss.config.ts" showLineNumbers
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}
```
### Create `app/styles/app.css`
Create an `app.css` file in the `app/styles` directory and import `tailwindcss`
```css title="app/styles/app.css"
@import "tailwindcss" source("../");
```
### Import `app.css`
```tsx title="app/routes/__root.tsx" showLineNumbers {5,21-26} showLineNumbers
import type { ReactNode } from "react"
import { createRootRoute, Outlet } from "@tanstack/react-router"
import { Meta, Scripts } from "@tanstack/start"
import appCss from "@/styles/app.css?url"
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
},
],
links: [
{
rel: "stylesheet",
href: appCss,
},
],
}),
component: RootComponent,
})
```
### Edit tsconfig.json file
Add the following code to the `tsconfig.json` file to resolve paths.
```ts title="tsconfig.json" showLineNumbers {9-12}
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"]
}
}
}
```
### Run the CLI
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@canary init
```
This will create a `components.json` file in the root of your project and configure CSS variables inside `app/styles/app.css`.
### That's it
You can now start adding components to your project.
```bash
npx shadcn@latest add button
npx shadcn@canary add button
```
The command above will add the `Button` component to your project. You can then import it like this:
@@ -26,7 +117,10 @@ The command above will add the `Button` component to your project. You can then
```tsx title="app/routes/index.tsx" showLineNumbers {1,6}
import { Button } from "@/components/ui/button"
function App() {
function Home() {
const router = useRouter()
const state = Route.useLoaderData()
return (
<div>
<Button>Click me</Button>
@@ -36,9 +130,3 @@ function App() {
```
</Steps>
If you want to add all `shadcn/ui` components, you can run the following command:
```bash
npx shadcn@latest add --all
```

View File

@@ -103,7 +103,7 @@ You can read more about the registry item schema and file types in the [registry
### Install the shadcn CLI
```bash
npm install shadcn@latest
npm install shadcn@canary
```
### Add a build script

View File

@@ -1,39 +0,0 @@
import { debounce, useQueryState } from "nuqs"
import globalRegistries from "@/registry/directory.json"
const normalizeQuery = (query: string) =>
query.toLowerCase().replaceAll(" ", "").replaceAll("@", "")
function finderFn<T extends (typeof globalRegistries)[0]>(
registry: T,
query: string
) {
const normalizedName = normalizeQuery(registry.name)
const normalizedDecription = normalizeQuery(registry.description)
const normalizedQuery = normalizeQuery(query)
return (
normalizedName.includes(normalizedQuery) ||
normalizedDecription.includes(normalizedQuery)
)
}
const searchDirectory = (query: string | null) => {
if (!query) return globalRegistries
return globalRegistries.filter((registry) => finderFn(registry, query))
}
export const useSearchRegistry = () => {
const [query, setQuery] = useQueryState("q", {
defaultValue: "",
limitUrlUpdates: debounce(250),
})
return {
query,
registries: searchDirectory(query),
setQuery,
}
}

View File

@@ -1,5 +1,5 @@
import { baseColors } from "@/registry/base-colors"
export const THEMES = baseColors
.filter((theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name))
.sort((a, b) => a.name.localeCompare(b.name))
export const THEMES = baseColors.filter(
(theme) => !["slate", "stone", "gray", "zinc"].includes(theme.name)
)

View File

@@ -22,10 +22,10 @@
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "^10.1.0",
"@faker-js/faker": "^8.2.0",
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accessible-icon": "^1.1.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-aspect-ratio": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.2",
@@ -38,7 +38,7 @@
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-menubar": "^1.1.5",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-navigation-menu": "^1.2.4",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-portal": "^1.1.3",
"@radix-ui/react-progress": "^1.1.1",
@@ -51,9 +51,9 @@
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-tooltip": "^1.1.7",
"@tabler/icons-react": "^3.31.0",
"@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-form": "^1.20.0",
@@ -73,14 +73,13 @@
"fumadocs-mdx": "13.0.2",
"fumadocs-ui": "16.0.5",
"input-otp": "^1.4.2",
"jotai": "^2.15.0",
"jotai": "^2.1.0",
"little-date": "^1.0.0",
"lodash": "^4.17.21",
"lucide-react": "0.474.0",
"motion": "^12.12.1",
"next": "16.0.0",
"next-themes": "0.4.6",
"nuqs": "^2.7.2",
"postcss": "^8.5.1",
"react": "19.2.0",
"react-day-picker": "^9.7.0",

View File

@@ -2,12 +2,10 @@
"@8bitcn": "https://8bitcn.com/r/{name}.json",
"@97cn": "https://97cn.itzik.co/r/{name}.json",
"@aceternity": "https://ui.aceternity.com/registry/{name}.json",
"@aevr": "https://ui.aevr.space/r/{name}.json",
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json",
"@alexcarpenter": "https://ui.alexcarpenter.me/r/{name}.json",
"@algolia": "https://sitesearch.algolia.com/r/{name}.json",
"@alpine": "https://alpine-registry.vercel.app/r/{name}.json",
"@aliimam": "https://aliimam.in/r/{name}.json",
"@animate-ui": "https://animate-ui.com/r/{name}.json",
"@assistant-ui": "https://r.assistant-ui.com/{name}.json",
"@austin-ui": "https://austin-ui.netlify.app/r/{name}.json",
@@ -18,9 +16,7 @@
"@bucharitesh": "https://bucharitesh.in/r/{name}.json",
"@clerk": "https://clerk.com/r/{name}.json",
"@coss": "https://coss.com/ui/r/{name}.json",
"@commercn": "https://commercn.com/r/{name}.json",
"@chisom-ui": "https://chisom-ui.netlify.app/r/{name}.json",
"@creative-tim": "https://www.creative-tim.com/ui/r/{name}.json",
"@cult-ui": "https://cult-ui.com/r/{name}.json",
"@diceui": "https://diceui.com/r/{name}.json",
"@efferd": "https://efferd.com/r/{name}.json",
@@ -35,21 +31,17 @@
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
"@kanpeki": "https://kanpeki.vercel.app/r/{name}.json",
"@kokonutui": "https://kokonutui.com/r/{name}.json",
"@lens-blocks": "https://lensblocks.com/r/{name}.json",
"@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json",
"@lytenyte": "https://www.1771technologies.com/r/{name}.json",
"@magicui": "https://magicui.design/r/{name}.json",
"@magicui-pro": "https://pro.magicui.design/registry/{name}",
"@motion-primitives": "https://motion-primitives.com/c/{name}.json",
"@mui-treasury": "https://mui-treasury.com/r/{name}.json",
"@nativeui": "https://nativeui.io/registry/{name}.json",
"@ncdai": "https://chanhdai.com/r/{name}.json",
"@nuqs": "https://nuqs.dev/r/{name}.json",
"@oui": "https://oui.mw10013.workers.dev/r/{name}.json",
"@paceui": "https://ui.paceui.com/r/{name}.json",
"@plate": "https://platejs.org/r/{name}.json",
"@prompt-kit": "https://prompt-kit.com/c/{name}.json",
"@prosekit": "https://prosekit.dev/r/{name}.json",
"@react-bits": "https://reactbits.dev/r/{name}.json",
"@react-market": "https://www.react-market.com/get/{name}.json",
"@retroui": "https://retroui.dev/r/{name}.json",
@@ -58,7 +50,6 @@
"@roiui": "https://roiui.com/r/{name}.json",
"@solaceui": "https://www.solaceui.com/r/{name}.json",
"@scrollxui": "https://www.scrollxui.dev/registry/{name}.json",
"@systaliko-ui": "https://systaliko-ui.vercel.app/r/{name}.json",
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json",
"@shadcn-map": "http://shadcn-map.vercel.app/r/{name}.json",
"@shadcn-studio": "https://shadcnstudio.com/r/{name}.json",
@@ -75,14 +66,10 @@
"@wds": "https://wds-shadcn-registry.netlify.app/r/{name}.json",
"@wandry-ui": "https://ui.wandry.com.ua/r/{name}.json",
"@wigggle-ui": "https://wigggle-ui.vercel.app/r/{name}.json",
"@paykit-sdk": "https://www.usepaykit.dev/r/{name}.json",
"@pixelact-ui": "https://www.pixelactui.com/r/{name}.json",
"@zippystarter": "https://zippystarter.com/r/{name}.json",
"@shadcndesign": "https://shadcndesign-free.vercel.app/r/{name}.json",
"@ha-components": "https://hacomponents.keshuac.com/r/{name}.json",
"@shadix-ui": "https://shadix-ui.vercel.app/r/{name}.json",
"@utilcn": "https://utilcn.dev/r/{name}.json",
"@hextaui": "https://hextaui.com/r/{name}.json",
"@taki": "https://taki-ui.com/r/{name}.json",
"@square-ui": "https://square.lndev.me/registry/{name}.json"
"@utilcn": "https://utilcn.dev/r/{name}.json"
}

File diff suppressed because one or more lines are too long

View File

@@ -150,7 +150,7 @@
@apply bg-selection text-selection-foreground;
}
html {
@apply overscroll-y-none scroll-smooth;
@apply overscroll-none scroll-smooth;
}
body {
font-synthesis-weight: none;

View File

@@ -42,9 +42,9 @@
"packageManager": "pnpm@9.0.6",
"dependencies": {
"@babel/core": "^7.22.1",
"@changesets/changelog-github": "^0.5.1",
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@ianvs/prettier-plugin-sort-imports": "^3.7.2",
"@manypkg/cli": "^0.20.0",

View File

@@ -7,12 +7,8 @@ export const transformImport: Transformer = async ({
config,
isRemote,
}) => {
const utilsAlias = config.aliases?.utils
const workspaceAlias =
typeof utilsAlias === "string" && utilsAlias.includes("/")
? utilsAlias.split("/")[0]
: "@"
const utilsImport = `${workspaceAlias}/lib/utils`
const workspaceAlias = config.aliases?.utils?.split("/")[0]?.slice(1)
const utilsImport = `@${workspaceAlias}/lib/utils`
if (![".tsx", ".ts", ".jsx", ".js"].includes(sourceFile.getExtension())) {
return sourceFile
@@ -35,9 +31,7 @@ export const transformImport: Transformer = async ({
?.getNamedImports()
.some((namedImport) => namedImport.getName() === "cn")
if (!isCnImport || !config.aliases.utils) {
continue
}
if (!isCnImport) continue
specifier.setLiteralValue(
utilsImport === updated

View File

@@ -70,7 +70,7 @@ import { Foo } from "bar"
import { Label} from "ui/label"
import { Box } from "~/src/components/box"
import { cn, foo, bar } from "~/lib"
import { cn, foo, bar } from "~/lib/utils"
import { bar } from "~/lib/utils/bar"
"
`;
@@ -82,7 +82,7 @@ import { Foo } from "bar"
import { Label} from "ui/label"
import { Box } from "~/src/components/box"
import { cn } from "~/src/utils"
import { cn } from "~/lib/utils"
import { bar } from "~/lib/utils/bar"
"
`;
@@ -94,7 +94,7 @@ import { Foo } from "bar"
import { Label} from "ui/label"
import { Box } from "~/src/components/box"
import { cn } from "~/src/utils"
import { cn } from "~/lib/utils"
import { bar } from "~/lib/utils/bar"
"
`;
@@ -106,7 +106,7 @@ import { Foo } from "bar"
import { Label} from "ui/label"
import { Box } from "~/src/components/box"
import { cn } from "~/src/utils"
import { cn } from "~/lib/utils"
import { bar } from "~/lib/utils/bar"
"
`;

View File

@@ -2,38 +2,6 @@ import { expect, test } from "vitest"
import { transform } from "../../src/utils/transformers"
test('transform nested workspace folder for utils, website/src/utils', async () => {
expect(
await transform({
filename: "test.ts",
raw: `import { Button } from "website/src/components/ui/button"
import { Box } from "website/src/components/box"
import { cn } from "website/src/utils"
`,
config: {
tsx: true,
tailwind: {
baseColor: "neutral",
cssVariables: true,
},
aliases: {
components: "website/src/components",
lib: "website/src/lib",
utils: "website/src/utils",
},
},
})
).toMatchInlineSnapshot(`
"import { Button } from "website/src/components/ui/button"
import { Box } from "website/src/components/box"
import { cn } from "website/src/utils"
"
`)
})
test("transform import", async () => {
expect(
await transform({

1236
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff