first publish

This commit is contained in:
knso committed 2026-09-06 17:21:52 +02:00
commit b8e41b4d55
147 files changed
+19367

No files matched your search

+73
View File
@@ -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;