Skip to content

feat(bestax-migrate): convert Pagination's page links and ellipsis in their li's place - #819

Merged
allxsmith merged 7 commits into
mainfrom
feat/bulma-classes-pagination-links
Sep 29, 2026
Merged

allxsmith merged 7 commits into
mainfrom
feat/bulma-classes-pagination-links

Conversation

@allxsmith

Copy link
Copy Markdown
Owner

Refs #809: its first item, Pagination's page links and ellipsis. Pagination.Link and Pagination.Ellipsis render the bare <li> around the element too, so each converts in that <li>'s place:

<ul className="pagination-list">
  <li>
    <a className="pagination-link is-current" aria-label="Page 46" aria-current="page" tabIndex={0}>46</a>
  </li>
  <li>
    <span className="pagination-ellipsis">&hellip;</span>
  </li>
</ul>
// becomes
<Pagination.List>
  <Pagination.Link active aria-label="Page 46" aria-current="page" tabIndex={0}>46</Pagination.Link>
  <Pagination.Ellipsis />
</Pagination.List>

How it decides

  • The <li> goes with it. A new entry field, parent, says the target renders a bare <tag> around the element. The element converts only as the only child of a bare one (no class, no attributes but a key, nothing beside it), and takes its place. The <li>'s key moves onto the component, so a .map() of <li key>s converts. The transform reports the element around each one as a new fact, soleChildOf. This is the reverse of how .select takes in its <select>.
  • is-current. active renders aria-current="page" before the props it's given, so a new modifier option, needsAttr, makes is-current the prop only beside an aria-current of the element's own. Without one it stays a class, which renders the same, and the same holds under a clsx condition. is-disabled stays a class, since disabled also writes aria-disabled and tabIndex={-1}.
  • tabIndex. A link writes tabIndex={0} before the props it's given, so it's a defaults value, as on Previous and Next.
  • The ellipsis. Pagination.Ellipsis renders its own … and writes a className it's given in place of its own class. Two more fields cover that: rendersText means it converts only holding exactly that text (&hellip; or the character) and closes itself, and classNameReplaces means it converts only with no other class.

The other surfaces

  • MCP (feat(bestax-mcp)): the lookup says the component renders the <li> around the element, what the ellipsis takes, and the aria-current condition on is-current. The agree test gives the planner the <li>, the text and the aria-current, and it agrees across the sweep, including a lookup check for the ellipsis that runs after the tag check, as the planner's does.
  • ESLint (fix(eslint-plugin)): regenerated.
  • Skill and docs: component-map rows, a prop-map row for is-current, the ellipsis in the prop map's color and no-helper lists, the context, children and defaults explanations in unmappables in place of the family recipe, and the guide's list. bestax-migrate/CLAUDE.md names parent among the planner's shapes.

Checked

  • The truth test renders each entry inside its <li>, with each modifier (given what it needs) and each helper, and the fuzz covers them. The props e2e typechecks them, active included.
  • Plan tests pin each rule: the bare <li> and each way it isn't, a key on both, is-current with and without aria-current (and under a condition), and the ellipsis's text and class.
  • The pagination fixture adds the links, the ellipsis and each refusal, and renders the same before and after.
  • pnpm all passes locally.

The rest of #809 (the tabs' inner elements, .icon-text, .file) stays open there.

… their li's place

Pagination.Link and Pagination.Ellipsis render the bare <li> around the
element too, so a .pagination-link or .pagination-ellipsis converts only as
the only child of a bare <li>, and takes that <li>'s place, key and all.
A new entry field, parent, says so, and the transform reports the element
around each one (soleChildOf) to check it.

A link writes tabIndex before the props it's given, so it's a default.
active also renders aria-current="page", so a new modifier option,
needsAttr, makes is-current a prop only beside an aria-current of the
element's own; without one it stays a class. is-disabled stays a class.

Pagination.Ellipsis renders its own ellipsis and writes a className it's
given in place of its class, so two more fields, rendersText and
classNameReplaces, have it convert only holding exactly that and carrying
no other class.

Refs #809
…ent, or its own text

A lookup of .pagination-link or .pagination-ellipsis says the component renders the bare <li>
around it, to write in that <li>'s place; that the ellipsis renders its own text and takes no
other class; and that is-current becomes active only beside an aria-current of the element's
own. The index carries parent, rendersText and classNameReplaces, and the agree test gives the
planner the <li>, the text and the aria-current the lookup states as conditions.
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d5368c7f-c3fd-4f9e-b133-7d7125a46e4c

📥 Commits

Reviewing files that changed from the base of the PR and between fb3b3d6 and d65aacf.

⛔ Files ignored due to path filters (1)
  • eslint-plugin/src/generated/metadata.ts is excluded by !**/generated/**
📒 Files selected for processing (20)
  • bestax-mcp/data/bulma-classes.json
  • bestax-mcp/data/skills.json
  • bestax-mcp/src/__tests__/bulma-classes-agree.test.ts
  • bestax-mcp/src/__tests__/server.test.ts
  • bestax-mcp/src/bulma-classes.ts
  • bestax-migrate/CLAUDE.md
  • bestax-migrate/e2e/bulma-classes-props.test.ts
  • bestax-migrate/e2e/bulma-classes-truth.test.ts
  • bestax-migrate/src/sources/bulma-classes/__testfixtures__/pagination.input.tsx
  • bestax-migrate/src/sources/bulma-classes/__testfixtures__/pagination.output.tsx
  • bestax-migrate/src/sources/bulma-classes/__tests__/plan.test.ts
  • bestax-migrate/src/sources/bulma-classes/__tests__/transform.test.ts
  • bestax-migrate/src/sources/bulma-classes/class-map.ts
  • bestax-migrate/src/sources/bulma-classes/plan.ts
  • bestax-migrate/src/sources/bulma-classes/transform.ts
  • docs/docs/guides/getting-started/migration/bulma-classes.md
  • scripts/gen-mcp-index.mjs
  • skills/bestax-migrate/references/bulma-classes/component-map.md
  • skills/bestax-migrate/references/bulma-classes/prop-map.md
  • skills/bestax-migrate/references/bulma-classes/unmappables.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

The core is the pagination-link and pagination-ellipsis entries in bestax-migrate/src/sources/bulma-classes/class-map.ts against bulma-ui's Pagination.tsx; the parent, rendersText, classNameReplaces and needsAttr checks in plan.ts; and in transform.ts the soleChildOf and text facts and the replacesParent step that puts the component in the <li>'s place. Priorities: does each conversion render exactly the markup it replaces, including the <li>, its key, comments, aria-current and tabIndex; and is everything that would differ refused.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://08deccf3.bestax.pages.dev

Comment thread bestax-migrate/src/sources/bulma-classes/plan.ts
Comment thread bestax-migrate/e2e/bulma-classes-truth.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 2 blocking · 3 advisory

# Severity Area Finding Location
1 🟡 Minor Correctness The only-child refusal is computed from the element's own parent, so it can never fire for a parent entry — a .pagination-link/.pagination-ellipsis whose bare <li> is the only child of a component converts silently, moving any cloneElement-injected props/ref from the <li> onto the <a>/<span> bestax-migrate/src/sources/bulma-classes/plan.ts:324
2 🟡 Minor Coverage neededFor is threaded only into the fixed-token sweep, so the new active={cond} conversion (is-current under a clsx condition beside aria-current) is decided by plan.test.ts but never rendered and compared against the raw markup bestax-migrate/e2e/bulma-classes-truth.test.ts:288
3 🔵 Advisory Correctness A key on the <a> alone (the <li> unkeyed) is not refused: it moves from a position React ignores to the array element itself, turning an index-keyed list into a page-keyed one. Markup is identical and the "before" was a React key warning, so this is an improvement rather than a defect — but it is a reconciliation change the attr:key refusal does not cover bestax-migrate/src/sources/bulma-classes/plan.ts:338
4 🔵 Advisory Correctness needsAttr.value is never read by the planner — only attributes.has(name) is checked — so .is-current beside aria-current="step" also converts. That is sound (the element's own aria-current reaches the <a> through {...rest} and wins over the 'page' the prop writes), but the field is documentation/messaging only, not a guard bestax-migrate/src/sources/bulma-classes/class-map.ts:46
5 🔵 Advisory Robustness .pagination-link inside a .menu-list is planned twice: the <li> refuses as Menu.Item and leaves a context:Menu.Item TODO, while the <a> converts and deletes that same <li>. The resulting markup is correct; the leftover TODO describes an element that is gone. Contrived markup, no test covers it bestax-migrate/src/sources/bulma-classes/transform.ts:1396

Overall: The change is sound and unusually well fenced — the new parent / rendersText / classNameReplaces / needsAttr fields each encode a real property of Pagination.Link and Pagination.Ellipsis as written (bulma-ui/src/components/Pagination.tsx:309-347), and I checked each against the component: the <li> is bare, tabIndex and aria-current="page" are written before {...rest} so an element's own value wins, the ellipsis ignores children and lets a given className replace its own, and is-disabled correctly stays a class because disabled also writes aria-disabled and tabIndex="-1". The riskiest part is the new "the component lands in the holder's position" shape, because every existing refusal is phrased about the element — finding 1 is the one place that asymmetry still bites. Look at that first, then at finding 2, since the truth suite is what the table's safety claim rests on.

Residual risk:

  • Markup equality for the shipped conversions — refuted empirically. pnpm exec turbo run test --filter=bestax-migrate passes (2071 tests), including e2e/bulma-classes-fixtures-render.test.ts, which renders Pages and PageRefusals before and after and compares normalised HTML — so the <li>, the <li>'s key through pages.map, aria-current="page", tabIndex={0}, the has-text-danger helper and the self-closing ellipsis are confirmed identical, not merely reasoned. bestax-mcp (243 tests) and the eslint plugin pass; gen:mcp:check, gen:eslint-meta:check, gen:catalog:check, check:conformance (22/22) and format:check are all clean.
  • Something other than a bare <li> slipping through — refuted. holderOf yields a holder only when soleChild(holder) === element, and soleChild filters only whitespace React drops, so a comment, a sibling text node, a {expr} wrapper, a fragment or {cond && <a/>} all leave soleChildOf unset and refuse; a computed className on the <li> gives tokens: null and an empty one tokens: [], both of which fail tokens === undefined. The fixture covers the class-on-<li>, sibling-content and no-<li> variants.
  • The ellipsis converting on text it does not render — refuted. textOf compares jsxTextRenders of the single text child against …, so ..., a {expr}, a self-closing span and a text-plus-comment child all refuse; &hellip; decodes to the same character, including across the multi-line form.
  • Class-attribute order differing (pagination-link is-current my-app → pagination-link my-app is-current) — accepted, not a risk. normalizeHtml sorts classes by design, and class order in the attribute has no CSS effect.
  • Open: the two findings above, plus advisories 3–5. Nothing in the MCP lookup or the skills/docs prose reads false to me: the <li>, ellipsis and aria-current sentences in bulma-classes.ts and unmappables.md each match what the components do.

🏄 Clean little set on this one — the codemod paddles out, grabs the <li> on the way past, and lands the whole page link in one piece. Just two soft spots in the lineup: nobody's checking who's holding the <li> when a component wraps it, and the truth suite quietly skips the active={cond} wave. Patch those and it's good to go.

…onent's only child

The only-child refusal reads the element's own parent, which for a page
link is the <li> it takes the place of, so a <li> that was a component's
only child converted: whatever that component handed the <li> through
cloneElement would land on the <a>. A new fact, holderOnlyChildOf, carries
that component for the element around, and the planner refuses on it.

The truth test now renders is-current under a clsx condition too, beside
the aria-current it needs, and asserts each such flag was rendered.
@allxsmith

Copy link
Copy Markdown
Owner Author

On the three advisories from the deep review:

  1. Leaving it, as you say it's an improvement: a key on the <a> alone sat where React ignores it, and in the component's place it keys the list as the author wrote it.
  2. Right, needsAttr.value isn't a guard, since the element's own aria-current wins whatever its value. It's there for the lookup and the docs, which tell a reader what to add (aria-current="page", what the prop would render).
  3. Agreed it's contrived (a .pagination-link in a .menu-list item) and the markup comes out right. Leaving the stale TODO rather than teaching the menu item to look inside for a link that replaces it.

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify

Check the fixes for the two open threads in 0e72683: the holderOnlyChildOf fact and its refusal in plan(), and the conditional pass in the truth test that now renders is-current beside the aria-current it needs.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://08e18069.bestax.pages.dev

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review (verify) — 2 resolved · 0 open

# Thread Verdict Location
1 only-child refusal never reached the <li> the component replaces ✅ Verified fixed — resolved bestax-migrate/src/sources/bulma-classes/plan.ts:343
2 needsAttr not threaded into the conditional pass, so active={cond} was never rendered ✅ Verified fixed — resolved bestax-migrate/e2e/bulma-classes-truth.test.ts:783

Thread 1 — holderOnlyChildOf. The fact is set from onlyChildOf(elementPath.parent, bestaxLocals) (transform.ts:1094) inside the same holderOf(elementPath) guard that produces soleChildOf, so it reads the <li>'s own parent — the wrapper this finding was about — and PASS_THROUGH_PARENTS / bestaxLocals keep a fragment or a real Pagination.List from tripping it. The refusal sits inside if (parent) at plan.ts:343, after the bare holder check, so it covers both parent entries in class-map.ts (pagination-link, pagination-ellipsis) and nothing else. The transform test drives it end to end: a .pagination-link in a bare <li> inside a local Tooltip stays markup and reports only-child:Pagination.Link.

Thread 2 — conditional pass. onTag is now defaultsFor(entry, tag) plus neededFor(entry, [token]), so plan() keeps is-current in conditional and renderBoth writes active={when['is-current']} against clsx-shaped raw markup across every CONDITIONS value. The new needing.filter(token => !flagged.has(token)) assertion is non-vacuous by construction: plan.ts:734 admits a needsAttr modifier as a flag only when attributes.has(modifier.needsAttr.name), so dropping the attribute again empties conversion.conditional, leaves flagged without is-current, and fails the assertion.

Overall: This pass settled the two threads the prior deep review left open and reviewed no commits — no new findings were sought or raised. Both fixes are real and empirically checked: the conditional truth suites pass at 503 tests and the whole package is green at 2073 tests across 52 suites, with no fixture churn, so nothing that used to convert stopped. Nothing is left for the human here beyond a normal read of the diff.

Residual risk: none newly identified in this pass — verify mode raises no new findings. Carried over from the first review and unchanged: needsAttr has exactly one entry in class-map.ts (is-current → aria-current), so the second conditional test's "beside each other modifier" pass still has no case to exercise for it; that becomes real only if pagination-link gains a second modifier.

🏄 Both waves got ridden clean, dude — the <li> keeps its wrapper's vibes and the truth suite actually renders the active flag now. 2073 tests green, nothing left bobbing out back. Good to go.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://e2559e9e.bestax.pages.dev

@allxsmith allxsmith added the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📸 Story screenshots at handoff — 23476ca

No Storybook stories map to this PR's changed files — nothing to screenshot. (workflow run)

…gination-links

# Conflicts:
#	bestax-mcp/data/skills.json
#	bestax-migrate/src/sources/bulma-classes/__tests__/plan.test.ts
#	bestax-migrate/src/sources/bulma-classes/plan.ts
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://c493f672.bestax.pages.dev

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://b2f64cc5.bestax.pages.dev

@allxsmith
allxsmith merged commit 83b2772 into main Sep 29, 2026
13 checks passed
@allxsmith
allxsmith deleted the feat/bulma-classes-pagination-links branch September 29, 2026 03:06
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.22.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.12.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.18.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.2.13 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deep-review needs-human-review Loop converged (or contested): awaiting owner review + manual merge released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant