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:`.
70 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|