diff --git a/packages/ui/src/v2/Callout/Callout.stories.tsx b/packages/ui/src/v2/Callout/Callout.stories.tsx new file mode 100644 index 000000000..4d2b80dba --- /dev/null +++ b/packages/ui/src/v2/Callout/Callout.stories.tsx @@ -0,0 +1,97 @@ +// 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 { 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; + +type Story = StoryObj; + +export const Playground: Story = {}; + +export const Sizes: Story = { + render: () => ( +
+ {sizes.map(size => ( + {message} + ))} +
+ ), +}; + +export const Variants: Story = { + render: () => ( +
+ {variants.map(variant => ( + {message} + ))} +
+ ), +}; + +export const Colors: Story = { + render: () => ( +
+ {colors.map(color => ( + {message} + ))} +
+ ), +}; + +export const HighContrast: Story = { + render: () => ( +
+ {message} + {message} +
+ ), +}; + +export const CustomIcon: Story = { + render: () => ( +
+ }>{message} + {message} +
+ ), +}; + +export const Skeleton: Story = { + render: () => ( +
+ +
+ ), +}; diff --git a/packages/ui/src/v2/Callout/Callout.tsx b/packages/ui/src/v2/Callout/Callout.tsx new file mode 100644 index 000000000..780ca16b4 --- /dev/null +++ b/packages/ui/src/v2/Callout/Callout.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 { 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, "color"> + & VariantProps + & { + // 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 = , children, ...rest + } = props; + const slots = callout({ size, variant, color, highContrast }); + + return ( +
+ {icon !== null && ( + + {icon} + + )} +
{children}
+
+ ); +} diff --git a/packages/ui/src/v2/Callout/CalloutSkeleton.tsx b/packages/ui/src/v2/Callout/CalloutSkeleton.tsx new file mode 100644 index 000000000..b283ae755 --- /dev/null +++ b/packages/ui/src/v2/Callout/CalloutSkeleton.tsx @@ -0,0 +1,27 @@ +// 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 } from "react"; +import type { VariantProps } from "tailwind-variants/lite"; + +import { calloutSkeleton } from "./variants"; + +export type CalloutSkeletonProps = Omit, "children"> & VariantProps; + +// Loading placeholder paired with Callout: a pulse block matching its size. +export function CalloutSkeleton(props: CalloutSkeletonProps) { + const { size, className, ...rest } = props; + + return
; +} diff --git a/packages/ui/src/v2/Callout/variants.ts b/packages/ui/src/v2/Callout/variants.ts new file mode 100644 index 000000000..f2fba7602 --- /dev/null +++ b/packages/ui/src/v2/Callout/variants.ts @@ -0,0 +1,104 @@ +// 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 { 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, + }, +});