From 56963ebc36a8e8fe0912b5f0fba6b376ed856cce Mon Sep 17 00:00:00 2001 From: Josh Black Date: Fri, 31 Jul 2026 13:17:04 -0500 Subject: [PATCH 1/7] Add merge props migration driver Refine the adoption report, add validated migration examples, and introduce the coordinator skill and bounded batch agent for ADR-025. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 00bfd295-6ed9-4119-8d63-103a0b3c63e8 --- .changeset/clean-radios-merge.md | 5 + .changeset/tidy-token-events.md | 5 + .changeset/visible-class-merging.md | 5 + .../migrate-to-merge-props-batch.agent.md | 68 ++++++ .../skills/migrate-to-merge-props/SKILL.md | 207 ++++++++++++++++++ packages/react/src/Radio/Radio.test.tsx | 1 + packages/react/src/Radio/Radio.tsx | 32 ++- packages/react/src/Radio/Radio.types.test.tsx | 6 + packages/react/src/Token/TokenBase.tsx | 54 +++-- .../react/src/Token/__tests__/Token.test.tsx | 11 + .../src/VisuallyHidden/VisuallyHidden.tsx | 10 +- 11 files changed, 362 insertions(+), 42 deletions(-) create mode 100644 .changeset/clean-radios-merge.md create mode 100644 .changeset/tidy-token-events.md create mode 100644 .changeset/visible-class-merging.md create mode 100644 .github/agents/migrate-to-merge-props-batch.agent.md create mode 100644 .github/skills/migrate-to-merge-props/SKILL.md create mode 100644 packages/react/src/Radio/Radio.types.test.tsx diff --git a/.changeset/clean-radios-merge.md b/.changeset/clean-radios-merge.md new file mode 100644 index 00000000000..5ddb7fd741d --- /dev/null +++ b/.changeset/clean-radios-merge.md @@ -0,0 +1,5 @@ +--- +'@primer/react': major +--- + +Radio: Prevent overriding the input type so the component always renders radio semantics. diff --git a/.changeset/tidy-token-events.md b/.changeset/tidy-token-events.md new file mode 100644 index 00000000000..7da0e9d015b --- /dev/null +++ b/.changeset/tidy-token-events.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Token: Run removal keyboard behavior before consumer keydown handlers. diff --git a/.changeset/visible-class-merging.md b/.changeset/visible-class-merging.md new file mode 100644 index 00000000000..4c07d423a18 --- /dev/null +++ b/.changeset/visible-class-merging.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +VisuallyHidden: Merge consumer class names with component styles consistently. diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md new file mode 100644 index 00000000000..375d777ce30 --- /dev/null +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -0,0 +1,68 @@ +--- +name: migrate-to-merge-props-batch +description: Performs one bounded ADR-025 mergeProps migration batch and returns validation and migration-report evidence to the coordinator. +tools: + - read + - search + - edit + - execute +skills: + - migrate-to-merge-props + - style-guide + - changesets + - storybook +--- + +You implement exactly one batch assigned by the `migrate-to-merge-props` +coordinator. Use that skill for the ADR, validated patterns, scope, stack +contract, and validation requirements. + +## Required input + +Do not begin without: + +- The current stack branch. +- Exact component names, file paths, and report locations. +- The maximum batch size. +- Total and affected-file counts before the batch. +- Targeted validation commands. + +Confirm the branch and findings still match the assignment. Stop if the batch +is too large, overlaps another entry, includes unassigned files, or requires an +unresolved architecture, accessibility, or public API decision. + +## Workflow + +1. Read ADR-025, the repository instructions, `mergeProps`, and the validated + production examples named by the coordinator skill. +2. Confirm every assigned finding still appears in the report. +3. Inspect public props, spread order, handlers, styles, class names, refs, + invariant attributes, tests, stories, and nearby prior art. +4. Implement only the assigned findings and tightly coupled tests, type tests, + or changesets. +5. Pass component props first and consumer props second. +6. Keep component class names as strings. +7. Omit absent optional consumer handlers from the second argument. +8. Compose refs separately. +9. Omit or gate invariant attributes in public types and apply them after the + merged spread. +10. Format, lint, test, type-check, and run any requested Storybook, + accessibility, visual, or build validation. +11. Regenerate the report and confirm the assigned findings disappeared with + no new findings. + +Do not disable enforcement, add blanket suppressions, manipulate report output, +expose `mergeProps` publicly, fix unrelated findings, select another batch, +create stack entries, commit, or change stack structure. + +## Result format + +| Field | Content | +| ------------- | -------------------------------------------------------------------- | +| Branch | Current stack branch | +| Assigned | Component names, paths, and original locations | +| Resolved | Findings removed from the regenerated report | +| Files changed | Source, tests, types, stories, and changesets edited | +| Remaining | Updated finding and affected-file counts | +| Validation | Failed commands and relevant summaries, or `Passed` | +| Blockers | Findings requiring an ADR, API, accessibility, or ownership decision | diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md new file mode 100644 index 00000000000..e7fb7b75593 --- /dev/null +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -0,0 +1,207 @@ +--- +name: migrate-to-merge-props +description: 'Use when: coordinating ADR-025 adoption through measured mergeProps batches in a stacked migration. Plans bounded batches, invokes migrate-to-merge-props-batch, verifies report progress, and removes temporary migration tooling at completion.' +--- + +# Migrate to mergeProps + +Coordinate adoption of +[`ADR-025: Prop merging conventions`](../../../contributor-docs/adrs/adr-025-prop-merging.md). + +ADR-025 is the source of truth for correctness. The ESLint rule and migration +report identify likely work, but a finding is not complete until the component +preserves its behavior, accessibility contract, public types, refs, handlers, +class names, and styles according to the ADR. + +## Sources of truth and migration artifacts + +| Artifact | Location | Role | Lifetime | +| ----------------- | ------------------------------------------------------------------------------------- | ----------------------------------------- | --------- | +| ADR | `contributor-docs/adrs/adr-025-prop-merging.md` | Defines the target prop-merging contract | Permanent | +| Style guidance | `contributor-docs/style.md`, "Merge shared props intentionally" | Shows the component-authoring convention | Permanent | +| Utility | `packages/react/src/utils/mergeProps.ts` | Implements shared merge behavior | Permanent | +| Adoption rule | `primer/prefer-merge-props` in `packages/eslint-config/src/rules/preferMergeProps.ts` | Detects unmerged component root props | Permanent | +| Migration report | `node script/merge-props-migration-status.mts` | Measures remaining findings | Temporary | +| Batch agent | `.github/agents/migrate-to-merge-props-batch.agent.md` | Implements one assigned migration batch | Temporary | +| Coordinator skill | `.github/skills/migrate-to-merge-props/SKILL.md` | Plans, delegates, measures, and cleans up | Temporary | + +## Scope + +- Included: findings reported in `packages/react/src/**/*.{ts,tsx}` by + `primer/prefer-merge-props`. +- Excluded: tests, stories, nested JSX, generated output, `.agents/**/*`, and + pure pass-through roots that have one spread and no component-authored prop + other than `ref` or `key`. +- Refs are always composed separately with `useMergedRefs`. +- Default maximum batch size: five findings. +- Default run size: two sequential batches per invocation. +- Never split one source file across separate batches. + +## Validated migration patterns + +Use these production examples as prior art: + +- `packages/react/src/VisuallyHidden/VisuallyHidden.tsx`: class-name-only + merging. +- `packages/react/src/Token/TokenBase.tsx`: internal behavior first, optional + consumer event handler second, and ref composition outside `mergeProps`. +- `packages/react/src/Radio/Radio.tsx`: a component-owned attribute omitted + from the public type and applied after merged props. + +Apply these rules: + +1. Pass component-authored props first and consumer-authored props second. +2. Keep the component-authored `className` as a string. Let `mergeProps` use + `clsx` to combine it with consumer class values. +3. Do not add an optional consumer handler to the second argument when it is + absent. An explicit `undefined` entry would replace the internal handler + instead of composing it. +4. Keep refs outside `mergeProps`. +5. Put invariant attributes after the merged spread and omit them from the + public prop type. Use a discriminated union when ownership depends on the + rendered element. +6. Preserve intentional consumer `undefined` precedence for ordinary props. +7. Add tests for event order, cancellation, controlled attributes, type + exclusions, or other behavior that is not already covered. + +## Stack contract + +Use one linear stack: + +1. `copilot/update-style-docs-prop-merging`: ADR, utility, adoption rule, and + report foundation. +2. `migrate/merge-props-migrator`: validated examples, tooling refinements, + coordinator skill, and batch agent. +3. `migrate/merge-props-batch-01` through + `migrate/merge-props-batch-NN`: one bounded migration batch per entry. +4. `migrate/merge-props-cleanup`: temporary artifact removal after the report + reaches zero. + +Use only non-interactive stack commands: + +```shell +gh stack view --json +gh stack add migrate/merge-props-batch-NN +gh stack submit --auto +``` + +Do not create the cleanup entry while in-scope findings remain. + +## Coordinator workflow + +### 1. Resume the stack + +Inspect: + +```shell +git status --short --branch +gh stack view --json +``` + +Confirm the current branch is the top active entry. If a lower entry changed, +rebase the upstack before selecting findings. Do not mix unrelated worktree +changes into a migration entry. + +### 2. Establish the baseline + +Run: + +```shell +node script/merge-props-migration-status.mts +``` + +Record the total finding count, affected-file count, exact paths and locations, +findings already assigned to stack entries, and blockers. Do not edit generated +output or exclusions to lower the count. + +### 3. Plan non-overlapping batches + +Group findings by component family and remediation pattern. Each batch must: + +- Contain at most the selected finding limit. +- Own complete files rather than splitting a file between entries. +- Be reviewable as one coherent change. +- Avoid files already assigned to an existing stack entry. +- Include tightly coupled tests and changesets. + +Calculate the remaining batch count using the selected limit. + +### 4. Ask how much work to run + +Use a structured prompt containing: + +| Field | Default | Constraint | +| ----------- | ------- | ---------------------------------------- | +| Batch size | `5` | Integer greater than or equal to `1` | +| Run mode | Limited | Limited batches or all remaining batches | +| Batch count | `2` | Integer greater than or equal to `1` | + +Show the baseline, calculated remaining batches, and exact assignments before +editing. Prefer a small set of sequential batches over running the full +migration at once. + +### 5. Run each selected batch + +For each batch: + +1. Create `migrate/merge-props-batch-NN` with `gh stack add`. +2. Invoke `migrate-to-merge-props-batch` with the branch, exact paths and + locations, maximum size, before counts, and targeted validation commands. +3. Review the returned diff and evidence. +4. Add or update changesets for consumer-facing behavior or type changes. +5. Commit only the assigned batch. +6. Regenerate the report. +7. Confirm assigned findings disappeared, no new findings appeared, and counts + changed by the expected amount. +8. Stop before another entry if validation or report evidence fails. + +Run code-editing batch agents sequentially because every branch depends on the +previous branch. + +### 6. Handle blockers + +Do not force a mechanical merge when a finding requires a decision about: + +- Consumer veto before internal behavior. +- Whether an ARIA, semantic, or stable identifier prop is component-owned. +- A breaking public type change. +- Polymorphic attribute ownership. +- Ref composition. + +Return the file, component, current behavior, ADR conflict, and smallest +decision needed. + +### 7. Finish or pause + +In limited mode, stop after the selected number of entries and leave the stack +resumable. + +When the report reaches zero, add `migrate/merge-props-cleanup` and remove: + +- `.github/skills/migrate-to-merge-props/` +- `.github/agents/migrate-to-merge-props-batch.agent.md` +- `script/merge-props-migration-status.mts` +- The `merge-props` job from `.github/workflows/migration-status.yml` + +Keep ADR-025, style guidance, `mergeProps`, the ESLint rule, and regression +tests. Mark ADR-025 adopted and implemented in the cleanup entry after final +validation. + +### 8. Submit and report + +Run: + +```shell +gh stack submit --auto +gh stack view --json +``` + +Return completed stack entries, requested/completed/remaining batches, resolved +paths, updated counts, validation failures, and blockers. Do not claim +completion before the report is zero and the cleanup entry exists. + +## Example invocation + +> Use `/migrate-to-merge-props` with at most five findings per entry. Run two +> batches, regenerate the report after each entry, and stop on unexpected +> results. diff --git a/packages/react/src/Radio/Radio.test.tsx b/packages/react/src/Radio/Radio.test.tsx index 1a7b9756dcd..d20071927db 100644 --- a/packages/react/src/Radio/Radio.test.tsx +++ b/packages/react/src/Radio/Radio.test.tsx @@ -21,6 +21,7 @@ describe('Radio', () => { const radio = getByRole('radio') expect(radio).toBeDefined() + expect(radio).toHaveAttribute('type', 'radio') }) it('renders data-component attribute', () => { diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx index 5ea79439a9f..d14e073f96e 100644 --- a/packages/react/src/Radio/Radio.tsx +++ b/packages/react/src/Radio/Radio.tsx @@ -5,6 +5,7 @@ import {clsx} from 'clsx' import sharedClasses from '../Checkbox/shared.module.css' import classes from './Radio.module.css' import type {WithSlotMarker} from '../utils/types' +import {mergeProps} from '../utils/mergeProps' export type RadioProps = { /** @@ -32,7 +33,7 @@ export type RadioProps = { * Indicates whether the radio button must be checked before the form can be submitted */ required?: boolean -} & InputHTMLAttributes +} & Omit, 'type'> /** * An accessible, native radio component for selecting one option from a list. @@ -56,7 +57,6 @@ const Radio = React.forwardRef( const radioGroupContext = useContext(RadioGroupContext) const handleOnChange: ChangeEventHandler = e => { radioGroupContext?.onChange && radioGroupContext.onChange(e) - onChange && onChange(e) } const name = nameProp || radioGroupContext?.name @@ -69,17 +69,25 @@ const Radio = React.forwardRef( return ( ) diff --git a/packages/react/src/Radio/Radio.types.test.tsx b/packages/react/src/Radio/Radio.types.test.tsx new file mode 100644 index 00000000000..a08856d1c1e --- /dev/null +++ b/packages/react/src/Radio/Radio.types.test.tsx @@ -0,0 +1,6 @@ +import Radio from '.' + +export function doesNotAcceptType() { + // @ts-expect-error Radio always renders an input with type="radio" + return +} diff --git a/packages/react/src/Token/TokenBase.tsx b/packages/react/src/Token/TokenBase.tsx index 03202c6f0cb..17025fc667f 100644 --- a/packages/react/src/Token/TokenBase.tsx +++ b/packages/react/src/Token/TokenBase.tsx @@ -1,10 +1,10 @@ import type {KeyboardEvent} from 'react' import React from 'react' -import {clsx} from 'clsx' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './TokenBase.module.css' import {defaultTokenSize, type TokenSizeKeys} from './constants' import {isTokenInteractive} from './utils' +import {mergeProps} from '../utils/mergeProps' export type {TokenSizeKeys} @@ -55,30 +55,38 @@ const TokenBase = React.forwardRef { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { + onRemove() + } + } + return ( ) => { - onKeyDown && onKeyDown(event) - - if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { - onRemove() - } - }} - className={clsx(classes.TokenBase, className)} - data-cursor-is-interactive={isTokenInteractive({ - as: Component, - onClick: rest.onClick, - onFocus: rest.onFocus, - tabIndex: rest.tabIndex, - disabled: rest.disabled, - })} - data-size={size} - id={id?.toString()} - {...(Component === 'button' - ? (rest as React.ButtonHTMLAttributes) - : Component === 'a' - ? (rest as React.AnchorHTMLAttributes) - : (rest as React.HTMLAttributes))} + {...mergeProps( + { + onKeyDown: handleKeyDown, + className: classes.TokenBase, + 'data-cursor-is-interactive': isTokenInteractive({ + as: Component, + onClick: rest.onClick, + onFocus: rest.onFocus, + tabIndex: rest.tabIndex, + disabled: rest.disabled, + }), + 'data-size': size, + }, + { + ...(Component === 'button' + ? (rest as React.ButtonHTMLAttributes) + : Component === 'a' + ? (rest as React.AnchorHTMLAttributes) + : (rest as React.HTMLAttributes)), + ...(onKeyDown ? {onKeyDown} : {}), + className, + id: id?.toString(), + }, + )} // TypeScript cannot resolve polymorphic ref types at compile time // This assertion is safe because the ref will match the actual rendered element // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/react/src/Token/__tests__/Token.test.tsx b/packages/react/src/Token/__tests__/Token.test.tsx index 73c01124c2a..ac07bbe22fe 100644 --- a/packages/react/src/Token/__tests__/Token.test.tsx +++ b/packages/react/src/Token/__tests__/Token.test.tsx @@ -63,6 +63,17 @@ const testTokenComponent = (Component: React.ComponentType { + const calls: string[] = [] + const {getByText} = HTMLRender( + calls.push('remove')} onKeyDown={() => calls.push('consumer')} />, + ) + + fireEvent.keyDown(getByText('token'), {key: 'Backspace'}) + + expect(calls).toEqual(['remove', 'consumer']) + }) + it('adds className to rendered component', () => { const {getByText} = HTMLRender() const domNode = getByText('token') diff --git a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx index d05c496352f..d2b8231d952 100644 --- a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx +++ b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx @@ -1,6 +1,6 @@ -import {clsx} from 'clsx' import type React from 'react' import {type HTMLAttributes} from 'react' +import {mergeProps} from '../utils/mergeProps' import classes from './VisuallyHidden.module.css' /** @@ -13,12 +13,8 @@ import classes from './VisuallyHidden.module.css' * * @see https://www.scottohara.me/blog/2023/03/21/visually-hidden-hack.html */ -export const VisuallyHidden = ({className, children, ...rest}: VisuallyHiddenProps) => { - return ( - - {children} - - ) +export const VisuallyHidden = ({children, ...rest}: VisuallyHiddenProps) => { + return {children} } export type VisuallyHiddenProps = React.PropsWithChildren< From 9475d36ff3c5c2f7e6d8b4bd6724d85521083ab4 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Fri, 31 Jul 2026 13:28:21 -0500 Subject: [PATCH 2/7] Keep migration driver focused on coordination Move implementation and adoption-tool changes out of the driver entry and document stack ownership for foundation, driver, batch, and cleanup work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 00bfd295-6ed9-4119-8d63-103a0b3c63e8 --- .changeset/clean-radios-merge.md | 5 -- .changeset/tidy-token-events.md | 5 -- .changeset/visible-class-merging.md | 5 -- .../migrate-to-merge-props-batch.agent.md | 9 ++- .../skills/migrate-to-merge-props/SKILL.md | 67 ++++++++++++++----- packages/react/src/Radio/Radio.test.tsx | 1 - packages/react/src/Radio/Radio.tsx | 32 ++++----- packages/react/src/Radio/Radio.types.test.tsx | 6 -- packages/react/src/Token/TokenBase.tsx | 54 +++++++-------- .../react/src/Token/__tests__/Token.test.tsx | 11 --- .../src/VisuallyHidden/VisuallyHidden.tsx | 10 ++- 11 files changed, 98 insertions(+), 107 deletions(-) delete mode 100644 .changeset/clean-radios-merge.md delete mode 100644 .changeset/tidy-token-events.md delete mode 100644 .changeset/visible-class-merging.md delete mode 100644 packages/react/src/Radio/Radio.types.test.tsx diff --git a/.changeset/clean-radios-merge.md b/.changeset/clean-radios-merge.md deleted file mode 100644 index 5ddb7fd741d..00000000000 --- a/.changeset/clean-radios-merge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': major ---- - -Radio: Prevent overriding the input type so the component always renders radio semantics. diff --git a/.changeset/tidy-token-events.md b/.changeset/tidy-token-events.md deleted file mode 100644 index 7da0e9d015b..00000000000 --- a/.changeset/tidy-token-events.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -Token: Run removal keyboard behavior before consumer keydown handlers. diff --git a/.changeset/visible-class-merging.md b/.changeset/visible-class-merging.md deleted file mode 100644 index 4c07d423a18..00000000000 --- a/.changeset/visible-class-merging.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -VisuallyHidden: Merge consumer class names with component styles consistently. diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index 375d777ce30..b093977be22 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -33,8 +33,8 @@ unresolved architecture, accessibility, or public API decision. ## Workflow -1. Read ADR-025, the repository instructions, `mergeProps`, and the validated - production examples named by the coordinator skill. +1. Read ADR-025, the repository instructions, `mergeProps`, its tests, and any + relevant migrated component from a lower batch entry. 2. Confirm every assigned finding still appears in the report. 3. Inspect public props, spread order, handlers, styles, class names, refs, invariant attributes, tests, stories, and nearby prior art. @@ -53,7 +53,10 @@ unresolved architecture, accessibility, or public API decision. Do not disable enforcement, add blanket suppressions, manipulate report output, expose `mergeProps` publicly, fix unrelated findings, select another batch, -create stack entries, commit, or change stack structure. +create stack entries, commit, or change stack structure. Do not refine the +adoption rule, report, utility, workflow, coordinator, or agent in a batch +entry. Return those issues as blockers so the coordinator can update the owning +lower stack entry and rebase. ## Result format diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index e7fb7b75593..89773b78497 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -37,16 +37,17 @@ class names, and styles according to the ADR. - Default run size: two sequential batches per invocation. - Never split one source file across separate batches. -## Validated migration patterns +## Migration rules and prior art -Use these production examples as prior art: +Before the first implementation batch, inspect: -- `packages/react/src/VisuallyHidden/VisuallyHidden.tsx`: class-name-only - merging. -- `packages/react/src/Token/TokenBase.tsx`: internal behavior first, optional - consumer event handler second, and ref composition outside `mergeProps`. -- `packages/react/src/Radio/Radio.tsx`: a component-owned attribute omitted - from the public type and applied after merged props. +- The component example in `contributor-docs/style.md`. +- `packages/react/src/utils/mergeProps.ts`. +- `packages/react/src/utils/__tests__/mergeProps.test.ts`. + +After implementation batches exist, inspect a relevant migrated component from +a lower stack entry as prior art. Do not add reference component migrations to +the driver entry. Apply these rules: @@ -64,16 +65,44 @@ Apply these rules: 7. Add tests for event order, cancellation, controlled attributes, type exclusions, or other behavior that is not already covered. +## Stack ownership contract + +Every change must live in the entry that owns its concern: + +- `copilot/update-style-docs-prop-merging` owns ADR-025, `mergeProps`, the + adoption rule, rule tests, report script, and migration-status workflow. + Refinements to any of those artifacts must be committed to this foundation + entry. +- `migrate/merge-props-migrator` owns only the coordinator skill and batch + agent. It must not contain component migrations, migration changesets, or + adoption-tool refinements. +- `migrate/merge-props-batch-NN` owns only its assigned component + implementation, tightly coupled tests or stories, and required changesets. +- `migrate/merge-props-cleanup` owns final temporary-artifact removal and the + ADR status update. + +If batch work exposes a problem in the adoption rule, report, utility, or +workflow: + +1. Stop the batch without working around the problem. +2. Navigate to `copilot/update-style-docs-prop-merging`. +3. Commit the refinement there. +4. Rebase every upstack entry. +5. Regenerate the report before resuming the batch. + +If the coordinator or agent instructions need refinement, update +`migrate/merge-props-migrator` and rebase the batch entries above it. + ## Stack contract Use one linear stack: -1. `copilot/update-style-docs-prop-merging`: ADR, utility, adoption rule, and - report foundation. -2. `migrate/merge-props-migrator`: validated examples, tooling refinements, - coordinator skill, and batch agent. +1. `copilot/update-style-docs-prop-merging`: ADR, utility, adoption mechanism, + report, workflow, and all refinements to those artifacts. +2. `migrate/merge-props-migrator`: coordinator skill and batch agent only. 3. `migrate/merge-props-batch-01` through - `migrate/merge-props-batch-NN`: one bounded migration batch per entry. + `migrate/merge-props-batch-NN`: one bounded component implementation batch + per entry. 4. `migrate/merge-props-cleanup`: temporary artifact removal after the report reaches zero. @@ -102,6 +131,9 @@ Confirm the current branch is the top active entry. If a lower entry changed, rebase the upstack before selecting findings. Do not mix unrelated worktree changes into a migration entry. +Inspect the diff for each existing stack entry and confirm it still follows the +stack ownership contract before creating another batch. + ### 2. Establish the baseline Run: @@ -149,11 +181,12 @@ For each batch: locations, maximum size, before counts, and targeted validation commands. 3. Review the returned diff and evidence. 4. Add or update changesets for consumer-facing behavior or type changes. -5. Commit only the assigned batch. -6. Regenerate the report. -7. Confirm assigned findings disappeared, no new findings appeared, and counts +5. Confirm the entry contains no adoption-tool or driver changes. +6. Commit only the assigned batch. +7. Regenerate the report. +8. Confirm assigned findings disappeared, no new findings appeared, and counts changed by the expected amount. -8. Stop before another entry if validation or report evidence fails. +9. Stop before another entry if validation or report evidence fails. Run code-editing batch agents sequentially because every branch depends on the previous branch. diff --git a/packages/react/src/Radio/Radio.test.tsx b/packages/react/src/Radio/Radio.test.tsx index d20071927db..1a7b9756dcd 100644 --- a/packages/react/src/Radio/Radio.test.tsx +++ b/packages/react/src/Radio/Radio.test.tsx @@ -21,7 +21,6 @@ describe('Radio', () => { const radio = getByRole('radio') expect(radio).toBeDefined() - expect(radio).toHaveAttribute('type', 'radio') }) it('renders data-component attribute', () => { diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx index d14e073f96e..5ea79439a9f 100644 --- a/packages/react/src/Radio/Radio.tsx +++ b/packages/react/src/Radio/Radio.tsx @@ -5,7 +5,6 @@ import {clsx} from 'clsx' import sharedClasses from '../Checkbox/shared.module.css' import classes from './Radio.module.css' import type {WithSlotMarker} from '../utils/types' -import {mergeProps} from '../utils/mergeProps' export type RadioProps = { /** @@ -33,7 +32,7 @@ export type RadioProps = { * Indicates whether the radio button must be checked before the form can be submitted */ required?: boolean -} & Omit, 'type'> +} & InputHTMLAttributes /** * An accessible, native radio component for selecting one option from a list. @@ -57,6 +56,7 @@ const Radio = React.forwardRef( const radioGroupContext = useContext(RadioGroupContext) const handleOnChange: ChangeEventHandler = e => { radioGroupContext?.onChange && radioGroupContext.onChange(e) + onChange && onChange(e) } const name = nameProp || radioGroupContext?.name @@ -69,25 +69,17 @@ const Radio = React.forwardRef( return ( ) diff --git a/packages/react/src/Radio/Radio.types.test.tsx b/packages/react/src/Radio/Radio.types.test.tsx deleted file mode 100644 index a08856d1c1e..00000000000 --- a/packages/react/src/Radio/Radio.types.test.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import Radio from '.' - -export function doesNotAcceptType() { - // @ts-expect-error Radio always renders an input with type="radio" - return -} diff --git a/packages/react/src/Token/TokenBase.tsx b/packages/react/src/Token/TokenBase.tsx index 17025fc667f..03202c6f0cb 100644 --- a/packages/react/src/Token/TokenBase.tsx +++ b/packages/react/src/Token/TokenBase.tsx @@ -1,10 +1,10 @@ import type {KeyboardEvent} from 'react' import React from 'react' +import {clsx} from 'clsx' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './TokenBase.module.css' import {defaultTokenSize, type TokenSizeKeys} from './constants' import {isTokenInteractive} from './utils' -import {mergeProps} from '../utils/mergeProps' export type {TokenSizeKeys} @@ -55,38 +55,30 @@ const TokenBase = React.forwardRef { - const handleKeyDown = (event: KeyboardEvent) => { - if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { - onRemove() - } - } - return ( ) - : Component === 'a' - ? (rest as React.AnchorHTMLAttributes) - : (rest as React.HTMLAttributes)), - ...(onKeyDown ? {onKeyDown} : {}), - className, - id: id?.toString(), - }, - )} + onKeyDown={(event: KeyboardEvent) => { + onKeyDown && onKeyDown(event) + + if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { + onRemove() + } + }} + className={clsx(classes.TokenBase, className)} + data-cursor-is-interactive={isTokenInteractive({ + as: Component, + onClick: rest.onClick, + onFocus: rest.onFocus, + tabIndex: rest.tabIndex, + disabled: rest.disabled, + })} + data-size={size} + id={id?.toString()} + {...(Component === 'button' + ? (rest as React.ButtonHTMLAttributes) + : Component === 'a' + ? (rest as React.AnchorHTMLAttributes) + : (rest as React.HTMLAttributes))} // TypeScript cannot resolve polymorphic ref types at compile time // This assertion is safe because the ref will match the actual rendered element // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/react/src/Token/__tests__/Token.test.tsx b/packages/react/src/Token/__tests__/Token.test.tsx index ac07bbe22fe..73c01124c2a 100644 --- a/packages/react/src/Token/__tests__/Token.test.tsx +++ b/packages/react/src/Token/__tests__/Token.test.tsx @@ -63,17 +63,6 @@ const testTokenComponent = (Component: React.ComponentType { - const calls: string[] = [] - const {getByText} = HTMLRender( - calls.push('remove')} onKeyDown={() => calls.push('consumer')} />, - ) - - fireEvent.keyDown(getByText('token'), {key: 'Backspace'}) - - expect(calls).toEqual(['remove', 'consumer']) - }) - it('adds className to rendered component', () => { const {getByText} = HTMLRender() const domNode = getByText('token') diff --git a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx index d2b8231d952..d05c496352f 100644 --- a/packages/react/src/VisuallyHidden/VisuallyHidden.tsx +++ b/packages/react/src/VisuallyHidden/VisuallyHidden.tsx @@ -1,6 +1,6 @@ +import {clsx} from 'clsx' import type React from 'react' import {type HTMLAttributes} from 'react' -import {mergeProps} from '../utils/mergeProps' import classes from './VisuallyHidden.module.css' /** @@ -13,8 +13,12 @@ import classes from './VisuallyHidden.module.css' * * @see https://www.scottohara.me/blog/2023/03/21/visually-hidden-hack.html */ -export const VisuallyHidden = ({children, ...rest}: VisuallyHiddenProps) => { - return {children} +export const VisuallyHidden = ({className, children, ...rest}: VisuallyHiddenProps) => { + return ( + + {children} + + ) } export type VisuallyHiddenProps = React.PropsWithChildren< From 241b8d78c89ec3c441c45b337e42b8fb265d685b Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 11 Aug 2026 16:49:06 -0500 Subject: [PATCH 3/7] Preserve inline class name merging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9541ca7f-0281-4223-a839-39d75393d922 --- .github/agents/migrate-to-merge-props-batch.agent.md | 6 ++++-- .github/skills/migrate-to-merge-props/SKILL.md | 11 +++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index b093977be22..17ab40cb4cd 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -41,11 +41,13 @@ unresolved architecture, accessibility, or public API decision. 4. Implement only the assigned findings and tightly coupled tests, type tests, or changesets. 5. Pass component props first and consumer props second. -6. Keep component class names as strings. +6. Destructure consumer class names and merge them inline with component class + names using `clsx`; omit `className` from the second `mergeProps` argument. 7. Omit absent optional consumer handlers from the second argument. 8. Compose refs separately. 9. Omit or gate invariant attributes in public types and apply them after the - merged spread. + merged spread. Preserve existing consumer precedence for attributes that + were historically overridable. 10. Format, lint, test, type-check, and run any requested Storybook, accessibility, visual, or build validation. 11. Regenerate the report and confirm the assigned findings disappeared with diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index 89773b78497..15ea349a377 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -52,15 +52,18 @@ the driver entry. Apply these rules: 1. Pass component-authored props first and consumer-authored props second. -2. Keep the component-authored `className` as a string. Let `mergeProps` use - `clsx` to combine it with consumer class values. +2. Destructure the consumer `className` and combine it with component class + names inline using `clsx`. Omit `className` from the consumer props passed to + `mergeProps`; do not delegate class-name composition to the utility. 3. Do not add an optional consumer handler to the second argument when it is absent. An explicit `undefined` entry would replace the internal handler instead of composing it. 4. Keep refs outside `mergeProps`. 5. Put invariant attributes after the merged spread and omit them from the - public prop type. Use a discriminated union when ownership depends on the - rendered element. + public prop type. Only treat an attribute as invariant when the component + already owns it; preserve existing consumer precedence for historically + overridable attributes. Use a discriminated union when ownership depends on + the rendered element. 6. Preserve intentional consumer `undefined` precedence for ordinary props. 7. Add tests for event order, cancellation, controlled attributes, type exclusions, or other behavior that is not already covered. From c59c26e54bdffedce730b7951664f9ad56a14936 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 11 Aug 2026 16:49:50 -0500 Subject: [PATCH 4/7] Limit migration changesets to contract changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9541ca7f-0281-4223-a839-39d75393d922 --- .../agents/migrate-to-merge-props-batch.agent.md | 10 +++++++--- .github/skills/migrate-to-merge-props/SKILL.md | 13 ++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index 17ab40cb4cd..c89377ee9f7 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -39,7 +39,7 @@ unresolved architecture, accessibility, or public API decision. 3. Inspect public props, spread order, handlers, styles, class names, refs, invariant attributes, tests, stories, and nearby prior art. 4. Implement only the assigned findings and tightly coupled tests, type tests, - or changesets. + or changesets required by genuine public contract changes. 5. Pass component props first and consumer props second. 6. Destructure consumer class names and merge them inline with component class names using `clsx`; omit `className` from the second `mergeProps` argument. @@ -48,9 +48,13 @@ unresolved architecture, accessibility, or public API decision. 9. Omit or gate invariant attributes in public types and apply them after the merged spread. Preserve existing consumer precedence for attributes that were historically overridable. -10. Format, lint, test, type-check, and run any requested Storybook, +10. Treat the migration as a behavior-preserving refactor and do not add a + changeset by default. Add one only when correctness requires a public + behavior or type change, such as documenting through types that an accepted + prop was never applied. +11. Format, lint, test, type-check, and run any requested Storybook, accessibility, visual, or build validation. -11. Regenerate the report and confirm the assigned findings disappeared with +12. Regenerate the report and confirm the assigned findings disappeared with no new findings. Do not disable enforcement, add blanket suppressions, manipulate report output, diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index 15ea349a377..83a6cda8ba1 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -67,6 +67,10 @@ Apply these rules: 6. Preserve intentional consumer `undefined` precedence for ordinary props. 7. Add tests for event order, cancellation, controlled attributes, type exclusions, or other behavior that is not already covered. +8. Treat component migrations as behavior-preserving refactors and do not add a + changeset by default. Add one only when correctness requires a genuine public + contract change, such as updating a type to communicate that an accepted prop + was never applied. ## Stack ownership contract @@ -80,7 +84,8 @@ Every change must live in the entry that owns its concern: agent. It must not contain component migrations, migration changesets, or adoption-tool refinements. - `migrate/merge-props-batch-NN` owns only its assigned component - implementation, tightly coupled tests or stories, and required changesets. + implementation, tightly coupled tests or stories, and changesets required by + unavoidable public contract changes. - `migrate/merge-props-cleanup` owns final temporary-artifact removal and the ADR status update. @@ -157,7 +162,8 @@ Group findings by component family and remediation pattern. Each batch must: - Own complete files rather than splitting a file between entries. - Be reviewable as one coherent change. - Avoid files already assigned to an existing stack entry. -- Include tightly coupled tests and changesets. +- Include tightly coupled tests and only the changesets required by genuine + public contract changes. Calculate the remaining batch count using the selected limit. @@ -183,7 +189,8 @@ For each batch: 2. Invoke `migrate-to-merge-props-batch` with the branch, exact paths and locations, maximum size, before counts, and targeted validation commands. 3. Review the returned diff and evidence. -4. Add or update changesets for consumer-facing behavior or type changes. +4. Do not add a changeset for a behavior-preserving migration. Add or update one + only when the batch requires a genuine public behavior or type change. 5. Confirm the entry contains no adoption-tool or driver changes. 6. Commit only the assigned batch. 7. Regenerate the report. From 27384de0a436bfaba67a064195112fe81c75813f Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 11 Aug 2026 16:50:43 -0500 Subject: [PATCH 5/7] Require refs before merged props Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9541ca7f-0281-4223-a839-39d75393d922 --- .github/agents/migrate-to-merge-props-batch.agent.md | 3 ++- .github/skills/migrate-to-merge-props/SKILL.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index c89377ee9f7..2b845ebc9ba 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -44,7 +44,8 @@ unresolved architecture, accessibility, or public API decision. 6. Destructure consumer class names and merge them inline with component class names using `clsx`; omit `className` from the second `mergeProps` argument. 7. Omit absent optional consumer handlers from the second argument. -8. Compose refs separately. +8. Compose refs separately and place the `ref` prop before the `mergeProps` + spread. 9. Omit or gate invariant attributes in public types and apply them after the merged spread. Preserve existing consumer precedence for attributes that were historically overridable. diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index 83a6cda8ba1..0449df63a71 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -58,7 +58,8 @@ Apply these rules: 3. Do not add an optional consumer handler to the second argument when it is absent. An explicit `undefined` entry would replace the internal handler instead of composing it. -4. Keep refs outside `mergeProps`. +4. Keep refs outside `mergeProps` and place the `ref` prop before the merged + spread. 5. Put invariant attributes after the merged spread and omit them from the public prop type. Only treat an attribute as invariant when the component already owns it; preserve existing consumer precedence for historically From 69589940097297206c173fb55eac4740e1b456d7 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 11 Aug 2026 17:00:16 -0500 Subject: [PATCH 6/7] Plan migration batches by component volume Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9541ca7f-0281-4223-a839-39d75393d922 --- .../migrate-to-merge-props-batch.agent.md | 10 ++-- .../skills/migrate-to-merge-props/SKILL.md | 46 +++++++++++++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index 2b845ebc9ba..24f570057b9 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -23,13 +23,14 @@ Do not begin without: - The current stack branch. - Exact component names, file paths, and report locations. -- The maximum batch size. +- The target changed LOC and acceptable fuzz. - Total and affected-file counts before the batch. - Targeted validation commands. Confirm the branch and findings still match the assignment. Stop if the batch -is too large, overlaps another entry, includes unassigned files, or requires an -unresolved architecture, accessibility, or public API decision. +is materially larger than the target, overlaps another entry, splits an +assigned component family without justification, includes unassigned files, or +requires an unresolved architecture, accessibility, or public API decision. ## Workflow @@ -57,6 +58,9 @@ unresolved architecture, accessibility, or public API decision. accessibility, visual, or build validation. 12. Regenerate the report and confirm the assigned findings disappeared with no new findings. +13. Measure additions plus deletions relative to the preceding entry. Treat the + LOC target as a planning guide rather than a hard cap when the assigned + component-family boundary is coherent. Do not disable enforcement, add blanket suppressions, manipulate report output, expose `mergeProps` publicly, fix unrelated findings, select another batch, diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index 0449df63a71..7b0aaf774c3 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -33,7 +33,10 @@ class names, and styles according to the ADR. pure pass-through roots that have one spread and no component-authored prop other than `ref` or `key`. - Refs are always composed separately with `useMergedRefs`. -- Default maximum batch size: five findings. +- Default batch target: roughly 500 changed lines, measured as additions plus + deletions relative to the preceding stack entry. +- The LOC target is intentionally fuzzy. Component families are the dominant + boundary, so a coherent batch may land below or moderately above 500 lines. - Default run size: two sequential batches per invocation. - Never split one source file across separate batches. @@ -157,30 +160,41 @@ output or exclusions to lower the count. ### 3. Plan non-overlapping batches -Group findings by component family and remediation pattern. Each batch must: +Use the `/primer-query` skill to query current `@primer/react` JSX usage for the +affected component families before assigning batches. Give a high-volume +component its own entry so review and rollout risk remain isolated. As a +default, treat 500 or more JSX uses as high volume, but adjust at an obvious +break in the current usage distribution. + +Group the remaining findings by component family and remediation pattern. Pack +multiple lower-volume component families into an entry until its estimated diff +approaches the selected changed-LOC target. Prefer keeping a family together +over hitting the target exactly. Each batch must: -- Contain at most the selected finding limit. - Own complete files rather than splitting a file between entries. +- Keep a component family together unless the family is independently too large + or contains distinct remediation risks. - Be reviewable as one coherent change. - Avoid files already assigned to an existing stack entry. - Include tightly coupled tests and only the changesets required by genuine public contract changes. -Calculate the remaining batch count using the selected limit. +Estimate the remaining batch count from the planned family groupings and changed +LOC target. Recalculate after each completed entry using its actual diff size. ### 4. Ask how much work to run Use a structured prompt containing: -| Field | Default | Constraint | -| ----------- | ------- | ---------------------------------------- | -| Batch size | `5` | Integer greater than or equal to `1` | -| Run mode | Limited | Limited batches or all remaining batches | -| Batch count | `2` | Integer greater than or equal to `1` | +| Field | Default | Constraint | +| ------------------ | ------- | ---------------------------------------- | +| Target changed LOC | `500` | Integer greater than or equal to `1` | +| Run mode | Limited | Limited batches or all remaining batches | +| Batch count | `2` | Integer greater than or equal to `1` | -Show the baseline, calculated remaining batches, and exact assignments before -editing. Prefer a small set of sequential batches over running the full -migration at once. +Show the baseline, usage-based high-volume classification, estimated remaining +batches, and exact assignments before editing. Prefer a small set of sequential +batches over running the full migration at once. ### 5. Run each selected batch @@ -188,7 +202,8 @@ For each batch: 1. Create `migrate/merge-props-batch-NN` with `gh stack add`. 2. Invoke `migrate-to-merge-props-batch` with the branch, exact paths and - locations, maximum size, before counts, and targeted validation commands. + locations, target changed LOC and acceptable fuzz, before counts, and + targeted validation commands. 3. Review the returned diff and evidence. 4. Do not add a changeset for a behavior-preserving migration. Add or update one only when the batch requires a genuine public behavior or type change. @@ -197,7 +212,10 @@ For each batch: 7. Regenerate the report. 8. Confirm assigned findings disappeared, no new findings appeared, and counts changed by the expected amount. -9. Stop before another entry if validation or report evidence fails. +9. Measure additions plus deletions relative to the preceding entry. Accept + moderate variance from the target when the component boundary is coherent; + stop and re-plan if the batch is materially larger than estimated. +10. Stop before another entry if validation or report evidence fails. Run code-editing batch agents sequentially because every branch depends on the previous branch. From 273dcf3969d3b5a7b1c09ba8f8cc5ee3fa190dc8 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Wed, 12 Aug 2026 11:20:06 -0500 Subject: [PATCH 7/7] Avoid clsx for single class names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 84a0d8ca-13d3-4cb2-ab24-18fa2d9a8a14 --- .github/agents/migrate-to-merge-props-batch.agent.md | 6 ++++-- .github/skills/migrate-to-merge-props/SKILL.md | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/agents/migrate-to-merge-props-batch.agent.md b/.github/agents/migrate-to-merge-props-batch.agent.md index 24f570057b9..acd11e4110d 100644 --- a/.github/agents/migrate-to-merge-props-batch.agent.md +++ b/.github/agents/migrate-to-merge-props-batch.agent.md @@ -42,8 +42,10 @@ requires an unresolved architecture, accessibility, or public API decision. 4. Implement only the assigned findings and tightly coupled tests, type tests, or changesets required by genuine public contract changes. 5. Pass component props first and consumer props second. -6. Destructure consumer class names and merge them inline with component class - names using `clsx`; omit `className` from the second `mergeProps` argument. +6. Destructure consumer class names. Merge component and consumer class names + inline using `clsx` only when there are multiple values to combine; pass a + single class name directly. Omit `className` from the second `mergeProps` + argument. 7. Omit absent optional consumer handlers from the second argument. 8. Compose refs separately and place the `ref` prop before the `mergeProps` spread. diff --git a/.github/skills/migrate-to-merge-props/SKILL.md b/.github/skills/migrate-to-merge-props/SKILL.md index 7b0aaf774c3..a1fc4a16344 100644 --- a/.github/skills/migrate-to-merge-props/SKILL.md +++ b/.github/skills/migrate-to-merge-props/SKILL.md @@ -55,9 +55,11 @@ the driver entry. Apply these rules: 1. Pass component-authored props first and consumer-authored props second. -2. Destructure the consumer `className` and combine it with component class - names inline using `clsx`. Omit `className` from the consumer props passed to - `mergeProps`; do not delegate class-name composition to the utility. +2. Destructure the consumer `className`. Combine component and consumer class + names inline using `clsx` only when there are multiple values to combine. If + only one class name remains, pass it directly instead of wrapping it in + `clsx`. Omit `className` from the consumer props passed to `mergeProps`; do + not delegate class-name composition to the utility. 3. Do not add an optional consumer handler to the second argument when it is absent. An explicit `undefined` entry would replace the internal handler instead of composing it.