Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/add-text-heading-variant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@cloudflare/kumo": minor
---

Add the `Text` `heading` variant with a 16px semibold default and a 20px
`size="lg"` option. The variant defaults to a `span`, so callers choose heading
semantics explicitly with `as`. Deprecate the numbered `heading1`, `heading2`,
and `heading3` variants.
18 changes: 5 additions & 13 deletions packages/kumo-docs-astro/src/components/demos/TextDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,14 @@ export function TextVariantsDemo() {
return (
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-col justify-end gap-1 rounded-lg border border-kumo-hairline bg-kumo-base p-4">
<Text variant="heading1" as="h1">
Heading 1
</Text>
<Text variant="mono-secondary">text-3xl (30px)</Text>
</div>
<div className="flex flex-col justify-end gap-1 rounded-lg border border-kumo-hairline bg-kumo-base p-4">
<Text variant="heading2" as="h2">
Heading 2
</Text>
<Text variant="mono-secondary">text-2xl (24px)</Text>
<Text variant="heading">Heading</Text>
<Text variant="mono-secondary">text-lg (16px)</Text>
</div>
<div className="flex flex-col justify-end gap-1 rounded-lg border border-kumo-hairline bg-kumo-base p-4">
<Text variant="heading3" as="h3">
Heading 3
<Text variant="heading" size="lg" as="h2">
Heading large
</Text>
<Text variant="mono-secondary">text-lg (16px)</Text>
<Text variant="mono-secondary">text-xl (20px)</Text>
</div>
<div className="flex flex-col justify-end gap-1 rounded-lg border border-kumo-hairline bg-kumo-base p-4">
<Text>Body</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function CollapseSizeExample({
preserveContentSize ? "w-64" : "w-full min-w-0",
)}
>
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
Web Analytics
</Text>
<Text variant="secondary">
Expand Down
20 changes: 10 additions & 10 deletions packages/kumo-docs-astro/src/components/skill/design-tips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const designTips = [
exampleCode: `<Text>Content text</Text>`,
jsx: (
<LayerCard className="grid w-full gap-1 p-5">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
API tokens
</Text>
<Text>Production token expires in 30 days.</Text>
Expand All @@ -55,7 +55,7 @@ export const designTips = [
exampleCode: `<Text size="lg">Content text</Text>`,
jsx: (
<LayerCard className="grid w-full gap-1 p-5">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
API tokens
</Text>
<Text size="lg">Production token expires in 30 days.</Text>
Expand Down Expand Up @@ -130,11 +130,11 @@ export const designTips = [
examples: [
{
variant: "good",
exampleCode: `<Text as="h3" variant="heading3">Account settings</Text>
exampleCode: `<Text as="h3" variant="heading">Account settings</Text>
<Text as="strong" bold>required</Text>`,
jsx: (
<div className="grid gap-1">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
Account settings
</Text>
<Text>
Expand Down Expand Up @@ -178,7 +178,7 @@ export const designTips = [
variant: "good",
exampleCode: `<div className="grid gap-6">
<div className="grid gap-1.5">
<Text as="h3">Web Analytics</Text>
<Text as="h3" variant="heading">Web Analytics</Text>
<Text>Measure site traffic without changing your code.</Text>
</div>
<Button>Configure</Button>
Expand All @@ -187,7 +187,7 @@ export const designTips = [
<LayerCard className="w-full p-5">
<div className="grid gap-6">
<div className="grid gap-1.5">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
Web Analytics
</Text>
<Text variant="secondary" DANGEROUS_className="text-pretty">
Expand All @@ -202,14 +202,14 @@ export const designTips = [
{
variant: "bad",
exampleCode: `<div className="grid gap-4">
<Text as="h3">Web Analytics</Text>
<Text as="h3" variant="heading">Web Analytics</Text>
<Text>Measure site traffic without changing your code.</Text>
<Button>Configure</Button>
</div>`,
jsx: (
<LayerCard className="w-full p-5">
<div className="grid gap-4">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
Web Analytics
</Text>
<Text variant="secondary" DANGEROUS_className="text-pretty">
Expand Down Expand Up @@ -292,7 +292,7 @@ export const designTips = [
jsx: (
<LayerCard className="w-full shadow-md ring ring-kumo-line">
<LayerCard.Primary className="grid gap-1 p-5">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
Workers API
</Text>
<Text variant="secondary">Last deployed 4 minutes ago</Text>
Expand All @@ -306,7 +306,7 @@ export const designTips = [
jsx: (
<LayerCard className="w-full border border-kumo-line shadow-md ring-0">
<LayerCard.Primary className="grid gap-1 p-5 ring-0">
<Text as="h3" variant="heading3">
<Text as="h3" variant="heading">
Workers API
</Text>
<Text variant="secondary">Last deployed 4 minutes ago</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ const GITHUB_RELEASE_URL = "https://github.com/cloudflare/kumo/releases/tag/%40c
>
<LinkSimple className="size-4 text-kumo-subtle" />
</span>
<Text as="span" variant="heading2">{v.version}</Text>
<Text as="span" variant="heading" size="lg">{v.version}</Text>
</a>
<Badge variant={badgeConfig[v.bump].variant}>{badgeConfig[v.bump].label}</Badge>
<a
Expand Down
23 changes: 17 additions & 6 deletions packages/kumo-figma/src/generators/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ describe("Text Generator - getAllVariantData", () => {
expect(variant.combinedClasses).toBeDefined();
expect(variant.parsed).toBeDefined();
expect(variant.description).toBeDefined();
expect(typeof variant.isHeadingVariant).toBe("boolean");
expect(typeof variant.isCopyVariant).toBe("boolean");
expect(typeof variant.isMonoVariant).toBe("boolean");
}
Expand All @@ -255,16 +256,26 @@ describe("Text Generator - getAllVariantData", () => {
}
});

it("should have size=null for non-copy, non-mono variants (headings)", () => {
it("should expose the heading variant's supported size combinations", () => {
const data = getAllVariantData();
const headingVariants = data.variants.filter(
(v) => !v.isCopyVariant && !v.isMonoVariant,
);
const headingVariants = data.variants.filter((v) => v.isHeadingVariant);

// Should have at least some heading variants
expect(headingVariants.length).toBeGreaterThan(0);
expect(headingVariants.some((variant) => variant.size === null)).toBe(true);
expect(headingVariants.some((variant) => variant.size !== null)).toBe(true);
});

it("should keep fixed-size variants at size=null", () => {
const data = getAllVariantData();
const fixedSizeVariants = data.variants.filter(
(variant) =>
!variant.isHeadingVariant &&
!variant.isCopyVariant &&
!variant.isMonoVariant,
);

for (const variant of headingVariants) {
expect(fixedSizeVariants.length).toBeGreaterThan(0);
for (const variant of fixedSizeVariants) {
expect(variant.size).toBeNull();
}
});
Expand Down
93 changes: 83 additions & 10 deletions packages/kumo-figma/src/generators/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { logComplete } from "../logger";
* Text Component Generator
*
* Generates a single Text ComponentSet in Figma with properties:
* - variant: heading1, heading2, heading3, body, secondary, success, error, mono, mono-secondary
* - size: xs, sm, base, lg (only applies to body/secondary/success/error variants)
* - variant: heading, heading1, heading2, heading3, body, secondary, success, error, mono, mono-secondary
* - size: heading supports default/lg; body variants support xs/sm/base/lg
*
* Reads variant definitions from component-registry.json (the source of truth).
*
Expand All @@ -14,7 +14,8 @@ import { logComplete } from "../logger";
* Unlike Button or Badge, Text components are purely typographic - no backgrounds or borders.
*
* ### Variant Categories (derived from text.tsx source)
* - Headings (heading1, heading2, heading3): Fixed sizes, semibold weight, no size prop
* - Heading: Semibold at 16px by default and 20px with size=lg
* - Deprecated headings (heading1, heading2, heading3): Fixed sizes
* - Copy (body, secondary, success, error): Supports size variants
* - Monospace (mono, mono-secondary): Special optical sizing (lg→base, default→sm)
*
Expand Down Expand Up @@ -64,9 +65,17 @@ const TEXT_BASE_CLASS = "text-kumo-default";
* Variant categories derived from text.tsx source code (lines 162-163)
* These determine which variants support size and special optical sizing
*/
const HEADING_VARIANT = "heading";
const COPY_VARIANTS = ["body", "secondary", "success", "error"];
const MONO_VARIANTS = ["mono", "mono-secondary"];

/**
* Check if variant is the preferred heading variant (supports default/lg).
*/
function isHeadingVariant(variant: string): boolean {
return variant === HEADING_VARIANT;
}

/**
* Check if variant is a copy variant (supports size)
*/
Expand Down Expand Up @@ -110,13 +119,21 @@ async function createTextComponent(

// Determine effective size classes based on variant type
// From text.tsx lines 180-186:
// - Heading: variant classes provide 16px; lg overrides it to 20px
// - Copy variants: use the size prop directly
// - Mono variants: lg→base, default→sm (optical sizing)
// - Headings: no size classes
// - Deprecated headings: no additional size classes
let effectiveSizeClasses = "";
let sizeDesc = "";

if (isCopyVariant(variant) && size) {
if (isHeadingVariant(variant)) {
if (size === "lg") {
effectiveSizeClasses = "text-xl";
sizeDesc = "Large heading text";
} else {
sizeDesc = "Default heading text";
}
} else if (isCopyVariant(variant) && size) {
effectiveSizeClasses = sizeProp.classes[size] || "";
sizeDesc = sizeProp.descriptions[size] || "";
} else if (isMonoVariant(variant)) {
Expand All @@ -129,7 +146,7 @@ async function createTextComponent(
sizeDesc = "Default text (optically adjusted to small)";
}
}
// Headings: no size classes applied
// Deprecated headings: no additional size classes applied

// Combine base + variant + size classes for parsing
const combinedClasses =
Expand Down Expand Up @@ -247,7 +264,23 @@ export async function generateTextComponents(
text: "variant=" + variant,
});

if (isCopyVariant(variant)) {
if (isHeadingVariant(variant)) {
// Heading: generate the default 16px and large 20px combinations.
let currentX = labelColumnWidth;

const defaultComponent = await createTextComponent(variant, null);
defaultComponent.x = currentX;
defaultComponent.y = currentRow * rowHeight + headerRowHeight;
currentX = currentX + defaultComponent.width + componentGap;
components.push(defaultComponent);

const lgComponent = await createTextComponent(variant, "lg");
lgComponent.x = currentX;
lgComponent.y = currentRow * rowHeight + headerRowHeight;
components.push(lgComponent);

currentRow++;
} else if (isCopyVariant(variant)) {
// Copy variants: generate all size combinations
let currentX = labelColumnWidth;
for (let j = 0; j < sizes.length; j++) {
Expand Down Expand Up @@ -287,7 +320,7 @@ export async function generateTextComponents(

currentRow++;
} else {
// Headings: no size variants (from text.tsx lines 125-129: size?: never)
// Deprecated headings retain their fixed sizes for compatibility.
const component = await createTextComponent(variant, null);
component.x = labelColumnWidth;
component.y = currentRow * rowHeight + headerRowHeight;
Expand Down Expand Up @@ -466,17 +499,53 @@ export function getAllVariantData() {
combinedClasses: string;
parsed: ReturnType<typeof parseTailwindClasses>;
description: string;
isHeadingVariant: boolean;
isCopyVariant: boolean;
isMonoVariant: boolean;
}> = [];

for (const variant of variantConfig.variants) {
const variantClasses = variantConfig.variantClasses[variant] || "";
const variantDesc = variantConfig.variantDescriptions[variant] || "";
const isHeading = isHeadingVariant(variant);
const isCopy = isCopyVariant(variant);
const isMono = isMonoVariant(variant);

if (isCopy) {
if (isHeading) {
const defaultCombined = `${TEXT_BASE_CLASS} ${variantClasses}`.trim();
const defaultParsed = parseTailwindClasses(defaultCombined);

variants.push({
variant,
size: null,
variantClasses,
sizeClasses: "",
combinedClasses: defaultCombined,
parsed: defaultParsed,
description: `${variantDesc}. Default heading text`,
isHeadingVariant: true,
isCopyVariant: false,
isMonoVariant: false,
});

const lgSizeClasses = "text-xl";
const lgCombined =
`${TEXT_BASE_CLASS} ${variantClasses} ${lgSizeClasses}`.trim();
const lgParsed = parseTailwindClasses(lgCombined);

variants.push({
variant,
size: "lg",
variantClasses,
sizeClasses: lgSizeClasses,
combinedClasses: lgCombined,
parsed: lgParsed,
description: `${variantDesc}. Large heading text`,
isHeadingVariant: true,
isCopyVariant: false,
isMonoVariant: false,
});
} else if (isCopy) {
// Copy variants: all sizes
for (const size of variantConfig.sizes) {
const sizeClasses = variantConfig.sizeClasses[size] || "";
Expand All @@ -493,6 +562,7 @@ export function getAllVariantData() {
combinedClasses,
parsed,
description: `${variantDesc}. ${sizeDesc}`,
isHeadingVariant: false,
isCopyVariant: true,
isMonoVariant: false,
});
Expand All @@ -513,6 +583,7 @@ export function getAllVariantData() {
combinedClasses: defaultCombined,
parsed: defaultParsed,
description: `${variantDesc}. Default text (optically adjusted to small)`,
isHeadingVariant: false,
isCopyVariant: false,
isMonoVariant: true,
});
Expand All @@ -531,11 +602,12 @@ export function getAllVariantData() {
combinedClasses: lgCombined,
parsed: lgParsed,
description: `${variantDesc}. Large text (optically adjusted to base)`,
isHeadingVariant: false,
isCopyVariant: false,
isMonoVariant: true,
});
} else {
// Headings: no size variants
// Deprecated headings retain their fixed sizes.
const combinedClasses = `${TEXT_BASE_CLASS} ${variantClasses}`.trim();
const parsed = parseTailwindClasses(combinedClasses);

Expand All @@ -547,6 +619,7 @@ export function getAllVariantData() {
combinedClasses,
parsed,
description: variantDesc,
isHeadingVariant: false,
isCopyVariant: false,
isMonoVariant: false,
});
Expand Down
Loading
Loading