Skip to content
Draft
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ lint-staged.config.js
jest.config.js
jest.config.react18.js
babel.config.js
codemods/
plopfile.mjs
release.config.js
coverage/
Expand Down
61 changes: 61 additions & 0 deletions .superpowers/sdd/task-3-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Task 3: Display

## Implementation

- Added `Display` with required `display-1` through `display-5` variants.
- Uses `Role.div`, shared typography classes, forwarded div refs, and obfuscated class names.
- Exported from `src/display` and the root package entry.

## TDD evidence

- RED: `npm test -- src/display/display.test.tsx --runInBand` failed because `./display` was absent.
- GREEN: focused suite passes 11 tests after implementation.

## Verification

- `npm test -- src/display/display.test.tsx --runInBand` — pass, 11 tests.
- `npm run type-check` — pass.
- `npx @doist/react-compiler-tracker --check-files --show-errors src/display/display.tsx` — no new errors.

## Files

- `src/display/display.tsx`
- `src/display/display.module.css`
- `src/display/display.test.tsx`
- `src/display/index.ts`
- `src/index.ts`

## Self-review

- Checked exact typography values, Role render behavior, type exports, root export, ref forwarding, a11y, responsive typography classes, and no manual memoization.
- `git diff --check` passes.

## Concerns

None.

## Review fix

- Display now opts into the shared SF for Web font-family class (`'SF Pro Display', sans-serif`); Text and Heading retain the existing default font-family class.
- Replaced Display test-ID queries with text/role queries and retained the variant and behavior assertions.
- Moved Display metrics to component-scoped custom properties and centralized its common weight and line-height rule.

## Fix verification

- `npm test -- src/display/display.test.tsx --runInBand` — RED: 5 variant assertions failed after temporarily removing the SF/display classes; GREEN: passes after restoration.
- `npm test -- src/display/display.test.tsx src/text/text.test.tsx src/heading/heading.test.tsx --runInBand` — pass, 3 suites, 60 tests.
- `npm run type-check` — pass.
- `npx @doist/react-compiler-tracker --check-files --show-errors src/display/display.tsx` — no new errors.
- `git diff --check` — pass.

## Mapping fix

- Display now obtains `font-family-sf-for-web` from `typography.module.css`, where the class is defined, rather than from its local CSS module.
- Added a source-mapping regression test because the global CSS mock returns every property name and cannot expose a wrong CSS-module lookup.

## Mapping verification

- `npm test -- src/display/display.test.tsx --runInBand` — RED: shared CSS-module mapping assertion failed before the import fix; GREEN: pass, 1 suite, 12 tests.
- `npm run type-check` — pass.
- `npx @doist/react-compiler-tracker --check-files --show-errors src/display/display.tsx` — no new errors.
- `git diff --check` — pass.
35 changes: 35 additions & 0 deletions codemods/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Reactist typography codemod

Migrates the breaking Text and Heading typography APIs to named SF variants.

## Run

```bash
npx jscodeshift@17.3.0 \
--transform ./node_modules/@doist/reactist/codemods/typography-variants.js \
--extensions js,jsx,ts,tsx \
--parser tsx \
src
```

Run Prettier and the application's type-check after the transform.

## Exact mappings

| Legacy Text size | Regular or omitted | Semibold | Bold |
| ---------------- | ------------------ | ----------- | --------- |
| subtitle | subheader-2 | subheader-1 | Manual |
| body or omitted | body-3 | body-2 | body-1 |
| copy | callout-2 | callout-1 | Manual |
| caption | caption-3 | caption-2 | caption-1 |

Heading maps only exact 32px/700 uses to heading-1 and exact 20px/700 uses to heading-3.

## Manual migrations

The transform changes only exact static mappings. Ambiguous size/weight combinations, dynamic
expressions, and prop spreads remain unchanged. Each unresolved element receives a
TODO(reactist-codemod) comment, and the command prints its file and line.

Resolve every TODO before upgrading to the new Reactist major version. Do not mechanically choose
the nearest variant: confirm the intended visual hierarchy with design.
20 changes: 20 additions & 0 deletions codemods/__testfixtures__/typography-variants-heading.input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react'

import { Heading, Heading as Title } from '@doist/reactist'

export function ExactHeadings() {
return (
<>
<Heading level={1} size="largest">
Large
</Heading>
<Heading level={1}>Current default</Heading>
<Title level={2} size="larger">
Visual 20
</Title>
<Heading level={4} size="largest" weight="regular">
Visual 20
</Heading>
</>
)
}
22 changes: 22 additions & 0 deletions codemods/__testfixtures__/typography-variants-heading.output.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react'

import { Heading, Heading as Title } from '@doist/reactist'

export function ExactHeadings() {
return (
<>
<Heading level={1} variant="heading-1">
Large
</Heading>
<Heading level={1} variant="heading-3">
Current default
</Heading>
<Title level={2} variant="heading-3">
Visual 20
</Title>
<Heading level={4} variant="heading-3">
Visual 20
</Heading>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Heading } from '@doist/reactist'

export function HeadingVariants() {
return (
<>
<Heading level={1} variant="heading-1">
Page title
</Heading>
<Heading level={4} variant="heading-3">
Prominent subsection
</Heading>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Heading } from '@doist/reactist'

export function HeadingVariants() {
return (
<>
<Heading level={1} variant="heading-1">
Page title
</Heading>
<Heading level={4} variant="heading-3">
Prominent subsection
</Heading>
</>
)
}
61 changes: 61 additions & 0 deletions codemods/__testfixtures__/typography-variants-manual.input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as React from 'react'

import { Heading, Text } from '@doist/reactist'

export function ManualCases({ size, weight, useLabel, level, props }) {
return (
<>
<Text size="copy" weight="bold">
No exact match
</Text>
<Text size={size}>Dynamic</Text>
<Text weight={weight}>Dynamic weight</Text>
<Text as={useLabel ? 'label' : 'span'}>Dynamic element</Text>
<Text as={getComponent().Foo}>Unsupported element</Text>
<Text as="span" render={<em />}>
Existing render
</Text>
<Text size="body" size="caption">
Duplicate size
</Text>
<Text weight="semibold" weight="bold">
Duplicate weight
</Text>
<Text as="span" as="label">
Duplicate element
</Text>
<Text {...props}>Spread</Text>
<Heading level={1} size="larger">
24px
</Heading>
<Heading level={2}>16px</Heading>
<Heading level={3}>14px</Heading>
<Heading level={3} size="smaller">
12px
</Heading>
<Heading level={1} weight="medium">
Medium
</Heading>
<Heading level={1} weight="light">
Light
</Heading>
<Heading level={level}>Dynamic level</Heading>
<Heading level={1} size={size}>
Dynamic size
</Heading>
<Heading level={1} weight={weight}>
Dynamic weight
</Heading>
<Heading {...props}>Spread</Heading>
<Heading {...props} level={level} size={size} weight={weight}>
Spread and dynamic props
</Heading>
</>
)
}

export function Shadowed() {
const Text = (props: React.ComponentProps<'span'>) => <span {...props} />

return <Text size="caption">Shadowed Text</Text>
}
100 changes: 100 additions & 0 deletions codemods/__testfixtures__/typography-variants-manual.output.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as React from 'react'

import { Heading, Text } from '@doist/reactist'

export function ManualCases({ size, weight, useLabel, level, props }) {
return (
<>
<Text size="copy" weight="bold">
{/* TODO(reactist-codemod): Text size and weight have no exact variant */}
No exact match
</Text>
<Text size={size}>
{/* TODO(reactist-codemod): dynamic Text size */}
Dynamic
</Text>
<Text weight={weight}>
{/* TODO(reactist-codemod): dynamic Text weight */}
Dynamic weight
</Text>
<Text as={useLabel ? 'label' : 'span'}>
{/* TODO(reactist-codemod): dynamic Text as target */}
Dynamic element
</Text>
<Text as={getComponent().Foo}>
{/* TODO(reactist-codemod): dynamic Text as target */}
Unsupported element
</Text>
<Text as="span" render={<em />}>
{/* TODO(reactist-codemod): Text already has render prop */}
Existing render
</Text>
<Text size="body" size="caption">
{/* TODO(reactist-codemod): duplicate Text size props */}
Duplicate size
</Text>
<Text weight="semibold" weight="bold">
{/* TODO(reactist-codemod): duplicate Text weight props */}
Duplicate weight
</Text>
<Text as="span" as="label">
{/* TODO(reactist-codemod): duplicate Text as props */}
Duplicate element
</Text>
<Text {...props}>
{/* TODO(reactist-codemod): spread props may supply or override typography props */}
Spread
</Text>
<Heading level={1} size="larger">
{/* TODO(reactist-codemod): Heading metrics have no exact variant */}
24px
</Heading>
<Heading level={2}>
{/* TODO(reactist-codemod): Heading metrics have no exact variant */}
16px
</Heading>
<Heading level={3}>
{/* TODO(reactist-codemod): Heading metrics have no exact variant */}
14px
</Heading>
<Heading level={3} size="smaller">
{/* TODO(reactist-codemod): Heading metrics have no exact variant */}
12px
</Heading>
<Heading level={1} weight="medium">
{/* TODO(reactist-codemod): Heading metrics have no exact variant */}
Medium
</Heading>
<Heading level={1} weight="light">
{/* TODO(reactist-codemod): Heading metrics have no exact variant */}
Light
</Heading>
<Heading level={level}>
{/* TODO(reactist-codemod): dynamic Heading level */}
Dynamic level
</Heading>
<Heading level={1} size={size}>
{/* TODO(reactist-codemod): dynamic Heading size */}
Dynamic size
</Heading>
<Heading level={1} weight={weight}>
{/* TODO(reactist-codemod): dynamic Heading weight */}
Dynamic weight
</Heading>
<Heading {...props}>
{/* TODO(reactist-codemod): spread props may supply or override typography props; dynamic Heading level */}
Spread
</Heading>
<Heading {...props} level={level} size={size} weight={weight}>
{/* TODO(reactist-codemod): spread props may supply or override typography props; dynamic Heading level; dynamic Heading size; dynamic Heading weight */}
Spread and dynamic props
</Heading>
</>
)
}

export function Shadowed() {
const Text = (props: React.ComponentProps<'span'>) => <span {...props} />

return <Text size="caption">Shadowed Text</Text>
}
40 changes: 40 additions & 0 deletions codemods/__testfixtures__/typography-variants-safety.input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react'

import { Heading, Text } from '@doist/reactist'

const anchorRef = React.createRef<HTMLAnchorElement>()

function RequiredLink({ targetId }: { targetId: string }) {
return <a href={'#' + targetId} />
}

function span() {
return <span />
}

const UI = { Link: RequiredLink }

export function SafetyCases() {
return (
<>
<Text as="span">Safe span</Text>
<Text as="a" href="/x" ref={anchorRef}>
Anchor with props
</Text>
<Text as={RequiredLink} targetId="project-name">
Custom component with required props
</Text>
<Text as={span}>Lowercase variable component</Text>
<Text as={UI.Link}>Static member component</Text>
<Text variant="body-3" size="body">
Mixed Text props
</Text>
<Heading variant="heading-1" size="largest">
Mixed Heading props
</Heading>
<Heading render={<button type="button" />} level={1}>
Mixed Heading render props
</Heading>
</>
)
}
Loading
Loading