first publish
This commit is contained in:
commit
b8e41b4d55
147 files changed
+19367
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TransitionLink } from "@/components/motion/TransitionLink";
|
||||
|
||||
export interface ButtonProps {
|
||||
href: string;
|
||||
label: string;
|
||||
/** Mono suffix that rises out of a mask inside the button on hover (≥ 1024); its width is reserved. */
|
||||
suffix?: string;
|
||||
/** Mono micro line under the button, e.g. „Telefonisch oder online · Deutsch oder Englisch“. */
|
||||
micro?: string;
|
||||
/** `primary` = 1 px accent outline + gold sheen fill on hover; `ghost` = neutral hairline, no fill. */
|
||||
variant?: "primary" | "ghost";
|
||||
/** `target="_blank" rel="noopener noreferrer"`. */
|
||||
external?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BUTTON_BASE =
|
||||
"group/btn relative inline-flex items-center overflow-hidden whitespace-nowrap border px-[2.1em] py-[1.05em] font-sans text-[0.8125rem] font-medium uppercase leading-none tracking-[0.2em] text-fg transition-[color,background-color,border-color] duration-300";
|
||||
|
||||
/* Sheen fill: --gold-sheen at 250 % width, background-position 0 % → 100 % in .6 s. Text turns ink. */
|
||||
export const BUTTON_PRIMARY =
|
||||
"border-accent hover:text-ink focus-visible:text-ink before:pointer-events-none before:absolute before:inset-0 before:bg-[image:var(--gold-sheen)] before:bg-[length:250%_100%] before:bg-[position:0%_50%] before:opacity-0 before:transition-[opacity,background-position] before:duration-[600ms] before:ease-[var(--ease-out-quart)] hover:before:bg-[position:100%_50%] hover:before:opacity-100 focus-visible:before:bg-[position:100%_50%] focus-visible:before:opacity-100";
|
||||
|
||||
const GHOST = "border-line hover:border-accent focus-visible:border-accent";
|
||||
|
||||
function isInternal(href: string) {
|
||||
return href.startsWith("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Outline button (radius 0, no shadow). Internal routes use the curtain
|
||||
* (`TransitionLink`); mailto/tel/http render a plain `<a>`.
|
||||
*/
|
||||
export function Button({ href, label, suffix, micro, variant = "primary", external = false, className }: ButtonProps) {
|
||||
const cls = cn(BUTTON_BASE, variant === "primary" ? BUTTON_PRIMARY : GHOST, className);
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<span className="relative z-10">{label}</span>
|
||||
{suffix && (
|
||||
/* The mask sits INSIDE the button and its width is reserved, so the
|
||||
control never resizes under the pointer – only the line rises. */
|
||||
<span aria-hidden="true" className="relative z-10 hidden overflow-hidden lg:block">
|
||||
<span className="block translate-y-full whitespace-nowrap pl-3 font-mono text-[11px] normal-case leading-[1.6] tracking-[0.06em] transition-transform duration-[350ms] ease-[var(--ease-out-quart)] group-hover/btn:translate-y-0 group-focus-visible/btn:translate-y-0">
|
||||
{suffix}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const link =
|
||||
isInternal(href) && !external ? (
|
||||
<TransitionLink href={href} label={label} className={cls}>
|
||||
{inner}
|
||||
</TransitionLink>
|
||||
) : (
|
||||
<a href={href} target={external ? "_blank" : undefined} rel={external ? "noopener noreferrer" : undefined} className={cls}>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
|
||||
if (!micro) return link;
|
||||
return (
|
||||
<span className="inline-flex flex-col items-start gap-3">
|
||||
{link}
|
||||
<span className="font-mono text-[11px] tracking-[0.06em] text-fg-muted">{micro}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
Reference in new issue
Block a user