mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-10-30 18:22:59 +00:00
- Enable all 15 finance component docs (from .disabled → .mdx) - Create stock-market.mdx, 8 chart docs, 8 block docs, 2 framework docs - Add direction.tsx component (RTL/LTR support from upstream shadcn) - Add xs/icon-xs button sizes, data-variant/data-size attributes - Fix build-registry.mts to handle default exports in namespace re-exports - Add default props to order-entry, positions-list, orders-history, trading-panel - Fix 18 navigation stubs → re-export from root implementations - Add 44 orphaned MDX files to nav (installation, dark-mode, desktop, guides, etc) - Migrate 6 framer-motion imports → motion/react - Bump React 19.2.0→19.2.4, Next.js 16.0.1→16.1.6, motion→12.34.0 - Align lucide-react, sonner v2, tailwind-merge v3, postcss-selector-parser v7 - Remove framer-motion dep (superseded by motion) - Fix @types/react-dom, TypeScript versions across workspaces
690 lines
22 KiB
TypeScript
690 lines
22 KiB
TypeScript
// @sts-nocheck
|
|
import { existsSync, promises as fs, readFileSync } from "fs"
|
|
import { tmpdir } from "os"
|
|
import path, { basename } from "path"
|
|
import { cwd } from "process"
|
|
// Removed lodash.template - using functions instead
|
|
import { rimraf } from "rimraf"
|
|
import { Project, ScriptKind, SourceFile, SyntaxKind } from "ts-morph"
|
|
|
|
import { colorMapping, colors } from "../registry/colors"
|
|
import { registry } from "../registry/registry"
|
|
import { Registry, registrySchema } from "../registry/schema"
|
|
import { styles } from "../registry/styles"
|
|
import { themes } from "../registry/themes"
|
|
|
|
const REGISTRY_PATH = path.join(process.cwd(), "public/registry")
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build __registry__/index.tsx.
|
|
// ----------------------------------------------------------------------------
|
|
async function buildRegistry(registry: Registry) {
|
|
const project = new Project({
|
|
compilerOptions: {},
|
|
})
|
|
|
|
async function createTempSourceFile(filename: string) {
|
|
const dir = await fs.mkdtemp(path.join(tmpdir(), "hanzo-"))
|
|
return path.join(dir, filename)
|
|
}
|
|
|
|
let index = `// @ts-nocheck
|
|
// This file is autogenerated by scripts/build-registry.ts
|
|
// Do not edit this file directly.
|
|
import * as React from "react"
|
|
|
|
export const Index: Record<string, any> = {
|
|
`
|
|
|
|
// Only build for "default" style (single theme system)
|
|
const style = { name: "default" }
|
|
|
|
// Build component index
|
|
for (const item of registry) {
|
|
const resolveFiles = item.files.map(
|
|
(file) => `registry/${style.name}/${file}`
|
|
)
|
|
|
|
// Extract namespace from type (e.g., "components:3d" -> namespace="3d", type="ui")
|
|
// Standard types like "components:ui" have no namespace
|
|
const typeParts = item.type.split(":")
|
|
const baseType = typeParts[0] // "components"
|
|
const typeVariant = typeParts[1] // "ui", "3d", "ai", etc.
|
|
|
|
// Determine if this is a namespaced component type
|
|
const namespacedTypes = ["3d", "ai", "code", "animation", "layout", "marketing", "navigation", "special-effects", "text-effects", "visualization", "finance"]
|
|
const isNamespaced = namespacedTypes.includes(typeVariant)
|
|
const namespace = isNamespaced ? typeVariant : undefined
|
|
const type = isNamespaced ? "ui" : typeVariant
|
|
|
|
let sourceFilename = ""
|
|
|
|
let chunks: any = []
|
|
if (item.type === "components:block") {
|
|
const file = resolveFiles[0]
|
|
const filename = path.basename(file)
|
|
const raw = await fs.readFile(file, "utf8")
|
|
const tempFile = await createTempSourceFile(filename)
|
|
const sourceFile = project.createSourceFile(tempFile, raw, {
|
|
scriptKind: ScriptKind.TSX,
|
|
})
|
|
|
|
// Find all imports.
|
|
const imports = new Map<
|
|
string,
|
|
{
|
|
module: string
|
|
text: string
|
|
isDefault?: boolean
|
|
}
|
|
>()
|
|
sourceFile.getImportDeclarations().forEach((node) => {
|
|
const modulePath = node.getModuleSpecifier().getLiteralValue()
|
|
node.getNamedImports().forEach((item) => {
|
|
imports.set(item.getText(), {
|
|
module: modulePath,
|
|
text: node.getText(),
|
|
})
|
|
})
|
|
|
|
const defaultImport = node.getDefaultImport()
|
|
if (defaultImport) {
|
|
imports.set(defaultImport.getText(), {
|
|
module: modulePath,
|
|
text: defaultImport.getText(),
|
|
isDefault: true,
|
|
})
|
|
}
|
|
})
|
|
|
|
// Find all opening tags with x-chunk attribute.
|
|
const components = sourceFile
|
|
.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
|
|
.filter((node) => {
|
|
return node.getAttribute("x-chunk") !== undefined
|
|
})
|
|
|
|
chunks = await Promise.all(
|
|
components.map(async (component, index) => {
|
|
const chunkName = `${item.name}-chunk-${index}`
|
|
|
|
// Get the value of x-chunk attribute.
|
|
const attr = component
|
|
.getAttributeOrThrow("x-chunk")
|
|
.asKindOrThrow(SyntaxKind.JsxAttribute)
|
|
|
|
const description = attr
|
|
.getInitializerOrThrow()
|
|
.asKindOrThrow(SyntaxKind.StringLiteral)
|
|
.getLiteralValue()
|
|
|
|
// Delete the x-chunk attribute.
|
|
attr.remove()
|
|
|
|
// Add a new attribute to the component.
|
|
component.addAttribute({
|
|
name: "x-chunk",
|
|
initializer: `"${chunkName}"`,
|
|
})
|
|
|
|
// Get the value of x-chunk-container attribute.
|
|
const containerAttr = component
|
|
.getAttribute("x-chunk-container")
|
|
?.asKindOrThrow(SyntaxKind.JsxAttribute)
|
|
|
|
const containerClassName = containerAttr
|
|
?.getInitializer()
|
|
?.asKindOrThrow(SyntaxKind.StringLiteral)
|
|
.getLiteralValue()
|
|
|
|
containerAttr?.remove()
|
|
|
|
const parentJsxElement = component.getParentIfKindOrThrow(
|
|
SyntaxKind.JsxElement
|
|
)
|
|
|
|
// Find all opening tags on component.
|
|
const children = parentJsxElement
|
|
.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
|
|
.map((node) => {
|
|
return node.getTagNameNode().getText()
|
|
})
|
|
.concat(
|
|
parentJsxElement
|
|
.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
|
|
.map((node) => {
|
|
return node.getTagNameNode().getText()
|
|
})
|
|
)
|
|
|
|
const componentImports = new Map<
|
|
string,
|
|
string | string[] | Set<string>
|
|
>()
|
|
children.forEach((child) => {
|
|
const importLine = imports.get(child)
|
|
if (importLine) {
|
|
const imports = componentImports.get(importLine.module) || []
|
|
|
|
const newImports = importLine.isDefault
|
|
? importLine.text
|
|
: new Set([...imports, child])
|
|
|
|
componentImports.set(
|
|
importLine.module,
|
|
importLine?.isDefault ? newImports : Array.from(newImports)
|
|
)
|
|
}
|
|
})
|
|
|
|
const componnetImportLines = Array.from(
|
|
componentImports.keys()
|
|
).map((key) => {
|
|
const values = componentImports.get(key)
|
|
const specifier = Array.isArray(values)
|
|
? `{${values.join(",")}}`
|
|
: values
|
|
|
|
return `import ${specifier} from "${key}"`
|
|
})
|
|
|
|
const code = `
|
|
${componnetImportLines.join("\n")}
|
|
|
|
export default function Component() {
|
|
return (${parentJsxElement.getText()})
|
|
}`
|
|
|
|
const targetFile = file.replace(item.name, `${chunkName}`)
|
|
const targetDir = namespace ? `${namespace}/${chunkName}` : `${type}/${chunkName}`
|
|
const targetFilePath = path.join(
|
|
cwd(),
|
|
`registry/${style.name}/${targetDir}.tsx`
|
|
)
|
|
|
|
// Write component file.
|
|
rimraf.sync(targetFilePath)
|
|
await fs.writeFile(targetFilePath, code, "utf8")
|
|
|
|
const componentPath = namespace
|
|
? `@/registry/${style.name}/${namespace}/${chunkName}`
|
|
: `@/registry/${style.name}/${type}/${chunkName}`
|
|
|
|
return {
|
|
name: chunkName,
|
|
description,
|
|
component: `React.lazy(() => import("${componentPath}")),`,
|
|
file: targetFile,
|
|
container: {
|
|
className: containerClassName,
|
|
},
|
|
}
|
|
})
|
|
)
|
|
|
|
// // Write the source file for blocks only.
|
|
const sourceDir = namespace ? `${namespace}/${item.name}` : `${type}/${item.name}`
|
|
sourceFilename = `__registry__/${style.name}/${sourceDir}.tsx`
|
|
const sourcePath = path.join(process.cwd(), sourceFilename)
|
|
const sourceParentDir = path.dirname(sourcePath)
|
|
if (!existsSync(sourceParentDir)) {
|
|
await fs.mkdir(sourceParentDir, { recursive: true })
|
|
}
|
|
|
|
rimraf.sync(sourcePath)
|
|
await fs.writeFile(sourcePath, sourceFile.getText())
|
|
}
|
|
|
|
// Construct import path based on namespace
|
|
const componentImportPath = namespace
|
|
? `@/registry/${style.name}/${namespace}/${item.name}`
|
|
: `@/registry/${style.name}/${type}/${item.name}`
|
|
|
|
// Generate re-export file for namespaced components
|
|
if (namespace && item.type !== "components:block") {
|
|
const namespaceDir = path.join(process.cwd(), `registry/${style.name}/${namespace}`)
|
|
if (!existsSync(namespaceDir)) {
|
|
await fs.mkdir(namespaceDir, { recursive: true })
|
|
}
|
|
|
|
const reexportPath = path.join(namespaceDir, `${item.name}.tsx`)
|
|
// Check if the source UI file has a default export
|
|
const sourceUiPath = path.join(process.cwd(), `registry/${style.name}/ui/${item.name}.tsx`)
|
|
let hasDefaultExport = false
|
|
try {
|
|
const sourceContent = readFileSync(sourceUiPath, "utf8")
|
|
hasDefaultExport = /export\s+default\s/.test(sourceContent)
|
|
} catch {}
|
|
const reexportContent = hasDefaultExport
|
|
? `export { default } from "../ui/${item.name}"\nexport * from "../ui/${item.name}"\n`
|
|
: `export * from "../ui/${item.name}"\n`
|
|
|
|
await fs.writeFile(reexportPath, reexportContent, "utf8")
|
|
}
|
|
|
|
index += `
|
|
"${item.name}": {
|
|
name: "${item.name}",
|
|
type: "${item.type}",
|
|
registryDependencies: ${JSON.stringify(item.registryDependencies)},
|
|
component: React.lazy(() => import("${componentImportPath}")),
|
|
source: "${sourceFilename}",
|
|
files: [${resolveFiles.map((file) => `"${file}"`)}],
|
|
category: "${item.category}",
|
|
subcategory: "${item.subcategory}",
|
|
chunks: [${chunks.map(
|
|
(chunk) => `{
|
|
name: "${chunk.name}",
|
|
description: "${chunk.description}",
|
|
component: ${chunk.component}
|
|
file: "${chunk.file}",
|
|
container: {
|
|
className: "${chunk.container.className}"
|
|
}
|
|
}`
|
|
)}]
|
|
},`
|
|
}
|
|
|
|
index += `
|
|
}
|
|
`
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/index.json.
|
|
// ----------------------------------------------------------------------------
|
|
const names = registry.filter((item) =>
|
|
item.type?.startsWith("components:")
|
|
)
|
|
const registryJson = JSON.stringify(names, null, 2)
|
|
rimraf.sync(path.join(REGISTRY_PATH, "index.json"))
|
|
await fs.writeFile(
|
|
path.join(REGISTRY_PATH, "index.json"),
|
|
registryJson,
|
|
"utf8"
|
|
)
|
|
|
|
// Write style index.
|
|
rimraf.sync(path.join(process.cwd(), "__registry__/index.tsx"))
|
|
await fs.writeFile(path.join(process.cwd(), "__registry__/index.tsx"), index)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/styles/default/[name].json.
|
|
// ----------------------------------------------------------------------------
|
|
async function buildStyles(registry: Registry) {
|
|
// Only build for "default" style (single theme system)
|
|
const style = { name: "default" }
|
|
const targetPath = path.join(REGISTRY_PATH, "styles", style.name)
|
|
|
|
// Create directory if it doesn't exist.
|
|
if (!existsSync(targetPath)) {
|
|
await fs.mkdir(targetPath, { recursive: true })
|
|
}
|
|
|
|
for (const item of registry) {
|
|
// Include all component types (ui, ai, 3d, animation, code, etc.)
|
|
if (!item.type?.startsWith("components:")) {
|
|
continue
|
|
}
|
|
|
|
const files = item.files?.map((file) => {
|
|
const filePath = path.join(process.cwd(), "registry", style.name, file)
|
|
|
|
// Check if file exists for this style variant
|
|
if (!existsSync(filePath)) {
|
|
return null
|
|
}
|
|
|
|
const content = readFileSync(filePath, "utf8")
|
|
|
|
return {
|
|
name: basename(file),
|
|
content,
|
|
}
|
|
}).filter(Boolean)
|
|
|
|
// Skip if no files exist for this style variant
|
|
if (!files || files.length === 0) {
|
|
continue
|
|
}
|
|
|
|
const payload = {
|
|
...item,
|
|
files,
|
|
}
|
|
|
|
await fs.writeFile(
|
|
path.join(targetPath, `${item.name}.json`),
|
|
JSON.stringify(payload, null, 2),
|
|
"utf8"
|
|
)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/styles/index.json.
|
|
// ----------------------------------------------------------------------------
|
|
const stylesJson = JSON.stringify(styles, null, 2)
|
|
await fs.writeFile(
|
|
path.join(REGISTRY_PATH, "styles/index.json"),
|
|
stylesJson,
|
|
"utf8"
|
|
)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/colors/index.json.
|
|
// ----------------------------------------------------------------------------
|
|
async function buildThemes() {
|
|
const colorsTargetPath = path.join(REGISTRY_PATH, "colors")
|
|
rimraf.sync(colorsTargetPath)
|
|
if (!existsSync(colorsTargetPath)) {
|
|
await fs.mkdir(colorsTargetPath, { recursive: true })
|
|
}
|
|
|
|
const colorsData: Record<string, any> = {}
|
|
for (const [color, value] of Object.entries(colors)) {
|
|
if (typeof value === "string") {
|
|
colorsData[color] = value
|
|
continue
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
colorsData[color] = value.map((item) => ({
|
|
...item,
|
|
rgbChannel: item.rgb.replace(/^rgb\((\d+),(\d+),(\d+)\)$/, "$1 $2 $3"),
|
|
hslChannel: item.hsl.replace(
|
|
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
|
|
"$1 $2 $3"
|
|
),
|
|
}))
|
|
continue
|
|
}
|
|
|
|
if (typeof value === "object") {
|
|
colorsData[color] = {
|
|
...value,
|
|
rgbChannel: value.rgb.replace(/^rgb\((\d+),(\d+),(\d+)\)$/, "$1 $2 $3"),
|
|
hslChannel: value.hsl.replace(
|
|
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
|
|
"$1 $2 $3"
|
|
),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
await fs.writeFile(
|
|
path.join(colorsTargetPath, "index.json"),
|
|
JSON.stringify(colorsData, null, 2),
|
|
"utf8"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/colors/[base].json.
|
|
// ----------------------------------------------------------------------------
|
|
// Template functions (replaced lodash.template)
|
|
const getBaseStyles = () => `@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
`
|
|
|
|
const getBaseStylesWithVariables = (colors: any) => `@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
:root {
|
|
--background: ${colors.light["background"]};
|
|
--foreground: ${colors.light["foreground"]};
|
|
|
|
--card: ${colors.light["card"]};
|
|
--card-foreground: ${colors.light["card-foreground"]};
|
|
|
|
--popover: ${colors.light["popover"]};
|
|
--popover-foreground: ${colors.light["popover-foreground"]};
|
|
|
|
--primary: ${colors.light["primary"]};
|
|
--primary-foreground: ${colors.light["primary-foreground"]};
|
|
|
|
--secondary: ${colors.light["secondary"]};
|
|
--secondary-foreground: ${colors.light["secondary-foreground"]};
|
|
|
|
--muted: ${colors.light["muted"]};
|
|
--muted-foreground: ${colors.light["muted-foreground"]};
|
|
|
|
--accent: ${colors.light["accent"]};
|
|
--accent-foreground: ${colors.light["accent-foreground"]};
|
|
|
|
--destructive: ${colors.light["destructive"]};
|
|
--destructive-foreground: ${colors.light["destructive-foreground"]};
|
|
|
|
--border: ${colors.light["border"]};
|
|
--input: ${colors.light["input"]};
|
|
--ring: ${colors.light["ring"]};
|
|
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
.dark {
|
|
--background: ${colors.dark["background"]};
|
|
--foreground: ${colors.dark["foreground"]};
|
|
|
|
--card: ${colors.dark["card"]};
|
|
--card-foreground: ${colors.dark["card-foreground"]};
|
|
|
|
--popover: ${colors.dark["popover"]};
|
|
--popover-foreground: ${colors.dark["popover-foreground"]};
|
|
|
|
--primary: ${colors.dark["primary"]};
|
|
--primary-foreground: ${colors.dark["primary-foreground"]};
|
|
|
|
--secondary: ${colors.dark["secondary"]};
|
|
--secondary-foreground: ${colors.dark["secondary-foreground"]};
|
|
|
|
--muted: ${colors.dark["muted"]};
|
|
--muted-foreground: ${colors.dark["muted-foreground"]};
|
|
|
|
--accent: ${colors.dark["accent"]};
|
|
--accent-foreground: ${colors.dark["accent-foreground"]};
|
|
|
|
--destructive: ${colors.dark["destructive"]};
|
|
--destructive-foreground: ${colors.dark["destructive-foreground"]};
|
|
|
|
--border: ${colors.dark["border"]};
|
|
--input: ${colors.dark["input"]};
|
|
--ring: ${colors.dark["ring"]};
|
|
}
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border;
|
|
}
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
}`
|
|
|
|
for (const baseColor of ["slate", "gray", "zinc", "neutral", "stone"]) {
|
|
const base: Record<string, any> = {
|
|
inlineColors: {},
|
|
cssVars: {},
|
|
}
|
|
for (const [mode, values] of Object.entries(colorMapping)) {
|
|
base["inlineColors"][mode] = {}
|
|
base["cssVars"][mode] = {}
|
|
for (const [key, value] of Object.entries(values)) {
|
|
if (typeof value === "string") {
|
|
const resolvedColor = value.replace(/{{base}}-/g, `${baseColor}-`)
|
|
base["inlineColors"][mode][key] = resolvedColor
|
|
|
|
const [resolvedBase, scale] = resolvedColor.split("-")
|
|
const color = scale
|
|
? colorsData[resolvedBase].find(
|
|
(item) => item.scale === parseInt(scale)
|
|
)
|
|
: colorsData[resolvedBase]
|
|
if (color) {
|
|
base["cssVars"][mode][key] = color.hslChannel
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build css vars.
|
|
base["inlineColorsTemplate"] = getBaseStyles()
|
|
base["cssVarsTemplate"] = getBaseStylesWithVariables(base["cssVars"])
|
|
|
|
await fs.writeFile(
|
|
path.join(REGISTRY_PATH, `colors/${baseColor}.json`),
|
|
JSON.stringify(base, null, 2),
|
|
"utf8"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/themes.css
|
|
// ----------------------------------------------------------------------------
|
|
const getThemeStylesWithVariables = (themeName: string, colors: any) => `
|
|
.theme-${themeName} {
|
|
--background: ${colors.light["background"]};
|
|
--foreground: ${colors.light["foreground"]};
|
|
|
|
--muted: ${colors.light["muted"]};
|
|
--muted-foreground: ${colors.light["muted-foreground"]};
|
|
|
|
--popover: ${colors.light["popover"]};
|
|
--popover-foreground: ${colors.light["popover-foreground"]};
|
|
|
|
--card: ${colors.light["card"]};
|
|
--card-foreground: ${colors.light["card-foreground"]};
|
|
|
|
--border: ${colors.light["border"]};
|
|
--input: ${colors.light["input"]};
|
|
|
|
--primary: ${colors.light["primary"]};
|
|
--primary-foreground: ${colors.light["primary-foreground"]};
|
|
|
|
--secondary: ${colors.light["secondary"]};
|
|
--secondary-foreground: ${colors.light["secondary-foreground"]};
|
|
|
|
--accent: ${colors.light["accent"]};
|
|
--accent-foreground: ${colors.light["accent-foreground"]};
|
|
|
|
--destructive: ${colors.light["destructive"]};
|
|
--destructive-foreground: ${colors.light["destructive-foreground"]};
|
|
|
|
--ring: ${colors.light["ring"]};
|
|
|
|
--radius: ${colors.light["radius"]};
|
|
}
|
|
|
|
.dark .theme-${themeName} {
|
|
--background: ${colors.dark["background"]};
|
|
--foreground: ${colors.dark["foreground"]};
|
|
|
|
--muted: ${colors.dark["muted"]};
|
|
--muted-foreground: ${colors.dark["muted-foreground"]};
|
|
|
|
--popover: ${colors.dark["popover"]};
|
|
--popover-foreground: ${colors.dark["popover-foreground"]};
|
|
|
|
--card: ${colors.dark["card"]};
|
|
--card-foreground: ${colors.dark["card-foreground"]};
|
|
|
|
--border: ${colors.dark["border"]};
|
|
--input: ${colors.dark["input"]};
|
|
|
|
--primary: ${colors.dark["primary"]};
|
|
--primary-foreground: ${colors.dark["primary-foreground"]};
|
|
|
|
--secondary: ${colors.dark["secondary"]};
|
|
--secondary-foreground: ${colors.dark["secondary-foreground"]};
|
|
|
|
--accent: ${colors.dark["accent"]};
|
|
--accent-foreground: ${colors.dark["accent-foreground"]};
|
|
|
|
--destructive: ${colors.dark["destructive"]};
|
|
--destructive-foreground: ${colors.dark["destructive-foreground"]};
|
|
|
|
--ring: ${colors.dark["ring"]};
|
|
}`
|
|
|
|
const themeCSS = []
|
|
for (const theme of themes) {
|
|
themeCSS.push(
|
|
getThemeStylesWithVariables(theme.name, theme.cssVars)
|
|
)
|
|
}
|
|
|
|
await fs.writeFile(
|
|
path.join(REGISTRY_PATH, `themes.css`),
|
|
themeCSS.join("\n"),
|
|
"utf8"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Build registry/themes/[theme].json
|
|
// ----------------------------------------------------------------------------
|
|
rimraf.sync(path.join(REGISTRY_PATH, "themes"))
|
|
for (const baseColor of ["slate", "gray", "zinc", "neutral", "stone"]) {
|
|
const payload = {
|
|
name: baseColor,
|
|
label: baseColor.charAt(0).toUpperCase() + baseColor.slice(1),
|
|
cssVars: {},
|
|
}
|
|
|
|
for (const [mode, values] of Object.entries(colorMapping)) {
|
|
payload["cssVars"][mode] = {}
|
|
for (const [key, value] of Object.entries(values)) {
|
|
if (typeof value === "string") {
|
|
const resolvedColor = value.replace(/{{base}}-/g, `${baseColor}-`)
|
|
payload["cssVars"][mode][key] = resolvedColor
|
|
|
|
const [resolvedBase, scale] = resolvedColor.split("-")
|
|
const color = scale
|
|
? colorsData[resolvedBase].find(
|
|
(item) => item.scale === parseInt(scale)
|
|
)
|
|
: colorsData[resolvedBase]
|
|
if (color) {
|
|
payload["cssVars"][mode][key] = color.hslChannel
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const targetPath = path.join(REGISTRY_PATH, "themes")
|
|
|
|
// Create directory if it doesn't exist.
|
|
if (!existsSync(targetPath)) {
|
|
await fs.mkdir(targetPath, { recursive: true })
|
|
}
|
|
|
|
await fs.writeFile(
|
|
path.join(targetPath, `${payload.name}.json`),
|
|
JSON.stringify(payload, null, 2),
|
|
"utf8"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = registrySchema.safeParse(registry)
|
|
|
|
if (!result.success) {
|
|
console.error(result.error)
|
|
process.exit(1)
|
|
}
|
|
|
|
await buildRegistry(result.data)
|
|
await buildStyles(result.data)
|
|
await buildThemes()
|
|
|
|
console.log("✅ Done!")
|
|
} catch (error) {
|
|
console.error(error)
|
|
process.exit(1)
|
|
}
|