Files
siftlode/frontend/src/components/ui/LinkButton.tsx
T
peter cc8e0648db fix(design): the button and chip hover colours were never generated — an act: variant instead of a runtime prefix (E28)
buttonStyles/chipStyles glued the hover guard on at runtime (`${ACT}hover:text-fg`). Tailwind emits CSS
only for a class it finds whole in the source, so all 22 guarded hovers shipped with no rule: no text or
edge colour on hover for ghost/outline/danger buttons, no white layer on the player's control row, no
underline on a link button, no accent hover on token/suggest/ghost chips. The stylesheet had 0 rules
for `not-aria-disabled`.

- `@custom-variant act` (index.css): the same guard as the state layers; `:disabled` never matches an
  <a>, so one variant serves buttons and links and the per-element variant maps go.
- ui/tailwindScan.contract.test.ts: every class the primitives can emit stands whole in their source.
- a lint rule refuses a variant right after an interpolation; the `enabled:hover:` rule now points at
  `act:hover:`.
2026-09-25 14:13:51 +02:00

70 lines
1.7 KiB
TypeScript

import clsx from "clsx";
import type { AnchorHTMLAttributes, ReactNode } from "react";
import {
buttonClass,
iconButtonClass,
type ButtonSize,
type ButtonVariant,
type IconButtonSize,
type IconButtonVariant,
} from "./buttonStyles";
// A link that looks like a button (a download, a sign-in that leaves the app). It stays an <a> — the
// browser's own open-in-new-tab, copy-link and download behaviour, and the right role for a screen
// reader — and wears the `Button` / `IconButton` face from buttonStyles, so it can't drift from the
// buttons beside it. A link in a sentence is a plain `<a>` (or `Button variant="link"` for an action).
type Anchor = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "className" | "children">;
export function LinkButton({
variant = "outline",
size = "md",
icon,
fullWidth,
className,
children,
...rest
}: Anchor & {
variant?: ButtonVariant;
size?: ButtonSize;
/** Leading icon (a lucide element; sized here). */
icon?: ReactNode;
fullWidth?: boolean;
className?: string;
children: ReactNode;
}) {
return (
<a className={clsx(buttonClass({ variant, size, fullWidth }), className)} {...rest}>
{icon}
{children}
</a>
);
}
/** The glyph-only form: `label` is the accessible name and the tooltip, as on `IconButton`. */
export function IconLink({
label,
variant = "ghost",
size = "sm",
className,
children,
...rest
}: Anchor & {
label: string;
variant?: IconButtonVariant;
size?: IconButtonSize;
className?: string;
children: ReactNode;
}) {
return (
<a
aria-label={label}
title={label}
className={clsx(iconButtonClass({ variant, size }), className)}
{...rest}
>
{children}
</a>
);
}