Enam varian di empat tinggi (24 · 28 · 32 · 36), radius 10. API-nya kompatibel dengan Button milik shadcn.
npx shadcn@latest add @glc/buttonnpm install @radix-ui/react-slot class-variance-authoritySetiap komponen bergantung pada glc-theme. Lewati langkah ini kalau sudah pernah dijalankan.
npx shadcn@latest add @glc/glc-themeimport * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/registry/glc/lib/utils"
/**
* Company Books button — the `Button` set from Company BookV1.0.fig.
*
* Deliberately API-compatible with shadcn/ui's Button: same six variant names,
* same `asChild`, same `size="icon"`. Anything written against shadcn's Button
* keeps working; what changes is the geometry (the file's 24/28/32/36 heights
* rather than shadcn's 32/36/40) and the destructive skin, which the file draws
* as a red *tint* with red text rather than a solid red fill.
*/
const buttonVariants = cva(
[
"inline-flex shrink-0 items-center justify-center whitespace-nowrap",
"rounded-lg border border-transparent font-medium",
"transition-[background-color,color,box-shadow] duration-150",
"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20 focus-visible:border-ring",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
],
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-[var(--accent-hover)]",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[var(--surface-pressed)]",
outline:
"border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground",
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground",
link: "text-foreground underline underline-offset-[3px] hover:text-[var(--text-brand)]",
// The file draws destructive as a 10%-alpha red fill with red label.
destructive:
"bg-[var(--danger-tint)] text-[var(--text-error)] hover:bg-[var(--danger-tint-strong)] focus-visible:ring-destructive/20",
},
size: {
xs: "h-6 gap-1 px-2 text-xs leading-4 has-[>svg]:px-1.5",
sm: "h-7 gap-1 px-2.5 text-sm leading-5 has-[>svg]:px-2",
default: "h-8 gap-1.5 px-2.5 text-sm leading-5 has-[>svg]:px-2",
lg: "h-9 gap-1.5 px-2.5 text-sm leading-5 has-[>svg]:px-2",
icon: "size-8 p-0",
"icon-xs": "size-6 p-0",
"icon-sm": "size-7 p-0",
"icon-lg": "size-9 p-0",
},
fullWidth: {
true: "w-full",
false: "",
},
},
defaultVariants: {
variant: "default",
size: "default",
fullWidth: false,
},
}
)
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
/** Render as the child element (shadcn's `asChild`), e.g. wrapping a link. */
asChild?: boolean
/** Node rendered before the label. Prefer a Phosphor icon. */
leading?: React.ReactNode
/** Node rendered after the label. */
trailing?: React.ReactNode
}
function Button({
className,
variant,
size,
fullWidth,
asChild = false,
leading,
trailing,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, fullWidth, className }))}
{...props}
>
{/* Slot accepts exactly one child, so `asChild` passes `children`
* through untouched — compose icons inside the child element instead. */}
{asChild ? (
children
) : (
<>
{leading}
{children}
{trailing}
</>
)}
</Comp>
)
}
export { Button, buttonVariants }Berkas di atas meng-import @/registry/glc/lib/utils. Ubah menjadi lokasi cn di proyekmu — biasanya @/lib/utils. CLI melakukan ini otomatis.
import { Button } from "@/components/ui/button"
<Button variant="outline" size="sm">Simpan</Button>import { Button } from "@/registry/glc/ui/button"
export function ButtonDemo() {
return <Button>Simpan jurnal</Button>
}import { Button } from "@/registry/glc/ui/button"
export function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Destructive</Button>
</div>
)
}destructive bukan isian solid
import { Button } from "@/registry/glc/ui/button"
export function ButtonSizes() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button size="xs">xs · 24</Button>
<Button size="sm">sm · 28</Button>
<Button size="default">default · 32</Button>
<Button size="lg">lg · 36</Button>
</div>
)
}Hati-hati mencampur
import { Button } from "@/registry/glc/ui/button"
function PlusIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden
>
<path d="M12 5v14M5 12h14" />
</svg>
)
}
export function ButtonWithIcon() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button leading={<PlusIcon />}>Tambah faktur</Button>
<Button variant="outline" trailing={<PlusIcon />}>
Trailing
</Button>
{/* Icon-only buttons still need an accessible name. */}
<Button size="icon" variant="outline" aria-label="Tambah faktur">
<PlusIcon />
</Button>
</div>
)
}Tombol yang isinya hanya ikon wajib punya aria-label. Tanpa itu, pembaca layar hanya mengumumkan “tombol”.
import Link from "next/link"
import { Button } from "@/registry/glc/ui/button"
/**
* `asChild` hands the styling to the child element, so this renders a real
* anchor — keyboard, middle-click and "open in new tab" all keep working.
*/
export function ButtonAsChild() {
return (
<Button asChild>
<Link href="/docs/installation">Buka panduan instalasi</Link>
</Button>
)
}Saat asChild aktif, leading dan trailing diabaikan — Slot hanya menerima satu anak, jadi susun ikonnya di dalam elemen anak itu sendiri.
import { Button } from "@/registry/glc/ui/button"
export function ButtonStates() {
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Button disabled>Disabled</Button>
<Button variant="outline" disabled>
Disabled outline
</Button>
<Button fullWidth>Full width</Button>
</div>
)
}| Prop | Tipe | Default | Keterangan |
|---|---|---|---|
variant | "default" | "secondary" | "outline" | "ghost" | "link" | "destructive" | "default" | Kulit visual tombol. |
size | "xs" | "sm" | "default" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" | Tinggi kontrol. Varian icon-* berbentuk bujur sangkar tanpa padding horizontal. |
fullWidth | boolean | false | Melebar memenuhi kontainer induk. |
asChild | boolean | false | Merender elemen anak alih-alih <button>. Untuk membungkus tautan. |
leading | React.ReactNode | — | Node sebelum label. Diabaikan saat asChild aktif. |
trailing | React.ReactNode | — | Node sesudah label. Diabaikan saat asChild aktif. |
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/registry/glc/lib/utils"
/**
* Company Books button — the `Button` set from Company BookV1.0.fig.
*
* Deliberately API-compatible with shadcn/ui's Button: same six variant names,
* same `asChild`, same `size="icon"`. Anything written against shadcn's Button
* keeps working; what changes is the geometry (the file's 24/28/32/36 heights
* rather than shadcn's 32/36/40) and the destructive skin, which the file draws
* as a red *tint* with red text rather than a solid red fill.
*/
const buttonVariants = cva(
[
"inline-flex shrink-0 items-center justify-center whitespace-nowrap",
"rounded-lg border border-transparent font-medium",
"transition-[background-color,color,box-shadow] duration-150",
"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20 focus-visible:border-ring",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
],
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-[var(--accent-hover)]",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[var(--surface-pressed)]",
outline:
"border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground",
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground",
link: "text-foreground underline underline-offset-[3px] hover:text-[var(--text-brand)]",
// The file draws destructive as a 10%-alpha red fill with red label.
destructive:
"bg-[var(--danger-tint)] text-[var(--text-error)] hover:bg-[var(--danger-tint-strong)] focus-visible:ring-destructive/20",
},
size: {
xs: "h-6 gap-1 px-2 text-xs leading-4 has-[>svg]:px-1.5",
sm: "h-7 gap-1 px-2.5 text-sm leading-5 has-[>svg]:px-2",
default: "h-8 gap-1.5 px-2.5 text-sm leading-5 has-[>svg]:px-2",
lg: "h-9 gap-1.5 px-2.5 text-sm leading-5 has-[>svg]:px-2",
icon: "size-8 p-0",
"icon-xs": "size-6 p-0",
"icon-sm": "size-7 p-0",
"icon-lg": "size-9 p-0",
},
fullWidth: {
true: "w-full",
false: "",
},
},
defaultVariants: {
variant: "default",
size: "default",
fullWidth: false,
},
}
)
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
/** Render as the child element (shadcn's `asChild`), e.g. wrapping a link. */
asChild?: boolean
/** Node rendered before the label. Prefer a Phosphor icon. */
leading?: React.ReactNode
/** Node rendered after the label. */
trailing?: React.ReactNode
}
function Button({
className,
variant,
size,
fullWidth,
asChild = false,
leading,
trailing,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, fullWidth, className }))}
{...props}
>
{/* Slot accepts exactly one child, so `asChild` passes `children`
* through untouched — compose icons inside the child element instead. */}
{asChild ? (
children
) : (
<>
{leading}
{children}
{trailing}
</>
)}
</Comp>
)
}
export { Button, buttonVariants }