Split Link/Anchor from button navigators
Rename button-styled Link/Anchor to ButtonLink/ ButtonAnchor and add underlined text Link/Anchor so names match look and element. Hero meta uses plain Anchors for contact and custom links. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -34,8 +34,9 @@ export type ButtonProps
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
// Clickable action (Radix "Button"). Renders a <button>; a styled <a> (Anchor)
|
||||
// or router link (Link) are separate components. See contrib/claude/ui.md.
|
||||
// Clickable action (Radix "Button"). Renders a <button>; button-styled
|
||||
// navigation uses ButtonLink / ButtonAnchor, underlined text uses Link /
|
||||
// Anchor. See contrib/claude/ui.md.
|
||||
export function Button(props: ButtonProps) {
|
||||
const {
|
||||
size, variant, color, highContrast, active, className,
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
import { ArrowSquareOutIcon } from "@phosphor-icons/react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Anchor } from "./Anchor";
|
||||
import { ButtonAnchor } from "./ButtonAnchor";
|
||||
|
||||
const sizes = [1, 2, 3, 4] as const;
|
||||
const variants = ["classic", "solid", "soft", "surface", "outline", "ghost"] as const;
|
||||
|
||||
export default {
|
||||
title: "v2/Anchor",
|
||||
component: Anchor,
|
||||
title: "v2/ButtonAnchor",
|
||||
component: ButtonAnchor,
|
||||
args: {
|
||||
children: "Documentation",
|
||||
href: "#",
|
||||
@@ -38,9 +38,9 @@ export default {
|
||||
highContrast: false,
|
||||
active: false,
|
||||
},
|
||||
} satisfies Meta<typeof Anchor>;
|
||||
} satisfies Meta<typeof ButtonAnchor>;
|
||||
|
||||
type Story = StoryObj<typeof Anchor>;
|
||||
type Story = StoryObj<typeof ButtonAnchor>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
@@ -48,9 +48,9 @@ export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-3">
|
||||
{sizes.map(size => (
|
||||
<Anchor key={size} href="#" size={size}>
|
||||
<ButtonAnchor key={size} href="#" size={size}>
|
||||
Documentation
|
||||
</Anchor>
|
||||
</ButtonAnchor>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
@@ -60,9 +60,9 @@ export const Variants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{variants.map(variant => (
|
||||
<Anchor key={variant} href="#" variant={variant}>
|
||||
<ButtonAnchor key={variant} href="#" variant={variant}>
|
||||
{variant}
|
||||
</Anchor>
|
||||
</ButtonAnchor>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
@@ -70,8 +70,8 @@ export const Variants: Story = {
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: () => (
|
||||
<Anchor href="#" variant="soft" color="neutral" iconEnd={<ArrowSquareOutIcon />}>
|
||||
<ButtonAnchor href="#" variant="soft" color="neutral" iconEnd={<ArrowSquareOutIcon />}>
|
||||
External link
|
||||
</Anchor>
|
||||
</ButtonAnchor>
|
||||
),
|
||||
};
|
||||
53
packages/ui/src/v2/Button/ButtonAnchor.tsx
Normal file
53
packages/ui/src/v2/Button/ButtonAnchor.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { button } from "./variants";
|
||||
|
||||
export type ButtonAnchorProps
|
||||
= Omit<ComponentProps<"a">, "color">
|
||||
& VariantProps<typeof button>
|
||||
& {
|
||||
iconStart?: ReactNode;
|
||||
iconEnd?: ReactNode;
|
||||
};
|
||||
|
||||
// External (or mailto:/tel:) navigation that looks like a Button. Renders an
|
||||
// <a>; for in-app button navigation use ButtonLink, for underlined text use
|
||||
// Link/Anchor. See contrib/claude/ui.md.
|
||||
export function ButtonAnchor(props: ButtonAnchorProps) {
|
||||
const {
|
||||
size, variant, color, highContrast, active, className,
|
||||
iconStart, iconEnd, children, ...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={button({ size, variant, color, highContrast, active, className })}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
72
packages/ui/src/v2/Button/ButtonLink.stories.tsx
Normal file
72
packages/ui/src/v2/Button/ButtonLink.stories.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
import { ButtonLink } from "./ButtonLink";
|
||||
|
||||
const navItems = ["Documents", "Subprocessors", "Updates", "Requests"] as const;
|
||||
|
||||
export default {
|
||||
title: "v2/ButtonLink",
|
||||
component: ButtonLink,
|
||||
args: {
|
||||
children: "Documents",
|
||||
to: "#",
|
||||
size: 2,
|
||||
variant: "ghost",
|
||||
color: "neutral",
|
||||
highContrast: false,
|
||||
active: false,
|
||||
},
|
||||
decorators: [
|
||||
Story => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof ButtonLink>;
|
||||
|
||||
type Story = StoryObj<typeof ButtonLink>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
// Mirrors the Compliance Portal top-bar nav: ghost links with a persistent active
|
||||
// pill on the selected item.
|
||||
export const Nav: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-1">
|
||||
{navItems.map((item, index) => (
|
||||
<ButtonLink
|
||||
key={item}
|
||||
to="#"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={2}
|
||||
active={index === 0}
|
||||
>
|
||||
{item}
|
||||
</ButtonLink>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
54
packages/ui/src/v2/Button/ButtonLink.tsx
Normal file
54
packages/ui/src/v2/Button/ButtonLink.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { button } from "./variants";
|
||||
|
||||
export type ButtonLinkProps
|
||||
= Omit<ComponentProps<typeof RouterLink>, "color">
|
||||
& VariantProps<typeof button>
|
||||
& {
|
||||
iconStart?: ReactNode;
|
||||
iconEnd?: ReactNode;
|
||||
};
|
||||
|
||||
// In-app navigation that looks like a Button. Renders a react-router Link; for
|
||||
// a button-styled <a> use ButtonAnchor, for underlined text use Link/Anchor.
|
||||
// Set `active` for the selected nav state. See contrib/claude/ui.md.
|
||||
export function ButtonLink(props: ButtonLinkProps) {
|
||||
const {
|
||||
size, variant, color, highContrast, active, className,
|
||||
iconStart, iconEnd, children, ...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<RouterLink
|
||||
className={button({ size, variant, color, highContrast, active, className })}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
@@ -20,9 +20,9 @@
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Button (Radix "Button"). A <button>; an Anchor / Link are separate
|
||||
// components (see contrib/claude/ui.md). The variant × color surface treatment
|
||||
// resolves in the compound variants below.
|
||||
// Button (Radix "Button"). A <button>; ButtonLink / ButtonAnchor share these
|
||||
// styles for button-looking navigation (see contrib/claude/ui.md). The
|
||||
// variant × color surface treatment resolves in the compound variants below.
|
||||
export const button = tv({
|
||||
base: [
|
||||
"inline-flex shrink-0 items-center justify-center border border-transparent font-medium whitespace-nowrap",
|
||||
@@ -59,7 +59,7 @@ export const button = tv({
|
||||
true: "",
|
||||
false: "",
|
||||
},
|
||||
// Persistent "selected" treatment for nav items built on Anchor / Link
|
||||
// Persistent "selected" treatment for nav items built on ButtonLink / ButtonAnchor
|
||||
// (ghost / soft surfaces). Look-only — does not change structure.
|
||||
active: {
|
||||
true: "",
|
||||
|
||||
74
packages/ui/src/v2/Link/Anchor.stories.tsx
Normal file
74
packages/ui/src/v2/Link/Anchor.stories.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { ArrowSquareOutIcon, GlobeSimpleIcon } from "@phosphor-icons/react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Anchor } from "./Anchor";
|
||||
|
||||
const sizes = [1, 2, 3, 4] as const;
|
||||
|
||||
export default {
|
||||
title: "v2/Anchor",
|
||||
component: Anchor,
|
||||
args: {
|
||||
children: "blaxel.ai",
|
||||
href: "https://example.com",
|
||||
size: 2,
|
||||
color: "neutral",
|
||||
highContrast: false,
|
||||
},
|
||||
} satisfies Meta<typeof Anchor>;
|
||||
|
||||
type Story = StoryObj<typeof Anchor>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
{sizes.map(size => (
|
||||
<Anchor key={size} href="https://example.com" size={size}>
|
||||
blaxel.ai
|
||||
</Anchor>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<Anchor href="https://example.com" size={2} iconStart={<GlobeSimpleIcon />}>
|
||||
blaxel.ai
|
||||
</Anchor>
|
||||
<Anchor href="https://example.com" size={2} iconEnd={<ArrowSquareOutIcon />}>
|
||||
Documentation
|
||||
</Anchor>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const Plain: Story = {
|
||||
args: {
|
||||
underline: false,
|
||||
iconStart: <GlobeSimpleIcon />,
|
||||
},
|
||||
};
|
||||
@@ -21,28 +21,28 @@
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { button } from "./variants";
|
||||
import { link } from "./variants";
|
||||
|
||||
export type AnchorProps
|
||||
= Omit<ComponentProps<"a">, "color">
|
||||
& VariantProps<typeof button>
|
||||
& VariantProps<typeof link>
|
||||
& {
|
||||
iconStart?: ReactNode;
|
||||
iconEnd?: ReactNode;
|
||||
};
|
||||
|
||||
// Styled <a> sharing the Button look (Radix "Button"). Use for external links
|
||||
// or mailto:/tel:; for in-app navigation use Link (router). See
|
||||
// Underlined text <a> for external, mailto:, or tel: URLs. For in-app text
|
||||
// links use Link; for button-looking <a> use ButtonAnchor. See
|
||||
// contrib/claude/ui.md.
|
||||
export function Anchor(props: AnchorProps) {
|
||||
const {
|
||||
size, variant, color, highContrast, active, className,
|
||||
size, color, highContrast, underline, className,
|
||||
iconStart, iconEnd, children, ...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={button({ size, variant, color, highContrast, active, className })}
|
||||
className={link({ size, color, highContrast, underline, className })}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
@@ -18,24 +18,23 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { ArrowRightIcon } from "@phosphor-icons/react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
import { Link } from "./Link";
|
||||
|
||||
const navItems = ["Documents", "Subprocessors", "Updates", "Requests"] as const;
|
||||
const sizes = [1, 2, 3, 4] as const;
|
||||
|
||||
export default {
|
||||
title: "v2/Link",
|
||||
component: Link,
|
||||
args: {
|
||||
children: "Documents",
|
||||
children: "View all updates",
|
||||
to: "#",
|
||||
size: 2,
|
||||
variant: "ghost",
|
||||
color: "neutral",
|
||||
highContrast: false,
|
||||
active: false,
|
||||
},
|
||||
decorators: [
|
||||
Story => (
|
||||
@@ -50,23 +49,28 @@ type Story = StoryObj<typeof Link>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
// Mirrors the Compliance Portal top-bar nav: ghost links with a persistent active
|
||||
// pill on the selected item.
|
||||
export const Nav: Story = {
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-1">
|
||||
{navItems.map((item, index) => (
|
||||
<Link
|
||||
key={item}
|
||||
to="#"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={2}
|
||||
active={index === 0}
|
||||
>
|
||||
{item}
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
{sizes.map(size => (
|
||||
<Link key={size} to="#" size={size}>
|
||||
View all updates
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: () => (
|
||||
<Link to="#" size={2} iconEnd={<ArrowRightIcon />}>
|
||||
View all updates
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
|
||||
export const Plain: Story = {
|
||||
args: {
|
||||
underline: false,
|
||||
},
|
||||
};
|
||||
@@ -22,28 +22,27 @@ import type { ComponentProps, ReactNode } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { button } from "./variants";
|
||||
import { link } from "./variants";
|
||||
|
||||
export type LinkProps
|
||||
= Omit<ComponentProps<typeof RouterLink>, "color">
|
||||
& VariantProps<typeof button>
|
||||
& VariantProps<typeof link>
|
||||
& {
|
||||
iconStart?: ReactNode;
|
||||
iconEnd?: ReactNode;
|
||||
};
|
||||
|
||||
// In-app navigation link sharing the Button look (Radix "Button"). Renders a
|
||||
// react-router Link; for external links use Anchor, for actions use Button. Set
|
||||
// `active` for the selected nav state. See contrib/claude/ui.md.
|
||||
// Underlined in-app text link (react-router). For external/mailto use Anchor;
|
||||
// for button-looking navigation use ButtonLink. See contrib/claude/ui.md.
|
||||
export function Link(props: LinkProps) {
|
||||
const {
|
||||
size, variant, color, highContrast, active, className,
|
||||
size, color, highContrast, underline, className,
|
||||
iconStart, iconEnd, children, ...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<RouterLink
|
||||
className={button({ size, variant, color, highContrast, active, className })}
|
||||
className={link({ size, color, highContrast, underline, className })}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
78
packages/ui/src/v2/Link/variants.ts
Normal file
78
packages/ui/src/v2/Link/variants.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Text link (Link / Anchor). Default is always underlined; `underline={false}`
|
||||
// keeps plain text until hover (meta rows, icon+label chrome). Sizes align
|
||||
// with Text; color × highContrast compounds mirror typography neutrals.
|
||||
// Button-looking navigation uses ButtonLink / ButtonAnchor instead.
|
||||
export const link = tv({
|
||||
base: [
|
||||
"inline-flex items-center underline-offset-2",
|
||||
"cursor-pointer outline-none transition-colors",
|
||||
"focus-visible:rounded-1 focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
1: "gap-1 text-1 [&_svg]:size-3.5",
|
||||
2: "gap-1.5 text-2 [&_svg]:size-4",
|
||||
3: "gap-1.5 text-3 [&_svg]:size-4",
|
||||
4: "gap-2 text-4 [&_svg]:size-5",
|
||||
},
|
||||
color: {
|
||||
neutral: "",
|
||||
gold: "",
|
||||
red: "",
|
||||
green: "",
|
||||
amber: "",
|
||||
sky: "",
|
||||
},
|
||||
highContrast: {
|
||||
true: "",
|
||||
false: "",
|
||||
},
|
||||
// Look-only — does not change structure. false = underline on hover.
|
||||
underline: {
|
||||
true: "underline",
|
||||
false: "no-underline hover:underline",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{ color: "neutral", highContrast: false, class: "text-sand-11 hover:text-sand-12" },
|
||||
{ color: "neutral", highContrast: true, class: "text-sand-12 hover:text-sand-12" },
|
||||
{ color: "gold", highContrast: false, class: "text-gold-11 hover:text-gold-12" },
|
||||
{ color: "gold", highContrast: true, class: "text-gold-12 hover:text-gold-12" },
|
||||
{ color: "red", highContrast: false, class: "text-red-11 hover:text-red-12" },
|
||||
{ color: "red", highContrast: true, class: "text-red-12 hover:text-red-12" },
|
||||
{ color: "green", highContrast: false, class: "text-green-11 hover:text-green-12" },
|
||||
{ color: "green", highContrast: true, class: "text-green-12 hover:text-green-12" },
|
||||
{ color: "amber", highContrast: false, class: "text-amber-11 hover:text-amber-12" },
|
||||
{ color: "amber", highContrast: true, class: "text-amber-12 hover:text-amber-12" },
|
||||
{ color: "sky", highContrast: false, class: "text-sky-11 hover:text-sky-12" },
|
||||
{ color: "sky", highContrast: true, class: "text-sky-12 hover:text-sky-12" },
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 2,
|
||||
color: "neutral",
|
||||
highContrast: false,
|
||||
underline: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user