@@ -8,7 +8,7 @@ import { Check, Menu, Terminal } from 'lucide-react';
88import { Button } from '@constructive-io/ui/button' ;
99
1010import { ThemeToggle } from '@/components/site/theme-toggle' ;
11- import { getBasePrimitive } from '@/lib/base-primitives' ;
11+ import { getBasePrimitive , registryInstall } from '@/lib/base-primitives' ;
1212
1313function normalizePath ( path : string ) {
1414 if ( path . length > 1 && path . endsWith ( '/' ) ) return path . slice ( 0 , - 1 ) ;
@@ -27,7 +27,20 @@ function crumbFor(path: string): string {
2727 return 'Registry' ;
2828}
2929
30- const CLI = 'pnpm dlx shadcn@4.13.1 add @constructive/button' ;
30+ function installActionFor ( path : string ) {
31+ const normalizedPath = normalizePath ( path ) ;
32+ if ( ! normalizedPath . startsWith ( '/blocks/ui/' ) ) return null ;
33+
34+ const name = normalizedPath . slice ( '/blocks/ui/' . length ) ;
35+ const primitive = getBasePrimitive ( name ) ;
36+ if ( ! primitive ) return null ;
37+
38+ return {
39+ command : registryInstall ( primitive ) ,
40+ label : `shadcn add @constructive/${ primitive . name } ` ,
41+ title : primitive . title ,
42+ } ;
43+ }
3144
3245type SiteTopbarProps = {
3346 onMenuClick ?: ( ) => void ;
@@ -36,15 +49,22 @@ type SiteTopbarProps = {
3649export function SiteTopbar ( { onMenuClick } : SiteTopbarProps ) {
3750 const pathname = usePathname ( ) ?? '' ;
3851 const crumb = crumbFor ( pathname ) ;
39- const [ copied , setCopied ] = useState ( false ) ;
52+ const installAction = installActionFor ( pathname ) ;
53+ const [ copiedCommand , setCopiedCommand ] = useState < string | null > ( null ) ;
54+ const copied = installAction ?. command === copiedCommand ;
4055
4156 async function copyCli ( ) {
57+ if ( ! installAction ) return ;
58+
59+ const { command } = installAction ;
4260 try {
43- await navigator . clipboard . writeText ( CLI ) ;
44- setCopied ( true ) ;
45- window . setTimeout ( ( ) => setCopied ( false ) , 1600 ) ;
61+ await navigator . clipboard . writeText ( command ) ;
62+ setCopiedCommand ( command ) ;
63+ window . setTimeout ( ( ) => {
64+ setCopiedCommand ( ( current ) => ( current === command ? null : current ) ) ;
65+ } , 1600 ) ;
4666 } catch {
47- setCopied ( false ) ;
67+ setCopiedCommand ( ( current ) => ( current === command ? null : current ) ) ;
4868 }
4969 }
5070
@@ -59,7 +79,7 @@ export function SiteTopbar({ onMenuClick }: SiteTopbarProps) {
5979 aria-label = "Open navigation"
6080 onClick = { onMenuClick }
6181 >
62- < Menu className = "size-4" />
82+ < Menu aria-hidden />
6383 </ Button >
6484
6585 < nav
@@ -80,20 +100,28 @@ export function SiteTopbar({ onMenuClick }: SiteTopbarProps) {
80100
81101 < ThemeToggle />
82102
83- < Button
84- type = "button"
85- variant = "secondary"
86- size = "sm"
87- className = "hidden sm:inline-flex"
88- onClick = { copyCli }
89- >
90- { copied ? (
91- < Check className = "size-3.5 text-emerald-500" />
92- ) : (
93- < Terminal className = "size-3.5" />
94- ) }
95- < span className = "font-mono text-xs" > { copied ? 'Copied' : 'npx shadcn add' } </ span >
96- </ Button >
103+ { installAction ? (
104+ < Button
105+ type = "button"
106+ variant = "secondary"
107+ size = "sm"
108+ className = "hidden sm:inline-flex"
109+ onClick = { copyCli }
110+ aria-label = {
111+ copied
112+ ? `${ installAction . title } install command copied`
113+ : `Copy ${ installAction . title } install command`
114+ }
115+ title = { installAction . command }
116+ >
117+ { copied ? (
118+ < Check data-icon = "inline-start" className = "text-emerald-500" />
119+ ) : (
120+ < Terminal data-icon = "inline-start" />
121+ ) }
122+ < span className = "font-mono text-xs" > { copied ? 'Copied' : installAction . label } </ span >
123+ </ Button >
124+ ) : null }
97125
98126 < Button asChild size = "sm" >
99127 < Link href = "/blocks" > Setup</ Link >
0 commit comments