mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-10-30 18:22:59 +00:00
- Fix gray/zinc alias confusion (gray→neutral, zinc→zen) - Consolidate duplicate gray color definition - Add finance components to ui/ directory - Clean up documentation structure - Remove test artifacts and backup files The color aliases were incorrectly mapped: Before: gray→zen, zinc→neutral (wrong) After: gray→neutral, zinc→zen (correct) This ensures Hanzo's branded 'zen' color properly maps to zinc while gray correctly references the standard neutral palette. Verified: - Production build: 544 pages ✓ - All tests passing: 219/219 ✓ - Visual verification: /colors page working ✓ - Code review: Approved ✓ Ready for deployment to ui.hanzo.ai
320 lines
11 KiB
TypeScript
320 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import Image from "next/image"
|
|
import { Index } from "@/__registry__"
|
|
import { useTheme } from "next-themes"
|
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
|
import { vs, vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { CopyButton, CopyWithClassNames } from "@/components/copy-button"
|
|
import { HanzoButton } from "@/components/hanzo-button"
|
|
import { Icons } from "@/components/icons"
|
|
import { StyleSwitcher } from "@/components/style-switcher"
|
|
import { ThemeWrapper } from "@/components/theme-wrapper"
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from "@/registry/default/ui/tabs"
|
|
import { getActiveStyle, styles, type Style } from "@/registry/styles"
|
|
|
|
interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
name: string
|
|
extractClassname?: boolean
|
|
extractedClassNames?: string
|
|
align?: "center" | "start" | "end"
|
|
description?: string
|
|
type?: "block" | "component" | "example"
|
|
hideCode?: boolean
|
|
styleName?: Style["name"]
|
|
minHeight?: string
|
|
}
|
|
|
|
interface CodeBlockProps {
|
|
"data-rehype-pretty-code-fragment"?: string
|
|
children?: React.ReactNode
|
|
}
|
|
|
|
interface CodeButtonProps {
|
|
value?: string
|
|
__rawString__?: string
|
|
}
|
|
|
|
export function ComponentPreview({
|
|
name,
|
|
children,
|
|
className,
|
|
extractClassname,
|
|
extractedClassNames,
|
|
align = "center",
|
|
description,
|
|
type,
|
|
hideCode = false,
|
|
styleName,
|
|
minHeight,
|
|
...props
|
|
}: ComponentPreviewProps) {
|
|
const { theme, resolvedTheme } = useTheme()
|
|
|
|
// Use provided styleName or default to active style
|
|
const activeStyle = styleName || getActiveStyle().name
|
|
const index = styles.findIndex((style) => style.name === activeStyle)
|
|
|
|
// Check if this is a finance component - they need more height
|
|
const isFinanceComponent =
|
|
name.includes("chart") ||
|
|
name.includes("market") ||
|
|
name.includes("screener") ||
|
|
name.includes("trading") ||
|
|
name.includes("order") ||
|
|
name.includes("position") ||
|
|
name.includes("symbol") ||
|
|
name.includes("company") ||
|
|
name.includes("financial") ||
|
|
name.includes("technical") ||
|
|
name.includes("news")
|
|
|
|
// Set default min height based on component type
|
|
const defaultMinHeight = isFinanceComponent ? "600px" : "350px"
|
|
const previewMinHeight = minHeight || defaultMinHeight
|
|
|
|
// Render blocks with static images for mobile, iframe for desktop
|
|
// This matches shadcn's approach and avoids chunk loading issues
|
|
if (type === "block") {
|
|
const style = activeStyle
|
|
return (
|
|
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
|
|
<Image
|
|
src={`/r/styles/${style}/${name}-light.png`}
|
|
alt={name}
|
|
width={1440}
|
|
height={900}
|
|
className="bg-background absolute top-0 left-0 z-20 w-[970px] max-w-none sm:w-[1280px] md:hidden dark:hidden md:dark:hidden"
|
|
/>
|
|
<Image
|
|
src={`/r/styles/${style}/${name}-dark.png`}
|
|
alt={name}
|
|
width={1440}
|
|
height={900}
|
|
className="bg-background absolute top-0 left-0 z-20 hidden w-[970px] max-w-none sm:w-[1280px] md:hidden dark:block md:dark:hidden"
|
|
/>
|
|
<div className="bg-background absolute inset-0 hidden w-[1600px] md:block">
|
|
<iframe src={`/view/${name}`} className="size-full" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const Codes = React.Children.toArray(children) as React.ReactElement[]
|
|
const Code = Codes[index]
|
|
|
|
// Single theme system - no style nesting in Index
|
|
const Component = Index[name]?.component
|
|
|
|
if (!Component) {
|
|
return (
|
|
<p className="text-sm text-muted-foreground">
|
|
Component{" "}
|
|
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
|
{name}
|
|
</code>{" "}
|
|
not found in registry.
|
|
</p>
|
|
)
|
|
}
|
|
|
|
const Preview = React.useMemo(() => {
|
|
return <Component />
|
|
}, [Component])
|
|
|
|
// Fetch code from registry JSON if not provided as children
|
|
const [registryCode, setRegistryCode] = React.useState<string | null>(null)
|
|
|
|
React.useEffect(() => {
|
|
// Only fetch if no children were provided
|
|
if (!Code) {
|
|
fetch(`/registry/styles/${activeStyle}/${name}.json`)
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
.then((data) => {
|
|
if (data?.files?.[0]?.content) {
|
|
setRegistryCode(data.files[0].content)
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// Silently fail - component will still render preview
|
|
})
|
|
}
|
|
}, [activeStyle, name, Code])
|
|
|
|
const codeString = React.useMemo(() => {
|
|
/**
|
|
* Transform registry imports to namespaced @hanzo/ui imports
|
|
* Patterns:
|
|
* - @/registry/default/ui/* → @hanzo/ui/*
|
|
* - @/registry/code/* → @hanzo/ui/code/*
|
|
* - @/registry/3d/* → @hanzo/ui/3d/*
|
|
* - @/registry/animation/* → @hanzo/ui/animation/*
|
|
* - @/registry/pattern/* → @hanzo/ui/pattern/*
|
|
* - @/registry/navigation/* → @hanzo/ui/navigation/*
|
|
* - @/registry/form/* → @hanzo/ui/form/*
|
|
* - @/registry/device/* → @hanzo/ui/device/*
|
|
* - @/registry/dock/* → @hanzo/ui/dock/*
|
|
* - @/registry/project/* → @hanzo/ui/project/*
|
|
* - @/registry/ui/* → @hanzo/ui/ui/*
|
|
*/
|
|
const transformImports = (code: string) => {
|
|
return (
|
|
code
|
|
// Transform core components: @/registry/default/ui/* → @hanzo/ui/*
|
|
.replace(
|
|
/from ["']@\/registry\/default\/ui\/([^"']+)["']/g,
|
|
'from "@hanzo/ui/$1"'
|
|
)
|
|
// Transform namespaced imports: @/registry/{namespace}/* → @hanzo/ui/{namespace}/*
|
|
.replace(
|
|
/from ["']@\/registry\/(code|3d|animation|pattern|navigation|form|device|dock|project|ui)\/([^"']+)["']/g,
|
|
'from "@hanzo/ui/$1/$2"'
|
|
)
|
|
)
|
|
}
|
|
|
|
// First try to get code from MDX children (backward compatibility)
|
|
const codeProps = Code?.props as CodeBlockProps | undefined
|
|
if (
|
|
typeof codeProps?.["data-rehype-pretty-code-fragment"] !== "undefined"
|
|
) {
|
|
const [Button] = React.Children.toArray(
|
|
codeProps.children
|
|
) as React.ReactElement[]
|
|
const buttonProps = Button?.props as CodeButtonProps | undefined
|
|
const childrenCode =
|
|
buttonProps?.value || buttonProps?.__rawString__ || null
|
|
if (childrenCode) {
|
|
return transformImports(childrenCode)
|
|
}
|
|
}
|
|
|
|
// Fall back to registry code and transform imports
|
|
if (registryCode) {
|
|
return transformImports(registryCode)
|
|
}
|
|
|
|
return registryCode
|
|
}, [Code, registryCode])
|
|
|
|
return (
|
|
<div
|
|
className={cn("group relative my-4 flex flex-col space-y-2", className)}
|
|
{...props}
|
|
>
|
|
<Tabs defaultValue="preview" className="relative mr-auto w-full">
|
|
<div className="flex items-center justify-between pb-3">
|
|
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0">
|
|
<TabsTrigger
|
|
value="preview"
|
|
className="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
|
>
|
|
Preview
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="code"
|
|
className="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
|
>
|
|
Code
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
<TabsContent value="preview" className="relative rounded-md border">
|
|
<div className="flex items-center justify-between p-4">
|
|
<StyleSwitcher />
|
|
<div className="flex items-center gap-2">
|
|
{activeStyle === "default" && description && codeString ? (
|
|
<HanzoButton
|
|
block={{
|
|
code: codeString,
|
|
name,
|
|
style: activeStyle,
|
|
description,
|
|
}}
|
|
/>
|
|
) : null}
|
|
{codeString ? (
|
|
<CopyButton
|
|
value={codeString}
|
|
variant="outline"
|
|
className="h-7 w-7 text-foreground opacity-100 hover:bg-muted hover:text-foreground [&_svg]:size-3.5"
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<ThemeWrapper defaultTheme="zinc">
|
|
<div
|
|
className={cn(
|
|
"preview flex w-full justify-center",
|
|
isFinanceComponent ? "p-4" : "p-10",
|
|
{
|
|
"items-center": align === "center",
|
|
"items-start": align === "start",
|
|
"items-end": align === "end",
|
|
}
|
|
)}
|
|
style={{ minHeight: previewMinHeight }}
|
|
>
|
|
<React.Suspense
|
|
fallback={
|
|
<div className="flex items-center text-sm text-muted-foreground">
|
|
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
|
Loading...
|
|
</div>
|
|
}
|
|
>
|
|
{Preview}
|
|
</React.Suspense>
|
|
</div>
|
|
</ThemeWrapper>
|
|
</TabsContent>
|
|
<TabsContent value="code">
|
|
<div className="flex flex-col space-y-4">
|
|
<div className="w-full rounded-md border [&_pre]:my-0 [&_pre]:max-h-[350px] [&_pre]:overflow-auto">
|
|
{Code ||
|
|
(codeString && (
|
|
<div className="relative">
|
|
<div className="absolute right-4 top-4 z-10">
|
|
<CopyButton
|
|
value={codeString}
|
|
variant="ghost"
|
|
className="h-7 w-7 text-foreground opacity-70 hover:bg-muted hover:opacity-100 [&_svg]:size-3.5"
|
|
/>
|
|
</div>
|
|
<SyntaxHighlighter
|
|
language="tsx"
|
|
style={resolvedTheme === "dark" ? vscDarkPlus : vs}
|
|
customStyle={{
|
|
margin: 0,
|
|
borderRadius: "0.5rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.5",
|
|
padding: "1.5rem",
|
|
background:
|
|
resolvedTheme === "dark" ? "#1e1e1e" : "#ffffff",
|
|
}}
|
|
showLineNumbers={false}
|
|
PreTag="div"
|
|
wrapLines={false}
|
|
wrapLongLines={false}
|
|
>
|
|
{codeString.trim()}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|