From e8fcbe1f298a6bed3320a877720999d1e8325a00 Mon Sep 17 00:00:00 2001 From: nanda Date: Wed, 19 Aug 2026 11:25:38 -0700 Subject: [PATCH] feat(text): add heading variant --- .changeset/add-text-heading-variant.md | 8 + .../src/components/demos/TextDemo.tsx | 18 +- .../components/skill/CollapseSizeExample.tsx | 2 +- .../src/components/skill/design-tips.tsx | 20 +-- .../src/pages/changelog/[...page].astro | 2 +- .../kumo-figma/src/generators/text.test.ts | 23 ++- packages/kumo-figma/src/generators/text.ts | 93 ++++++++-- .../kumo/src/components/text/text.test.tsx | 43 ++++- packages/kumo/src/components/text/text.tsx | 166 +++++++++++------- .../src/components/text/text.type-spec.tsx | 59 ++++--- 10 files changed, 305 insertions(+), 129 deletions(-) create mode 100644 .changeset/add-text-heading-variant.md diff --git a/.changeset/add-text-heading-variant.md b/.changeset/add-text-heading-variant.md new file mode 100644 index 0000000000..81792c6c29 --- /dev/null +++ b/.changeset/add-text-heading-variant.md @@ -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. diff --git a/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx b/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx index 16ec180b71..e91dbc1df0 100644 --- a/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx @@ -4,22 +4,14 @@ export function TextVariantsDemo() { return (
- - Heading 1 - - text-3xl (30px) -
-
- - Heading 2 - - text-2xl (24px) + Heading + text-lg (16px)
- - Heading 3 + + Heading large - text-lg (16px) + text-xl (20px)
Body diff --git a/packages/kumo-docs-astro/src/components/skill/CollapseSizeExample.tsx b/packages/kumo-docs-astro/src/components/skill/CollapseSizeExample.tsx index 4c2e898793..e8fdaa4b15 100644 --- a/packages/kumo-docs-astro/src/components/skill/CollapseSizeExample.tsx +++ b/packages/kumo-docs-astro/src/components/skill/CollapseSizeExample.tsx @@ -32,7 +32,7 @@ export function CollapseSizeExample({ preserveContentSize ? "w-64" : "w-full min-w-0", )} > - + Web Analytics diff --git a/packages/kumo-docs-astro/src/components/skill/design-tips.tsx b/packages/kumo-docs-astro/src/components/skill/design-tips.tsx index 167a15cc8d..11204992ad 100644 --- a/packages/kumo-docs-astro/src/components/skill/design-tips.tsx +++ b/packages/kumo-docs-astro/src/components/skill/design-tips.tsx @@ -43,7 +43,7 @@ export const designTips = [ exampleCode: `Content text`, jsx: ( - + API tokens Production token expires in 30 days. @@ -55,7 +55,7 @@ export const designTips = [ exampleCode: `Content text`, jsx: ( - + API tokens Production token expires in 30 days. @@ -130,11 +130,11 @@ export const designTips = [ examples: [ { variant: "good", - exampleCode: `Account settings + exampleCode: `Account settings required`, jsx: (
- + Account settings @@ -178,7 +178,7 @@ export const designTips = [ variant: "good", exampleCode: `
- Web Analytics + Web Analytics Measure site traffic without changing your code.
@@ -187,7 +187,7 @@ export const designTips = [
- + Web Analytics @@ -202,14 +202,14 @@ export const designTips = [ { variant: "bad", exampleCode: `
- Web Analytics + Web Analytics Measure site traffic without changing your code.
`, jsx: (
- + Web Analytics @@ -292,7 +292,7 @@ export const designTips = [ jsx: ( - + Workers API Last deployed 4 minutes ago @@ -306,7 +306,7 @@ export const designTips = [ jsx: ( - + Workers API Last deployed 4 minutes ago diff --git a/packages/kumo-docs-astro/src/pages/changelog/[...page].astro b/packages/kumo-docs-astro/src/pages/changelog/[...page].astro index befd7271bf..69f2fa36a9 100644 --- a/packages/kumo-docs-astro/src/pages/changelog/[...page].astro +++ b/packages/kumo-docs-astro/src/pages/changelog/[...page].astro @@ -161,7 +161,7 @@ const GITHUB_RELEASE_URL = "https://github.com/cloudflare/kumo/releases/tag/%40c > - {v.version} + {v.version} {badgeConfig[v.bump].label} { 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"); } @@ -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(); } }); diff --git a/packages/kumo-figma/src/generators/text.ts b/packages/kumo-figma/src/generators/text.ts index 431f4fb7f7..f88785ab25 100644 --- a/packages/kumo-figma/src/generators/text.ts +++ b/packages/kumo-figma/src/generators/text.ts @@ -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). * @@ -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) * @@ -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) */ @@ -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)) { @@ -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 = @@ -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++) { @@ -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; @@ -466,6 +499,7 @@ export function getAllVariantData() { combinedClasses: string; parsed: ReturnType; description: string; + isHeadingVariant: boolean; isCopyVariant: boolean; isMonoVariant: boolean; }> = []; @@ -473,10 +507,45 @@ export function getAllVariantData() { 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] || ""; @@ -493,6 +562,7 @@ export function getAllVariantData() { combinedClasses, parsed, description: `${variantDesc}. ${sizeDesc}`, + isHeadingVariant: false, isCopyVariant: true, isMonoVariant: false, }); @@ -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, }); @@ -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); @@ -547,6 +619,7 @@ export function getAllVariantData() { combinedClasses, parsed, description: variantDesc, + isHeadingVariant: false, isCopyVariant: false, isMonoVariant: false, }); diff --git a/packages/kumo/src/components/text/text.test.tsx b/packages/kumo/src/components/text/text.test.tsx index e3035b5055..2e4eb127cf 100644 --- a/packages/kumo/src/components/text/text.test.tsx +++ b/packages/kumo/src/components/text/text.test.tsx @@ -1,15 +1,44 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; import { render } from "@testing-library/react"; import { Text } from "./text"; describe("Text", () => { - it("renders heading variant with the required `as` element", () => { + it("renders heading as a 16px semibold span by default", () => { + const { container } = render(Heading); + const heading = container.querySelector("span"); + + expect(heading).toBeTruthy(); + expect(heading?.classList.contains("text-lg")).toBe(true); + expect(heading?.classList.contains("font-semibold")).toBe(true); + expect(container.querySelector("h1, h2, h3, h4, h5, h6")).toBeNull(); + }); + + it("renders a large heading at 20px using the requested element", () => { const { container } = render( + + Section title + , + ); + const heading = container.querySelector("h2"); + + expect(heading).toBeTruthy(); + expect(heading?.classList.contains("text-xl")).toBe(true); + expect(heading?.classList.contains("font-semibold")).toBe(true); + }); + + it("warns when a deprecated heading variant is used", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + render( - Page Title + Legacy heading , ); - expect(container.querySelector("h1")).toBeTruthy(); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('variant="heading1" is deprecated'), + ); + warn.mockRestore(); }); it("renders body variant as

by default", () => { @@ -27,10 +56,10 @@ describe("Text", () => { expect(container.querySelector("p")).toBeNull(); }); - it("allows heading variants to opt out of semantic heading via as='span'", () => { + it("allows heading to opt out of semantic heading via as='span'", () => { const { container } = render( - - Decorative big text + + Decorative heading text , ); expect(container.querySelector("span")).toBeTruthy(); diff --git a/packages/kumo/src/components/text/text.tsx b/packages/kumo/src/components/text/text.tsx index db0cdc1d23..2981a0c39e 100644 --- a/packages/kumo/src/components/text/text.tsx +++ b/packages/kumo/src/components/text/text.tsx @@ -12,17 +12,27 @@ import { resolveVariant } from "../../utils/resolve-variant"; /** Text variant and size definitions mapping names to their Tailwind classes. */ export const KUMO_TEXT_VARIANTS = { variant: { + heading: { + classes: "text-lg font-semibold", + description: "Heading text (16px by default, 20px at large size)", + }, + /** @deprecated Use `heading` and set `size` and `as` explicitly. */ heading1: { classes: "text-3xl font-semibold", - description: "Large heading for page titles", + description: + "Deprecated large heading for page titles; use heading instead", }, + /** @deprecated Use `heading` and set `size` and `as` explicitly. */ heading2: { classes: "text-2xl font-semibold", - description: "Medium heading for section titles", + description: + "Deprecated medium heading for section titles; use heading instead", }, + /** @deprecated Use `heading` and set `size` and `as` explicitly. */ heading3: { classes: "text-lg font-semibold", - description: "Small heading for subsections", + description: + "Deprecated small heading for subsections; use heading instead", }, body: { classes: "text-kumo-default", @@ -118,6 +128,50 @@ export const KUMO_TEXT_STYLING = { export type KumoTextVariant = keyof typeof KUMO_TEXT_VARIANTS.variant; export type KumoTextSize = keyof typeof KUMO_TEXT_VARIANTS.size; +type Heading = "heading"; +type DeprecatedHeading = "heading1" | "heading2" | "heading3"; +type Copy = "body" | "secondary" | "success" | "error"; +type Monospace = "mono" | "mono-secondary"; +type TextSize = KumoTextSize; +type TextVariant = KumoTextVariant; + +const DEPRECATED_HEADING_VARIANTS: readonly DeprecatedHeading[] = [ + "heading1", + "heading2", + "heading3", +]; + +function isDeprecatedHeadingVariant( + variant: TextVariant, +): variant is DeprecatedHeading { + return (DEPRECATED_HEADING_VARIANTS as readonly TextVariant[]).includes( + variant, + ); +} + +function resolveTextSizeClasses(variant: TextVariant, size: TextSize) { + if (variant === "heading") { + return size === "lg" ? "text-xl" : ""; + } + + if (isDeprecatedHeadingVariant(variant)) { + return ""; + } + + if (["mono", "mono-secondary"].includes(variant)) { + // Monospace fonts need to be 1pt smaller than body text to optically match. + return size === "lg" + ? KUMO_TEXT_VARIANTS.size.base.classes + : KUMO_TEXT_VARIANTS.size.sm.classes; + } + + return resolveVariant( + KUMO_TEXT_VARIANTS.size, + size, + KUMO_TEXT_DEFAULT_VARIANTS.size, + ).classes; +} + export interface KumoTextVariantsProps { variant?: KumoTextVariant; size?: KumoTextSize; @@ -133,21 +187,10 @@ export function textVariants({ variant, KUMO_TEXT_DEFAULT_VARIANTS.variant, ).classes, - resolveVariant( - KUMO_TEXT_VARIANTS.size, - size, - KUMO_TEXT_DEFAULT_VARIANTS.size, - ).classes, + resolveTextSizeClasses(variant, size), ); } -// Legacy types for backwards compatibility -type Heading = "heading1" | "heading2" | "heading3"; -type Copy = "body" | "secondary" | "success" | "error"; -type Monospace = "mono" | "mono-secondary"; -type TextSize = KumoTextSize; -type TextVariant = KumoTextVariant; - /** Valid HTML elements for the Text component's `as` prop. */ export type TextElement = | "h1" @@ -203,28 +246,37 @@ type TextPropsInternal = BaseTextProps & ? { variant: Variant; bold?: never; - size?: never; + size?: "lg"; truncate?: boolean; /** - * Required for heading variants. Pick the element that reflects - * this text's place in the document outline (`"h1"` for a page - * title, `"h2"` for a section title, etc.) or `"span"` for - * decorative heading-styled text that is NOT a section heading. - * - * Previously optional (defaulted to ``), which silently - * excluded real section headings from the document outline. - * Making it required surfaces the decision at the type level. + * Optional element override. Defaults to ``. Pass the + * appropriate heading element (`"h1"`–`"h6"`) when this text + * belongs in the document outline. */ - as: TextElement; + as?: TextElement; } - : never); + : Variant extends DeprecatedHeading + ? { + variant: Variant; + bold?: never; + size?: never; + truncate?: boolean; + /** + * Required for deprecated heading variants. Pick the element + * that reflects this text's place in the document outline, or + * `"span"` for decorative heading-styled text. + */ + as: TextElement; + } + : never); /** * Text component props. * * @example * ```tsx - * Page Title + * Page Title + * Decorative heading text * Default paragraph text. * Muted helper text * Something went wrong @@ -234,9 +286,10 @@ type TextPropsInternal = BaseTextProps & export interface TextProps { /** * Text style variant. Determines color, font, and weight. - * - `"heading1"` — Large page title (30px, semibold) - * - `"heading2"` — Section title (24px, semibold) - * - `"heading3"` — Subsection title (18px, semibold) + * - `"heading"` — Heading text (16px by default, 20px with `size="lg"`; semibold) + * - `"heading1"` — Deprecated; use `"heading"` (30px, semibold) + * - `"heading2"` — Deprecated; use `"heading"` (24px, semibold) + * - `"heading3"` — Deprecated; use `"heading"` (16px, semibold) * - `"body"` — Default body text * - `"secondary"` — Muted text for secondary information * - `"success"` — Success state text @@ -247,11 +300,11 @@ export interface TextProps { */ variant?: KumoTextVariant; /** - * Text size (only applies to body/secondary/success/error variants). - * - `"xs"` — 12px - * - `"sm"` — 14px - * - `"base"` — 16px - * - `"lg"` — 18px + * Text size. Supported values depend on the variant: + * - `"heading"` — 16px when omitted, or 20px with `"lg"` + * - Body variants — `"xs"` (12px), `"sm"` (13px), `"base"` (14px), + * or `"lg"` (16px) + * - Monospace variants — 13px when omitted, or 14px with `"lg"` * @default "base" */ size?: KumoTextSize; @@ -265,10 +318,10 @@ export interface TextProps { * `"small"`, `"abbr"`, `"time"`), form-related (`"label"`, `"legend"`), * list/definition (`"dt"`, `"dd"`, `"li"`), and `"figcaption"`. * - * - **Required** for heading variants (`"heading1"`, `"heading2"`, - * `"heading3"`) — pick the element that reflects this text's place in - * the document outline, or `"span"` for decorative heading-styled text - * that is not a section heading. + * - **Optional** for `"heading"` (defaults to `"span"`). Pass the heading + * element that reflects this text's place in the document outline. + * - **Required** for deprecated heading variants (`"heading1"`, + * `"heading2"`, `"heading3"`). * - **Optional** for body variants (defaults to `"p"`) and monospace * variants (defaults to `"span"`). */ @@ -284,8 +337,8 @@ export interface TextProps { * * @example * ```tsx - * Page Title - * Section Title + * Page Title + * Section Title * Default body text * ``` */ @@ -304,16 +357,22 @@ function _Text( ref: ForwardedRef, ) { const isCopy = ["body", "secondary", "success", "error"].includes(variant); - const isMono = ["mono", "mono-secondary"].includes(variant); + const isDeprecatedHeading = isDeprecatedHeadingVariant(variant); + + if (process.env.NODE_ENV !== "production" && isDeprecatedHeading) { + console.warn( + `[Kumo Text]: variant="${variant}" is deprecated. Use variant="heading" and set size and as explicitly.`, + ); + } - // Heading variants no longer auto-select h1/h2/h3 to avoid coupling visual - // presentation to semantic HTML. Use the `as` prop to set the appropriate - // heading level for your document outline (e.g., as="h2"). + // Heading variants do not auto-select h1/h2/h3, keeping visual presentation + // separate from the document outline. Use `as` to opt into semantic HTML. const Component = useMemo(() => { if (as) return as; if (["mono", "mono-secondary"].includes(variant)) return "span"; - // Headings and body text default to span; use `as` for semantic elements - if (["heading1", "heading2", "heading3"].includes(variant)) return "span"; + if (variant === "heading" || isDeprecatedHeadingVariant(variant)) { + return "span"; + } return "p"; }, [variant, as]); @@ -330,19 +389,8 @@ function _Text( variant, KUMO_TEXT_DEFAULT_VARIANTS.variant, ).classes, - isCopy - ? resolveVariant( - KUMO_TEXT_VARIANTS.size, - size, - KUMO_TEXT_DEFAULT_VARIANTS.size, - ).classes - : "", + resolveTextSizeClasses(variant, size), isCopy && bold ? "font-medium" : "", - // Monospace fonts need to be 1pt smaller than body text to optically match - isMono && - (size === "lg" - ? KUMO_TEXT_VARIANTS.size.base.classes - : KUMO_TEXT_VARIANTS.size.sm.classes), truncate && "min-w-0 truncate", DANGEROUS_className, )} diff --git a/packages/kumo/src/components/text/text.type-spec.tsx b/packages/kumo/src/components/text/text.type-spec.tsx index 3adeb31440..e94107f3cd 100644 --- a/packages/kumo/src/components/text/text.type-spec.tsx +++ b/packages/kumo/src/components/text/text.type-spec.tsx @@ -18,27 +18,33 @@ import { Text } from "./text"; // Positive cases — these MUST compile cleanly. // --------------------------------------------------------------------------- -// Heading variant with required `as`. +// Heading has no implied semantic element and supports an optional large size. +const _headingDefault = Decorative heading; const _headingH1 = ( - - Page Title + + Page title ); const _headingH2 = ( - - Section Title + + Section title ); -const _headingH3 = ( - - Subsection + +// Deprecated heading variants remain available for backwards compatibility. +const _deprecatedHeadingH1 = ( + + Legacy page title ); - -// Heading variant using `as="span"` for decorative (non-section) usage. -const _decorativeHeading = ( - - Big bold label +const _deprecatedHeadingH2 = ( + + Legacy section title + +); +const _deprecatedHeadingH3 = ( + + Legacy subsection ); @@ -90,7 +96,7 @@ const _small = ( ); const _time = 2026-04-27; const _headingAsLabel = ( - + Form heading ); @@ -101,25 +107,33 @@ const _headingAsLabel = ( // tsc itself fails the typecheck with "Unused '@ts-expect-error' directive". // --------------------------------------------------------------------------- -// Missing `as` on heading1 → type error. -// @ts-expect-error — heading variants require `as` +// Heading only supports its default size and `lg`. +const _invalidHeadingSize = ( + // @ts-expect-error — heading does not support body text sizes + + Invalid + +); + +// Deprecated heading variants continue to require `as`. +// @ts-expect-error — deprecated heading variants require `as` const _missingAsH1 = Missing as; -// Missing `as` on heading2 → type error. -// @ts-expect-error — heading variants require `as` +// @ts-expect-error — deprecated heading variants require `as` const _missingAsH2 = Missing as; -// Missing `as` on heading3 → type error. -// @ts-expect-error — heading variants require `as` +// @ts-expect-error — deprecated heading variants require `as` const _missingAsH3 = Missing as; // Silence unused-variable warnings for all the sentinels above. // This file is never executed; it exists purely for type checking. export const __typeSpec = { + _headingDefault, _headingH1, _headingH2, - _headingH3, - _decorativeHeading, + _deprecatedHeadingH1, + _deprecatedHeadingH2, + _deprecatedHeadingH3, _bodyDefault, _bodyExplicit, _bodyInline, @@ -141,6 +155,7 @@ export const __typeSpec = { _small, _time, _headingAsLabel, + _invalidHeadingSize, _missingAsH1, _missingAsH2, _missingAsH3,