From a6be5464a573d79eb6bb79b68ac7623f1cd1fc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 26 Jun 2026 21:55:04 +0200 Subject: [PATCH] Add v2 Anchor and Link components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round out the button family with the two navigational siblings described in the UI guide: Anchor renders a styled for external links and Link renders a react-router link for in-app navigation, both sharing Button's tv styles. Add an `active` look variant so nav items can carry a persistent selected state without forking structure. Signed-off-by: Émile Ré --- packages/ui/src/v2/Button/Anchor.stories.tsx | 71 ++++++++++++++++++++ packages/ui/src/v2/Button/Anchor.tsx | 47 +++++++++++++ packages/ui/src/v2/Button/Link.stories.tsx | 66 ++++++++++++++++++ packages/ui/src/v2/Button/Link.tsx | 48 +++++++++++++ packages/ui/src/v2/Button/variants.ts | 22 ++++++ 5 files changed, 254 insertions(+) create mode 100644 packages/ui/src/v2/Button/Anchor.stories.tsx create mode 100644 packages/ui/src/v2/Button/Anchor.tsx create mode 100644 packages/ui/src/v2/Button/Link.stories.tsx create mode 100644 packages/ui/src/v2/Button/Link.tsx diff --git a/packages/ui/src/v2/Button/Anchor.stories.tsx b/packages/ui/src/v2/Button/Anchor.stories.tsx new file mode 100644 index 000000000..c4b455edc --- /dev/null +++ b/packages/ui/src/v2/Button/Anchor.stories.tsx @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { ArrowSquareOutIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Anchor } from "./Anchor"; + +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, + args: { + children: "Documentation", + href: "#", + size: 2, + variant: "solid", + color: "gold", + highContrast: false, + active: false, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const Playground: Story = {}; + +export const Sizes: Story = { + render: () => ( +
+ {sizes.map(size => ( + + Documentation + + ))} +
+ ), +}; + +export const Variants: Story = { + render: () => ( +
+ {variants.map(variant => ( + + {variant} + + ))} +
+ ), +}; + +export const WithIcon: Story = { + render: () => ( + }> + External link + + ), +}; diff --git a/packages/ui/src/v2/Button/Anchor.tsx b/packages/ui/src/v2/Button/Anchor.tsx new file mode 100644 index 000000000..c348f801f --- /dev/null +++ b/packages/ui/src/v2/Button/Anchor.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { ComponentProps, ReactNode } from "react"; +import type { VariantProps } from "tailwind-variants/lite"; + +import { button } from "./variants"; + +export type AnchorProps + = Omit, "color"> + & VariantProps + & { + iconStart?: ReactNode; + iconEnd?: ReactNode; + }; + +// Styled
sharing the Button look (Radix "Button"). Use for external links +// or mailto:/tel:; for in-app navigation use Link (router). See +// contrib/claude/ui.md. +export function Anchor(props: AnchorProps) { + const { + size, variant, color, highContrast, active, className, + iconStart, iconEnd, children, ...rest + } = props; + + return ( + + {iconStart} + {children} + {iconEnd} + + ); +} diff --git a/packages/ui/src/v2/Button/Link.stories.tsx b/packages/ui/src/v2/Button/Link.stories.tsx new file mode 100644 index 000000000..1163d326b --- /dev/null +++ b/packages/ui/src/v2/Button/Link.stories.tsx @@ -0,0 +1,66 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { Meta, StoryObj } from "@storybook/react"; +import { MemoryRouter } from "react-router"; + +import { Link } from "./Link"; + +const navItems = ["Documents", "Subprocessors", "Updates", "Requests"] as const; + +export default { + title: "v2/Link", + component: Link, + args: { + children: "Documents", + to: "#", + size: 2, + variant: "ghost", + color: "neutral", + highContrast: false, + active: false, + }, + decorators: [ + Story => ( + + + + ), + ], +} satisfies Meta; + +type Story = StoryObj; + +export const Playground: Story = {}; + +// Mirrors the Trust Center top-bar nav: ghost links with a persistent active +// pill on the selected item. +export const Nav: Story = { + render: () => ( +
+ {navItems.map((item, index) => ( + + {item} + + ))} +
+ ), +}; diff --git a/packages/ui/src/v2/Button/Link.tsx b/packages/ui/src/v2/Button/Link.tsx new file mode 100644 index 000000000..4f979bf2d --- /dev/null +++ b/packages/ui/src/v2/Button/Link.tsx @@ -0,0 +1,48 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 LinkProps + = Omit, "color"> + & VariantProps + & { + 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. +export function Link(props: LinkProps) { + const { + size, variant, color, highContrast, active, className, + iconStart, iconEnd, children, ...rest + } = props; + + return ( + + {iconStart} + {children} + {iconEnd} + + ); +} diff --git a/packages/ui/src/v2/Button/variants.ts b/packages/ui/src/v2/Button/variants.ts index 4cb77f456..8f7d9b7ae 100644 --- a/packages/ui/src/v2/Button/variants.ts +++ b/packages/ui/src/v2/Button/variants.ts @@ -53,6 +53,12 @@ export const button = tv({ true: "", false: "", }, + // Persistent "selected" treatment for nav items built on Anchor / Link + // (ghost / soft surfaces). Look-only — does not change structure. + active: { + true: "", + false: "", + }, }, compoundVariants: [ // ── solid: filled step-9 background, white text (dark for light hues) ── @@ -104,6 +110,21 @@ export const button = tv({ { variant: "ghost", color: "amber", class: "text-amber-11 hover:bg-amber-3" }, { variant: "ghost", color: "sky", class: "text-sky-11 hover:bg-sky-3" }, + // ── active: persistent selected background + high-contrast text ── + // ghost goes from transparent to the step-3 tint; soft bumps to step-4. + { variant: "ghost", color: "neutral", active: true, class: "bg-sand-3 text-sand-12" }, + { variant: "ghost", color: "gold", active: true, class: "bg-gold-3 text-gold-12" }, + { variant: "ghost", color: "red", active: true, class: "bg-red-3 text-red-12" }, + { variant: "ghost", color: "green", active: true, class: "bg-green-3 text-green-12" }, + { variant: "ghost", color: "amber", active: true, class: "bg-amber-3 text-amber-12" }, + { variant: "ghost", color: "sky", active: true, class: "bg-sky-3 text-sky-12" }, + { variant: "soft", color: "neutral", active: true, class: "bg-sand-4 text-sand-12" }, + { variant: "soft", color: "gold", active: true, class: "bg-gold-4 text-gold-12" }, + { variant: "soft", color: "red", active: true, class: "bg-red-4 text-red-12" }, + { variant: "soft", color: "green", active: true, class: "bg-green-4 text-green-12" }, + { variant: "soft", color: "amber", active: true, class: "bg-amber-4 text-amber-12" }, + { variant: "soft", color: "sky", active: true, class: "bg-sky-4 text-sky-12" }, + // ── high-contrast text bump for the tinted variants ── { variant: ["soft", "surface", "outline", "ghost"], color: "neutral", highContrast: true, class: "text-sand-12" }, { variant: ["soft", "surface", "outline", "ghost"], color: "gold", highContrast: true, class: "text-gold-12" }, @@ -117,6 +138,7 @@ export const button = tv({ variant: "solid", color: "gold", highContrast: false, + active: false, }, });