Add v2 Callout component
Implement the Callout component from the Probo Radix UI Figma: a short contextual message with a leading icon (defaults to an info icon), with size 1-3, the soft/surface/outline variants across the sand/gold/red/green/amber/sky scales, and highContrast. Includes a paired CalloutSkeleton. Styling uses tailwind-variants/lite slots (root/icon/text). Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
97
packages/ui/src/v2/Callout/Callout.stories.tsx
Normal file
97
packages/ui/src/v2/Callout/Callout.stories.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// 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 { WarningIcon } from "@phosphor-icons/react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
import { Callout } from "./Callout";
|
||||||
|
import { CalloutSkeleton } from "./CalloutSkeleton";
|
||||||
|
|
||||||
|
const message = "You will need admin privileges to install and access this application.";
|
||||||
|
|
||||||
|
const sizes = [1, 2, 3] as const;
|
||||||
|
const variants = ["soft", "surface", "outline"] as const;
|
||||||
|
const colors = ["neutral", "gold", "red", "green", "amber", "sky"] as const;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "v2/Callout",
|
||||||
|
component: Callout,
|
||||||
|
args: {
|
||||||
|
children: message,
|
||||||
|
size: 2,
|
||||||
|
variant: "soft",
|
||||||
|
color: "neutral",
|
||||||
|
highContrast: false,
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Callout>;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Callout>;
|
||||||
|
|
||||||
|
export const Playground: Story = {};
|
||||||
|
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-3">
|
||||||
|
{sizes.map(size => (
|
||||||
|
<Callout key={size} size={size}>{message}</Callout>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Variants: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-3">
|
||||||
|
{variants.map(variant => (
|
||||||
|
<Callout key={variant} variant={variant}>{message}</Callout>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Colors: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-3">
|
||||||
|
{colors.map(color => (
|
||||||
|
<Callout key={color} color={color}>{message}</Callout>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HighContrast: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-3">
|
||||||
|
<Callout color="sky">{message}</Callout>
|
||||||
|
<Callout color="sky" highContrast>{message}</Callout>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomIcon: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-3">
|
||||||
|
<Callout color="amber" icon={<WarningIcon weight="fill" />}>{message}</Callout>
|
||||||
|
<Callout color="neutral" icon={null}>{message}</Callout>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Skeleton: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex w-96 flex-col gap-3">
|
||||||
|
<CalloutSkeleton size={2} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
48
packages/ui/src/v2/Callout/Callout.tsx
Normal file
48
packages/ui/src/v2/Callout/Callout.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// 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 { InfoIcon } from "@phosphor-icons/react";
|
||||||
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
import type { VariantProps } from "tailwind-variants/lite";
|
||||||
|
|
||||||
|
import { callout } from "./variants";
|
||||||
|
|
||||||
|
export type CalloutProps
|
||||||
|
= Omit<ComponentProps<"div">, "color">
|
||||||
|
& VariantProps<typeof callout>
|
||||||
|
& {
|
||||||
|
// Leading icon. Defaults to an info icon; pass `null` to omit it.
|
||||||
|
icon?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Short contextual message (Radix "Callout") with a leading icon. See
|
||||||
|
// contrib/claude/ui.md.
|
||||||
|
export function Callout(props: CalloutProps) {
|
||||||
|
const {
|
||||||
|
size, variant, color, highContrast, className,
|
||||||
|
icon = <InfoIcon weight="fill" />, children, ...rest
|
||||||
|
} = props;
|
||||||
|
const slots = callout({ size, variant, color, highContrast });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={slots.root({ className })} {...rest}>
|
||||||
|
{icon !== null && (
|
||||||
|
<span aria-hidden className={slots.icon()}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className={slots.text()}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
packages/ui/src/v2/Callout/CalloutSkeleton.tsx
Normal file
27
packages/ui/src/v2/Callout/CalloutSkeleton.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// 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 } from "react";
|
||||||
|
import type { VariantProps } from "tailwind-variants/lite";
|
||||||
|
|
||||||
|
import { calloutSkeleton } from "./variants";
|
||||||
|
|
||||||
|
export type CalloutSkeletonProps = Omit<ComponentProps<"div">, "children"> & VariantProps<typeof calloutSkeleton>;
|
||||||
|
|
||||||
|
// Loading placeholder paired with Callout: a pulse block matching its size.
|
||||||
|
export function CalloutSkeleton(props: CalloutSkeletonProps) {
|
||||||
|
const { size, className, ...rest } = props;
|
||||||
|
|
||||||
|
return <div aria-hidden className={calloutSkeleton({ size, className })} {...rest} />;
|
||||||
|
}
|
||||||
104
packages/ui/src/v2/Callout/variants.ts
Normal file
104
packages/ui/src/v2/Callout/variants.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// 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 { tv } from "tailwind-variants/lite";
|
||||||
|
|
||||||
|
// Short contextual message with a leading icon (Radix "Callout").
|
||||||
|
// root the container surface (background/border + hue text color)
|
||||||
|
// icon leading icon, inherits the hue text color
|
||||||
|
// text the message body
|
||||||
|
export const callout = tv({
|
||||||
|
slots: {
|
||||||
|
root: "flex items-start rounded-3 border border-transparent",
|
||||||
|
icon: "shrink-0",
|
||||||
|
text: "min-w-0 flex-1",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
1: { root: "gap-2 p-2 text-1", icon: "[&_svg]:size-4" },
|
||||||
|
2: { root: "gap-3 p-3 text-2", icon: "[&_svg]:size-5" },
|
||||||
|
3: { root: "gap-3 p-4 text-3", icon: "[&_svg]:size-6" },
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
soft: {},
|
||||||
|
surface: {},
|
||||||
|
outline: {},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
neutral: {},
|
||||||
|
gold: {},
|
||||||
|
red: {},
|
||||||
|
green: {},
|
||||||
|
amber: {},
|
||||||
|
sky: {},
|
||||||
|
},
|
||||||
|
highContrast: {
|
||||||
|
true: {},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
// soft: tinted background
|
||||||
|
{ variant: "soft", color: "neutral", class: { root: "bg-sand-3 text-sand-11" } },
|
||||||
|
{ variant: "soft", color: "gold", class: { root: "bg-gold-3 text-gold-11" } },
|
||||||
|
{ variant: "soft", color: "red", class: { root: "bg-red-3 text-red-11" } },
|
||||||
|
{ variant: "soft", color: "green", class: { root: "bg-green-3 text-green-11" } },
|
||||||
|
{ variant: "soft", color: "amber", class: { root: "bg-amber-3 text-amber-11" } },
|
||||||
|
{ variant: "soft", color: "sky", class: { root: "bg-sky-3 text-sky-11" } },
|
||||||
|
|
||||||
|
// surface: subtle background + border
|
||||||
|
{ variant: "surface", color: "neutral", class: { root: "bg-sand-2 border-sand-6 text-sand-11" } },
|
||||||
|
{ variant: "surface", color: "gold", class: { root: "bg-gold-2 border-gold-6 text-gold-11" } },
|
||||||
|
{ variant: "surface", color: "red", class: { root: "bg-red-2 border-red-6 text-red-11" } },
|
||||||
|
{ variant: "surface", color: "green", class: { root: "bg-green-2 border-green-6 text-green-11" } },
|
||||||
|
{ variant: "surface", color: "amber", class: { root: "bg-amber-2 border-amber-6 text-amber-11" } },
|
||||||
|
{ variant: "surface", color: "sky", class: { root: "bg-sky-2 border-sky-6 text-sky-11" } },
|
||||||
|
|
||||||
|
// outline: border only
|
||||||
|
{ variant: "outline", color: "neutral", class: { root: "border-sand-6 text-sand-11" } },
|
||||||
|
{ variant: "outline", color: "gold", class: { root: "border-gold-6 text-gold-11" } },
|
||||||
|
{ variant: "outline", color: "red", class: { root: "border-red-6 text-red-11" } },
|
||||||
|
{ variant: "outline", color: "green", class: { root: "border-green-6 text-green-11" } },
|
||||||
|
{ variant: "outline", color: "amber", class: { root: "border-amber-6 text-amber-11" } },
|
||||||
|
{ variant: "outline", color: "sky", class: { root: "border-sky-6 text-sky-11" } },
|
||||||
|
|
||||||
|
// high-contrast text (icon + body inherit the root color)
|
||||||
|
{ color: "neutral", highContrast: true, class: { root: "text-sand-12" } },
|
||||||
|
{ color: "gold", highContrast: true, class: { root: "text-gold-12" } },
|
||||||
|
{ color: "red", highContrast: true, class: { root: "text-red-12" } },
|
||||||
|
{ color: "green", highContrast: true, class: { root: "text-green-12" } },
|
||||||
|
{ color: "amber", highContrast: true, class: { root: "text-amber-12" } },
|
||||||
|
{ color: "sky", highContrast: true, class: { root: "text-sky-12" } },
|
||||||
|
],
|
||||||
|
defaultVariants: {
|
||||||
|
size: 2,
|
||||||
|
variant: "soft",
|
||||||
|
color: "neutral",
|
||||||
|
highContrast: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const calloutSkeleton = tv({
|
||||||
|
base: "w-full animate-pulse rounded-3 bg-sand-3",
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
1: "h-9",
|
||||||
|
2: "h-11",
|
||||||
|
3: "h-14",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user