shadcn, restyled · @jengacore/button
Button
shadcn Button with jengacore variants, 30/34/44px heights and a `shortcut` hint. Same API as upstream.
Separate Radix and Base UI builds
Install
terminal
npx shadcn@latest add @jengacore/buttonWithout the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/button.json
Registry dependencies
npm packages
class-variance-authorityradix-ui@base-ui/react
Source
components/ui/button.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { LoaderCircle } from "lucide-react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
/**
* jengacore Button: shadcn API, jengacore look.
* One `default` (primary) per view. `destructive` only behind a confirm dialog that repeats the object.
* Labels are verbs in sentence case ("Approve batch"). Heights 30 / 34 / 44px; `lg` for teller and touch screens.
*/
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-[13.5px] leading-5 font-medium whitespace-nowrap select-none " +
"transition-[background-color,border-color,color,transform,filter] duration-fast ease-jc active:translate-y-px " +
"outline-none focus-ring " +
"disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-100 aria-invalid:outline-destructive " +
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:stroke-[1.75] [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-forest-900 dark:hover:bg-[#6ee7b7] contrast:hover:bg-forest-900",
secondary: "bg-secondary text-secondary-foreground hover:brightness-[.97] dark:hover:brightness-110",
outline: "border border-border bg-card text-foreground hover:border-input hover:bg-accent contrast:border-input",
ghost: "text-foreground/85 hover:bg-accent hover:text-foreground",
destructive: "bg-destructive text-destructive-foreground hover:brightness-95 dark:hover:brightness-110",
link: "h-auto px-0 text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-[34px] px-3.5 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
sm: "h-[30px] gap-1.5 px-2.5 text-[12.5px] [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-11 px-5 text-[15px]",
icon: "size-[34px]",
"icon-xs": "size-6 rounded-sm [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-[30px]",
"icon-lg": "size-11",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
shortcut,
loading = false,
disabled,
children,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
/** Keyboard hint shown after the label, e.g. "B" for Block. */
shortcut?: string
/** Working: disables the button and swaps the leading icon for a spinner; keep a verb label ("Posting…"). */
loading?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{asChild ? (
children
) : (
<>
{loading ? <LoaderCircle className="animate-spin" aria-hidden /> : null}
{loading ? React.Children.toArray(children).filter((c) => !React.isValidElement(c) || c.type === React.Fragment || typeof c.type === "string") : children}
{shortcut ? (
<kbd className="rounded-[3px] border border-current px-1 font-mono text-[10.5px] leading-4 opacity-60">{shortcut}</kbd>
) : null}
</>
)}
</Comp>
)
}
export { Button, buttonVariants }