mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-09 06:55:07 +00:00
feat(cli): tree resolver
This commit is contained in:
@@ -41,7 +41,8 @@
|
||||
"pub:beta": "pnpm build && pnpm publish --no-git-checks --access public --tag beta",
|
||||
"pub:next": "pnpm build && pnpm publish --no-git-checks --access public --tag next",
|
||||
"pub:release": "pnpm build && pnpm publish --access public",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"test:dev": "COMPONENTS_REGISTRY_URL=http://localhost:3003 vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antfu/ni": "^0.21.4",
|
||||
|
||||
@@ -68,6 +68,11 @@ export const add = new Command()
|
||||
|
||||
const registryIndex = await getRegistryIndex()
|
||||
|
||||
if (!registryIndex) {
|
||||
handleError(new Error("Failed to fetch registry index."))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let selectedComponents = options.all
|
||||
? registryIndex.map((entry) => entry.name)
|
||||
: options.components
|
||||
@@ -98,7 +103,7 @@ export const add = new Command()
|
||||
const payload = await fetchTree(config.style, tree)
|
||||
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
||||
|
||||
if (!payload.length) {
|
||||
if (!payload?.length) {
|
||||
logger.warn("Selected components not found. Exiting.")
|
||||
process.exit(0)
|
||||
}
|
||||
@@ -133,11 +138,11 @@ export const add = new Command()
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
}
|
||||
|
||||
const existingComponent = item.files.filter((file) =>
|
||||
existsSync(path.resolve(targetDir, file.name))
|
||||
const existingComponent = item.files?.filter((file) =>
|
||||
existsSync(path.resolve(targetDir, file.path))
|
||||
)
|
||||
|
||||
if (existingComponent.length && !options.overwrite) {
|
||||
if (existingComponent?.length && !options.overwrite) {
|
||||
if (selectedComponents.includes(item.name)) {
|
||||
spinner.stop()
|
||||
const { overwrite } = await prompts({
|
||||
@@ -162,12 +167,16 @@ export const add = new Command()
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of item.files) {
|
||||
let filePath = path.resolve(targetDir, file.name)
|
||||
for (const file of item.files ?? []) {
|
||||
let filePath = path.resolve(targetDir, file.path)
|
||||
|
||||
// Run transformers.
|
||||
if (!file.content) {
|
||||
continue
|
||||
}
|
||||
|
||||
const content = await transform({
|
||||
filename: file.name,
|
||||
filename: file.path,
|
||||
raw: file.content,
|
||||
config,
|
||||
baseColor,
|
||||
|
||||
@@ -59,13 +59,18 @@ export const diff = new Command()
|
||||
|
||||
const registryIndex = await getRegistryIndex()
|
||||
|
||||
if (!registryIndex) {
|
||||
handleError(new Error("Failed to fetch registry index."))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!options.component) {
|
||||
const targetDir = config.resolvedPaths.components
|
||||
|
||||
// Find all components that exist in the project.
|
||||
const projectComponents = registryIndex.filter((item) => {
|
||||
for (const file of item.files) {
|
||||
const filePath = path.resolve(targetDir, file)
|
||||
for (const file of item.files ?? []) {
|
||||
const filePath = path.resolve(targetDir, file.path)
|
||||
if (existsSync(filePath)) {
|
||||
return true
|
||||
}
|
||||
@@ -139,6 +144,10 @@ async function diffComponent(
|
||||
const payload = await fetchTree(config.style, [component])
|
||||
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
||||
|
||||
if (!payload) {
|
||||
return []
|
||||
}
|
||||
|
||||
const changes = []
|
||||
|
||||
for (const item of payload) {
|
||||
@@ -148,8 +157,8 @@ async function diffComponent(
|
||||
continue
|
||||
}
|
||||
|
||||
for (const file of item.files) {
|
||||
const filePath = path.resolve(targetDir, file.name)
|
||||
for (const file of item.files ?? []) {
|
||||
const filePath = path.resolve(targetDir, file.path)
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
continue
|
||||
@@ -157,8 +166,12 @@ async function diffComponent(
|
||||
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
|
||||
if (!file.content) {
|
||||
continue
|
||||
}
|
||||
|
||||
const registryContent = await transform({
|
||||
filename: file.name,
|
||||
filename: file.path,
|
||||
raw: file.content,
|
||||
config,
|
||||
baseColor,
|
||||
@@ -167,7 +180,6 @@ async function diffComponent(
|
||||
const patch = diffLines(registryContent as string, fileContent)
|
||||
if (patch.length > 1) {
|
||||
changes.push({
|
||||
file: file.name,
|
||||
filePath,
|
||||
patch,
|
||||
})
|
||||
|
||||
@@ -19,10 +19,11 @@ import {
|
||||
getRegistryBaseColors,
|
||||
getRegistryItem,
|
||||
getRegistryStyles,
|
||||
registryResolveItemsTree,
|
||||
} from "@/src/utils/registry"
|
||||
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
|
||||
import { updateDestinations } from "@/src/utils/updaters/update-destinations"
|
||||
import { updateFiles } from "@/src/utils/updaters/update-files"
|
||||
import { updateRegistryDependencies } from "@/src/utils/updaters/update-registry-dependencies"
|
||||
import {
|
||||
buildTailwindThemeColorsFromCssVars,
|
||||
updateTailwindConfig,
|
||||
@@ -97,10 +98,10 @@ export const init = new Command()
|
||||
if (!opts.force && !opts.defaults) {
|
||||
logger.info("")
|
||||
}
|
||||
const spinner = ora(`Writing components.json...`).start()
|
||||
const spinner = ora(`Writing components.json.`).start()
|
||||
const targetPath = path.resolve(cwd, "components.json")
|
||||
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8")
|
||||
spinner.succeed(`Writing components.json.`)
|
||||
spinner.succeed()
|
||||
|
||||
const fullConfig = await resolveConfigPaths(cwd, config)
|
||||
|
||||
@@ -291,60 +292,25 @@ export async function promptForMinimalConfig(
|
||||
}
|
||||
|
||||
export async function runInit(config: Config) {
|
||||
const initializersSpinner = ora(`Initializing project...`)?.start()
|
||||
await updateDestinations(config)
|
||||
const [payload, baseColor] = await Promise.all([
|
||||
getRegistryItem(config.style, "index"),
|
||||
getRegistryBaseColor(config.tailwind.baseColor),
|
||||
])
|
||||
|
||||
if (!payload) {
|
||||
const initializersSpinner = ora(`Initializing project.`)?.start()
|
||||
const tree = await registryResolveItemsTree(["index"], config)
|
||||
|
||||
if (!tree) {
|
||||
initializersSpinner?.fail()
|
||||
logger.error(`Something went wrong during the initialization process.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Inline the base color in the tailwind config.
|
||||
if (config.tailwind.cssVariables && baseColor) {
|
||||
payload.cssVars = {
|
||||
light: {
|
||||
...baseColor.cssVars.light,
|
||||
...payload.cssVars?.light,
|
||||
},
|
||||
dark: {
|
||||
...baseColor.cssVars.dark,
|
||||
...payload.cssVars?.dark,
|
||||
},
|
||||
}
|
||||
console.log(tree.tailwind?.config?.theme)
|
||||
|
||||
// Move the css vars to the tailwind config.
|
||||
if (payload.tailwind?.config && baseColor.cssVars?.light) {
|
||||
payload.tailwind.config = deepmerge(payload.tailwind.config, {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: buildTailwindThemeColorsFromCssVars(
|
||||
baseColor.cssVars.light
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
await updateTailwindConfig(tree.tailwind?.config, config)
|
||||
await updateTailwindCss(tree.cssVars, config)
|
||||
initializersSpinner?.succeed()
|
||||
|
||||
if (payload.tailwind?.config) {
|
||||
await updateTailwindConfig(payload.tailwind?.config, config)
|
||||
}
|
||||
|
||||
if (payload.cssVars) {
|
||||
await updateTailwindCss(payload.cssVars, config)
|
||||
}
|
||||
|
||||
await updateFiles(payload.files, config)
|
||||
initializersSpinner?.succeed(`Initializing project.`)
|
||||
|
||||
// Install dependencies.
|
||||
if (payload.dependencies) {
|
||||
const dependenciesSpinner = ora(`Installing dependencies...`)?.start()
|
||||
await updateDependencies(payload.dependencies, config)
|
||||
dependenciesSpinner?.succeed(`Installing dependencies.`)
|
||||
}
|
||||
const dependenciesSpinner = ora(`Installing dependencies.`)?.start()
|
||||
// await updateRegistryDependencies(tree.registryDependencies, config)
|
||||
await updateDependencies(tree.dependencies, config)
|
||||
dependenciesSpinner?.succeed()
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export function handleError(error: unknown) {
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
console.log(error.issues)
|
||||
logger.error("Validation failed:")
|
||||
for (const [key, value] of Object.entries(error.flatten().fieldErrors)) {
|
||||
logger.error(`- ${cyan(key)}: ${value}`)
|
||||
|
||||
@@ -5,11 +5,14 @@ import { logger } from "@/src/utils/logger"
|
||||
import {
|
||||
registryBaseColorSchema,
|
||||
registryIndexSchema,
|
||||
registryItemFileSchema,
|
||||
registryItemSchema,
|
||||
registryItemWithContentSchema,
|
||||
registryWithContentSchema,
|
||||
registryItemTypeSchema,
|
||||
registryResolvedItemsTreeSchema,
|
||||
stylesSchema,
|
||||
} from "@/src/utils/registry/schema"
|
||||
import { buildTailwindThemeColorsFromCssVars } from "@/src/utils/updaters/update-tailwind-config"
|
||||
import deepmerge from "deepmerge"
|
||||
import { HttpsProxyAgent } from "https-proxy-agent"
|
||||
import { cyan } from "kleur/colors"
|
||||
import fetch from "node-fetch"
|
||||
@@ -28,7 +31,8 @@ export async function getRegistryIndex() {
|
||||
|
||||
return registryIndexSchema.parse(result)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch components from registry.`)
|
||||
logger.error("\n")
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +42,9 @@ export async function getRegistryStyles() {
|
||||
|
||||
return stylesSchema.parse(result)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch styles from registry.`)
|
||||
logger.error("\n")
|
||||
handleError(error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +54,7 @@ export async function getRegistryItem(style: string, name: string) {
|
||||
|
||||
return registryItemSchema.parse(result)
|
||||
} catch (error) {
|
||||
logger.error("\n")
|
||||
handleError(error)
|
||||
|
||||
return null
|
||||
@@ -123,16 +130,15 @@ export async function fetchTree(
|
||||
try {
|
||||
const paths = tree.map((item) => `styles/${style}/${item.name}.json`)
|
||||
const result = await fetchRegistry(paths)
|
||||
|
||||
return registryWithContentSchema.parse(result)
|
||||
return registryIndexSchema.parse(result)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch tree from registry.`)
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getItemTargetPath(
|
||||
config: Config,
|
||||
item: Pick<z.infer<typeof registryItemWithContentSchema>, "type">,
|
||||
item: z.infer<typeof registryItemSchema>,
|
||||
override?: string
|
||||
) {
|
||||
if (override) {
|
||||
@@ -183,3 +189,130 @@ async function fetchRegistry(paths: string[]) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRegistryItemFileTargetPath(
|
||||
file: z.infer<typeof registryItemFileSchema>,
|
||||
config: Config,
|
||||
override?: string
|
||||
) {
|
||||
if (override) {
|
||||
return override
|
||||
}
|
||||
|
||||
if (file.type === "registry:ui") {
|
||||
return config.resolvedPaths.ui
|
||||
}
|
||||
|
||||
if (file.type === "registry:lib") {
|
||||
return config.resolvedPaths.utils
|
||||
}
|
||||
|
||||
if (file.type === "registry:component") {
|
||||
return config.resolvedPaths.components
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function registryResolveItemsTree(
|
||||
names: z.infer<typeof registryItemSchema>["name"][],
|
||||
config: Config
|
||||
) {
|
||||
const index = await getRegistryIndex()
|
||||
if (!index) {
|
||||
return null
|
||||
}
|
||||
|
||||
let items = (
|
||||
await Promise.all(
|
||||
names.map(async (name) => {
|
||||
const item = await getRegistryItem(config.style, name)
|
||||
return item
|
||||
})
|
||||
)
|
||||
).filter((item): item is NonNullable<typeof item> => item !== null)
|
||||
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const registryDependencies: string[] = items
|
||||
.map((item) => item.registryDependencies ?? [])
|
||||
.flat()
|
||||
|
||||
const uniqueDependencies = Array.from(new Set(registryDependencies))
|
||||
const tree = await resolveTree(index, [...names, ...uniqueDependencies])
|
||||
let payload = await fetchTree(config.style, tree)
|
||||
|
||||
if (!payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (names.includes("index")) {
|
||||
const index = await getInitRegistryItem(config)
|
||||
if (index) {
|
||||
payload.unshift(index)
|
||||
}
|
||||
}
|
||||
|
||||
let tailwind = {}
|
||||
payload.forEach((item) => {
|
||||
console.log(item.tailwind?.config?.theme)
|
||||
tailwind = deepmerge(tailwind, item.tailwind ?? {})
|
||||
})
|
||||
|
||||
let cssVars = {}
|
||||
payload.forEach((item) => {
|
||||
cssVars = deepmerge(cssVars, item.cssVars ?? {})
|
||||
})
|
||||
|
||||
return registryResolvedItemsTreeSchema.parse({
|
||||
dependencies: deepmerge.all(payload.map((item) => item.dependencies ?? [])),
|
||||
devDependencies: deepmerge.all(
|
||||
payload.map((item) => item.devDependencies ?? [])
|
||||
),
|
||||
files: deepmerge.all(payload.map((item) => item.files ?? [])),
|
||||
tailwind,
|
||||
cssVars,
|
||||
})
|
||||
}
|
||||
|
||||
async function getInitRegistryItem(config: Config) {
|
||||
const [payload, baseColor] = await Promise.all([
|
||||
getRegistryItem(config.style, "index"),
|
||||
getRegistryBaseColor(config.tailwind.baseColor),
|
||||
])
|
||||
|
||||
if (!payload || !baseColor) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Inline the base color in the tailwind config.
|
||||
// TODO: Remove this when we have theme handling.
|
||||
if (config.tailwind.cssVariables && baseColor) {
|
||||
payload.cssVars = {
|
||||
light: {
|
||||
...baseColor.cssVars.light,
|
||||
...payload.cssVars?.light,
|
||||
},
|
||||
dark: {
|
||||
...baseColor.cssVars.dark,
|
||||
...payload.cssVars?.dark,
|
||||
},
|
||||
}
|
||||
|
||||
if (payload.tailwind?.config && baseColor.cssVars?.light) {
|
||||
payload.tailwind.config = deepmerge(payload.tailwind.config, {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: buildTailwindThemeColorsFromCssVars(
|
||||
baseColor.cssVars.light
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -1,34 +1,59 @@
|
||||
import { z } from "zod"
|
||||
|
||||
// TODO: Extract this to a shared package.
|
||||
export const legacyRegistryItemSchema = z.object({
|
||||
name: z.string(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
devDependencies: z.array(z.string()).optional(),
|
||||
registryDependencies: z.array(z.string()).optional(),
|
||||
files: z.array(z.string()),
|
||||
type: z
|
||||
.enum(["components:ui", "components:component", "components:example"])
|
||||
export const registryItemTypeSchema = z.enum([
|
||||
"components:ui",
|
||||
"components:component",
|
||||
"components:example",
|
||||
"components:block",
|
||||
"components:chart",
|
||||
"registry:style",
|
||||
"registry:lib",
|
||||
"registry:component",
|
||||
"registry:ui",
|
||||
"registry:hook",
|
||||
])
|
||||
|
||||
export const registryItemFileSchema = z.object({
|
||||
path: z.string(),
|
||||
content: z.string().optional(),
|
||||
type: registryItemTypeSchema,
|
||||
})
|
||||
|
||||
export const registryItemTailwindSchema = z.object({
|
||||
config: z
|
||||
.object({
|
||||
content: z.array(z.string()).optional(),
|
||||
theme: z.record(z.string(), z.any()).optional(),
|
||||
plugins: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const registryIndexSchema = z.array(legacyRegistryItemSchema)
|
||||
|
||||
export const registryItemWithContentSchema = legacyRegistryItemSchema.extend({
|
||||
files: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
type: z.enum([
|
||||
"components:ui",
|
||||
"components:component",
|
||||
"components:example",
|
||||
]),
|
||||
})
|
||||
),
|
||||
export const registryItemCssVarsSchema = z.object({
|
||||
light: z.record(z.string(), z.string()).optional(),
|
||||
dark: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
export const registryWithContentSchema = z.array(registryItemWithContentSchema)
|
||||
export const registryItemSchema = z.object({
|
||||
name: z.string(),
|
||||
type: registryItemTypeSchema,
|
||||
description: z.string().optional(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
devDependencies: z.array(z.string()).optional(),
|
||||
registryDependencies: z.array(z.string()).optional(),
|
||||
files: z.array(registryItemFileSchema).optional(),
|
||||
tailwind: registryItemTailwindSchema.optional(),
|
||||
cssVars: registryItemCssVarsSchema.optional(),
|
||||
})
|
||||
|
||||
export type RegistryItem = z.infer<typeof registryItemSchema>
|
||||
|
||||
export const registryIndexSchema = z.array(
|
||||
registryItemSchema.extend({
|
||||
files: z.array(z.union([z.string(), registryItemFileSchema])).optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export const stylesSchema = z.array(
|
||||
z.object({
|
||||
@@ -50,33 +75,10 @@ export const registryBaseColorSchema = z.object({
|
||||
cssVarsTemplate: z.string(),
|
||||
})
|
||||
|
||||
export const registryCssVarsSchema = z.object({
|
||||
light: z.record(z.string(), z.string()).optional(),
|
||||
dark: z.record(z.string(), z.string()).optional(),
|
||||
export const registryResolvedItemsTreeSchema = registryItemSchema.pick({
|
||||
dependencies: true,
|
||||
devDependencies: true,
|
||||
files: true,
|
||||
tailwind: true,
|
||||
cssVars: true,
|
||||
})
|
||||
|
||||
export const registryItemSchema = z.object({
|
||||
name: z.string(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
devDependencies: z.array(z.string()).optional(),
|
||||
registryDependencies: z.array(z.string()).optional(),
|
||||
files: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
type: z.enum(["utils", "ui", "component"]),
|
||||
})
|
||||
),
|
||||
tailwind: z
|
||||
.object({
|
||||
config: z.object({
|
||||
content: z.array(z.string()).optional(),
|
||||
theme: z.record(z.string(), z.any()).optional(),
|
||||
plugins: z.array(z.string()).optional(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
cssVars: registryCssVarsSchema.optional(),
|
||||
})
|
||||
|
||||
export type RegistryItem = z.infer<typeof registryItemSchema>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { getPackageManager } from "@/src/utils/get-package-manager"
|
||||
import { RegistryItem } from "@/src/utils/registry/schema"
|
||||
import { execa } from "execa"
|
||||
|
||||
export async function updateDependencies(
|
||||
dependencies: string[],
|
||||
dependencies: RegistryItem["dependencies"],
|
||||
config: Config
|
||||
) {
|
||||
if (!dependencies) {
|
||||
return
|
||||
}
|
||||
|
||||
const packageManager = await getPackageManager(config.resolvedPaths.cwd)
|
||||
|
||||
await execa(
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { promises as fs } from "fs"
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { RegistryItem } from "@/src/utils/registry/schema"
|
||||
import * as templates from "@/src/utils/templates"
|
||||
|
||||
export async function updateFiles(
|
||||
files: RegistryItem["files"],
|
||||
config: Config
|
||||
) {
|
||||
const extension = config.tsx ? "ts" : "js"
|
||||
await fs.writeFile(
|
||||
`${config.resolvedPaths.utils}.${extension}`,
|
||||
extension === "ts" ? templates.UTILS : templates.UTILS_JS,
|
||||
"utf8"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { fetchTree, getRegistryIndex, resolveTree } from "@/src/utils/registry"
|
||||
import { RegistryItem } from "@/src/utils/registry/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
export async function updateRegistryDependencies(
|
||||
registryDependencies: RegistryItem["registryDependencies"],
|
||||
config: Config
|
||||
) {
|
||||
if (!registryDependencies?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const registryIndex = await getRegistryIndex()
|
||||
if (!registryIndex) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tree = await resolveTree(registryIndex, registryDependencies)
|
||||
const payload = await fetchTree(config.style, tree)
|
||||
|
||||
if (!payload?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
payload.map(async (dependency) => {
|
||||
console.log(dependency)
|
||||
// const targetPath = await getRegistryItemFileTargetPath(file, config)
|
||||
|
||||
// if (!targetPath) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// console.log(targetPath)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import { promises as fs } from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import {
|
||||
RegistryItem,
|
||||
registryItemTailwindSchema,
|
||||
} from "@/src/utils/registry/schema"
|
||||
import deepmerge from "deepmerge"
|
||||
import objectToString from "stringify-object"
|
||||
import { type Config as TailwindConfig } from "tailwindcss"
|
||||
@@ -14,6 +18,7 @@ import {
|
||||
SyntaxKind,
|
||||
VariableStatement,
|
||||
} from "ts-morph"
|
||||
import { z } from "zod"
|
||||
|
||||
export type UpdaterTailwindConfig = Omit<TailwindConfig, "plugins"> & {
|
||||
// We only want string plugins for now.
|
||||
@@ -21,9 +26,14 @@ export type UpdaterTailwindConfig = Omit<TailwindConfig, "plugins"> & {
|
||||
}
|
||||
|
||||
export async function updateTailwindConfig(
|
||||
tailwindConfig: UpdaterTailwindConfig,
|
||||
tailwindConfig:
|
||||
| z.infer<typeof registryItemTailwindSchema>["config"]
|
||||
| undefined,
|
||||
config: Config
|
||||
) {
|
||||
if (!tailwindConfig) {
|
||||
return
|
||||
}
|
||||
const raw = await fs.readFile(config.resolvedPaths.tailwindConfig, "utf8")
|
||||
const output = await transformTailwindConfig(raw, tailwindConfig, config)
|
||||
await fs.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { promises as fs } from "fs"
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { registryCssVarsSchema } from "@/src/utils/registry/schema"
|
||||
import { registryItemCssVarsSchema } from "@/src/utils/registry/schema"
|
||||
import postcss from "postcss"
|
||||
import AtRule from "postcss/lib/at-rule"
|
||||
import Root from "postcss/lib/root"
|
||||
@@ -8,9 +8,13 @@ import Rule from "postcss/lib/rule"
|
||||
import { z } from "zod"
|
||||
|
||||
export async function updateTailwindCss(
|
||||
cssVars: z.infer<typeof registryCssVarsSchema>,
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema> | undefined,
|
||||
config: Config
|
||||
) {
|
||||
if (!cssVars) {
|
||||
return
|
||||
}
|
||||
|
||||
const raw = await fs.readFile(config.resolvedPaths.tailwindCss, "utf8")
|
||||
let output = await transformTailwindCss(raw, cssVars)
|
||||
|
||||
@@ -19,7 +23,7 @@ export async function updateTailwindCss(
|
||||
|
||||
export async function transformTailwindCss(
|
||||
input: string,
|
||||
cssVars: z.infer<typeof registryCssVarsSchema>
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema>
|
||||
) {
|
||||
const result = await postcss([
|
||||
updateCssVarsPlugin(cssVars),
|
||||
@@ -94,7 +98,9 @@ function updateBaseLayerPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateCssVarsPlugin(cssVars: z.infer<typeof registryCssVarsSchema>) {
|
||||
function updateCssVarsPlugin(
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema>
|
||||
) {
|
||||
return {
|
||||
postcssPlugin: "update-css-vars",
|
||||
Once(root: Root) {
|
||||
@@ -140,7 +146,7 @@ function addOrUpdateVars(
|
||||
}
|
||||
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
const prop = `--${key}`
|
||||
const prop = `--${key.replace(/^--/, "")}`
|
||||
const newDecl = postcss.decl({
|
||||
prop,
|
||||
value,
|
||||
|
||||
@@ -0,0 +1,938 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`registryResolveItemTree > should resolve index 1`] = `
|
||||
{
|
||||
"cssVars": {
|
||||
"dark": {
|
||||
"brand-primary": "#831843",
|
||||
"brand-secondary": "#ccfbf1",
|
||||
},
|
||||
"light": {
|
||||
"--radius": "0.5rem",
|
||||
"brand-primary": "#a855f7",
|
||||
"brand-secondary": "#10b981",
|
||||
},
|
||||
},
|
||||
"dependencies": [
|
||||
"tailwindcss-animate",
|
||||
"class-variance-authority",
|
||||
"lucide-react",
|
||||
"",
|
||||
"@radix-ui/react-label",
|
||||
"clsx",
|
||||
"tailwind-merge",
|
||||
"@radix-ui/react-slot",
|
||||
"cmdk@1.0.0",
|
||||
"@radix-ui/react-dialog",
|
||||
],
|
||||
"devDependencies": [],
|
||||
"files": [
|
||||
{
|
||||
"content": ""use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
",
|
||||
"path": "ui/label.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
{
|
||||
"content": "import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
",
|
||||
"path": "lib/utils.ts",
|
||||
"type": "registry:lib",
|
||||
},
|
||||
{
|
||||
"content": ""use client"
|
||||
|
||||
import { useMediaQuery } from "@/registry/default/hooks/use-media-query"
|
||||
import { cn } from "@/registry/default/lib/utils"
|
||||
import { Button } from "@/registry/default/ui/button"
|
||||
|
||||
export default function ExampleBlock() {
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)")
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"shadow-brand p-12",
|
||||
isDesktop
|
||||
? "bg-muted text-muted-foreground"
|
||||
: "bg-primary text-primary-foreground"
|
||||
)}
|
||||
>
|
||||
<Button className="bg-brand-primary border-brand-secondary border-4">
|
||||
A button
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
",
|
||||
"path": "block/hello-block.tsx",
|
||||
"type": "components:block",
|
||||
},
|
||||
{
|
||||
"content": "import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
",
|
||||
"path": "ui/button.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
{
|
||||
"content": ""use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/registry/default/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
",
|
||||
"path": "ui/command.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
{
|
||||
"content": ""use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
",
|
||||
"path": "ui/dialog.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
],
|
||||
"tailwind": {
|
||||
"config": {
|
||||
"plugins": [
|
||||
"require("tailwindcss-animate")",
|
||||
],
|
||||
"theme": {
|
||||
"extend": {
|
||||
"borderRadius": {
|
||||
"lg": "var(--radius)",
|
||||
"md": "calc(var(--radius) - 2px)",
|
||||
"sm": "calc(var(--radius) - 4px)",
|
||||
},
|
||||
"boxShadow": {
|
||||
"brand": "0px 0px 10px 0px rgba(255, 255, 255, 0.10)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`registryResolveItemTree > should resolve items tree 1`] = `
|
||||
{
|
||||
"cssVars": {},
|
||||
"dependencies": [
|
||||
"@radix-ui/react-slot",
|
||||
],
|
||||
"devDependencies": [],
|
||||
"files": [
|
||||
{
|
||||
"content": "import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
",
|
||||
"path": "ui/button.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
],
|
||||
"tailwind": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`registryResolveItemTree > should resolve multiple items tree 1`] = `
|
||||
{
|
||||
"cssVars": {},
|
||||
"dependencies": [
|
||||
"@radix-ui/react-slot",
|
||||
"cmdk@1.0.0",
|
||||
"@radix-ui/react-dialog",
|
||||
],
|
||||
"devDependencies": [],
|
||||
"files": [
|
||||
{
|
||||
"content": "import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
",
|
||||
"path": "ui/button.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
{
|
||||
"content": "import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
",
|
||||
"path": "ui/input.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
{
|
||||
"content": ""use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/registry/default/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
",
|
||||
"path": "ui/command.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
{
|
||||
"content": ""use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
",
|
||||
"path": "ui/dialog.tsx",
|
||||
"type": "components:ui",
|
||||
},
|
||||
],
|
||||
"tailwind": {},
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { registryResolveItemsTree } from "../../../src/utils/registry"
|
||||
|
||||
describe("registryResolveItemTree", () => {
|
||||
test("should resolve items tree", async () => {
|
||||
expect(
|
||||
await registryResolveItemsTree(["button"], {
|
||||
style: "new-york",
|
||||
tailwind: {
|
||||
baseColor: "stone",
|
||||
},
|
||||
})
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("should resolve multiple items tree", async () => {
|
||||
expect(
|
||||
await registryResolveItemsTree(["button", "input", "command"], {
|
||||
style: "default",
|
||||
tailwind: {
|
||||
baseColor: "zinc",
|
||||
},
|
||||
})
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("should resolve index", async () => {
|
||||
expect(
|
||||
await registryResolveItemsTree(["index", "label"], {
|
||||
style: "default",
|
||||
tailwind: {
|
||||
baseColor: "zinc",
|
||||
},
|
||||
})
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user