diff --git a/packages/ui/src/v2/typography/Heading.stories.tsx b/packages/ui/src/v2/typography/Heading.stories.tsx new file mode 100644 index 000000000..1d0f7cbc6 --- /dev/null +++ b/packages/ui/src/v2/typography/Heading.stories.tsx @@ -0,0 +1,111 @@ +// 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 { Heading } from "./Heading"; +import { HeadingSkeleton } from "./HeadingSkeleton"; + +const sample = "The quick brown fox"; + +const sizes = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const; +const weights = ["light", "regular", "medium", "bold"] as const; +const colors = ["neutral", "gold", "red", "green", "amber", "sky"] as const; + +export default { + title: "v2/Typography/Heading", + component: Heading, + args: { + children: sample, + level: 1, + size: 6, + weight: "bold", + color: "neutral", + highContrast: false, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const Playground: Story = {}; + +export const Sizes: Story = { + render: () => ( +
+ {sizes.map(size => ( + + {size} + {" — "} + {sample} + + ))} +
+ ), +}; + +export const Weights: Story = { + render: () => ( +
+ {weights.map(weight => ( + + {weight} + {" — "} + {sample} + + ))} +
+ ), +}; + +export const Align: Story = { + render: () => ( +
+ Left-aligned + Center-aligned + Right-aligned +
+ ), +}; + +export const Colors: Story = { + render: () => ( +
+ {colors.map(color => ( + + {color} + {" — "} + {sample} + + ))} +
+ ), +}; + +export const HighContrast: Story = { + render: () => ( +
+ Low contrast (step 11) + High contrast (step 12) +
+ ), +}; + +export const Skeleton: Story = { + render: () => ( +
+ + +
+ ), +}; diff --git a/packages/ui/src/v2/typography/Heading.tsx b/packages/ui/src/v2/typography/Heading.tsx new file mode 100644 index 000000000..24eb8a7cc --- /dev/null +++ b/packages/ui/src/v2/typography/Heading.tsx @@ -0,0 +1,41 @@ +// 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 { heading } from "./variants"; + +type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; + +export type HeadingProps = ComponentProps<"h1"> & VariantProps & { + // The rendered heading element (h1–h6), driving document outline. Decoupled + // from `size`, which controls the visual scale. Defaults to h1. + level?: HeadingLevel; +}; + +// Foundational heading primitive (Radix "Heading"). Renders the semantic +// h1–h6 chosen by `level`; tune the visual scale with `size`. See +// contrib/claude/ui.md (typography). +export function Heading(props: HeadingProps) { + const { level = 1, size, weight, align, color, highContrast, className, ...rest } = props; + const Tag = `h${level}` as const; + + return ( + + ); +} diff --git a/packages/ui/src/v2/typography/HeadingSkeleton.tsx b/packages/ui/src/v2/typography/HeadingSkeleton.tsx new file mode 100644 index 000000000..49d44d4d1 --- /dev/null +++ b/packages/ui/src/v2/typography/HeadingSkeleton.tsx @@ -0,0 +1,32 @@ +// 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 { headingSkeleton } from "./variants"; + +export type HeadingSkeletonProps = Omit, "children"> & VariantProps; + +// Loading placeholder paired with Heading: a pulse block matching a heading +// line at the given size. Defaults to full width; constrain with a width class. +export function HeadingSkeleton(props: HeadingSkeletonProps) { + const { size, className, ...rest } = props; + + return ( + + {"\u00A0"} + + ); +} diff --git a/packages/ui/src/v2/typography/variants.ts b/packages/ui/src/v2/typography/variants.ts index c2e3d82e7..ae5877973 100644 --- a/packages/ui/src/v2/typography/variants.ts +++ b/packages/ui/src/v2/typography/variants.ts @@ -15,8 +15,9 @@ import { tv } from "tailwind-variants/lite"; // Numbered type scale (text-1 … text-9). Each utility carries its paired -// font-size, line-height, and letter-spacing from the v2 theme. Shared between -// Text and its skeleton so the placeholder matches the real line height. +// font-size, line-height, and letter-spacing from the v2 theme. Shared across +// the typography primitives and their skeletons so placeholders match the real +// line height. const size = { 1: "text-1", 2: "text-2", @@ -29,49 +30,54 @@ const size = { 9: "text-9", } as const; -export const text = tv({ - variants: { - size, - weight: { - light: "font-light", - regular: "font-normal", - medium: "font-medium", - bold: "font-bold", - }, - align: { - left: "text-left", - center: "text-center", - right: "text-right", - }, - // Hue only; the resolved text step comes from the color × highContrast - // compound variants below (step 11 low-contrast, step 12 high-contrast). - color: { - neutral: "", - gold: "", - red: "", - green: "", - amber: "", - sky: "", - }, - highContrast: { - true: "", - false: "", - }, +// Shared look across Text and Heading: only their defaults differ. +const typographyVariants = { + size, + weight: { + light: "font-light", + regular: "font-normal", + medium: "font-medium", + bold: "font-bold", }, - compoundVariants: [ - { color: "neutral", highContrast: false, class: "text-sand-11" }, - { color: "neutral", highContrast: true, class: "text-sand-12" }, - { color: "gold", highContrast: false, class: "text-gold-11" }, - { color: "gold", highContrast: true, class: "text-gold-12" }, - { color: "red", highContrast: false, class: "text-red-11" }, - { color: "red", highContrast: true, class: "text-red-12" }, - { color: "green", highContrast: false, class: "text-green-11" }, - { color: "green", highContrast: true, class: "text-green-12" }, - { color: "amber", highContrast: false, class: "text-amber-11" }, - { color: "amber", highContrast: true, class: "text-amber-12" }, - { color: "sky", highContrast: false, class: "text-sky-11" }, - { color: "sky", highContrast: true, class: "text-sky-12" }, - ], + align: { + left: "text-left", + center: "text-center", + right: "text-right", + }, + // Hue only; the resolved text step comes from the color × highContrast + // compound variants below (step 11 low-contrast, step 12 high-contrast). + color: { + neutral: "", + gold: "", + red: "", + green: "", + amber: "", + sky: "", + }, + highContrast: { + true: "", + false: "", + }, +}; + +const colorCompoundVariants = [ + { color: "neutral", highContrast: false, class: "text-sand-11" }, + { color: "neutral", highContrast: true, class: "text-sand-12" }, + { color: "gold", highContrast: false, class: "text-gold-11" }, + { color: "gold", highContrast: true, class: "text-gold-12" }, + { color: "red", highContrast: false, class: "text-red-11" }, + { color: "red", highContrast: true, class: "text-red-12" }, + { color: "green", highContrast: false, class: "text-green-11" }, + { color: "green", highContrast: true, class: "text-green-12" }, + { color: "amber", highContrast: false, class: "text-amber-11" }, + { color: "amber", highContrast: true, class: "text-amber-12" }, + { color: "sky", highContrast: false, class: "text-sky-11" }, + { color: "sky", highContrast: true, class: "text-sky-12" }, +] as const; + +export const text = tv({ + variants: typographyVariants, + compoundVariants: [...colorCompoundVariants], defaultVariants: { size: 3, weight: "regular", @@ -80,8 +86,21 @@ export const text = tv({ }, }); +export const heading = tv({ + variants: typographyVariants, + compoundVariants: [...colorCompoundVariants], + defaultVariants: { + size: 6, + weight: "bold", + color: "neutral", + highContrast: false, + }, +}); + +const skeletonBase = "inline-block w-full animate-pulse select-none rounded-2 bg-sand-3 text-transparent"; + export const textSkeleton = tv({ - base: "inline-block w-full animate-pulse select-none rounded-2 bg-sand-3 text-transparent", + base: skeletonBase, variants: { size, }, @@ -89,3 +108,13 @@ export const textSkeleton = tv({ size: 3, }, }); + +export const headingSkeleton = tv({ + base: skeletonBase, + variants: { + size, + }, + defaultVariants: { + size: 6, + }, +});