Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions components/ui/8bit/button-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type * as React from "react";

import { cn } from "@/lib/utils";

import {
ButtonGroup as ShadcnButtonGroup,
ButtonGroupText as ShadcnButtonGroupText,
buttonGroupVariants,
} from "@/components/ui/button-group";
import { Separator } from "@/components/ui/8bit/separator";

import "@/components/ui/8bit/styles/retro.css";

export { buttonGroupVariants };

// ─── ButtonGroup ──────────────────────────────────────────────────────────────

export type BitButtonGroupProps = React.ComponentProps<
typeof ShadcnButtonGroup
>;

/**
* 8-bit ButtonGroup wraps the shadcn ButtonGroup and adds a shared retro
* pixelated border around the whole group.
*
* API matches shadcn: `orientation`, `data-slot="button-group"`.
* No React context — child button sizing and layout is handled via CSS
* child selectors in `buttonGroupVariants`, identical to shadcn.
*/
function ButtonGroup({
className,
orientation = "horizontal",
children,
...props
}: BitButtonGroupProps) {
return (
<div
className={cn(
"relative inline-flex",
orientation === "vertical" ? "flex-col" : "flex-row"
)}
>
{/* Shared outer pixelated border */}
{/* Top */}
<div className="absolute -top-1.5 left-1.5 right-1.5 h-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
{/* Bottom */}
<div className="absolute -bottom-1.5 left-1.5 right-1.5 h-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
{/* Left */}
<div className="absolute -left-1.5 top-1.5 bottom-1.5 w-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
{/* Right */}
<div className="absolute -right-1.5 top-1.5 bottom-1.5 w-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
{/* Corners */}
<div className="absolute top-0 left-0 size-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
<div className="absolute top-0 right-0 size-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
<div className="absolute bottom-0 left-0 size-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />
<div className="absolute bottom-0 right-0 size-1.5 bg-foreground dark:bg-ring pointer-events-none z-10" />

<ShadcnButtonGroup
className={cn(className)}
orientation={orientation}
{...props}
>
{children}
</ShadcnButtonGroup>
</div>
);
}

// ─── ButtonGroupSeparator ────────────────────────────────────────────────────

export type BitButtonGroupSeparatorProps = React.ComponentProps<
typeof Separator
>;

/**
* 8-bit ButtonGroupSeparator renders a pixel-art dashed divider between items.
* Wraps the 8-bit Separator component (which uses a pixel-dash background pattern).
* Defaults to `orientation="vertical"` for use inside a horizontal ButtonGroup.
*/
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: BitButtonGroupSeparatorProps) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn("relative m-0! self-stretch", className)}
{...props}
/>
);
}

// ─── ButtonGroupText ─────────────────────────────────────────────────────────

export type BitButtonGroupTextProps = React.ComponentProps<
typeof ShadcnButtonGroupText
>;

/**
* 8-bit ButtonGroupText renders a retro-styled text label inside a ButtonGroup.
* Useful for split-button patterns (e.g. a label prefix before action buttons).
*/
function ButtonGroupText({ className, ...props }: BitButtonGroupTextProps) {
return (
<ShadcnButtonGroupText
className={cn("rounded-none border-none retro", className)}
{...props}
/>
);
}

export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText };
20 changes: 10 additions & 10 deletions components/ui/8bit/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ function Button({ children, asChild, ...props }: BitButtonProps) {
{children}

{variant !== "ghost" && variant !== "link" && size !== "icon" && (
<>
{/* Pixelated border */}
<div className="in-data-[slot=button-group]:hidden contents">
{/* Pixelated border — hidden when inside ButtonGroup */}
<div className="absolute -top-1.5 w-1/2 left-1.5 h-1.5 bg-foreground dark:bg-ring" />
<div className="absolute -top-1.5 w-1/2 right-1.5 h-1.5 bg-foreground dark:bg-ring" />
<div className="absolute -bottom-1.5 w-1/2 left-1.5 h-1.5 bg-foreground dark:bg-ring" />
Expand All @@ -84,27 +84,27 @@ function Button({ children, asChild, ...props }: BitButtonProps) {
<div className="absolute bottom-1.5 right-0 w-3 h-1.5 bg-foreground/20" />
</>
)}
</>
</div>
)}

{size === "icon" && (
<>
<div className="in-data-[slot=button-group]:hidden contents">
<div className="absolute top-0 left-0 w-full h-[5px] md:h-1.5 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute bottom-0 w-full h-[5px] md:h-1.5 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute top-1 -left-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute bottom-1 -left-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute top-1 -right-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute bottom-1 -right-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
</>
</div>
)}
</span>
) : (
<>
{children}

{variant !== "ghost" && variant !== "link" && size !== "icon" && (
<>
{/* Pixelated border */}
<div className="in-data-[slot=button-group]:hidden contents">
{/* Pixelated border — hidden when inside ButtonGroup */}
<div className="absolute -top-1.5 w-1/2 left-1.5 h-1.5 bg-foreground dark:bg-ring" />
<div className="absolute -top-1.5 w-1/2 right-1.5 h-1.5 bg-foreground dark:bg-ring" />
<div className="absolute -bottom-1.5 w-1/2 left-1.5 h-1.5 bg-foreground dark:bg-ring" />
Expand All @@ -126,18 +126,18 @@ function Button({ children, asChild, ...props }: BitButtonProps) {
<div className="absolute bottom-1.5 right-0 w-3 h-1.5 bg-foreground/20" />
</>
)}
</>
</div>
)}

{size === "icon" && (
<>
<div className="in-data-[slot=button-group]:hidden contents">
<div className="absolute top-0 left-0 w-full h-[5px] md:h-1.5 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute bottom-0 w-full h-[5px] md:h-1.5 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute top-1 -left-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute bottom-1 -left-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute top-1 -right-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
<div className="absolute bottom-1 -right-1 w-[5px] md:w-1.5 h-1/2 bg-foreground dark:bg-ring pointer-events-none" />
</>
</div>
)}
</>
)}
Expand Down
83 changes: 83 additions & 0 deletions components/ui/button-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"

const buttonGroupVariants = cva(
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)

function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}

function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "div"

return (
<Comp
className={cn(
"flex items-center gap-2 rounded-md border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}

function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"relative m-0! self-stretch bg-input data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
)
}

export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
4 changes: 4 additions & 0 deletions config/nav-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const components = [
title: "Button",
url: "/docs/components/button",
},
{
title: "Button Group",
url: "/docs/components/button-group",
},
{
title: "Calendar",
url: "/docs/components/calendar",
Expand Down
Loading