Skip to content

docs(bulma-ui): generate API page Props, Import, Overview and CSS variables (layout pilot) - #399

Closed
allxsmith wants to merge 11 commits into
mainfrom
claude/api-docs-generation-3nl3eb
Closed

allxsmith wants to merge 11 commits into
mainfrom
claude/api-docs-generation-3nl3eb

Conversation

@allxsmith

@allxsmith allxsmith commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Pull Request

Please do not merge — this is a pilot opened for review and preview comparison.

Description

The API reference pages are hand-written, and the sections that merely restate the source drift
from it. Nothing in CI compared a props table to its interface, so the tables diverged in shape
(3-column in elements/button.md, 4-column | Field | in layout/footer.md, | Prop | in the
components/avatar.md exemplar) and in content (button.md renders textColor as
`'primary'` | ... (see docs); hero.md and card.md instead dump 19-member colour unions).

This takes machine ownership of the four derivable regions and leaves everything else alone.
Regions are delimited by <!-- bestax:generated <id> --> markers:

Region Source
overview the summary sentence, from the component's TSDoc
import the public barrel
props the <X>Props interfaces, via the TypeScript compiler API
cssvars new section, parsed from the SCSS

Prose outside the markers is preserved byte-for-byte, so pages keep their admonitions and their
hand-written Usage / Accessibility / Related / Additional sections. Section order is now
Overview → Import → Usage → Accessibility → Related → Additional → Props → CSS & Sass Variables.
Deleting a marker pair opts a region out.

This is not typedoc. Three choices keep the output reading as hand-written:

  • Own members only. Resolving the heritage yields ~250 DOM attributes plus ~65 helper props, so
    the generator reads the interface's declared members and emits the one catch-all ... row the
    pages already wrote by hand.
  • Types from AST source text, never checker.typeToString — the checker expands
    (typeof validColors)[number] into 19 literals. Wide colour unions instead become a link to the
    existing Valid values page.
  • Prop descriptions were seeded from the existing docs tables, not the @property blocks,
    because the hand-written ones are consistently richer. Nothing curated was lost.

@property blocks migrate onto interface members as inline TSDoc, so descriptions are now
verifiable by position and show up in users' editors. Component TSDoc summaries were seeded from
the pages for the same reason — the existing ones ("Bulma Hero component root.") were worse than
the prose they'd have replaced.

Scope: layout/ only (6 pages). MANAGED_CATEGORIES in scripts/lib/api-sources.mjs gates the
rest; follow-ups add one category at a time.

Verification done

  • No regressions: every prop's Default and Description cell on all six pages diffed against its
    pre-migration version — no default changed, no description got shorter, no prop dropped.
  • Six props were added that the hand-written tables had missed: Hero textColor; Level as,
    hasTextCentered, href, target, rel. Each verified against the interface.
  • Live examples unchanged: block counts identical per page (7 / 1 / 6 / 5 / 5 / 3), all mount
    and render, zero runtime errors.
  • Generator is idempotent — a second run produces a clean tree.
  • pnpm all green; component-catalog.md regenerated byte-identical (the Overview round-trip is
    exact).

Two decisions worth a reviewer's opinion

  1. footer.scss declares $footer-color: false !default. Rendered verbatim as `false` rather
    than —, since sass-false means the color: declaration is skipped entirely and — would read
    as "no default". Easy to flip.
  2. The markers are visible in llms-full.txt (~46 now, ~350 once all 87 pages migrate);
    docusaurus-plugin-llms does not strip HTML comments. llms.txt itself stays clean because the
    generator writes a frontmatter description:. The alternative — an MDX <PropsTable/> — would
    remove the prop tables from that surface entirely, which seemed strictly worse.

Related Issue(s)

Refs #

Type of Change

  • Documentation
  • Build tooling
  • Bug fix
  • New feature
  • Refactor
  • Performance

Affected packages:

  • bulma-ui (@allxsmith/bestax-bulma) — inline TSDoc on src/layout/* (comments only; the
    summaries ship in the .d.ts)
  • docs (@allxsmith/bestax-docs) — the six migrated pages
  • create-bestax

Checklist

  • My code follows the project style guidelines
  • I have performed a self-review of my code
  • All new and existing tests passed (3678, unchanged — this touches comments only)
  • I have added/updated documentation as needed
  • If this PR changes commands, conventions, or package structure, the affected CLAUDE.md
    files are updated (docs/CLAUDE.md, bulma-ui/CLAUDE.md, CONTRIBUTING-COMPONENTS.md)
  • I have added tests that prove my fix is effective — n/a, no runtime behaviour changes
  • I have updated Storybook stories as needed — n/a
  • Any relevant dependencies are updated — none added

Screenshots / Demos

Best reviewed on the deploy preview, comparing each layout/ page against production:

  • /docs/api/layout/container — note there is no CSS & Sass Variables section (Container has
    SCSS but registers no CSS variables)
  • /docs/api/layout/footer — cv.getVar resolved to var(--bulma-scheme-main-bis); the false default
  • /docs/api/layout/hero — Hero.Head / Body / Foot sub-tables, the inlined HeroSize union,
    and exactly 8 CSS variables (Bulma registers navbar-item-*, tabs-*, title-* and subtitle-*
    inside Hero's per-colour loop; those are correctly excluded)
  • /docs/api/layout/level — one row, proving the singular cv.register-var path
  • /docs/api/layout/media — the six-line hsla() collapsed to one line

Additional Context

CI gate placement. The staleness check lives in scripts/check-conformance.mjs as two new
sub-checks rather than a new workflow step: docs-generated recomputes each managed page in memory
and diffs, docs-section-order holds the section order and marker presence. That file already runs
in CI, and it keeps this PR out of .github/**. Both failure modes were verified by tampering.

registerVarsKeys moved to scripts/lib/scss-vars.mjs so the generator shares one SCSS parser;
key extraction is unchanged, verified key-for-key across all 26 repo partials (366 keys, 0 diffs).

New requirement: generating reads node_modules/bulma for the stock components' SCSS — a
deliberate departure from gen-component-catalog.mjs's no-install rule. CI installs first, and the
script fails with an actionable message rather than an ENOENT. A Bulma version bump will now
produce a real diff here, which is the point.


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for forwarding the rel attribute when rendering level items as links.
  • Documentation

    • API reference pages now include consistently generated overview, import, props, and CSS/Sass variable sections.
    • Component prop documentation is clearer and more consistent, including defaults and inherited HTML attributes.
    • Updated contribution and documentation guidance for generating and validating API content.
  • Bug Fixes

    • Improved documentation checks to detect missing, outdated, or incorrectly ordered generated sections.

claude added 2 commits July 27, 2026 23:22
The API reference pages are hand-written, and the sections that merely
restate the source drift from it. Nothing in CI compared a props table to
its interface, so the tables diverged in shape (3-column in button.md,
4-column "| Field |" in footer.md, "| Prop |" in the avatar.md exemplar)
and in content (button.md renders textColor as "'primary' | ... (see
docs)"; hero.md and card.md instead dump 19-member colour unions).

Take machine ownership of the four derivable regions, delimited by
<!-- bestax:generated <id> --> markers, and leave everything else alone:

  overview  the summary sentence, from the component's TSDoc
  import    from the public barrel
  props     from the <X>Props interfaces (TypeScript compiler API)
  cssvars   new section, parsed from the SCSS

Prose outside the markers is preserved byte-for-byte, so a page keeps its
admonitions and its hand-written Usage/Accessibility/Related/Additional
sections. Section order now runs Overview, Import, Usage, ..., Props, CSS
& Sass Variables. Deleting a marker pair opts a region out.

Readability is the reason this is not typedoc. Props tables list only the
interface's own members plus one catch-all row, because resolving the
heritage yields ~250 DOM attributes and ~65 helper props. Types render
from AST source text rather than checker.typeToString, which expands
(typeof validColors)[number] into 19 literals; wide colour unions become a
link to the existing Valid values page.

Prop descriptions migrate from the @Property blocks above each interface
onto the members as inline TSDoc, seeded from the existing docs tables
where those are richer, so no curated prose is lost. @Property was a
comment about the interface: unverifiable, and invisible in IntelliSense.
Component TSDoc summaries are seeded FROM the pages for the same reason —
the existing ones ("Bulma Hero component root.") are worse than the prose
they would have replaced.

Pilot covers layout/ (6 pages); MANAGED_CATEGORIES gates the rest.

The staleness gate lives in check-conformance.mjs as two new sub-checks
(docs-generated recomputes each page in memory and diffs; docs-section-order
holds the order and marker presence) rather than a new CI step, since that
file already runs in CI. registerVarsKeys moves to scripts/lib/scss-vars.mjs
so the generator shares one SCSS parser; key extraction is unchanged,
verified key-for-key against all 26 partials.

Generating requires node_modules/bulma for the stock components' SCSS, a
deliberate departure from gen-component-catalog.mjs's no-install rule; it
fails with an actionable message rather than an ENOENT.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
Reviewing the rendered pages showed a regression against the hand-written
tables: container.md documented `fluid`, `widescreen`, `fullhd` and `isMax`
as defaulting to `false`, but the generator emitted an em dash. Those props
are destructured with no initializer, so there was nothing for the AST
default-reader to find.

An optional prop typed exactly `boolean` is `undefined` by default, which
every consumer treats as off, so `false` is both accurate and what the
tables already said. Infer it. Safe to do library-wide: nothing under
bulma-ui/src defaults a boolean to true outside its destructuring pattern,
and an explicit destructured default or `@defaultValue` still wins.

Verified by diffing every prop's Default and Description cell on all six
pilot pages against their pre-migration versions: no default changed, no
description got shorter, no prop was dropped. The generator additionally
documents six props the hand-written tables had missed — Hero's `textColor`
and Level's `as`, `hasTextCentered`, `href`, `target` and `rel`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 199 files, which is 99 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e126cd6c-88dc-4a0a-b85a-4623b2171a53

📥 Commits

Reviewing files that changed from the base of the PR and between ba0a54e and a387289.

📒 Files selected for processing (199)
  • CONTRIBUTING-COMPONENTS.md
  • bestax-migrate/CLAUDE.md
  • bestax-migrate/e2e/kitchen-sink.test.ts
  • bulma-ui/CLAUDE.md
  • bulma-ui/src/columns/Column.tsx
  • bulma-ui/src/columns/Columns.tsx
  • bulma-ui/src/components/Avatar.tsx
  • bulma-ui/src/components/Avatars.tsx
  • bulma-ui/src/components/Badge.tsx
  • bulma-ui/src/components/Breadcrumb.tsx
  • bulma-ui/src/components/Card.tsx
  • bulma-ui/src/components/Carousel.tsx
  • bulma-ui/src/components/Collapse.tsx
  • bulma-ui/src/components/Dialog.tsx
  • bulma-ui/src/components/Dropdown.tsx
  • bulma-ui/src/components/Loading.tsx
  • bulma-ui/src/components/Menu.tsx
  • bulma-ui/src/components/Message.tsx
  • bulma-ui/src/components/Modal.tsx
  • bulma-ui/src/components/Navbar.tsx
  • bulma-ui/src/components/Pagination.tsx
  • bulma-ui/src/components/Panel.tsx
  • bulma-ui/src/components/Reveal.tsx
  • bulma-ui/src/components/Sidebar.tsx
  • bulma-ui/src/components/Steps.tsx
  • bulma-ui/src/components/Tabs.tsx
  • bulma-ui/src/components/Toast.tsx
  • bulma-ui/src/components/Tooltip.tsx
  • bulma-ui/src/elements/Block.tsx
  • bulma-ui/src/elements/Box.tsx
  • bulma-ui/src/elements/Button.tsx
  • bulma-ui/src/elements/Buttons.tsx
  • bulma-ui/src/elements/Code.tsx
  • bulma-ui/src/elements/Content.tsx
  • bulma-ui/src/elements/Delete.tsx
  • bulma-ui/src/elements/Divider.tsx
  • bulma-ui/src/elements/Emphasis.tsx
  • bulma-ui/src/elements/Figure.tsx
  • bulma-ui/src/elements/Icon.tsx
  • bulma-ui/src/elements/IconText.tsx
  • bulma-ui/src/elements/Image.tsx
  • bulma-ui/src/elements/Link.tsx
  • bulma-ui/src/elements/LinkButton.tsx
  • bulma-ui/src/elements/ListItem.tsx
  • bulma-ui/src/elements/Notification.tsx
  • bulma-ui/src/elements/OrderedList.tsx
  • bulma-ui/src/elements/Paragraph.tsx
  • bulma-ui/src/elements/Pre.tsx
  • bulma-ui/src/elements/Progress.tsx
  • bulma-ui/src/elements/Skeleton.tsx
  • bulma-ui/src/elements/Span.tsx
  • bulma-ui/src/elements/Strong.tsx
  • bulma-ui/src/elements/SubTitle.tsx
  • bulma-ui/src/elements/Table.tsx
  • bulma-ui/src/elements/Tag.tsx
  • bulma-ui/src/elements/Tags.tsx
  • bulma-ui/src/elements/Tbody.tsx
  • bulma-ui/src/elements/Td.tsx
  • bulma-ui/src/elements/Tfoot.tsx
  • bulma-ui/src/elements/Th.tsx
  • bulma-ui/src/elements/Thead.tsx
  • bulma-ui/src/elements/Title.tsx
  • bulma-ui/src/elements/Tr.tsx
  • bulma-ui/src/elements/UnorderedList.tsx
  • bulma-ui/src/form/Autocomplete.tsx
  • bulma-ui/src/form/Checkbox.tsx
  • bulma-ui/src/form/Checkboxes.tsx
  • bulma-ui/src/form/Control.tsx
  • bulma-ui/src/form/DateInput.tsx
  • bulma-ui/src/form/DateInputBase.tsx
  • bulma-ui/src/form/DateTimeInput.tsx
  • bulma-ui/src/form/DateTimeInputBase.tsx
  • bulma-ui/src/form/Field.tsx
  • bulma-ui/src/form/File.tsx
  • bulma-ui/src/form/Input.tsx
  • bulma-ui/src/form/InputBase.tsx
  • bulma-ui/src/form/Numberinput.tsx
  • bulma-ui/src/form/Radio.tsx
  • bulma-ui/src/form/Radios.tsx
  • bulma-ui/src/form/Rate.tsx
  • bulma-ui/src/form/Select.tsx
  • bulma-ui/src/form/SelectBase.tsx
  • bulma-ui/src/form/Slider.tsx
  • bulma-ui/src/form/Switch.tsx
  • bulma-ui/src/form/Taginput.tsx
  • bulma-ui/src/form/TextArea.tsx
  • bulma-ui/src/form/TextAreaBase.tsx
  • bulma-ui/src/form/TimeInput.tsx
  • bulma-ui/src/form/TimeInputBase.tsx
  • bulma-ui/src/form/fieldProps.ts
  • bulma-ui/src/grid/Cell.tsx
  • bulma-ui/src/grid/Grid.tsx
  • bulma-ui/src/layout/Container.tsx
  • bulma-ui/src/layout/Footer.tsx
  • bulma-ui/src/layout/Hero.tsx
  • bulma-ui/src/layout/Level.tsx
  • bulma-ui/src/layout/Media.tsx
  • bulma-ui/src/layout/Section.tsx
  • docs/CLAUDE.md
  • docs/docs/api/columns/column.md
  • docs/docs/api/columns/columns.md
  • docs/docs/api/components/avatar.md
  • docs/docs/api/components/avatars.md
  • docs/docs/api/components/badge.md
  • docs/docs/api/components/breadcrumb.md
  • docs/docs/api/components/card.md
  • docs/docs/api/components/carousel.md
  • docs/docs/api/components/collapse.md
  • docs/docs/api/components/dialog.md
  • docs/docs/api/components/dropdown.md
  • docs/docs/api/components/loading.md
  • docs/docs/api/components/menu.md
  • docs/docs/api/components/message.md
  • docs/docs/api/components/modal.md
  • docs/docs/api/components/navbar.md
  • docs/docs/api/components/pagination.md
  • docs/docs/api/components/panel.md
  • docs/docs/api/components/reveal.md
  • docs/docs/api/components/sidebar.md
  • docs/docs/api/components/steps.md
  • docs/docs/api/components/tabs.md
  • docs/docs/api/components/toast.md
  • docs/docs/api/components/tooltip.md
  • docs/docs/api/elements/block.md
  • docs/docs/api/elements/box.md
  • docs/docs/api/elements/button.md
  • docs/docs/api/elements/buttons.md
  • docs/docs/api/elements/code.md
  • docs/docs/api/elements/content.md
  • docs/docs/api/elements/delete.md
  • docs/docs/api/elements/divider.md
  • docs/docs/api/elements/emphasis.md
  • docs/docs/api/elements/figure.md
  • docs/docs/api/elements/icon.md
  • docs/docs/api/elements/icontext.md
  • docs/docs/api/elements/image.md
  • docs/docs/api/elements/link.md
  • docs/docs/api/elements/linkbutton.md
  • docs/docs/api/elements/listitem.md
  • docs/docs/api/elements/notification.md
  • docs/docs/api/elements/orderedlist.md
  • docs/docs/api/elements/paragraph.md
  • docs/docs/api/elements/pre.md
  • docs/docs/api/elements/progress.md
  • docs/docs/api/elements/skeleton.md
  • docs/docs/api/elements/span.md
  • docs/docs/api/elements/strong.md
  • docs/docs/api/elements/subtitle.md
  • docs/docs/api/elements/table.md
  • docs/docs/api/elements/tag.md
  • docs/docs/api/elements/tags.md
  • docs/docs/api/elements/title.md
  • docs/docs/api/elements/unorderedlist.md
  • docs/docs/api/form/autocomplete.md
  • docs/docs/api/form/checkbox.md
  • docs/docs/api/form/checkboxes.md
  • docs/docs/api/form/control.md
  • docs/docs/api/form/datetime/dateinput.md
  • docs/docs/api/form/datetime/datetimeinput.md
  • docs/docs/api/form/datetime/timeinput.md
  • docs/docs/api/form/field.md
  • docs/docs/api/form/file.md
  • docs/docs/api/form/input.md
  • docs/docs/api/form/numberinput.md
  • docs/docs/api/form/radio.md
  • docs/docs/api/form/radios.md
  • docs/docs/api/form/rate.md
  • docs/docs/api/form/select.md
  • docs/docs/api/form/slider.md
  • docs/docs/api/form/switch.md
  • docs/docs/api/form/taginput.md
  • docs/docs/api/form/textarea.md
  • docs/docs/api/grid/cell.md
  • docs/docs/api/grid/grid.md
  • docs/docs/api/helpers/classnames.md
  • docs/docs/api/helpers/config.md
  • docs/docs/api/helpers/theme.md
  • docs/docs/api/helpers/usebulmaclasses.md
  • docs/docs/api/helpers/useprefixedclassnames.md
  • docs/docs/api/layout/container.md
  • docs/docs/api/layout/footer.md
  • docs/docs/api/layout/hero.md
  • docs/docs/api/layout/level.md
  • docs/docs/api/layout/media.md
  • docs/docs/api/layout/section.md
  • docs/package.json
  • docs/scripts/strip-generated-markers.mjs
  • package.json
  • scripts/check-conformance.mjs
  • scripts/check-docs-parity.mjs
  • scripts/check-docs-wording.mjs
  • scripts/codemod-property-tsdoc.mjs
  • scripts/gen-api-docs.mjs
  • scripts/gen-api-sources.mjs
  • scripts/lib/api-page.mjs
  • scripts/lib/api-sources.mjs
  • scripts/lib/props-extract.mjs
  • scripts/lib/scss-vars.mjs
  • scripts/migrate-api-pages.mjs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This change introduces generated API documentation for managed components, shared prop and SCSS metadata extraction, migration and TSDoc codemods, generated-page conformance checks, updated API pages, and revised contribution guidance.

Changes

API documentation pipeline

Layer / File(s) Summary
TSDoc migration and component updates
scripts/codemod-property-tsdoc.mjs, bulma-ui/src/layout/*
Prop descriptions move to inline TSDoc comments, with LevelItem additionally supporting and forwarding rel for anchor elements.
API page parsing and source configuration
scripts/lib/api-page.mjs, scripts/lib/api-sources.mjs
Shared helpers parse generated regions, sections, frontmatter, and tables while configuration defines managed categories and SCSS sources.
Prop and SCSS metadata extraction
scripts/lib/props-extract.mjs, scripts/lib/scss-vars.mjs
TypeScript and SCSS parsing produce prop tables, defaults, inherited-property rows, component roots, and CSS/Sass variable mappings.
API page generation and migration
scripts/gen-api-docs.mjs, scripts/migrate-api-pages.mjs, docs/docs/api/layout/*
Generated overview, import, props, and CSS-variable regions are rendered into migrated API pages.
Generation commands and conformance gates
package.json, scripts/check-conformance.mjs, CONTRIBUTING-COMPONENTS.md, docs/CLAUDE.md, bulma-ui/CLAUDE.md
Generation scripts and checks validate section order, markers, required CSS-variable sections, and stale generated output; contribution guidance is updated accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: deep-review

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: generated API docs for layout pages and related docs tooling.
Description check ✅ Passed The description follows the template well, covering summary, affected packages, type of change, checklist, screenshots, and context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/api-docs-generation-3nl3eb

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.

Comment thread scripts/lib/api-page.mjs Fixed
Comment thread scripts/migrate-api-pages.mjs Fixed
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://40f70b64.bestax.pages.dev

CodeQL reported 2 new alerts (1 high, 1 medium) against the parsers added in
this PR. The flagged shape is unbounded repetition immediately before a
literal — `[\w.-]*getVar(`, `[\w.-]*register-vars`, and a lazy `[\s\S]+?`
before `}` — which the engine retries at every start position, giving
polynomial backtracking (js/polynomial-redos).

These scripts only ever read repo-owned files, so the practical risk is nil,
but the tightened patterns also say what was actually meant: the optional
prefix is a sass module namespace, so match `(?:[\w-]+\.)?` rather than any
run of word characters, and the JSDoc `{Type}` capture cannot span a `}`, so
match `[^}]+` rather than a lazy any-character run. The `@property` line is
matched per line, so `[\s\S]` was equivalent to `.` there anyway.

Also drops a stray `\*ic?` alternative from the JSDoc line-stripper — a typo
that never matched anything the following `\*\s?` did not already handle.

Behaviour is unchanged, verified three ways: registerVarsKeys still matches
the original implementation key-for-key across all 26 partials (366 keys, 0
diffs), the 24 generated layout CSS-variable rows are identical, and `pnpm
gen` produces no diff. The @Property parser is exercised by a dry run over
elements/ — 214 members parsed, the same 6 orphans detected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://921b1c81.bestax.pages.dev

@coderabbitai coderabbitai 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.

Actionable comments posted: 9

🧹 Nitpick comments (4)
scripts/lib/props-extract.mjs (2)

124-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Barrel parsing misses multi-line export { … } from blocks.

Prettier wraps long named-export lists across lines, and the line-oriented regex only matches single-line forms; a wrapped entry makes extractComponent throw "not exported". Since a Program is already available, walking src/index.ts's export declarations via the AST would be robust.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/props-extract.mjs` around lines 124 - 145, Update exportedModules
to parse src/index.ts through the available Program AST instead of matching
individual lines, including multi-line ExportNamedDeclaration and export-all
declarations. Populate the existing Map with each exported name and its
category/module metadata so extractComponent recognizes wrapped named-export
blocks.

488-501: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Own members and expanded base members can produce duplicate rows.

When an interface redeclares a prop it also inherits from an expanded local base, both rows land in the table. A dedupe by name keeping the own (non-inherited) row would avoid it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/props-extract.mjs` around lines 488 - 501, Deduplicate the rows
assembled around memberRows by prop name after adding expanded base members,
retaining the own non-inherited row when both sources define the same name.
Apply this only to the combined rows collection and preserve distinct inherited
rows and existing ordering where no duplicate exists.
bulma-ui/src/layout/Level.tsx (1)

228-240: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider defaulting rel for target="_blank" anchors.

LevelItem forwards rel only when explicitly supplied. Since target="_blank" without rel="noopener noreferrer" is a known reverse-tabnabbing risk, consider defaulting rel to "noopener noreferrer" when target === '_blank' and no rel is provided.

🔒 Proposed default
   if (Tag === 'a') {
+    const resolvedRel =
+      rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined);
     return (
       <a
         className={levelItemClasses}
         href={href}
         target={target}
-        rel={rel}
+        rel={resolvedRel}
         {...rest}
       >
         {children}
       </a>
     );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bulma-ui/src/layout/Level.tsx` around lines 228 - 240, Update the anchor
branch in LevelItem so rel defaults to "noopener noreferrer" when target is
"_blank" and no rel prop was provided, while preserving any explicitly supplied
rel value.
scripts/migrate-api-pages.mjs (1)

225-239: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

buildOverview only extracts the first physical line of the Overview paragraph.

para = lines[i] (line 231) takes a single line before running firstSentence. If a future category's Overview paragraph is hard-wrapped across multiple lines (unlike the single-line paragraphs in the layout pages shown here), the sentence boundary detection would operate on a truncated fragment instead of the full paragraph, and the remaining wrapped text would fall into tail unprocessed.

Since this script is reused for other categories per its own header comment, it would help to confirm whether any pending category's Overview paragraphs are hard-wrapped across lines before the next run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-api-pages.mjs` around lines 225 - 239, Update buildOverview
to combine all contiguous non-empty lines of the leading Overview paragraph
before calling firstSentence, while preserving subsequent blank-line content and
admonitions in tail. Ensure wrapped paragraph text is included in sentence
detection and only the text after the first sentence is retained as the
generated remainder.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/codemod-property-tsdoc.mjs`:
- Around line 199-234: Update the description selection in the member loop using
docs and byName so that fromDocs is always preferred when available, with
fromTag only as the fallback. Remove the length-based comparison while
preserving the existing undocumented reporting and insertion behavior.
- Around line 2-33: Replace the invisible whitespace characters in the nested
`*/` examples within the header comment of the codemod script with an
ESLint-safe representation, such as escaped slashes, while preserving the
rendered documentation. Avoid embedding disable directives inside the block
comment; keep the comment syntactically valid and eliminate all
`no-irregular-whitespace` violations.

In `@scripts/gen-api-docs.mjs`:
- Around line 227-238: Update the body-generation flow around renderCssVars so
CSS variable rendering occurs only when regions.has("cssvars") is true; preserve
the existing rendering and replacement behavior for opted-in regions while
skipping SCSS I/O and errors when the cssvars markers are absent.

In `@scripts/lib/api-page.mjs`:
- Around line 221-245: Update upsertFrontmatter to serialize or quote value
safely before constructing the frontmatter line, ensuring colons followed by
spaces, newlines, and YAML-sensitive leading characters cannot produce invalid
YAML. Also escape key and after when creating the matching regular expressions
so they are treated as literal frontmatter keys rather than regex patterns.
- Around line 267-269: Update escapeCell to escape backslashes before escaping
pipe characters, so a backslash immediately preceding a pipe remains valid in
the generated cell content. Preserve the existing newline replacement and
trimming behavior.

In `@scripts/lib/scss-vars.mjs`:
- Around line 238-255: Update the loop in registerVarsKeys to avoid binding the
unused regex match result m; iterate over the regex execution without assigning
its returned match object, while preserving use of re.lastIndex and the existing
key extraction behavior.
- Around line 71-94: In the scanner loop, move the quoted-string branch in the
function containing the shown while loop ahead of both comment branches. Ensure
skipString handles strings before detecting // or /* so comment markers inside
quoted values are preserved, while actual comments continue using their existing
logic.

In `@scripts/migrate-api-pages.mjs`:
- Around line 31-38: Remove the unused splitLines import from the import list in
migrate-api-pages.mjs, leaving the other API page helpers unchanged, then ensure
the file satisfies the no-unused-vars ESLint check.
- Around line 158-198: Update the JSDoc tail extraction in the migration logic
around `tagsAt`, `tail`, and the `rest` text so it preserves content after the
first summary paragraph, stopping at the first blank comment line or `@` tag as
intended. Ensure non-tagged trailing prose is retained in the generated
replacement and therefore included in the existing `was`/`now` output, without
changing the summary replacement behavior.

---

Nitpick comments:
In `@bulma-ui/src/layout/Level.tsx`:
- Around line 228-240: Update the anchor branch in LevelItem so rel defaults to
"noopener noreferrer" when target is "_blank" and no rel prop was provided,
while preserving any explicitly supplied rel value.

In `@scripts/lib/props-extract.mjs`:
- Around line 124-145: Update exportedModules to parse src/index.ts through the
available Program AST instead of matching individual lines, including multi-line
ExportNamedDeclaration and export-all declarations. Populate the existing Map
with each exported name and its category/module metadata so extractComponent
recognizes wrapped named-export blocks.
- Around line 488-501: Deduplicate the rows assembled around memberRows by prop
name after adding expanded base members, retaining the own non-inherited row
when both sources define the same name. Apply this only to the combined rows
collection and preserve distinct inherited rows and existing ordering where no
duplicate exists.

In `@scripts/migrate-api-pages.mjs`:
- Around line 225-239: Update buildOverview to combine all contiguous non-empty
lines of the leading Overview paragraph before calling firstSentence, while
preserving subsequent blank-line content and admonitions in tail. Ensure wrapped
paragraph text is included in sentence detection and only the text after the
first sentence is retained as the generated remainder.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 417ad40c-8870-4b70-b35c-fb05ee0f684e

📥 Commits

Reviewing files that changed from the base of the PR and between ba0a54e and 2ae8488.

📒 Files selected for processing (24)
  • CONTRIBUTING-COMPONENTS.md
  • bulma-ui/CLAUDE.md
  • bulma-ui/src/layout/Container.tsx
  • bulma-ui/src/layout/Footer.tsx
  • bulma-ui/src/layout/Hero.tsx
  • bulma-ui/src/layout/Level.tsx
  • bulma-ui/src/layout/Media.tsx
  • bulma-ui/src/layout/Section.tsx
  • docs/CLAUDE.md
  • docs/docs/api/layout/container.md
  • docs/docs/api/layout/footer.md
  • docs/docs/api/layout/hero.md
  • docs/docs/api/layout/level.md
  • docs/docs/api/layout/media.md
  • docs/docs/api/layout/section.md
  • package.json
  • scripts/check-conformance.mjs
  • scripts/codemod-property-tsdoc.mjs
  • scripts/gen-api-docs.mjs
  • scripts/lib/api-page.mjs
  • scripts/lib/api-sources.mjs
  • scripts/lib/props-extract.mjs
  • scripts/lib/scss-vars.mjs
  • scripts/migrate-api-pages.mjs

Comment on lines +2 to +33
/**
* ONE-TIME migration: move prop documentation from the `@property` block above
* each `<X>Props` interface onto the interface members as inline TSDoc.
*
* /** /**
* * Props for the Footer component. * Props for the Footer component.
* * *​/
* * @property {string} [className] - export interface FooterProps … {
* * Additional CSS classes. /** Additional CSS classes. *​/
* *​/ className?: string;
*
* Why bother: `@property` is a comment ABOUT the interface. TypeScript cannot
* check it, so its hand-typed `{Type}` drifts from the real member type, and it
* never reaches a user's editor. Inline TSDoc is verifiable by position, shows
* up in IntelliSense, and is what `scripts/lib/props-extract.mjs` reads.
*
* Three rules that matter:
*
* 1. DESCRIPTIONS COME FROM THE DOCS PAGE FIRST. The hand-written tables in
* docs/docs/api are consistently richer than the `@property` lines
* (button.md explains what `ghost` and `text` render as; the source comment
* just says "Bulma color modifier"). Migrating from the source would quietly
* downgrade the docs.
* 2. MERGE, NEVER OVERWRITE. 21 modules already carry some inline member TSDoc
* and 6 are mixed; a member that already has a comment is left alone.
* 3. ORPHAN `@property` LINES ARE NOT DROPPED. 38 of them name props that live
* on a DOM base type rather than the interface. Those are rewritten as
* `@extraProp`, which props-extract renders as its own rows.
*
* Usage:
* node scripts/codemod-property-tsdoc.mjs --category=layout [--dry-run]
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Irregular whitespace trips no-irregular-whitespace (ESLint).

Lines 8, 10, and 11 use an invisible whitespace character between * and / to render */ literally inside the outer block comment without terminating it. Static analysis flags this as no-irregular-whitespace on all three lines, which will fail the ESLint gate.

Since the offending text is nested text inside one continuous /** ... */ block (lines 2-33), an inline eslint-disable-next-line comment can't be embedded there — wrap the whole header comment with disable/enable directives instead, or replace the invisible character with an escaped \/ that doesn't require special whitespace.

🔧 Proposed fix
 #!/usr/bin/env node
+/* eslint-disable no-irregular-whitespace */
 /**
  * ONE-TIME migration: move prop documentation from the `@property` block above
  * each `<X>Props` interface onto the interface members as inline TSDoc.
  ...
  * Usage:
  *   node scripts/codemod-property-tsdoc.mjs --category=layout [--dry-run]
  */
+/* eslint-enable no-irregular-whitespace */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* ONE-TIME migration: move prop documentation from the `@property` block above
* each `<X>Props` interface onto the interface members as inline TSDoc.
*
* /** /**
* * Props for the Footer component. * Props for the Footer component.
* * *​/
* * @property {string} [className] - export interface FooterProps … {
* * Additional CSS classes. /** Additional CSS classes. *​/
* *​/ className?: string;
*
* Why bother: `@property` is a comment ABOUT the interface. TypeScript cannot
* check it, so its hand-typed `{Type}` drifts from the real member type, and it
* never reaches a user's editor. Inline TSDoc is verifiable by position, shows
* up in IntelliSense, and is what `scripts/lib/props-extract.mjs` reads.
*
* Three rules that matter:
*
* 1. DESCRIPTIONS COME FROM THE DOCS PAGE FIRST. The hand-written tables in
* docs/docs/api are consistently richer than the `@property` lines
* (button.md explains what `ghost` and `text` render as; the source comment
* just says "Bulma color modifier"). Migrating from the source would quietly
* downgrade the docs.
* 2. MERGE, NEVER OVERWRITE. 21 modules already carry some inline member TSDoc
* and 6 are mixed; a member that already has a comment is left alone.
* 3. ORPHAN `@property` LINES ARE NOT DROPPED. 38 of them name props that live
* on a DOM base type rather than the interface. Those are rewritten as
* `@extraProp`, which props-extract renders as its own rows.
*
* Usage:
* node scripts/codemod-property-tsdoc.mjs --category=layout [--dry-run]
*/
/* eslint-disable no-irregular-whitespace */
/**
* ONE-TIME migration: move prop documentation from the `@property` block above
* each `<X>Props` interface onto the interface members as inline TSDoc.
*
* /** /**
* * Props for the Footer component. * Props for the Footer component.
* * *​/
* * `@property` {string} [className] - export interface FooterProps … {
* * Additional CSS classes. /** Additional CSS classes. *​/
* *​/ className?: string;
*
* Why bother: `@property` is a comment ABOUT the interface. TypeScript cannot
* check it, so its hand-typed `{Type}` drifts from the real member type, and it
* never reaches a user's editor. Inline TSDoc is verifiable by position, shows
* up in IntelliSense, and is what `scripts/lib/props-extract.mjs` reads.
*
* Three rules that matter:
*
* 1. DESCRIPTIONS COME FROM THE DOCS PAGE FIRST. The hand-written tables in
* docs/docs/api are consistently richer than the `@property` lines
* (button.md explains what `ghost` and `text` render as; the source comment
* just says "Bulma color modifier"). Migrating from the source would quietly
* downgrade the docs.
* 2. MERGE, NEVER OVERWRITE. 21 modules already carry some inline member TSDoc
* and 6 are mixed; a member that already has a comment is left alone.
* 3. ORPHAN `@property` LINES ARE NOT DROPPED. 38 of them name props that live
* on a DOM base type rather than the interface. Those are rewritten as
* `@extraProp`, which props-extract renders as its own rows.
*
* Usage:
* node scripts/codemod-property-tsdoc.mjs --category=layout [--dry-run]
*/
/* eslint-enable no-irregular-whitespace */
🧰 Tools
🪛 ESLint

[error] 8-8: Irregular whitespace not allowed.

(no-irregular-whitespace)


[error] 10-10: Irregular whitespace not allowed.

(no-irregular-whitespace)


[error] 11-11: Irregular whitespace not allowed.

(no-irregular-whitespace)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/codemod-property-tsdoc.mjs` around lines 2 - 33, Replace the
invisible whitespace characters in the nested `*/` examples within the header
comment of the codemod script with an ESLint-safe representation, such as
escaped slashes, while preserving the rendered documentation. Avoid embedding
disable directives inside the block comment; keep the comment syntactically
valid and eliminate all `no-irregular-whitespace` violations.

Sources: Path instructions, Linters/SAST tools

Comment on lines +199 to +234

// Descriptions available for this interface's props, docs page first.
//
// EXACT title match only. Falling back to the parent's page for a
// sub-component (HeroHead -> Hero) borrows descriptions that are wrong in
// context: Hero's `children` is "Hero content (often includes Hero.Head,
// Hero.Body, Hero.Foot)", which is nonsense on Hero.Head itself. Where a
// sub-component has no page, its `@property` lines are the better source.
const component = name.replace(/Props$/, '');
const docs = docsByTitle.get(component) ?? new Map();
const byName = new Map(tags.map(t => [t.name, t.desc]));

const ownNames = new Set(decl.members.map(memberName).filter(Boolean));

// Members that lack a comment get one.
for (const member of decl.members) {
const mName = memberName(member);
if (!mName) continue;
if (hasOwnJsDoc(member)) continue;
const fromDocs = docs.get(mName);
const fromTag = byName.get(mName);
const desc =
fromDocs && fromTag
? fromDocs.length >= fromTag.length
? fromDocs
: fromTag
: (fromDocs ?? fromTag);
if (!desc) {
report.undocumented.push(`${name}.${mName}`);
continue;
}
const start = member.getStart(sf);
const indent = lineIndent(original, start);
edits.push({ start, end: start, text: `/** ${desc} */\n${indent}` });
report.inserted++;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Length-based tie-break contradicts the stated "docs page first" rule.

The file's header (Rule 1) states descriptions should come from the docs page first, but when both fromDocs and fromTag exist, the code just picks whichever string is longer rather than always preferring fromDocs. A verbose @property line could win over a shorter, more accurate docs-page description, silently downgrading documentation quality — the exact outcome Rule 1 says to avoid.

🔧 Proposed fix to always prefer the docs page
-      const desc =
-        fromDocs && fromTag
-          ? fromDocs.length >= fromTag.length
-            ? fromDocs
-            : fromTag
-          : (fromDocs ?? fromTag);
+      const desc = fromDocs ?? fromTag;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Descriptions available for this interface's props, docs page first.
//
// EXACT title match only. Falling back to the parent's page for a
// sub-component (HeroHead -> Hero) borrows descriptions that are wrong in
// context: Hero's `children` is "Hero content (often includes Hero.Head,
// Hero.Body, Hero.Foot)", which is nonsense on Hero.Head itself. Where a
// sub-component has no page, its `@property` lines are the better source.
const component = name.replace(/Props$/, '');
const docs = docsByTitle.get(component) ?? new Map();
const byName = new Map(tags.map(t => [t.name, t.desc]));
const ownNames = new Set(decl.members.map(memberName).filter(Boolean));
// Members that lack a comment get one.
for (const member of decl.members) {
const mName = memberName(member);
if (!mName) continue;
if (hasOwnJsDoc(member)) continue;
const fromDocs = docs.get(mName);
const fromTag = byName.get(mName);
const desc =
fromDocs && fromTag
? fromDocs.length >= fromTag.length
? fromDocs
: fromTag
: (fromDocs ?? fromTag);
if (!desc) {
report.undocumented.push(`${name}.${mName}`);
continue;
}
const start = member.getStart(sf);
const indent = lineIndent(original, start);
edits.push({ start, end: start, text: `/** ${desc} */\n${indent}` });
report.inserted++;
}
// Descriptions available for this interface's props, docs page first.
//
// EXACT title match only. Falling back to the parent's page for a
// sub-component (HeroHead -> Hero) borrows descriptions that are wrong in
// context: Hero's `children` is "Hero content (often includes Hero.Head,
// Hero.Body, Hero.Foot)", which is nonsense on Hero.Head itself. Where a
// sub-component has no page, its `@property` lines are the better source.
const component = name.replace(/Props$/, '');
const docs = docsByTitle.get(component) ?? new Map();
const byName = new Map(tags.map(t => [t.name, t.desc]));
const ownNames = new Set(decl.members.map(memberName).filter(Boolean));
// Members that lack a comment get one.
for (const member of decl.members) {
const mName = memberName(member);
if (!mName) continue;
if (hasOwnJsDoc(member)) continue;
const fromDocs = docs.get(mName);
const fromTag = byName.get(mName);
const desc = fromDocs ?? fromTag;
if (!desc) {
report.undocumented.push(`${name}.${mName}`);
continue;
}
const start = member.getStart(sf);
const indent = lineIndent(original, start);
edits.push({ start, end: start, text: `/** ${desc} */\n${indent}` });
report.inserted++;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/codemod-property-tsdoc.mjs` around lines 199 - 234, Update the
description selection in the member loop using docs and byName so that fromDocs
is always preferred when available, with fromTag only as the fallback. Remove
the length-based comparison while preserving the existing undocumented reporting
and insertion behavior.

Comment thread scripts/gen-api-docs.mjs
Comment on lines +227 to +238
const bodies = {
overview: renderOverview(info),
import: renderImport(info),
props: renderProps(info),
cssvars: await renderCssVars(info, { relPath }),
};

let out = src;
for (const [id, body] of Object.entries(bodies)) {
if (body == null || !regions.has(id)) continue;
out = replaceRegion(out, id, body, `docs/docs/api/${relPath}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

renderCssVars runs even when the page opted out of the cssvars region.

The header doc says deleting a marker pair opts that region out (lines 12-14), but bodies.cssvars is computed unconditionally before the regions.has(id) gate is checked in the loop below. For a component with a non-empty SCSS_SOURCES entry, this means the SCSS file is still read and renderCssVars can still throw (e.g. missing root, line 172-176) even on a page that intentionally removed the cssvars markers — defeating the opt-out for exactly the region that does I/O and can fail.

🐛 Proposed fix
   const bodies = {
     overview: renderOverview(info),
     import: renderImport(info),
     props: renderProps(info),
-    cssvars: await renderCssVars(info, { relPath }),
+    cssvars: regions.has('cssvars')
+      ? await renderCssVars(info, { relPath })
+      : null,
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const bodies = {
overview: renderOverview(info),
import: renderImport(info),
props: renderProps(info),
cssvars: await renderCssVars(info, { relPath }),
};
let out = src;
for (const [id, body] of Object.entries(bodies)) {
if (body == null || !regions.has(id)) continue;
out = replaceRegion(out, id, body, `docs/docs/api/${relPath}`);
}
const bodies = {
overview: renderOverview(info),
import: renderImport(info),
props: renderProps(info),
cssvars: regions.has('cssvars')
? await renderCssVars(info, { relPath })
: null,
};
let out = src;
for (const [id, body] of Object.entries(bodies)) {
if (body == null || !regions.has(id)) continue;
out = replaceRegion(out, id, body, `docs/docs/api/${relPath}`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/gen-api-docs.mjs` around lines 227 - 238, Update the body-generation
flow around renderCssVars so CSS variable rendering occurs only when
regions.has("cssvars") is true; preserve the existing rendering and replacement
behavior for opted-in regions while skipping SCSS I/O and errors when the
cssvars markers are absent.

Comment thread scripts/lib/api-page.mjs
Comment on lines +221 to +245
export function upsertFrontmatter(src, key, value, after = null) {
const m = src.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
if (!m) throw new Error('page has no frontmatter block');
const eol = m[1].includes('\r\n') ? '\r\n' : '\n';
const lines = m[2].split(/\r?\n/);
const line = `${key}: ${value}`;
const at = lines.findIndex(l => new RegExp(`^${key}:`).test(l));

if (at >= 0) {
lines[at] = line;
} else {
const afterAt = after
? lines.findIndex(l => new RegExp(`^${after}:`).test(l))
: -1;
if (afterAt >= 0) lines.splice(afterAt + 1, 0, line);
else lines.push(line);
}
return (
src.slice(0, m.index) +
m[1] +
lines.join(eol) +
m[3] +
src.slice(m.index + m[0].length)
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Quote/escape the frontmatter value before writing it.

value is interpolated raw into ${key}: ${value}. renderPage passes the generated Overview sentence, so any sentence containing : , a leading #/{/[/quote, or an embedded newline produces invalid YAML and breaks the page build. Also, key/after are interpolated unescaped into new RegExp.

🛠️ Proposed fix
+function yamlScalar(value) {
+  const s = String(value).replace(/\r?\n/g, ' ').trim();
+  return /^[\w([{'"#&*!|>%@`-]|: |:$| `#/.test`(s) || /["']/.test(s)
+    ? `'${s.replace(/'/g, "''")}'`
+    : s;
+}
+
+const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
 export function upsertFrontmatter(src, key, value, after = null) {
   const m = src.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
   if (!m) throw new Error('page has no frontmatter block');
   const eol = m[1].includes('\r\n') ? '\r\n' : '\n';
   const lines = m[2].split(/\r?\n/);
-  const line = `${key}: ${value}`;
-  const at = lines.findIndex(l => new RegExp(`^${key}:`).test(l));
+  const line = `${key}: ${yamlScalar(value)}`;
+  const at = lines.findIndex(l => new RegExp(`^${escapeRe(key)}:`).test(l));
 
   if (at >= 0) {
     lines[at] = line;
   } else {
     const afterAt = after
-      ? lines.findIndex(l => new RegExp(`^${after}:`).test(l))
+      ? lines.findIndex(l => new RegExp(`^${escapeRe(after)}:`).test(l))
       : -1;

(The quoting heuristic above is illustrative — a tiny helper or a YAML serializer is fine, as long as : and newlines can't leak through.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function upsertFrontmatter(src, key, value, after = null) {
const m = src.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
if (!m) throw new Error('page has no frontmatter block');
const eol = m[1].includes('\r\n') ? '\r\n' : '\n';
const lines = m[2].split(/\r?\n/);
const line = `${key}: ${value}`;
const at = lines.findIndex(l => new RegExp(`^${key}:`).test(l));
if (at >= 0) {
lines[at] = line;
} else {
const afterAt = after
? lines.findIndex(l => new RegExp(`^${after}:`).test(l))
: -1;
if (afterAt >= 0) lines.splice(afterAt + 1, 0, line);
else lines.push(line);
}
return (
src.slice(0, m.index) +
m[1] +
lines.join(eol) +
m[3] +
src.slice(m.index + m[0].length)
);
}
function yamlScalar(value) {
const s = String(value).replace(/\r?\n/g, ' ').trim();
return /^[\w([{'"#&*!|>%@`-]|: |:$| `#/.test`(s) || /["']/.test(s)
? `'${s.replace(/'/g, "''")}'`
: s;
}
const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export function upsertFrontmatter(src, key, value, after = null) {
const m = src.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
if (!m) throw new Error('page has no frontmatter block');
const eol = m[1].includes('\r\n') ? '\r\n' : '\n';
const lines = m[2].split(/\r?\n/);
const line = `${key}: ${yamlScalar(value)}`;
const at = lines.findIndex(l => new RegExp(`^${escapeRe(key)}:`).test(l));
if (at >= 0) {
lines[at] = line;
} else {
const afterAt = after
? lines.findIndex(l => new RegExp(`^${escapeRe(after)}:`).test(l))
: -1;
if (afterAt >= 0) lines.splice(afterAt + 1, 0, line);
else lines.push(line);
}
return (
src.slice(0, m.index) +
m[1] +
lines.join(eol) +
m[3] +
src.slice(m.index + m[0].length)
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/api-page.mjs` around lines 221 - 245, Update upsertFrontmatter to
serialize or quote value safely before constructing the frontmatter line,
ensuring colons followed by spaces, newlines, and YAML-sensitive leading
characters cannot produce invalid YAML. Also escape key and after when creating
the matching regular expressions so they are treated as literal frontmatter keys
rather than regex patterns.

Comment thread scripts/lib/api-page.mjs
Comment on lines +267 to +269
export function escapeCell(text) {
return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Escape backslashes before pipes.

CodeQL flags this (failing check): a cell ending in \ immediately before a | yields \\|, which terminates the escape and splits the row into an extra column. Escape \ first.

🐛 Proposed fix
 export function escapeCell(text) {
-  return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
+  return String(text)
+    .replace(/\\/g, '\\\\')
+    .replace(/\|/g, '\\|')
+    .replace(/\r?\n/g, ' ')
+    .trim();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function escapeCell(text) {
return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
}
export function escapeCell(text) {
return String(text)
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|')
.replace(/\r?\n/g, ' ')
.trim();
}
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 268-268: Incomplete string escaping or encoding
This does not escape backslash characters in the input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/api-page.mjs` around lines 267 - 269, Update escapeCell to escape
backslashes before escaping pipe characters, so a backslash immediately
preceding a pipe remains valid in the generated cell content. Preserve the
existing newline replacement and trimming behavior.

Source: Linters/SAST tools

Comment thread scripts/lib/scss-vars.mjs
Comment on lines +71 to +94
while (i < src.length) {
const ch = src[i];

if (ch === '/' && src[i + 1] === '/') {
while (i < src.length && src[i] !== '\n') i++;
continue;
}
if (ch === '/' && src[i + 1] === '*') {
const end = src.indexOf('*/', i + 2);
i = end === -1 ? src.length : end + 2;
continue;
}
if (ch === '"' || ch === "'") {
const end = skipString(src, i);
buf += src.slice(i, end);
i = end;
continue;
}
if (atInterpolation(src, i)) {
const end = skipInterpolation(src, i);
buf += src.slice(i, end);
i = end;
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Comment detection runs before string detection, so // inside a quoted value is mis-lexed.

url("https://example.com/x.png") or any string containing // (or /*) truncates the rest of the line, silently dropping the declaration and desynchronising the brace/selector stack. Move the quote branch above the comment branches.

🐛 Proposed fix
   while (i < src.length) {
     const ch = src[i];
 
+    if (ch === '"' || ch === "'") {
+      const end = skipString(src, i);
+      buf += src.slice(i, end);
+      i = end;
+      continue;
+    }
     if (ch === '/' && src[i + 1] === '/') {
       while (i < src.length && src[i] !== '\n') i++;
       continue;
     }
     if (ch === '/' && src[i + 1] === '*') {
       const end = src.indexOf('*/', i + 2);
       i = end === -1 ? src.length : end + 2;
       continue;
     }
-    if (ch === '"' || ch === "'") {
-      const end = skipString(src, i);
-      buf += src.slice(i, end);
-      i = end;
-      continue;
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (i < src.length) {
const ch = src[i];
if (ch === '/' && src[i + 1] === '/') {
while (i < src.length && src[i] !== '\n') i++;
continue;
}
if (ch === '/' && src[i + 1] === '*') {
const end = src.indexOf('*/', i + 2);
i = end === -1 ? src.length : end + 2;
continue;
}
if (ch === '"' || ch === "'") {
const end = skipString(src, i);
buf += src.slice(i, end);
i = end;
continue;
}
if (atInterpolation(src, i)) {
const end = skipInterpolation(src, i);
buf += src.slice(i, end);
i = end;
continue;
}
while (i < src.length) {
const ch = src[i];
if (ch === '"' || ch === "'") {
const end = skipString(src, i);
buf += src.slice(i, end);
i = end;
continue;
}
if (ch === '/' && src[i + 1] === '/') {
while (i < src.length && src[i] !== '\n') i++;
continue;
}
if (ch === '/' && src[i + 1] === '*') {
const end = src.indexOf('*/', i + 2);
i = end === -1 ? src.length : end + 2;
continue;
}
if (atInterpolation(src, i)) {
const end = skipInterpolation(src, i);
buf += src.slice(i, end);
i = end;
continue;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/scss-vars.mjs` around lines 71 - 94, In the scanner loop, move
the quoted-string branch in the function containing the shown while loop ahead
of both comment branches. Ensure skipString handles strings before detecting //
or /* so comment markers inside quoted values are preserved, while actual
comments continue using their existing logic.

Comment thread scripts/lib/scss-vars.mjs
Comment on lines +238 to +255
export function registerVarsKeys(src) {
const keys = new Set();
const re = /cv\.register-vars\s*\(/g;
let m;
while ((m = re.exec(src))) {
let depth = 1;
let i = re.lastIndex;
while (i < src.length && depth > 0) {
if (src[i] === '(') depth++;
else if (src[i] === ')') depth--;
i++;
}
const block = src.slice(re.lastIndex, i);
for (const k of block.matchAll(/['"]([a-z0-9-]+)['"][ \t]*:/g))
keys.add(k[1]);
}
return keys;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

ESLint failure: m is assigned but never used.

Only re.lastIndex is consumed, so drop the binding to satisfy no-unused-vars.

🧹 Proposed fix
 export function registerVarsKeys(src) {
   const keys = new Set();
   const re = /cv\.register-vars\s*\(/g;
-  let m;
-  while ((m = re.exec(src))) {
+  while (re.exec(src) !== null) {
     let depth = 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function registerVarsKeys(src) {
const keys = new Set();
const re = /cv\.register-vars\s*\(/g;
let m;
while ((m = re.exec(src))) {
let depth = 1;
let i = re.lastIndex;
while (i < src.length && depth > 0) {
if (src[i] === '(') depth++;
else if (src[i] === ')') depth--;
i++;
}
const block = src.slice(re.lastIndex, i);
for (const k of block.matchAll(/['"]([a-z0-9-]+)['"][ \t]*:/g))
keys.add(k[1]);
}
return keys;
}
export function registerVarsKeys(src) {
const keys = new Set();
const re = /cv\.register-vars\s*\(/g;
while (re.exec(src) !== null) {
let depth = 1;
let i = re.lastIndex;
while (i < src.length && depth > 0) {
if (src[i] === '(') depth++;
else if (src[i] === ')') depth--;
i++;
}
const block = src.slice(re.lastIndex, i);
for (const k of block.matchAll(/['"]([a-z0-9-]+)['"][ \t]*:/g))
keys.add(k[1]);
}
return keys;
}
🧰 Tools
🪛 ESLint

[error] 242-242: 'm' is assigned a value but never used.

(no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/scss-vars.mjs` around lines 238 - 255, Update the loop in
registerVarsKeys to avoid binding the unused regex match result m; iterate over
the regex execution without assigning its returned match object, while
preserving use of re.lastIndex and the existing key extraction behavior.

Source: Linters/SAST tools

Comment on lines +31 to +38
import {
sectionSpans,
sectionBody,
splitLines,
openMarker,
closeMarker,
firstSentence,
} from './lib/api-page.mjs';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Unused import splitLines — ESLint error.

Flagged by ESLint ('splitLines' is defined but never used, no-unused-vars). splitLines is imported here but never referenced in this file.

As per coding guidelines, **/*.{ts,tsx,js,jsx,mjs,cjs}: "Run and satisfy ESLint, TypeScript checks, tests, formatting checks, and the repository's pre-PR quality gate before submitting changes."

🧹 Proposed fix
 import {
   sectionSpans,
   sectionBody,
-  splitLines,
   openMarker,
   closeMarker,
   firstSentence,
 } from './lib/api-page.mjs';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
sectionSpans,
sectionBody,
splitLines,
openMarker,
closeMarker,
firstSentence,
} from './lib/api-page.mjs';
import {
sectionSpans,
sectionBody,
openMarker,
closeMarker,
firstSentence,
} from './lib/api-page.mjs';
🧰 Tools
🪛 ESLint

[error] 34-34: 'splitLines' is defined but never used.

(no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-api-pages.mjs` around lines 31 - 38, Remove the unused
splitLines import from the import list in migrate-api-pages.mjs, leaving the
other API page helpers unchanged, then ensure the file satisfies the
no-unused-vars ESLint check.

Sources: Coding guidelines, Linters/SAST tools

Comment thread scripts/migrate-api-pages.mjs Outdated
`--category` went straight from argv into `join(REPO, 'bulma-ui', 'src', …)`,
then `readdir` and, on a non-dry run, `writeFile` on everything found there.
`--category=../../..` would walk out of the repo and rewrite files outside it.

Resolve the argument against the directories that actually exist under src/
instead of interpolating it into a path. A typo now names the valid choices
rather than failing with a bare ENOENT.

Verified: `--category=../../..` exits 2 without touching the filesystem;
`--category=layout` behaves as before. `pnpm gen` produces no diff and all 10
conformance checks pass.

Note this does not yet clear the CodeQL check — it reports 1 high + 1 medium
and the annotations are not readable from here (the repo uses CodeQL default
setup, so there is no workflow file, and no code-scanning API is available to
this session). This fixes a real traversal sink found by inspection; the
flagged lines still need confirming against the actual report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://0979af3d.bestax.pages.dev

claude added 2 commits July 28, 2026 11:42
Groundwork for migrating the remaining API categories.

`scripts/gen-api-sources.mjs` discovers the two maps that would otherwise be
hand-written across ~95 components: which SCSS partial(s) register each
component's CSS variables, and which pages deliberately import companions.
Both are committed so the diff stays reviewable, and the script re-runs after
a Bulma upgrade.

Deriving the root class turned out to be the hard part, and the pilot hid it.
Taking the first `usePrefixedClassNames` literal is only correct when a
component renders one prefixed element — true for every layout component,
false for 18 others. `Input` and `Select` resolved to `help`, `Toast` to
`toast-container`, `Badge` to `badge-wrapper`, so those pages would have shown
no variables at all. Match the component's own name against its candidate
classes instead, resolve wrappers through their `*Base`, and return null when
nothing matches confidently. `gen-api-sources` then REFUSES to emit rather
than guessing, because a wrong root class silently attaches another
component's variables to a page. Six components that the name rule cannot
decide are named explicitly in ROOT_CLASS_OVERRIDES with the reason.

That correction moves 53 -> 62 components with discoverable variables.

A component can also legitimately draw from more than one partial: Bulma's
form/shared.scss registers the `--bulma-input-*` set on a selector list
covering .control/.input/.textarea/.select, so all four really do respond to
them. Keep every match, ordered by path, and dedupe by variable name.

Ordering and generation are now separate concerns. ORDERED_CATEGORIES covers
every reference page including helpers/; MANAGED_CATEGORIES covers only pages
that are really a component props table. Without the split, helpers/config.md
and helpers/theme.md would keep Props near the top while every other page
moved it down — the same inconsistency this work exists to remove, just
smaller. `docs-section-order` also now constrains only the canonical sections,
so a page's own `## API` or `## Supported Props` keeps its author's placement.

helpers/ is reordered via a new --reorder-only mode (no markers, no codemod);
live-example counts are unchanged on all six pages. form/checkbox.md's
pre-existing duplicated footer sections are merged, keeping the colon style
that matches the avatar exemplar.

The layout pilot's generated output is byte-identical after all of this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
… tables

Wraps only the prop TABLES in the generated region rather than the whole
`## Props` section. 15 pages carry hand-written prose there — admonitions,
footnotes, and a 32-line prose sub-API in card.md — and the previous behaviour
would have put all of it inside a region the generator overwrites. Prose above
or below the tables now survives untouched. Prose BETWEEN tables cannot
survive, so the migration refuses those pages and names the offending lines
instead of eating them.

Also fixes a real loss the columns migration exposed: `@deprecated` never
reached the table. The hand-written rows led with "**Deprecated.** Use
`gapMobile` instead."; the extractor read only the JSDoc comment text, so the
five deprecated `gapSize*` props came out documented as ordinary props with no
hint they are on the way out — the one thing a reader most needs. The tag is
now rendered ahead of the description.

columns/ and grid/ migrated (4 pages, 74 member TSDoc comments, 4 seeded
summaries). Verified against origin/main: no default changed, no description
shortened, no prop dropped, live-example counts identical. Generator is
idempotent, typecheck and all 3678 tests pass.

Remaining: elements (30), components (22), form (19).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

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

Completes the rollout started for layout/columns/grid: elements (30),
components (22) and form (19) now carry the same generated regions, so all 87
pages share one section order and one table shape. A site with Props near the
top on some pages and at the bottom on others is worse than either order
applied consistently, which is what the partial rollout guaranteed.

The requirement driving this commit is that a generated page carry the SAME OR
MORE information than the hand-written one it replaces. Measuring that rather
than assuming it turned up losses per-page spot checks had missed, and
`scripts/check-docs-parity.mjs` now proves it: every page is diffed against
origin/main and the run fails on a dropped prop, default, description word,
code span, URL, live example or line of prose. It reports 0 losses across all
87 pages.

Extraction fixes found by that gate:

- Type aliases as props types. `ControlProps` and `SliderProps` are unions, and
  treating a props type as necessarily an interface rendered both pages with no
  table at all (42 props).
- Heritage through `Omit`/`Pick` and imported (alias) symbols — 242 props were
  landing in the catch-all row instead of a table.
- Defaults destructured in the body rather than the parameter (`Slider`,
  `TimeInputBase`), and defaults belonging to a base module a thin wrapper
  renders (`TimeInput` -> `TimeInputBase`).
- Type cells no longer show a bare alias name: short unions inline (including
  `(typeof xs)[number]` and lookup types), long ones get a `**Types:**`
  footnote built from the alias's own TSDoc.
- `**Subcomponents:**` renders each sub's summary sentence, which the
  hand-written pages carried and a bare name list dropped.
- A sub-component with its own page is linked rather than restated, so
  `Columns.Column` does not duplicate column.md's 25 rows.
- `ref` shows the forwarded element; MDX-hostile bare `<Tag>` text and
  colon-bearing frontmatter descriptions are escaped rather than breaking the
  docs build.

The codemod now merges rather than replaces: where the page and the source
comment say different things, both survive; where they say the same thing
differently, the fuller wording wins. Props the tables documented but no
interface declares are parked as `@extraProp`, carrying their type and default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

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

The markers tell gen-api-docs.mjs which regions it owns and warn a human editor
which lines `pnpm gen` will overwrite. Neither audience reads the built site,
but docusaurus-plugin-llms copies markdown from disk without stripping HTML
comments, so they were landing ~600 times in llms-full.txt and in all 81
per-page .md twins — the files agents actually ingest.

A build step rather than a Docusaurus plugin: postBuild hooks run under
Promise.all, so declaring a plugin after docusaurus-plugin-llms does not make it
run after. The first attempt did exactly that and stripped nothing, because it
raced ahead of the files it was meant to clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

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

`pnpm all` runs `test` and `test:coverage` inside one turbo invocation, so two
jest processes execute e2e/kitchen-sink.test.ts concurrently. Both copied the
fixture app into the same `.e2e-tmp/kitchen-sink` path and each began with an
rmSync of it, so one process would wipe and re-copy the fixture while the other
was asserting against its migrated output. The failure reads as though the
codemod silently did nothing — "Expected substring: not
'react-bulma-components'" on a file that migrates correctly in isolation.

mkdtemp gives each process its own directory, so the suites no longer interact
under any scheduler. Cleaned up in afterAll; BESTAX_E2E_KEEP=1 keeps it for
inspection.

Reproduced before fixing rather than assumed: two identical runs stay in
lockstep and never collide, but pairing an instrumented `--coverage` run against
a plain one (the pnpm all case) failed 2 of 6 runs. The same harness passes 8 of
8 with the fix, and `pnpm all` is green three times over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://5d1f9f1e.bestax.pages.dev

…page win on wording

Two problems, both found by measuring instead of assuming.

CSS variables. `Block` showing no table was correct — Bulma's block.scss really
does register none — but the audit behind that answer turned up six components
that DO own variables and were showing nothing: Skeleton, Code, Pre, Strong,
Delete and Column. Nothing to do with bulma-vs-repo (42 components already draw
their tables from Bulma). Three causes, all now fixed: the scan skipped
`sass/base` and `sass/utilities`, where Bulma keeps `--bulma-skeleton-*`,
`--bulma-code|pre|strong-*` and `--bulma-delete-*`; registrations at
`$variables-host` or inside a `@mixin` were rejected because they are not on the
component's own selector; and the semantic wrappers own a variable family
without rendering a Bulma class at all, so `varPrefix` now names that family
separately from `rootClass`. Host registrations are claimed by key prefix, which
is what keeps `body-*` and `hr-*` out of Code's table. The lead sentence adapts:
claiming a component "registers these on its own element" is false when Bulma
declares them at `:root`. Field and Navbar each gained a real variable too, and
`gen-api-docs` may now CREATE the cssvars section — it is the one region
conformance requires, so it was never opt-outable.

Wording. The merge preferred whichever text had the larger word set, which let a
stale source comment overwrite a hand-corrected page: "Offset for mobile."
became "Mobile column offset." on six rows, and 114 props changed that way. The
page is now authoritative for any prop it already documented — the comment only
wins when it fully contains the page's sentence — and where the two disagree the
codemod prints what it discarded, so the difference is reviewed rather than
lost. Previously-documented props: 953 unchanged, 117 extended (old sentence
intact), 12 merged, 1 replaced (a comma).

`scripts/check-docs-wording.mjs` is the review aid: it buckets every
previously-documented prop by how its description changed. Parity stays at 0
losses across all 87 pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

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

CI was red on this PR for `prettier --check scripts/check-docs-wording.mjs`,
which `pnpm all` had reported green. Two gate gaps, same shape:

- `pnpm all` ran `turbo run format:check`, which runs the PER-PACKAGE scripts —
  none of which cover `scripts/`, `docs/scripts/`, or any `.md`. CI runs the ROOT
  script, which does. `all` now calls the root one, so the pre-PR gate and CI
  check the same files.
- Nothing linted `scripts/` at all: `pnpm lint` is `turbo run lint`, and only
  bulma-ui/create-bestax/bestax-migrate define a lint script, each scoped to
  their own src. `lint` now also runs eslint over `scripts` and `docs/scripts`,
  so CI picks it up without a workflow change. Verified by planting an unused
  variable and confirming the gate fails.

That gap had hidden 7 eslint errors, now fixed: unused `m`, `splitLines`,
`contentWords`/`subset` (dead after the mergeDescriptions rewrite),
`UNKNOWN_SECTION_RANK`, a pre-existing unused `basename` import in
check-conformance.mjs, and three `no-irregular-whitespace` from the zero-width
characters used to render a nested `*/` in a comment.

Also from CodeRabbit's review: `escapeCell` escapes backslashes before pipes (a
cell ending in a backslash before a pipe produced a doubled escape, splitting
the row), and `upsertFrontmatter` escapes the key before interpolating it into a
RegExp.

Its finding that the SCSS scanner mis-lexes `//` inside a quoted string is NOT
valid — the scan is left-to-right, so the opening quote is always reached first
and skipString consumes the literal. A/B tested with a `url()` containing `//`
both inline and in a `!default`: identical output either way. Documented in
place so it is not re-filed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

Copy link
Copy Markdown
Owner Author

Superseded by a 4-PR stack

This PR grew from the 24-file layout/ pilot to 199 files, past CodeRabbit's review limit — its last status was "Review skipped: 192 files exceed the limit of 100", so the only review here is against 2ae8488, commit 2 of 12. The Claude deep review never ran either. Split so each part is reviewable:

PR Contents Files Base
#420 generator, CI gates, review aids — no page generated 19 main
#421 layout + columns + grid generated, helpers reordered 27 #420
#422 elements 67 #421
#423 components + form 90 #422

MANAGED_CATEGORIES and ORDERED_CATEGORIES start empty in #420 and grow one category per PR, so each intermediate state is self-consistent: pnpm all and all ten conformance checks pass on every branch, and check-docs-parity reports 0 losses across all 87 pages at each step.

The stack tip reproduces this PR's tree byte-for-byte over the paths it owns, rebased onto current main (which had moved 12 commits ahead). One regression the split caught: taking docs/package.json wholesale would have dropped the flatten-llms-tabs build step #408 added — restored on every branch.

Also split out: #419, the bestax-migrate e2e race, which was unrelated to the docs work.

Review threads from this PR, resolved against current code

  • Fixed: escapeCell now escapes backslashes before pipes (this was the CodeQL alert); upsertFrontmatter escapes the key before interpolating it into a RegExp, and quotes YAML scalars — the generated Menu description contains ": " and was breaking the docs build; the 7 eslint errors, which nothing was catching because scripts/ was never linted (pnpm lint is turbo run lint, and only the packages define a lint script). docs: add the API page generator, its CI gates and review aids (1/4) #420 closes that gap and the matching format:check gap that let CI go red while pnpm all reported green.
  • Already addressed: the "length tie-break contradicts docs-page-first" finding — the page is now authoritative for any prop it already documented.
  • Not valid: the claim that the SCSS scanner mis-lexes // inside a quoted string. The scan is left-to-right, so the opening quote is always reached first and skipString consumes the literal. A/B tested with url("https://e.com/a.png") inline and in a !default — identical output either way. Documented in place so it is not re-filed.
  • Changed deliberately, not overlooked: renderCssVars computing when the region is absent. The generator now creates the cssvars section, since docs-sections requires it whenever a component has SCSS_SOURCES — it was never opt-outable.

Leaving this open for you to close, since the history and review threads may still be worth referring back to.


Generated by Claude Code

@allxsmith allxsmith added the deep-review label Jul 31, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

@coderabbitai full review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

@allxsmith I will run a full review of #399.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 59 minutes.

@allxsmith

Copy link
Copy Markdown
Owner Author

Completed in PRs #420, #421, #422, #423. This PR is no longer needed.

@allxsmith allxsmith closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants