mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-28 07:04:20 +00:00
fix
This commit is contained in:
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
|
||||
import { highlightCode } from "@/lib/highlight-code"
|
||||
import { getDemoItem, getRegistryItem } from "@/lib/registry"
|
||||
import { transformForDisplay } from "@/lib/rehype"
|
||||
import { formatCode } from "@/lib/rehype"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
@@ -33,7 +33,6 @@ export async function ComponentSource({
|
||||
let code: string | undefined
|
||||
|
||||
if (name) {
|
||||
// Try demo item first, then fall back to registry item.
|
||||
const item =
|
||||
(await getDemoItem(name, styleName)) ??
|
||||
(await getRegistryItem(name, styleName))
|
||||
@@ -49,17 +48,9 @@ export async function ComponentSource({
|
||||
return null
|
||||
}
|
||||
|
||||
// Fix imports.
|
||||
// Replace @/registry/${style}/ with @/components/.
|
||||
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
|
||||
|
||||
// Replace export default with export.
|
||||
code = code.replaceAll("export default", "export")
|
||||
code = await formatCode(code, styleName)
|
||||
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
|
||||
|
||||
// Apply transforms (cn-* → Tailwind, IconPlaceholder → icons, etc.).
|
||||
code = await transformForDisplay(code, styleName)
|
||||
|
||||
const lang = language ?? title?.split(".").pop() ?? "tsx"
|
||||
const highlightedCode = await highlightCode(code, lang)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
export default function AccordionBorders() {
|
||||
return (
|
||||
<Accordion className="w-full">
|
||||
<Accordion className="w-full max-w-sm">
|
||||
<AccordionItem
|
||||
value="billing"
|
||||
className="border px-4 first:rounded-t-lg last:rounded-b-lg"
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
export default function AccordionCard() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Subscription & Billing</CardTitle>
|
||||
<CardDescription>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
export default function AccordionMultiple() {
|
||||
return (
|
||||
<Accordion multiple className="w-full">
|
||||
<Accordion multiple className="w-full max-w-md">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
What are the key considerations when implementing a comprehensive
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
export default function AccordionBorders() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<Accordion type="single" collapsible className="w-full max-w-sm">
|
||||
<AccordionItem
|
||||
value="billing"
|
||||
className="border px-4 first:rounded-t-lg last:rounded-b-lg"
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
export default function AccordionCard() {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Subscription & Billing</CardTitle>
|
||||
<CardDescription>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
export default function AccordionMultiple() {
|
||||
return (
|
||||
<Accordion type="multiple" className="w-full">
|
||||
<Accordion type="multiple" className="w-full max-w-md">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
What are the key considerations when implementing a comprehensive
|
||||
|
||||
@@ -7,32 +7,26 @@ import { Project, ScriptKind } from "ts-morph"
|
||||
import { type z } from "zod"
|
||||
|
||||
import { Index as StylesIndex } from "@/registry/__index__"
|
||||
import { BASES } from "@/registry/bases"
|
||||
import { Index as BasesIndex } from "@/registry/bases/__index__"
|
||||
|
||||
// Styles that have their own index in StylesIndex (built with style transforms).
|
||||
const INDEXED_STYLES = ["new-york-v4"]
|
||||
|
||||
// Get the base name from a style name (e.g., "base-nova" -> "base").
|
||||
// Returns null for legacy styles that don't use the examples system.
|
||||
function getBaseForStyle(styleName: string) {
|
||||
if (styleName.startsWith("radix-")) {
|
||||
return "radix"
|
||||
for (const base of BASES) {
|
||||
if (styleName.startsWith(`${base.name}-`)) {
|
||||
return base.name
|
||||
}
|
||||
}
|
||||
if (styleName.startsWith("base-")) {
|
||||
return "base"
|
||||
}
|
||||
// Legacy styles (e.g., "new-york-v4") don't use the examples system.
|
||||
return null
|
||||
}
|
||||
|
||||
// Get a demo component from the examples index.
|
||||
export function getDemoComponent(name: string, styleName: string) {
|
||||
const base = getBaseForStyle(styleName)
|
||||
if (!base) return undefined
|
||||
return ExamplesIndex[base]?.[name]?.component
|
||||
}
|
||||
|
||||
// Get a demo item with file content from the examples index.
|
||||
export async function getDemoItem(name: string, styleName: string) {
|
||||
const base = getBaseForStyle(styleName)
|
||||
if (!base) return null
|
||||
@@ -58,30 +52,25 @@ export async function getDemoItem(name: string, styleName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Map style names to their corresponding index and key.
|
||||
function getIndexForStyle(styleName: string) {
|
||||
// Use StylesIndex for styles that are built with transforms.
|
||||
if (INDEXED_STYLES.includes(styleName)) {
|
||||
return { index: StylesIndex, key: styleName }
|
||||
}
|
||||
// Fall back to BasesIndex for other base-style combinations.
|
||||
if (styleName.startsWith("radix-")) {
|
||||
return { index: BasesIndex, key: "radix" }
|
||||
}
|
||||
if (styleName.startsWith("base-")) {
|
||||
return { index: BasesIndex, key: "base" }
|
||||
|
||||
const base = getBaseForStyle(styleName)
|
||||
if (base) {
|
||||
return { index: BasesIndex, key: base }
|
||||
}
|
||||
|
||||
return { index: StylesIndex, key: styleName }
|
||||
}
|
||||
|
||||
export function getRegistryComponent(name: string, styleName: string) {
|
||||
// Check demo index first.
|
||||
const demoComponent = getDemoComponent(name, styleName)
|
||||
if (demoComponent) {
|
||||
return demoComponent
|
||||
}
|
||||
|
||||
// Fall back to registry.
|
||||
const { index, key } = getIndexForStyle(styleName)
|
||||
return index[key]?.[name]?.component
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs, { promises as fsPromises } from "fs"
|
||||
import path from "path"
|
||||
import type { configSchema } from "shadcn/schema"
|
||||
import { ExamplesIndex } from "@/examples/__index__"
|
||||
import {
|
||||
createStyleMap,
|
||||
transformIcons,
|
||||
@@ -11,31 +11,45 @@ import {
|
||||
import { Project, ScriptKind } from "ts-morph"
|
||||
import { u } from "unist-builder"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { type z } from "zod"
|
||||
|
||||
import { Index as StylesIndex } from "@/registry/__index__"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
import { BASES } from "@/registry/bases"
|
||||
import { Index as BasesIndex } from "@/registry/bases/__index__"
|
||||
|
||||
// Map style names to their corresponding index and key.
|
||||
function getIndexForStyle(styleName: string) {
|
||||
if (styleName.startsWith("radix-")) {
|
||||
return { index: BasesIndex, key: "radix" }
|
||||
function getBaseForStyle(styleName: string) {
|
||||
for (const base of BASES) {
|
||||
if (styleName.startsWith(`${base.name}-`)) {
|
||||
return base.name
|
||||
}
|
||||
}
|
||||
if (styleName.startsWith("base-")) {
|
||||
return { index: BasesIndex, key: "base" }
|
||||
return null
|
||||
}
|
||||
|
||||
function getDemoFilePath(name: string, styleName: string) {
|
||||
const base = getBaseForStyle(styleName)
|
||||
if (!base) return null
|
||||
|
||||
const demo = ExamplesIndex[base]?.[name]
|
||||
if (!demo) return null
|
||||
|
||||
return path.join(process.cwd(), demo.filePath)
|
||||
}
|
||||
|
||||
function getIndexForStyle(styleName: string) {
|
||||
const base = getBaseForStyle(styleName)
|
||||
if (base) {
|
||||
return { index: BasesIndex, key: base }
|
||||
}
|
||||
return { index: StylesIndex, key: styleName }
|
||||
}
|
||||
|
||||
// Extract style name from compound style (e.g., "radix-nova" → "nova").
|
||||
function getStyleFromStyleName(styleName: string) {
|
||||
const parts = styleName.split("-")
|
||||
return parts.length > 1 ? parts.slice(1).join("-") : styleName
|
||||
}
|
||||
|
||||
// Build minimal config for transforms.
|
||||
function buildDisplayConfig(styleName: string): z.infer<typeof configSchema> {
|
||||
function buildDisplayConfig(styleName: string) {
|
||||
return {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
style: styleName,
|
||||
@@ -69,7 +83,6 @@ function buildDisplayConfig(styleName: string): z.infer<typeof configSchema> {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for style maps to avoid repeated file reads.
|
||||
const styleMapCache = new Map<string, Record<string, string>>()
|
||||
|
||||
async function getStyleMap(styleName: string) {
|
||||
@@ -89,18 +102,25 @@ async function getStyleMap(styleName: string) {
|
||||
styleMapCache.set(style, styleMap)
|
||||
return styleMap
|
||||
} catch {
|
||||
// Return empty style map if file not found.
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function transformForDisplay(content: string, styleName: string) {
|
||||
try {
|
||||
// 1. Apply style transformation (cn-* → Tailwind classes).
|
||||
const styleMap = await getStyleMap(styleName)
|
||||
const transformed = await transformStyle(content, { styleMap })
|
||||
export async function formatCode(code: string, styleName: string) {
|
||||
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
|
||||
|
||||
// 2. Apply icon/menu/render transforms.
|
||||
for (const base of BASES) {
|
||||
code = code.replaceAll(`@/registry/bases/${base.name}/`, "@/components/")
|
||||
code = code.replaceAll(`@/examples/${base.name}/ui/`, "@/components/ui/")
|
||||
code = code.replaceAll(`@/examples/${base.name}/lib/`, "@/lib/")
|
||||
code = code.replaceAll(`@/examples/${base.name}/hooks/`, "@/hooks/")
|
||||
}
|
||||
|
||||
code = code.replaceAll("export default", "export")
|
||||
|
||||
try {
|
||||
const styleMap = await getStyleMap(styleName)
|
||||
const transformed = await transformStyle(code, { styleMap })
|
||||
const config = buildDisplayConfig(styleName)
|
||||
const project = new Project({ compilerOptions: {} })
|
||||
const sourceFile = project.createSourceFile("component.tsx", transformed, {
|
||||
@@ -119,9 +139,8 @@ export async function transformForDisplay(content: string, styleName: string) {
|
||||
|
||||
return sourceFile.getText()
|
||||
} catch (error) {
|
||||
// Return original content if transform fails.
|
||||
console.error("Transform failed:", error)
|
||||
return content
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +163,6 @@ export interface UnistTree {
|
||||
children: UnistNode[]
|
||||
}
|
||||
|
||||
// Collected node info for async processing.
|
||||
interface NodeToProcess {
|
||||
node: UnistNode
|
||||
type: "ComponentSource" | "ComponentPreview"
|
||||
@@ -159,7 +177,6 @@ export function rehypeComponent() {
|
||||
const activeStyle = await getActiveStyle()
|
||||
const nodesToProcess: NodeToProcess[] = []
|
||||
|
||||
// First pass: collect all nodes that need processing.
|
||||
visit(tree, (node: UnistNode) => {
|
||||
const { value: srcPath } =
|
||||
(getNodeAttributeByName(node, "src") as {
|
||||
@@ -206,50 +223,44 @@ export function rehypeComponent() {
|
||||
}
|
||||
})
|
||||
|
||||
// Second pass: process all collected nodes asynchronously.
|
||||
await Promise.all(
|
||||
nodesToProcess.map(async (item) => {
|
||||
try {
|
||||
let src: string
|
||||
let src: string | null = null
|
||||
|
||||
if (item.srcPath) {
|
||||
src = path.join(process.cwd(), item.srcPath)
|
||||
} else {
|
||||
const { index, key } = getIndexForStyle(item.styleName)
|
||||
const component = index[key]?.[item.name]
|
||||
src = getDemoFilePath(item.name, item.styleName)
|
||||
|
||||
if (item.type === "ComponentSource" && item.fileName) {
|
||||
src =
|
||||
component.files.find((file: unknown) => {
|
||||
if (typeof file === "string") {
|
||||
return (
|
||||
file.endsWith(`${item.fileName}.tsx`) ||
|
||||
file.endsWith(`${item.fileName}.ts`)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}) || component.files[0]?.path
|
||||
} else {
|
||||
src = component.files[0]?.path
|
||||
if (!src) {
|
||||
const { index, key } = getIndexForStyle(item.styleName)
|
||||
const component = index[key]?.[item.name]
|
||||
|
||||
if (item.type === "ComponentSource" && item.fileName) {
|
||||
src =
|
||||
component.files.find((file: unknown) => {
|
||||
if (typeof file === "string") {
|
||||
return (
|
||||
file.endsWith(`${item.fileName}.tsx`) ||
|
||||
file.endsWith(`${item.fileName}.ts`)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}) || component.files[0]?.path
|
||||
} else {
|
||||
src = component.files[0]?.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read the source file.
|
||||
let source = fs.readFileSync(src, "utf8")
|
||||
if (!src) {
|
||||
return
|
||||
}
|
||||
|
||||
// Replace imports.
|
||||
source = source.replaceAll(
|
||||
`@/registry/${item.styleName}/`,
|
||||
"@/components/"
|
||||
)
|
||||
source = source.replaceAll(`@/registry/bases/radix/`, "@/components/")
|
||||
source = source.replaceAll(`@/registry/bases/base/`, "@/components/")
|
||||
source = source.replaceAll("export default", "export")
|
||||
const raw = fs.readFileSync(src, "utf8")
|
||||
const source = await formatCode(raw, item.styleName)
|
||||
|
||||
// Apply transforms (cn-* → Tailwind, IconPlaceholder → icons, etc.).
|
||||
source = await transformForDisplay(source, item.styleName)
|
||||
|
||||
// Add code as children so that rehype can take over at build time.
|
||||
item.node.children?.push(
|
||||
u("element", {
|
||||
tagName: "pre",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { rimraf } from "rimraf"
|
||||
|
||||
const BASES = ["base", "radix"] as const
|
||||
import { BASES } from "@/registry/bases"
|
||||
|
||||
async function buildExamplesIndex() {
|
||||
const cwd = process.cwd()
|
||||
@@ -18,38 +18,36 @@ import * as React from "react"
|
||||
export const ExamplesIndex: Record<string, Record<string, any>> = {`
|
||||
|
||||
for (const base of BASES) {
|
||||
const baseDir = path.join(examplesDir, base)
|
||||
const baseDir = path.join(examplesDir, base.name)
|
||||
|
||||
// Check if base directory exists.
|
||||
try {
|
||||
await fs.access(baseDir)
|
||||
} catch {
|
||||
console.log(` Skipping ${base} - directory does not exist`)
|
||||
console.log(` Skipping ${base.name} - directory does not exist`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Find all demo files (excluding subdirectories like ui/).
|
||||
const allEntries = await fs.readdir(baseDir, { withFileTypes: true })
|
||||
const files = allEntries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".tsx"))
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
|
||||
console.log(` Found ${files.length} demos for ${base}`)
|
||||
console.log(` Found ${files.length} demos for ${base.name}`)
|
||||
|
||||
index += `
|
||||
"${base}": {`
|
||||
"${base.name}": {`
|
||||
|
||||
for (const file of files) {
|
||||
const name = file.replace(/\.tsx$/, "")
|
||||
const filePath = `examples/${base}/${file}`
|
||||
const filePath = `examples/${base.name}/${file}`
|
||||
|
||||
index += `
|
||||
"${name}": {
|
||||
name: "${name}",
|
||||
filePath: "${filePath}",
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import("./${base}/${name}")
|
||||
const mod = await import("./${base.name}/${name}")
|
||||
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || "${name}"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
@@ -64,7 +62,6 @@ export const ExamplesIndex: Record<string, Record<string, any>> = {`
|
||||
}
|
||||
`
|
||||
|
||||
// Write the index file.
|
||||
const indexPath = path.join(examplesDir, "__index__.tsx")
|
||||
rimraf.sync(indexPath)
|
||||
await fs.writeFile(indexPath, index)
|
||||
|
||||
@@ -488,51 +488,40 @@ async function buildConfig() {
|
||||
}
|
||||
|
||||
async function copyUIToExamples() {
|
||||
const bases = [
|
||||
{ name: "base", sourceStyle: "base-nova" },
|
||||
{ name: "radix", sourceStyle: "radix-nova" },
|
||||
]
|
||||
|
||||
const defaultStyle = "nova"
|
||||
const directories = ["ui", "lib", "hooks"]
|
||||
|
||||
for (const base of bases) {
|
||||
for (const base of BASES) {
|
||||
const sourceStyle = `${base.name}-${defaultStyle}`
|
||||
|
||||
for (const dir of directories) {
|
||||
const fromDir = path.join(
|
||||
process.cwd(),
|
||||
`registry/${base.sourceStyle}/${dir}`
|
||||
)
|
||||
const fromDir = path.join(process.cwd(), `registry/${sourceStyle}/${dir}`)
|
||||
const toDir = path.join(process.cwd(), `examples/${base.name}/${dir}`)
|
||||
|
||||
// Check if source directory exists.
|
||||
try {
|
||||
await fs.access(fromDir)
|
||||
} catch {
|
||||
console.log(
|
||||
` ⚠️ registry/${base.sourceStyle}/${dir} not found, skipping`
|
||||
)
|
||||
console.log(` ⚠️ registry/${sourceStyle}/${dir} not found, skipping`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Clean and create target directory.
|
||||
rimraf.sync(toDir)
|
||||
await fs.mkdir(toDir, { recursive: true })
|
||||
|
||||
// Copy all files and transform imports.
|
||||
const files = await fs.readdir(fromDir)
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(fromDir, file)
|
||||
const targetPath = path.join(toDir, file)
|
||||
|
||||
// Read, transform imports, and write.
|
||||
let content = await fs.readFile(sourcePath, "utf-8")
|
||||
content = content.replace(
|
||||
new RegExp(`@/registry/${base.sourceStyle}/`, "g"),
|
||||
new RegExp(`@/registry/${sourceStyle}/`, "g"),
|
||||
`@/examples/${base.name}/`
|
||||
)
|
||||
await fs.writeFile(targetPath, content)
|
||||
}
|
||||
console.log(
|
||||
` ✅ registry/${base.sourceStyle}/${dir} → examples/${base.name}/${dir}`
|
||||
` ✅ registry/${sourceStyle}/${dir} → examples/${base.name}/${dir}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user