feat: add new improved text component#3551
Conversation
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
📦 Alpha Package Version PublishedUse Use |
🔍 Visual review for your branch is published 🔍Here are the links to: |
Coverage Report for packages/react
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
This PR introduces F0TextV2 as a clean, simplified replacement for F0Text, implementing the first step of a migration strategy (create new → migrate usages → delete old → rename back). F0TextV2 provides a self-contained two-prop architecture (variant + size) with 8 semantic variants, 7 size overrides, and 12 color tokens, eliminating the dependency on ui/Text/ that F0Text has. The old F0Text is marked @deprecated but remains unchanged.
Changes:
- Adds F0TextV2 component with complete implementation, comprehensive unit tests (72 tests), and full Storybook stories
- Marks F0Text as
@deprecatedwith JSDoc comments directing users to F0TextV2 - Exports F0TextV2 via components/exports.ts following established patterns
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/components/exports.ts | Adds @deprecated JSDoc to F0Text export and exports new F0TextV2 |
| packages/react/src/components/F0TextV2/index.tsx | Main component implementation using CVA, Component() wrapper, forwardRef, and OneEllipsis integration |
| packages/react/src/components/F0TextV2/types.ts | Token type definitions for variant, size, color, alignment, decoration, and transform props |
| packages/react/src/components/F0TextV2/utils.ts | Variant class mappings, default tag map, and secondary color variant set |
| packages/react/src/components/F0TextV2/tests/Text.test.tsx | Comprehensive unit tests covering all variants, sizes, colors, decorations, transforms, and edge cases |
| packages/react/src/components/F0TextV2/stories/Text.stories.tsx | Complete Storybook stories including Snapshot story for Chromatic visual regression testing |
| packages/react/src/components/F0Text/index.tsx | Adds @deprecated JSDoc comment to F0Text exports |
Comments suppressed due to low confidence (1)
packages/react/src/components/F0TextV2/types.ts:18
- Documentation discrepancy: The comment table states that "label" defaults to "md (14px)" but the actual implementation uses "text-base" which is correct (14px). However, the table also lists description as "md (14px)" but description also uses "text-base" in the implementation. The documentation is correct about the px values but inconsistent with the Tailwind class naming.
More critically, the "label" row in line 15 claims it defaults to "md (14px) + secondary" but the implementation shows "label" uses "text-base" which is correct. The issue is the table header uses "Default Size" but the actual classes are embedded in variantVariants. Consider clarifying the table to show the actual Tailwind classes used (text-base, text-lg, text-sm, etc.) to avoid confusion.
* | Variant | Weight | Default Tag | Default Size |
* |-------------|-------------|-------------|------------------------|
* | title | semibold | h2 | 3xl (26px) |
* | heading | semibold | h3 | xl (18px) |
* | subtitle | normal | p | lg (16px) + secondary |
* | body | normal | p | md (14px) |
* | label | medium | label | md (14px) + secondary |
* | description | normal | p | md (14px) + secondary |
* | caption | normal | span | sm (12px) + secondary |
* | code | normal+mono | code | sm (12px) |
| export const F0TextV2 = Component( | ||
| { | ||
| name: "F0TextV2", | ||
| type: "info", | ||
| }, | ||
| F0TextV2Inner | ||
| ) |
There was a problem hiding this comment.
F0TextV2 should be wrapped with withDataTestId() in addition to Component(). Following the pattern used in F0Icon and other components, F0TextV2 needs data-testid support. Wrap the Component() result with withDataTestId() like this:
import { withDataTestId } from "@/lib/data-testid"
export const F0TextV2 = withDataTestId(
Component(
{
name: "F0TextV2",
type: "info",
},
F0TextV2Inner
)
)| ], | ||
| control: "select", | ||
| description: | ||
| "Semantic text color. description/subtitle/caption default to secondary.", |
There was a problem hiding this comment.
Documentation error: The description states "description/subtitle/caption default to secondary" but omits "label", which also defaults to secondary color. Looking at the SECONDARY_COLOR_VARIANTS set in utils.ts, it includes ["description", "subtitle", "caption", "label"]. The description should be updated to "description/subtitle/caption/label default to secondary." for accuracy.
| "Semantic text color. description/subtitle/caption default to secondary.", | |
| "Semantic text color. description/subtitle/caption/label default to secondary.", |
| import { describe, expect, it } from "vitest" | ||
| import "@testing-library/jest-dom/vitest" | ||
| import { zeroRender as render, screen } from "@/testing/test-utils" | ||
|
|
||
| import { F0TextV2 } from "../" | ||
|
|
||
| describe("F0TextV2 Component", () => { | ||
| describe("Title variant", () => { | ||
| it("renders with default size (3xl) and h2 tag", () => { | ||
| render(<F0TextV2 variant="title" content="Page Title" />) | ||
| const el = screen.getByText("Page Title") | ||
| expect(el.tagName).toBe("H2") | ||
| expect(el).toHaveClass("text-3xl", "font-semibold") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render(<F0TextV2 variant="title" size="2xl" content="Smaller Title" />) | ||
| const el = screen.getByText("Smaller Title") | ||
| expect(el.tagName).toBe("H2") | ||
| expect(el).toHaveClass("text-2xl", "font-semibold") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Heading variant", () => { | ||
| it("renders with default size (xl) and h3 tag", () => { | ||
| render(<F0TextV2 variant="heading" content="Section Heading" />) | ||
| const el = screen.getByText("Section Heading") | ||
| expect(el.tagName).toBe("H3") | ||
| expect(el).toHaveClass("text-xl", "font-semibold") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render(<F0TextV2 variant="heading" size="lg" content="Smaller Heading" />) | ||
| const el = screen.getByText("Smaller Heading") | ||
| expect(el.tagName).toBe("H3") | ||
| expect(el).toHaveClass("text-lg", "font-semibold") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Subtitle variant", () => { | ||
| it("renders with default size (lg/text-lg) and p tag", () => { | ||
| render(<F0TextV2 variant="subtitle" content="Subtitle text" />) | ||
| const el = screen.getByText("Subtitle text") | ||
| expect(el.tagName).toBe("P") | ||
| expect(el).toHaveClass("text-lg", "font-normal") | ||
| }) | ||
|
|
||
| it("defaults to secondary color", () => { | ||
| render(<F0TextV2 variant="subtitle" content="Subtitle secondary" />) | ||
| const el = screen.getByText("Subtitle secondary") | ||
| expect(el).toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("allows explicit color to override secondary default", () => { | ||
| render( | ||
| <F0TextV2 | ||
| variant="subtitle" | ||
| color="critical" | ||
| content="Critical subtitle" | ||
| /> | ||
| ) | ||
| const el = screen.getByText("Critical subtitle") | ||
| expect(el).toHaveClass("text-f1-foreground-critical") | ||
| }) | ||
|
|
||
| it('allows color="default" to override secondary default', () => { | ||
| render( | ||
| <F0TextV2 | ||
| variant="subtitle" | ||
| color="default" | ||
| content="Default subtitle" | ||
| /> | ||
| ) | ||
| const el = screen.getByText("Default subtitle") | ||
| expect(el).not.toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render( | ||
| <F0TextV2 variant="subtitle" size="lg" content="Bigger subtitle" /> | ||
| ) | ||
| const el = screen.getByText("Bigger subtitle") | ||
| expect(el).toHaveClass("text-lg") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Body variant", () => { | ||
| it("renders with default size (md) and p tag", () => { | ||
| render(<F0TextV2 variant="body" content="Body text" />) | ||
| const el = screen.getByText("Body text") | ||
| expect(el.tagName).toBe("P") | ||
| expect(el).toHaveClass("text-base", "font-normal") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render(<F0TextV2 variant="body" size="lg" content="Larger body" />) | ||
| const el = screen.getByText("Larger body") | ||
| expect(el.tagName).toBe("P") | ||
| expect(el).toHaveClass("text-lg", "font-normal") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Label variant", () => { | ||
| it("renders with default size (md/text-base) and label tag", () => { | ||
| render(<F0TextV2 variant="label" content="Field label" />) | ||
| const el = screen.getByText("Field label") | ||
| expect(el.tagName).toBe("LABEL") | ||
| expect(el).toHaveClass("text-base", "font-medium") | ||
| }) | ||
|
|
||
| it("defaults to secondary color", () => { | ||
| render(<F0TextV2 variant="label" content="Label secondary" />) | ||
| const el = screen.getByText("Label secondary") | ||
| expect(el).toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("allows explicit color to override secondary default", () => { | ||
| render( | ||
| <F0TextV2 variant="label" color="critical" content="Critical label" /> | ||
| ) | ||
| const el = screen.getByText("Critical label") | ||
| expect(el).toHaveClass("text-f1-foreground-critical") | ||
| }) | ||
|
|
||
| it('allows color="default" to override secondary default', () => { | ||
| render( | ||
| <F0TextV2 variant="label" color="default" content="Default label" /> | ||
| ) | ||
| const el = screen.getByText("Default label") | ||
| expect(el).not.toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render(<F0TextV2 variant="label" size="sm" content="Smaller label" />) | ||
| const el = screen.getByText("Smaller label") | ||
| expect(el.tagName).toBe("LABEL") | ||
| expect(el).toHaveClass("text-sm", "font-medium") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Description variant", () => { | ||
| it("renders with default size (md/text-base) and secondary color", () => { | ||
| render(<F0TextV2 variant="description" content="Help text" />) | ||
| const el = screen.getByText("Help text") | ||
| expect(el.tagName).toBe("P") | ||
| expect(el).toHaveClass("text-base", "font-normal") | ||
| expect(el).toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("allows explicit color to override secondary default", () => { | ||
| render( | ||
| <F0TextV2 variant="description" color="critical" content="Error help" /> | ||
| ) | ||
| const el = screen.getByText("Error help") | ||
| expect(el).toHaveClass("text-f1-foreground-critical") | ||
| }) | ||
|
|
||
| it('allows color="default" to override secondary default', () => { | ||
| render( | ||
| <F0TextV2 | ||
| variant="description" | ||
| color="default" | ||
| content="Default color description" | ||
| /> | ||
| ) | ||
| const el = screen.getByText("Default color description") | ||
| expect(el).not.toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render( | ||
| <F0TextV2 variant="description" size="sm" content="Smaller help" /> | ||
| ) | ||
| const el = screen.getByText("Smaller help") | ||
| expect(el).toHaveClass("text-sm") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Caption variant", () => { | ||
| it("renders with default size (sm) and span tag", () => { | ||
| render(<F0TextV2 variant="caption" content="Caption text" />) | ||
| const el = screen.getByText("Caption text") | ||
| expect(el.tagName).toBe("SPAN") | ||
| expect(el).toHaveClass("text-sm", "font-normal") | ||
| }) | ||
|
|
||
| it("defaults to secondary color", () => { | ||
| render(<F0TextV2 variant="caption" content="Caption secondary" />) | ||
| const el = screen.getByText("Caption secondary") | ||
| expect(el).toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("allows explicit color to override secondary default", () => { | ||
| render( | ||
| <F0TextV2 variant="caption" color="warning" content="Warning caption" /> | ||
| ) | ||
| const el = screen.getByText("Warning caption") | ||
| expect(el).toHaveClass("text-f1-foreground-warning") | ||
| }) | ||
|
|
||
| it('allows color="default" to override secondary default', () => { | ||
| render( | ||
| <F0TextV2 variant="caption" color="default" content="Default caption" /> | ||
| ) | ||
| const el = screen.getByText("Default caption") | ||
| expect(el).not.toHaveClass("text-f1-foreground-secondary") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render(<F0TextV2 variant="caption" size="xs" content="Tiny caption" />) | ||
| const el = screen.getByText("Tiny caption") | ||
| expect(el).toHaveClass("text-xs") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Code variant", () => { | ||
| it("renders with default size (sm), monospace font, and code tag", () => { | ||
| render(<F0TextV2 variant="code" content="const x = 1" />) | ||
| const el = screen.getByText("const x = 1") | ||
| expect(el.tagName).toBe("CODE") | ||
| expect(el).toHaveClass("text-sm", "font-normal", "font-mono") | ||
| }) | ||
|
|
||
| it("renders with explicit size override", () => { | ||
| render(<F0TextV2 variant="code" size="md" content="const y = 2" />) | ||
| const el = screen.getByText("const y = 2") | ||
| expect(el.tagName).toBe("CODE") | ||
| expect(el).toHaveClass("text-base", "font-mono") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Size is optional — default baked into variant", () => { | ||
| it("title defaults to 3xl", () => { | ||
| render(<F0TextV2 variant="title" content="T" />) | ||
| expect(screen.getByText("T")).toHaveClass("text-3xl") | ||
| }) | ||
|
|
||
| it("heading defaults to xl", () => { | ||
| render(<F0TextV2 variant="heading" content="H" />) | ||
| expect(screen.getByText("H")).toHaveClass("text-xl") | ||
| }) | ||
|
|
||
| it("subtitle defaults to lg (text-lg)", () => { | ||
| render(<F0TextV2 variant="subtitle" content="S" />) | ||
| expect(screen.getByText("S")).toHaveClass("text-lg") | ||
| }) | ||
|
|
||
| it("body defaults to md (text-base)", () => { | ||
| render(<F0TextV2 variant="body" content="B" />) | ||
| expect(screen.getByText("B")).toHaveClass("text-base") | ||
| }) | ||
|
|
||
| it("label defaults to md (text-base)", () => { | ||
| render(<F0TextV2 variant="label" content="L" />) | ||
| expect(screen.getByText("L")).toHaveClass("text-base") | ||
| }) | ||
|
|
||
| it("description defaults to md (text-base)", () => { | ||
| render(<F0TextV2 variant="description" content="D" />) | ||
| expect(screen.getByText("D")).toHaveClass("text-base") | ||
| }) | ||
|
|
||
| it("caption defaults to sm", () => { | ||
| render(<F0TextV2 variant="caption" content="C" />) | ||
| expect(screen.getByText("C")).toHaveClass("text-sm") | ||
| }) | ||
|
|
||
| it("code defaults to sm", () => { | ||
| render(<F0TextV2 variant="code" content="X" />) | ||
| expect(screen.getByText("X")).toHaveClass("text-sm") | ||
| }) | ||
| }) | ||
|
|
||
| describe('Size "default" keeps variant built-in size', () => { | ||
| it("title with size=default keeps 3xl", () => { | ||
| render(<F0TextV2 variant="title" size="default" content="T" />) | ||
| expect(screen.getByText("T")).toHaveClass("text-3xl") | ||
| }) | ||
|
|
||
| it("heading with size=default keeps xl", () => { | ||
| render(<F0TextV2 variant="heading" size="default" content="H" />) | ||
| expect(screen.getByText("H")).toHaveClass("text-xl") | ||
| }) | ||
|
|
||
| it("body with size=default keeps text-base", () => { | ||
| render(<F0TextV2 variant="body" size="default" content="B" />) | ||
| expect(screen.getByText("B")).toHaveClass("text-base") | ||
| }) | ||
|
|
||
| it("caption with size=default keeps sm", () => { | ||
| render(<F0TextV2 variant="caption" size="default" content="C" />) | ||
| expect(screen.getByText("C")).toHaveClass("text-sm") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Size independence", () => { | ||
| it("allows any size with any variant", () => { | ||
| render(<F0TextV2 variant="heading" size="3xl" content="Big heading" />) | ||
| const el = screen.getByText("Big heading") | ||
| expect(el).toHaveClass("text-3xl", "font-semibold") | ||
| }) | ||
|
|
||
| it("allows xs size with title variant", () => { | ||
| render(<F0TextV2 variant="title" size="xs" content="Tiny title" />) | ||
| const el = screen.getByText("Tiny title") | ||
| expect(el).toHaveClass("text-xs", "font-semibold") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Default variant", () => { | ||
| it("defaults to body when no variant specified", () => { | ||
| render(<F0TextV2 content="Default" />) | ||
| const el = screen.getByText("Default") | ||
| expect(el.tagName).toBe("P") | ||
| expect(el).toHaveClass("text-base", "font-normal") | ||
| }) | ||
| }) | ||
|
|
||
| describe("HTML tags determined by variant", () => { | ||
| it("title renders as h2", () => { | ||
| render(<F0TextV2 variant="title" content="T" />) | ||
| expect(screen.getByText("T").tagName).toBe("H2") | ||
| }) | ||
|
|
||
| it("heading renders as h3", () => { | ||
| render(<F0TextV2 variant="heading" content="H" />) | ||
| expect(screen.getByText("H").tagName).toBe("H3") | ||
| }) | ||
|
|
||
| it("subtitle renders as p", () => { | ||
| render(<F0TextV2 variant="subtitle" content="S" />) | ||
| expect(screen.getByText("S").tagName).toBe("P") | ||
| }) | ||
|
|
||
| it("body renders as p", () => { | ||
| render(<F0TextV2 variant="body" content="B" />) | ||
| expect(screen.getByText("B").tagName).toBe("P") | ||
| }) | ||
|
|
||
| it("label renders as label", () => { | ||
| render(<F0TextV2 variant="label" content="L" />) | ||
| expect(screen.getByText("L").tagName).toBe("LABEL") | ||
| }) | ||
|
|
||
| it("description renders as p", () => { | ||
| render(<F0TextV2 variant="description" content="D" />) | ||
| expect(screen.getByText("D").tagName).toBe("P") | ||
| }) | ||
|
|
||
| it("caption renders as span", () => { | ||
| render(<F0TextV2 variant="caption" content="C" />) | ||
| expect(screen.getByText("C").tagName).toBe("SPAN") | ||
| }) | ||
|
|
||
| it("code renders as code", () => { | ||
| render(<F0TextV2 variant="code" content="X" />) | ||
| expect(screen.getByText("X").tagName).toBe("CODE") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Color prop", () => { | ||
| it("renders with default color", () => { | ||
| render(<F0TextV2 color="default" content="Default" />) | ||
| expect(screen.getByText("Default")).toHaveClass("text-f1-foreground") | ||
| }) | ||
|
|
||
| it("renders with secondary color", () => { | ||
| render(<F0TextV2 color="secondary" content="Secondary" />) | ||
| expect(screen.getByText("Secondary")).toHaveClass( | ||
| "text-f1-foreground-secondary" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with tertiary color", () => { | ||
| render(<F0TextV2 color="tertiary" content="Tertiary" />) | ||
| expect(screen.getByText("Tertiary")).toHaveClass( | ||
| "text-f1-foreground-tertiary" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with critical color", () => { | ||
| render(<F0TextV2 color="critical" content="Critical" />) | ||
| expect(screen.getByText("Critical")).toHaveClass( | ||
| "text-f1-foreground-critical" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with warning color", () => { | ||
| render(<F0TextV2 color="warning" content="Warning" />) | ||
| expect(screen.getByText("Warning")).toHaveClass( | ||
| "text-f1-foreground-warning" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with positive color", () => { | ||
| render(<F0TextV2 color="positive" content="Positive" />) | ||
| expect(screen.getByText("Positive")).toHaveClass( | ||
| "text-f1-foreground-positive" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with info color", () => { | ||
| render(<F0TextV2 color="info" content="Info" />) | ||
| expect(screen.getByText("Info")).toHaveClass("text-f1-foreground-info") | ||
| }) | ||
|
|
||
| it("renders with inverse color", () => { | ||
| render(<F0TextV2 color="inverse" content="Inverse" />) | ||
| expect(screen.getByText("Inverse")).toHaveClass( | ||
| "text-f1-foreground-inverse" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with inverse-secondary color", () => { | ||
| render(<F0TextV2 color="inverse-secondary" content="InverseSecondary" />) | ||
| expect(screen.getByText("InverseSecondary")).toHaveClass( | ||
| "text-f1-foreground-inverse-secondary" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with disabled color", () => { | ||
| render(<F0TextV2 color="disabled" content="Disabled" />) | ||
| expect(screen.getByText("Disabled")).toHaveClass( | ||
| "text-f1-foreground-disabled" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with accent color", () => { | ||
| render(<F0TextV2 color="accent" content="Accent" />) | ||
| expect(screen.getByText("Accent")).toHaveClass( | ||
| "text-f1-foreground-accent" | ||
| ) | ||
| }) | ||
|
|
||
| it("renders with selected color", () => { | ||
| render(<F0TextV2 color="selected" content="Selected" />) | ||
| expect(screen.getByText("Selected")).toHaveClass( | ||
| "text-f1-foreground-selected" | ||
| ) | ||
| }) | ||
|
|
||
| it("combines color with variant independently", () => { | ||
| render( | ||
| <F0TextV2 | ||
| variant="heading" | ||
| color="critical" | ||
| content="Critical Heading" | ||
| /> | ||
| ) | ||
| const el = screen.getByText("Critical Heading") | ||
| expect(el).toHaveClass("font-semibold", "text-f1-foreground-critical") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Text decoration", () => { | ||
| it("renders with underline decoration", () => { | ||
| render(<F0TextV2 decoration="underline" content="Underlined" />) | ||
| expect(screen.getByText("Underlined")).toHaveClass("underline") | ||
| }) | ||
|
|
||
| it("renders with overline decoration", () => { | ||
| render(<F0TextV2 decoration="overline" content="Overlined" />) | ||
| expect(screen.getByText("Overlined")).toHaveClass("overline") | ||
| }) | ||
|
|
||
| it("renders with line-through decoration", () => { | ||
| render(<F0TextV2 decoration="line-through" content="Strikethrough" />) | ||
| expect(screen.getByText("Strikethrough")).toHaveClass("line-through") | ||
| }) | ||
|
|
||
| it("renders without decoration by default", () => { | ||
| render(<F0TextV2 content="No decoration" />) | ||
| const el = screen.getByText("No decoration") | ||
| expect(el).not.toHaveClass("underline") | ||
| expect(el).not.toHaveClass("overline") | ||
| expect(el).not.toHaveClass("line-through") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Text transform", () => { | ||
| it("renders with uppercase transform", () => { | ||
| render(<F0TextV2 transform="uppercase" content="Uppercase" />) | ||
| expect(screen.getByText("Uppercase")).toHaveClass("uppercase") | ||
| }) | ||
|
|
||
| it("renders with lowercase transform", () => { | ||
| render(<F0TextV2 transform="lowercase" content="Lowercase" />) | ||
| expect(screen.getByText("Lowercase")).toHaveClass("lowercase") | ||
| }) | ||
|
|
||
| it("renders with capitalize transform", () => { | ||
| render(<F0TextV2 transform="capitalize" content="Capitalize" />) | ||
| expect(screen.getByText("Capitalize")).toHaveClass("capitalize") | ||
| }) | ||
|
|
||
| it("renders without transform by default", () => { | ||
| render(<F0TextV2 content="No transform" />) | ||
| const el = screen.getByText("No transform") | ||
| expect(el).not.toHaveClass("uppercase") | ||
| expect(el).not.toHaveClass("lowercase") | ||
| expect(el).not.toHaveClass("capitalize") | ||
| }) | ||
| }) | ||
|
|
||
| describe("Combined props", () => { | ||
| it("combines variant, size, color, decoration, and transform", () => { | ||
| render( | ||
| <F0TextV2 | ||
| variant="label" | ||
| size="md" | ||
| color="critical" | ||
| decoration="underline" | ||
| transform="uppercase" | ||
| content="Combined" | ||
| /> | ||
| ) | ||
| const el = screen.getByText("Combined") | ||
| expect(el.tagName).toBe("LABEL") | ||
| expect(el).toHaveClass( | ||
| "font-medium", | ||
| "text-base", | ||
| "text-f1-foreground-critical", | ||
| "underline", | ||
| "uppercase" | ||
| ) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Missing test coverage for the align prop. The component supports left/center/right alignment via the align prop (defaulting to "left"), but there are no unit tests validating this functionality. Add test cases to verify:
- Default alignment is left
- Explicit align="left" applies text-left class
- align="center" applies text-center class
- align="right" applies text-right class
|
|
||
| /** | ||
| * Semantic text color. | ||
| * `description`, `subtitle`, and `caption` default to `"secondary"`. |
There was a problem hiding this comment.
Documentation error: The JSDoc comment states that only "description", "subtitle", and "caption" default to "secondary" color, but "label" also defaults to secondary according to SECONDARY_COLOR_VARIANTS. Update the comment to include all four variants: "description", "subtitle", "caption", and "label".
| * `description`, `subtitle`, and `caption` default to `"secondary"`. | |
| * `description`, `subtitle`, `caption`, and `label` default to `"secondary"`. |
| /> | ||
| <F0TextV2 | ||
| variant="description" | ||
| content="Description default (sm / 12px, secondary)" |
There was a problem hiding this comment.
The comment "Description default (sm / 12px, secondary)" is incorrect. According to the variant definition in variants.ts (line 24) and the types.ts documentation (line 16), the description variant uses text-base which is md/14px, not sm/12px. The comment should say "Description default (md / 14px, secondary)" to match the actual implementation.
| content="Description default (sm / 12px, secondary)" | |
| content="Description default (md / 14px, secondary)" |
| /> | ||
| <F0TextV2 | ||
| variant="caption" | ||
| content="Caption default (xs / 10px, secondary)" |
There was a problem hiding this comment.
The comment "Caption default (xs / 10px, secondary)" is incorrect. According to the variant definition in variants.ts (line 25) and the types.ts documentation (line 17), the caption variant uses text-sm which is sm/12px, not xs/10px. The comment should say "Caption default (sm / 12px, secondary)" to match the actual implementation.
| content="Caption default (xs / 10px, secondary)" | |
| content="Caption default (sm / 12px, secondary)" |
| import { F0TextV2Inner, type F0TextV2Props } from "./F0TextV2" | ||
|
|
||
| export const F0TextV2 = Component( | ||
| { | ||
| name: "F0TextV2", | ||
| type: "info", | ||
| }, | ||
| F0TextV2Inner |
There was a problem hiding this comment.
F0TextV2 uses the Component() wrapper for XRay support but is missing withDataTestId() wrapper for data-testid prop support. Based on the pattern in F0Icon (packages/react/src/components/F0Icon/index.tsx:10-18), components using Component() should also be wrapped with withDataTestId(). The export should be: export const F0TextV2 = withDataTestId(Component({ name: "F0TextV2", type: "info" }, F0TextV2Inner))
| import { F0TextV2Inner, type F0TextV2Props } from "./F0TextV2" | |
| export const F0TextV2 = Component( | |
| { | |
| name: "F0TextV2", | |
| type: "info", | |
| }, | |
| F0TextV2Inner | |
| import { withDataTestId } from "../../lib/test/withDataTestId" | |
| import { F0TextV2Inner, type F0TextV2Props } from "./F0TextV2" | |
| export const F0TextV2 = withDataTestId( | |
| Component( | |
| { | |
| name: "F0TextV2", | |
| type: "info", | |
| }, | |
| F0TextV2Inner | |
| ) |
| <F0TextV2 variant="label" size="md" content="Label md" /> | ||
| <F0TextV2 variant="label" size="sm" content="Label sm (default)" /> |
There was a problem hiding this comment.
The comment "Label sm (default)" is incorrect. According to the variant definition in variants.ts (line 23), the label variant defaults to text-base which is md/14px, not sm/12px. This line should not indicate (default), or it should be applied to the md size variant instead.
| <F0TextV2 variant="label" size="md" content="Label md" /> | |
| <F0TextV2 variant="label" size="sm" content="Label sm (default)" /> | |
| <F0TextV2 variant="label" size="md" content="Label md (default)" /> | |
| <F0TextV2 variant="label" size="sm" content="Label sm" /> |
| <F0TextV2 variant="description" size="md" content="Description md" /> | ||
| <F0TextV2 | ||
| variant="description" | ||
| size="sm" | ||
| content="Description sm (default)" | ||
| /> |
There was a problem hiding this comment.
The comment "Description sm (default)" is incorrect. According to the variant definition in variants.ts (line 24), the description variant defaults to text-base which is md/14px, not sm/12px. The default marker should be on line 586 (Description md) instead.
| <F0TextV2 variant="description" size="md" content="Description md" /> | |
| <F0TextV2 | |
| variant="description" | |
| size="sm" | |
| content="Description sm (default)" | |
| /> | |
| <F0TextV2 | |
| variant="description" | |
| size="md" | |
| content="Description md (default)" | |
| /> | |
| <F0TextV2 variant="description" size="sm" content="Description sm" /> |
| <F0TextV2 variant="caption" size="sm" content="Caption sm" /> | ||
| <F0TextV2 | ||
| variant="caption" | ||
| content="Caption default (xs, secondary)" |
There was a problem hiding this comment.
The comment "Caption default (xs, secondary)" is incorrect. According to the variant definition in variants.ts (line 25), the caption variant uses text-sm which is sm/12px, not xs/10px. The comment should say "Caption default (sm, secondary)" to match the actual implementation.
| content="Caption default (xs, secondary)" | |
| content="Caption default (sm, secondary)" |
| /> | ||
| <F0TextV2 variant="label" size="lg" content="Label lg (16px medium)" /> | ||
| <F0TextV2 variant="label" size="md" content="Label md (14px medium)" /> | ||
| <F0TextV2 variant="label" content="Label default (sm / 12px medium)" /> |
There was a problem hiding this comment.
The comment "Label default (sm / 12px medium)" is incorrect. According to the variant definition in variants.ts (line 23) and the types.ts documentation (line 15), the label variant uses text-base which is md/14px, not sm/12px. The comment should say "Label default (md / 14px medium)" or simply "Label md (default)" to match the actual implementation.
| <F0TextV2 variant="label" content="Label default (sm / 12px medium)" /> | |
| <F0TextV2 variant="label" content="Label default (md / 14px medium)" /> |
Description
Add new F0TextV2 component as a clean replacement for F0Text. This is the first step of a migration plan: create new → migrate usages → delete old → rename back.
F0TextV2 introduces a simplified two-prop architecture (variant + size) that is fully self-contained — no dependency on ui/Text/. It supports 8 semantic variants (title, heading, subtitle, body, label, description, caption, code), 7 size overrides, and 12 foreground color tokens. The old F0Text is marked @deprecated.
Screenshots (if applicable)
Implementation details