From 3e2d3c7d70dbc631b30f94c0aa6d908603955ab4 Mon Sep 17 00:00:00 2001 From: Jon Rohan Date: Fri, 29 May 2026 18:29:39 +0000 Subject: [PATCH 1/2] docs: make changeset descriptions terse and add re-evaluation instructions --- .../instructions/changesets.instructions.md | 30 ++++++++++++++++ .github/skills/changesets/SKILL.md | 36 +++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 .github/instructions/changesets.instructions.md diff --git a/.github/instructions/changesets.instructions.md b/.github/instructions/changesets.instructions.md new file mode 100644 index 00000000000..d488543f64f --- /dev/null +++ b/.github/instructions/changesets.instructions.md @@ -0,0 +1,30 @@ +--- +applyTo: '**' +--- + +# Keeping changesets accurate + +When you change source code in a package during a task, re-evaluate any existing +changeset files in `.changeset/` so they stay accurate and necessary. + +## Re-evaluate after every change + +After adding, editing, or reverting changes in this PR, check the following: + +- **Does a changeset still need to exist?** If your latest changes removed the + only consumer-facing impact (the work is now docs-only, test-only, or + CI/infra-only), delete the now-unnecessary changeset and add the + `skip changeset` label instead. +- **Is a changeset now required?** If you introduced new consumer-facing + behavior that no existing changeset covers, add one. +- **Is the bump type still correct?** If the scope grew (e.g. a `patch` became a + breaking change) or shrank, update the semver bump in the changeset frontmatter. +- **Is the description still accurate and terse?** Update the description to + match the final change, and keep it to one or two high-level, consumer-facing + sentences with no implementation detail. + +## Guidance + +For changeset format, semver bump selection, when a changeset is not needed, and +how to write terse descriptions, follow the `changesets` skill at +`.github/skills/changesets/SKILL.md`. diff --git a/.github/skills/changesets/SKILL.md b/.github/skills/changesets/SKILL.md index 23d10790d14..d3db5d68f78 100644 --- a/.github/skills/changesets/SKILL.md +++ b/.github/skills/changesets/SKILL.md @@ -27,11 +27,41 @@ Changeset files live in `.changeset/` and have a random name with a `.md` extens ActionMenu: Fix focus management when menu is closed with Escape key ``` -The description should be concise and follow this pattern: +The description becomes a single line item in the public changelog and release +notes, so keep it **terse**. Follow this pattern: - Start with the component or module name followed by a colon -- Describe WHAT changed and WHY -- Write from the consumer's perspective +- Use **one or two sentences** describing WHAT changed at a high level +- Write from the consumer's perspective — focus on the consumer-facing impact +- Do **not** include implementation details, file paths, CSS selector or symbol + names, internal rationale, or before/after explanations +- Do **not** use nested bullet lists or multi-paragraph walkthroughs. If a change + has several distinct parts, split it into + [separate changesets](#multiple-changesets-in-one-pr) instead + +### Writing good descriptions + +Aim for a description a consumer can scan in a few seconds. + +**Good** — terse and consumer-facing: + +```markdown +UnderlinePanels: Improve rendering performance when toggling tab icons +``` + +**Avoid** — verbose, implementation-focused, multi-clause: + +```markdown +UnderlinePanels: Eliminate the empty-tablist frame on mount and the cascading +re-render when icons toggle. Tabs and panels are now derived in render +(previously stored in state synced via `useEffect`), the list width is kept in a +ref instead of state, and `iconsVisible` / `loadingCounters` flow to each tab +via context — combined with `React.memo(Tab)`... +``` + +The "Avoid" example reads like a commit message or PR body. Save that level of +detail for the pull request description; the changeset should summarize the +consumer-facing effect only. ## How to create a changeset From 4a4d6524202cb50839b326a4f899c379a5cb43c0 Mon Sep 17 00:00:00 2001 From: Jon Rohan Date: Fri, 29 May 2026 18:50:06 +0000 Subject: [PATCH 2/2] Text: add whiteSpace prop to control CSS white-space property --- .changeset/text-white-space-prop.md | 5 ++++ packages/react/src/Text/Text.docs.json | 19 ++++++++++-- .../react/src/Text/Text.features.stories.tsx | 30 +++++++++++++++++++ packages/react/src/Text/Text.module.css | 20 +++++++++++++ packages/react/src/Text/Text.stories.tsx | 6 ++++ packages/react/src/Text/Text.test.tsx | 13 ++++++++ packages/react/src/Text/Text.tsx | 12 ++++++-- 7 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 .changeset/text-white-space-prop.md diff --git a/.changeset/text-white-space-prop.md b/.changeset/text-white-space-prop.md new file mode 100644 index 00000000000..eb44f170e05 --- /dev/null +++ b/.changeset/text-white-space-prop.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Text: Add `whiteSpace` prop to control the CSS `white-space` property diff --git a/packages/react/src/Text/Text.docs.json b/packages/react/src/Text/Text.docs.json index bc7eceb7ffe..8786d9c1cd5 100644 --- a/packages/react/src/Text/Text.docs.json +++ b/packages/react/src/Text/Text.docs.json @@ -33,6 +33,21 @@ }, { "id": "components-text-features--semibold-weight" + }, + { + "id": "components-text-features--pre-white-space" + }, + { + "id": "components-text-features--normal-white-space" + }, + { + "id": "components-text-features--nowrap-white-space" + }, + { + "id": "components-text-features--pre-wrap-white-space" + }, + { + "id": "components-text-features--pre-line-white-space" } ], "importPath": "@primer/react", @@ -41,7 +56,7 @@ "name": "as", "defaultValue": "'span'", "type": "React.ElementType" - }, + }, { "name": "size", "type": "'large' | 'medium' | 'small'" @@ -56,4 +71,4 @@ } ], "subcomponents": [] -} \ No newline at end of file +} diff --git a/packages/react/src/Text/Text.features.stories.tsx b/packages/react/src/Text/Text.features.stories.tsx index 2f96033076f..cbe5b656916 100644 --- a/packages/react/src/Text/Text.features.stories.tsx +++ b/packages/react/src/Text/Text.features.stories.tsx @@ -64,3 +64,33 @@ export const SemiboldWeight = () => ( Stylized text ) + +export const PreWhiteSpace = () => ( + + Stylized text + +) + +export const NormalWhiteSpace = () => ( + + Stylized text + +) + +export const NowrapWhiteSpace = () => ( + + Stylized text + +) + +export const PreWrapWhiteSpace = () => ( + + Stylized text + +) + +export const PreLineWhiteSpace = () => ( + + Stylized text + +) diff --git a/packages/react/src/Text/Text.module.css b/packages/react/src/Text/Text.module.css index 6b7b5552bcc..66ea88707dd 100644 --- a/packages/react/src/Text/Text.module.css +++ b/packages/react/src/Text/Text.module.css @@ -29,4 +29,24 @@ &:where([data-weight='semibold']) { font-weight: var(--base-text-weight-semibold); } + + &:where([data-white-space='pre']) { + white-space: pre; + } + + &:where([data-white-space='normal']) { + white-space: normal; + } + + &:where([data-white-space='nowrap']) { + white-space: nowrap; + } + + &:where([data-white-space='pre-wrap']) { + white-space: pre-wrap; + } + + &:where([data-white-space='pre-line']) { + white-space: pre-line; + } } diff --git a/packages/react/src/Text/Text.stories.tsx b/packages/react/src/Text/Text.stories.tsx index 36f2bab2566..77881a38f5f 100644 --- a/packages/react/src/Text/Text.stories.tsx +++ b/packages/react/src/Text/Text.stories.tsx @@ -38,4 +38,10 @@ Playground.argTypes = { }, options: ['light', 'normal', 'medium', 'semibold'], }, + whiteSpace: { + control: { + type: 'radio', + }, + options: ['pre', 'normal', 'nowrap', 'pre-wrap', 'pre-line'], + }, } diff --git a/packages/react/src/Text/Text.test.tsx b/packages/react/src/Text/Text.test.tsx index f6ed9002d77..5b640cf5c14 100644 --- a/packages/react/src/Text/Text.test.tsx +++ b/packages/react/src/Text/Text.test.tsx @@ -114,4 +114,17 @@ describe('Text', () => { expect(textElement).toBeInTheDocument() expect(textElement).toHaveClass(testClasses.ResponsiveLine) }) + + it.each(['pre', 'normal', 'nowrap', 'pre-wrap', 'pre-line'] as const)( + 'sets the data-white-space attribute when whiteSpace is %s', + whiteSpace => { + const {container} = render(Hello) + expect(container.firstChild).toHaveAttribute('data-white-space', whiteSpace) + }, + ) + + it('does not set the data-white-space attribute when whiteSpace is not provided', () => { + const {container} = render(Hello) + expect(container.firstChild).not.toHaveAttribute('data-white-space') + }) }) diff --git a/packages/react/src/Text/Text.tsx b/packages/react/src/Text/Text.tsx index 530c30ae8d1..9f27baf8323 100644 --- a/packages/react/src/Text/Text.tsx +++ b/packages/react/src/Text/Text.tsx @@ -10,16 +10,24 @@ export type TextProps = PolymorphicProps< { size?: 'large' | 'medium' | 'small' weight?: 'light' | 'normal' | 'medium' | 'semibold' + whiteSpace?: 'pre' | 'normal' | 'nowrap' | 'pre-wrap' | 'pre-line' className?: string } > // eslint-disable-next-line @typescript-eslint/no-explicit-any function Text(props: TextProps, ref: ForwardedRef) { - const {as: Component = 'span', className, size, weight, ...rest} = props + const {as: Component = 'span', className, size, weight, whiteSpace, ...rest} = props return ( - + ) }