Initial release — Astro Rocket v1.0.0

This commit is contained in:
Claude
2026-04-06 07:31:47 +00:00
commit ddd0c22311
275 changed files with 38839 additions and 0 deletions
@@ -0,0 +1,84 @@
---
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/lib/cn';
import { isExternalUrl } from '@/lib/utils';
import { buttonVariants } from './button.variants';
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
interface Props extends HTMLAttributes<'button'> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
fullWidth?: boolean;
href?: string;
target?: string;
rel?: string;
icon?: boolean;
}
const {
variant = 'primary',
size = 'md',
loading = false,
fullWidth = false,
href,
target,
icon = false,
class: className,
disabled,
...attrs
} = Astro.props;
// Auto-detect external URLs and apply appropriate attributes
const isExternal = href ? isExternalUrl(href) : false;
const linkTarget = target ?? (isExternal ? '_blank' : undefined);
const linkRel = isExternal ? 'noopener noreferrer' : undefined;
const Element = href ? 'a' : 'button';
const isDisabled = disabled || loading;
const isDisabledLink = href && isDisabled;
const classes = cn(
buttonVariants({ variant, size, fullWidth, icon }),
isDisabledLink && 'pointer-events-none',
className
);
---
<Element
class={classes}
disabled={!href ? isDisabled : undefined}
href={isDisabledLink ? undefined : href}
target={isDisabledLink ? undefined : linkTarget}
rel={isDisabledLink ? undefined : linkRel}
aria-disabled={isDisabledLink ? 'true' : undefined}
tabindex={isDisabledLink ? '-1' : undefined}
{...attrs}
>
{
loading ? <Icon name="loader" size="sm" class="animate-spin" /> : null
}
<slot />
</Element>
<style is:global>
/*
* Dark mode: soften the flat near-white primary button background with a
* subtle top-to-bottom gradient — same principle as the hero H1 gradient.
* color-mix blends 55% foreground + 45% foreground-secondary so the bottom
* of the button is slightly dimmer and brand-tinted without looking grey.
* Hover clears the gradient image so the Tailwind bg-foreground/90 takes over.
*/
.dark .btn-primary {
border: 1px solid var(--color-foreground-secondary);
background-image: linear-gradient(
180deg,
var(--color-foreground) 0%,
color-mix(in oklch, var(--color-foreground) 55%, var(--color-foreground-secondary)) 100%
);
}
.dark .btn-primary:hover {
background-image: none;
}
</style>
+92
View File
@@ -0,0 +1,92 @@
import { type ButtonHTMLAttributes, type AnchorHTMLAttributes, type Ref } from 'react';
import { cn } from '@/lib/cn';
import { isExternalUrl } from '@/lib/utils';
import { buttonVariants, type ButtonVariants } from './button.variants';
interface BaseProps {
ref?: Ref<HTMLButtonElement | HTMLAnchorElement>;
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
loading?: boolean;
fullWidth?: boolean;
icon?: boolean;
disabled?: boolean;
}
type ButtonAsButton = BaseProps &
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'ref'> & {
href?: never;
};
type ButtonAsLink = BaseProps &
Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'ref'> & {
href: string;
};
type ButtonProps = ButtonAsButton | ButtonAsLink;
const LoadingSpinner = () => (
<svg
className="animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
export function Button(props: ButtonProps) {
const {
ref,
variant = 'primary',
size = 'md',
loading = false,
fullWidth = false,
icon = false,
className,
children,
disabled,
...rest
} = props;
const classes = cn(buttonVariants({ variant, size, fullWidth, icon }), className);
if ('href' in props && props.href) {
const isExternal = isExternalUrl(props.href);
const linkProps = rest as AnchorHTMLAttributes<HTMLAnchorElement>;
return (
<a
ref={ref as Ref<HTMLAnchorElement>}
className={classes}
target={linkProps.target ?? (isExternal ? '_blank' : undefined)}
rel={isExternal ? 'noopener noreferrer' : linkProps.rel}
{...linkProps}
>
{loading && <LoadingSpinner />}
{children}
</a>
);
}
return (
<button
ref={ref as Ref<HTMLButtonElement>}
className={classes}
disabled={disabled || loading}
{...(rest as ButtonHTMLAttributes<HTMLButtonElement>)}
>
{loading && <LoadingSpinner />}
{children}
</button>
);
}
export default Button;
@@ -0,0 +1,51 @@
import { cva, type VariantProps } from 'class-variance-authority';
export const buttonVariants = cva(
[
'inline-flex items-center justify-center gap-2',
'font-medium rounded-md',
'transition-all duration-150 ease-out',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
'[&_svg]:pointer-events-none [&_svg]:shrink-0',
],
{
variants: {
variant: {
primary:
'btn-primary bg-foreground text-background hover:bg-foreground/90 active:scale-[0.98]',
secondary:
'bg-secondary text-secondary-foreground border border-border hover:bg-secondary-hover hover:border-border-strong active:scale-[0.98]',
outline:
'border border-foreground/25 bg-transparent text-foreground hover:bg-secondary hover:border-foreground/40 active:scale-[0.98]',
ghost:
'text-foreground-secondary hover:text-foreground hover:bg-secondary active:scale-[0.98]',
link: 'text-foreground-secondary hover:text-foreground underline-offset-4 hover:underline',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.98]',
},
size: {
sm: 'h-8 px-3 text-xs [&_svg]:h-4 [&_svg]:w-4',
md: 'h-10 px-4 text-sm [&_svg]:h-5 [&_svg]:w-5',
lg: 'h-12 px-5 text-base [&_svg]:h-5 [&_svg]:w-5',
},
fullWidth: {
true: 'w-full',
},
icon: {
true: 'rounded-md',
},
},
compoundVariants: [
{ icon: true, size: 'sm', class: 'h-8 w-8 px-0' },
{ icon: true, size: 'md', class: 'h-10 w-10 px-0' },
{ icon: true, size: 'lg', class: 'h-12 w-12 px-0' },
],
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;
+3
View File
@@ -0,0 +1,3 @@
export { default } from './Button.astro';
export { Button } from './Button';
export { buttonVariants, type ButtonVariants } from './button.variants';