Files
zeekay 597cfae467 Set the app in Zen on hanzo.ai's palette, with pill buttons
The theme takes @hanzo/ui DESIGN.md's values: a true-black page, #0a0a0a
surfaces, #171717 raised, #ededf1 ink and white-alpha hairlines in dark;
white, #0a0a0a ink and black-alpha hairlines in light. Each is an opaque
triplet, so Tailwind's alpha modifiers still apply.

Type is Zen and Zen Mono from @hanzo/font, at Zen's own weights. Buttons
are pills, the primary in the ink colour; the option cards built from a
Button keep a card's corners, and the folder actions wrap on a phone
rather than breaking a pill over two lines.
2026-10-20 12:47:20 -07:00

89 lines
2.6 KiB
TypeScript

import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { Loader } from 'lucide-react';
import { cn } from '../lib/utils';
// Pills, as on hanzo.ai: the primary is the ink colour (white on black, black on white).
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
none: '',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
const loaderVariants = cva('mr-2 animate-spin', {
variants: {
size: {
default: 'h-5 w-5',
sm: 'h-4 w-4',
lg: 'h-5 w-5',
},
},
defaultVariants: {
size: 'default',
},
});
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
/**
* Will display the loading spinner and disable the button.
*/
loading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, loading, ...props }, ref) => {
if (asChild) {
return (
<Slot className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
}
const isLoading = loading === true;
const isDisabled = props.disabled || isLoading;
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
disabled={isDisabled}
>
{isLoading && <Loader className={cn('mr-2 animate-spin', loaderVariants({ size }))} />}
{props.children}
</button>
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants };