Skip to content

refactor(*): migrate 'radix-ui' primitives to 'base-ui' - #1123

Open
sukvvon wants to merge 11 commits into
mainfrom
refactor/radix-to-base-ui
Open

refactor(*): migrate 'radix-ui' primitives to 'base-ui'#1123
sukvvon wants to merge 11 commits into
mainfrom
refactor/radix-to-base-ui

Conversation

@sukvvon

@sukvvon sukvvon commented Aug 6, 2026

Copy link
Copy Markdown
Member

Replaces all @radix-ui primitives with Base UI and removes the three @radix-ui/react-{dialog,dropdown-menu,tooltip} dependencies.

Component mapping

Radix Base UI
Dialog.Overlay Dialog.Backdrop
Dialog.Content Dialog.Popup
DropdownMenu.Content Menu.Positioner + Menu.Popup
Tooltip.Content Tooltip.Positioner + Tooltip.Popup
Tooltip.Provider delayDuration/skipDelayDuration delay/timeout
DropdownMenu.Item onSelect Menu.Item onClick
onSelect + e.preventDefault() closeOnClick={false}
onInteractOutside + preventDefault() onOpenChange + eventDetails.cancel()
`data-[state=open closed]`
--radix-*-transform-origin --transform-origin
--radix-dropdown-menu-content-available-height --available-height
focus: styling on items data-highlighted:

asChildrender

The dropdown adapters drop Radix's asChild boolean in favour of Base UI's render element prop.

On triggers render is required and children is not in the type. That matters: 22 of 24 <DropdownTrigger> call sites passed no asChild prop at all and silently relied on its = true default, so a boolean rename would have flipped them to wrapper mode with no compile error. Making render required turns every one of those sites into a type error instead.

On items render stays optional and children is kept — their previous default was already falsy, so wrapper-mode call sites are unchanged.

DropdownTrigger also gains an optional nativeButton passthrough. Base UI defaults it to true, so a trigger whose render is not a native <button> would otherwise be given button semantics — Radix's asChild had no such notion. Two call sites need nativeButton={false}: BrandContextMenu, whose trigger is a position: fixed virtual anchor <span>, and AuthenticatedUserMenu, whose trigger is a <div> wrapping the avatar.

Radix-only workarounds removed

SearchModal no longer needs the machinery that worked around Radix unmounting content during its exit animation — Base UI keeps popups mounted through it natively. Removed: forceMount, the shouldRenderSearch mount gate, searchModalTransitionMs, and the document.body.style.pointerEvents save/restore block.

That last one was not merely redundant: Radix portaled content outside the body's pointer-events scope, Base UI does not. Left in place it rendered the search modal visible but unclickable.

Radix DOM contracts consumed outside the migrated files

Radix's data-state and --radix-* are a DOM contract, so anything reading them breaks when the primitive is swapped — with no compile error. Three places consumed them:

  • src/styles/app.css animation selectors for .cart-panel, .cart-overlay, .search-modal-overlay, .search-modal-content and .dropdown-content
    move from [data-state='open'|'closed'] to [data-open] / [data-closed]. Base UI documents two idioms — data-open/data-closed for keyframe animations and data-starting-style/data-ending-style for transitions — and these rules are keyframe-based, so they use the former. Tooltip and the builder tooltips animate with transitions and use the latter.
  • PackagePills sized its menu with --radix-dropdown-menu-content-available-height, which no longer resolves;
    now --available-height. Confirmed in the browser that Base UI sets it on the positioner and it inherits to the popup.
  • LibraryLayout keeps the docs sidebar open while a dropdown launched from inside it is active, via
    expandedMenuRef.current?.querySelector('[data-state="open"]'). Base UI marks the open trigger with data-popup-open; the popup itself is portaled to body and so is out of that subtree, which rules out [data-open] here. Verified against the live sidebar: the new selector matches and the old one does not.

The last two were found while merging main, not by tsc — which is the concrete form of the silent-failure risk this section describes.

A fourth landed the same way, caught in review: PackagePills kept four onClick handlers calling e.preventDefault(). Under Radix that cancelled the close; under Base UI preventDefault() on a plain click handler does nothing, so its group-visibility toggle, colour picker, sub-package toggles and Add Packages would each have dismissed the menu on first use. They now use closeOnClick={false} like BaselineSection. The same edit removed an as unknown as React.MouseEvent cast that the old signature had required.

Other

  • The npm-stats and charts menus use Menu from @base-ui/react/menu directly rather than through a wrapper. An earlier revision of this branch kept a DropdownMenu* adapter so those call sites needed no edits, but three of its four parts only renamed Base UI's API (DropdownMenuMenu.Root, DropdownMenuTriggerMenu.Trigger, DropdownMenuItemMenu.Item with onSelect in place of onClick), which hid the real primitives for no benefit. Portal/Positioner/Popup is now written out at each of the 11 surfaces. The existing Dropdown.tsx is untouched — it carries the site's default popup styling, which these call sites do not want.
  • /ds/dropdown, /ds/cards and /ds/buttons copy and code samples updated to reference Base UI.

Verification

tsc, lint and the test suite pass.

Exercised in the browser, locally and on the preview deploy: blog author filter (open → select → filter applied), /ds/dropdown, /ds/buttons split button, navbar social menu, CopyPageDropdown, breadcrumb TOC, BrandContextMenu (anchors at cursor), LoginModal, CartDrawer, and SearchModal — including that its framework filter opens inside the modal without dismissing it. Console clean.

The preview deploy reaches the npm API, so the stats dropdowns were exercised there: all four ChartControls menus, BaselineSection presets, LatestBucketNavigator playback, and NPMStatsChart export/embed.

Three things that tsc cannot check were confirmed by reading computed style rather than by eye:

  • The docs sidebar guard: opening the sidebar's own FrameworkSelect and evaluating both selectors against that subtree — [data-popup-open] matches, [data-state="open"] does not.
  • PackagePills' max-height resolves to a real value again (1719px, matching --available-height on the positioner). With the stale --radix-* variable it computed to none.
  • CartDrawer plays cart-panel-in on open, then takes data-closed and plays cart-panel-out / cart-overlay-out on close — the CSS attribute rename works in both directions. The search modal's four animations were confirmed the same way via getAnimations().

closeOnClick={false} was checked where it replaces Radix's onSelect + preventDefault() idiom: clicking a baseline preset, and clicking the custom-ms input inside the playback menu, both leave the menu open.

Keyboard activation was checked directly, since Menu.Item takes onClick where Radix took onSelect: focusing an item in the blog author menu and pressing Enter, and separately Space, both apply the filter and close the menu. Base UI handles that internally — no extra key handling was needed.

Not exercised: AuthenticatedUserMenu and AvatarCropModal (need auth) and charts/ChartControls (admin only). Each is the same code path as something above — AvatarCropModal differs from the verified LoginModal only in max-w-md vs max-w-xs, AuthenticatedUserMenu uses the same DropdownItem render={...} as the navbar social menu, and charts/ChartControls has the same Menu.Positioner line as the verified npm-stats/ChartControls. The one thing that inference does not cover is AvatarCropModal's react-easy-crop widget interacting with Base UI's focus trap; the closest evidence is SearchModal's nested filter menu working.

Summary by CodeRabbit

  • Refactor

    • Migrated menus, dialogs, tooltips, and dropdowns to a unified component foundation.
    • Preserved existing navigation, selection, filtering, editing, chart controls, and modal workflows.
    • Improved popup handling and transitions across search, library, login, cart, and other overlays.
  • Documentation

    • Updated design-system descriptions and examples to reflect the new component implementation.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request replaces Radix UI dialog, dropdown, and tooltip primitives with Base UI components. Shared APIs and consumers now use render props, Base UI event handlers, positioning components, and updated state attributes.

Changes

Base UI migration

Layer / File(s) Summary
Menu foundation
package.json, src/components/Dropdown.tsx, src/components/ds/ui/index.tsx
Adds @base-ui/react, removes Radix packages, and updates shared dropdown APIs to Base UI menu primitives.
Dialog migration
src/components/*Modal.tsx, src/components/LibrariesOverlay.tsx, src/components/SearchModal.tsx, src/routes/stats/npm/index.tsx, src/styles/app.css
Migrates dialogs to Base UI and updates close handling, popup interaction, transition selectors, and search modal lifecycle handling.
Tooltip migration
src/ui/Tooltip.tsx, src/components/application-builder/parts.tsx, src/routes/ds.cards.tsx
Migrates tooltip rendering, timing, positioning, transitions, and documentation to Base UI.
Dropdown consumers
src/components/AuthenticatedUserMenu.tsx, src/components/BrandAssets.tsx, src/components/Navbar.tsx, src/components/Select.tsx, src/routes/ds.*.tsx, src/components/charts/ChartControls.tsx
Updates application and design-system dropdown triggers and items to use Base UI render props.
NPM statistics menus
src/components/npm-stats/*
Migrates statistics controls, presets, navigation, export, embed, and package menus to Base UI primitives and click handlers.

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

Suggested reviewers: seancassiere

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: migrating Radix UI primitives to Base UI.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/radix-to-base-ui

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
tanstack-com 5257538 Commit Preview URL

Branch Preview URL
Aug 08 2026, 02:40 AM

@sukvvon sukvvon self-assigned this Aug 6, 2026
@sukvvon
sukvvon marked this pull request as ready for review August 8, 2026 02:23
@sukvvon
sukvvon requested a review from a team August 8, 2026 02:24

@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 (5)
src/components/npm-stats/PackagePills.tsx (2)

243-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

A nested interactive button sits inside a menuitem.

The remove button is a child of Menu.Item, which renders with role="menuitem". Nested interactive controls inside a menu item are not reachable by keyboard in a menu composite, because the item itself owns focus. Screen reader users cannot invoke the remove action.

Consider rendering the remove action as its own Menu.Item for each sub-package.

🤖 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 `@src/components/npm-stats/PackagePills.tsx` around lines 243 - 254, Update the
sub-package menu structure around the remove button so the remove action is
rendered as its own sibling Menu.Item rather than a nested button inside another
menuitem. Preserve the existing onRemoveFromGroup arguments and stopPropagation
behavior while ensuring the action is independently keyboard reachable.

197-204: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove the cast from the Base UI Menu.Item handler.

Menu.Item.onClick has a Base UI-specific event shape, but handleColorClick only uses native DOM properties. Type Menu.Item’s handler consistently by replacing the cast with the actual event parameter type and passing e directly.

🤖 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 `@src/components/npm-stats/PackagePills.tsx` around lines 197 - 204, Update the
Menu.Item onClick handler around onColorClick to use the actual Base UI event
parameter type, removing the unknown React.MouseEvent cast and passing e
directly. Preserve the existing preventDefault call and arguments to
onColorClick.
src/components/SearchModal.tsx (1)

3368-3370: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The outside-press guard depends on an implicit CSS-class contract.

isSearchModalPortalTarget matches .dropdown-content. Only DropdownContent in src/components/Dropdown.tsx adds that class. The menus migrated in this PR that use Menu.Popup directly, for example src/components/charts/ChartControls.tsx and src/components/npm-stats/NPMStatsChart.tsx, do not add it. If any such menu is later rendered inside SearchModal, a click inside it closes the modal.

Consider matching the Base UI popup attribute instead of a project-specific class, for example [role="menu"], which is what src/components/LibrariesOverlay.tsx already uses for the same purpose.

♻️ Proposed change
 function isSearchModalPortalTarget(target: EventTarget | null) {
-  return target instanceof Element && !!target.closest('.dropdown-content')
+  return (
+    target instanceof Element &&
+    !!target.closest('.dropdown-content, [role="menu"]')
+  )
 }

Also applies to: 3403-3418

🤖 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 `@src/components/SearchModal.tsx` around lines 3368 - 3370, Update
isSearchModalPortalTarget to detect Base UI menu popups via their semantic
selector, such as [role="menu"], instead of relying only on the project-specific
.dropdown-content class. Preserve the existing Element/null safety and ensure
migrated Menu.Popup instances are recognized so inside-menu presses do not close
SearchModal.
src/components/charts/ChartControls.tsx (1)

42-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Several migrated menu triggers omit type="button". Moving each trigger into the render prop dropped the button type on four buttons. The default type is submit, so any of these inside a form submits it. Most other triggers in this PR set type="button".

  • src/components/charts/ChartControls.tsx#L42-L49: add type="button" to the time-range trigger button.
  • src/components/charts/ChartControls.tsx#L72-L85: add type="button" to the bin-type trigger button.
  • src/components/Breadcrumbs.tsx#L53-L61: add type="button" to the table-of-contents trigger button.
  • src/components/npm-stats/PackagePills.tsx#L132-L138: add type="button" to the "More options" trigger button.
🤖 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 `@src/components/charts/ChartControls.tsx` around lines 42 - 49, Set
type="button" on the trigger buttons rendered by Menu.Trigger in
ChartControls.tsx lines 42-49 and 72-85, Breadcrumbs.tsx lines 53-61, and
PackagePills.tsx lines 132-138, covering the time-range, bin-type,
table-of-contents, and “More options” triggers.
src/components/Dropdown.tsx (1)

36-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

children and render can both be set, and children is then dropped.

DropdownItemProps marks both children and render optional. If a call site passes both, this code renders only render. Consider a discriminated union so the type rejects that combination.

Also applies to: 125-135

🤖 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 `@src/components/Dropdown.tsx` around lines 36 - 41, Update DropdownItemProps
to a discriminated union that allows either children or render, but not both,
and ensure the render path at the referenced DropdownItem usage preserves that
mutually exclusive contract. Keep onSelect and className available on both
variants while rejecting call sites that provide both content props.
🤖 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 `@src/components/Breadcrumbs.tsx`:
- Around line 80-82: Update the heading rendering in the Breadcrumbs component
to sanitize heading.text before passing it to dangerouslySetInnerHTML,
preserving supported inline markup while removing unsafe content. Keep the
existing heading output behavior and ensure the sanitized value is used in the
rendered span.

In `@src/components/Dropdown.tsx`:
- Around line 60-70: Update DropdownTrigger to derive nativeButton from render,
passing false whenever the trigger is not a native button while preserving
explicit nativeButton values such as BrandContextMenu’s false setting. Keep
native button triggers using the existing true/default behavior and continue
forwarding className and render to Menu.Trigger.
- Around line 110-136: Update DropdownItem to preserve explicit keyboard
Enter/Space activation through the supported Menu.Item selection mechanism
rather than relying solely on onClick. Keep onSelect wired to the documented
item-selection prop, and ensure the render and non-render branches both preserve
that keyboard behavior.

In `@src/components/ds/ui/index.tsx`:
- Around line 827-857: Sanitize heading content before Breadcrumbs renders it
through dangerouslySetInnerHTML. Update collectHeadingsFromBlocks or the
MarkdownHeading construction path to sanitize MarkdownHeading.text while
preserving the existing heading markup and navigation behavior; ensure
Breadcrumbs only receives sanitized HTML.

In `@src/components/npm-stats/NPMStatsChart.tsx`:
- Around line 1747-1751: Update the height input handling around the onChange
callback to preserve an empty raw value while the user edits, instead of
converting it to 0 and clamping it to 240. Keep the existing 240–1200 bounds for
valid numeric input, and apply the minimum or other normalization on blur so
clearing the controlled field remains possible.
- Around line 1737-1755: Update the iframe-height input and the read-only input
and textarea in the Menu.Popup content to add an onKeyDown handler that stops
propagation, matching the existing PackagePills pattern, so field keyboard
interactions—including arrow keys, Escape, and digits—are not captured by menu
typeahead.

In `@src/components/npm-stats/PackagePills.tsx`:
- Around line 180-271: Update the Menu.Item handlers in PackagePills, including
the visibility item, color item, sub-package items, and add-packages item, to
use Base UI’s closeOnClick behavior instead of preventDefault. Set
closeOnClick={false} on actions that must keep the menu open, especially
onToggleVisibility and repeated sub-package actions, and remove the now-inert
e.preventDefault() calls; preserve closing behavior for one-shot actions if
appropriate.

In `@src/components/Select.tsx`:
- Around line 66-74: Update the selected option label span in Select to include
min-w-0 and flex-1 so long labels can shrink, and add right padding to reserve
space for the CaretUpDownIcon. Keep the existing truncation styling and caret
positioning unchanged.

In `@src/routes/ds.dropdown.tsx`:
- Line 33: Update the description in the route metadata to reference
src/components/ds/ui/index.tsx instead of src/components/Dropdown.tsx, keeping
the rest of the description unchanged.

---

Nitpick comments:
In `@src/components/charts/ChartControls.tsx`:
- Around line 42-49: Set type="button" on the trigger buttons rendered by
Menu.Trigger in ChartControls.tsx lines 42-49 and 72-85, Breadcrumbs.tsx lines
53-61, and PackagePills.tsx lines 132-138, covering the time-range, bin-type,
table-of-contents, and “More options” triggers.

In `@src/components/Dropdown.tsx`:
- Around line 36-41: Update DropdownItemProps to a discriminated union that
allows either children or render, but not both, and ensure the render path at
the referenced DropdownItem usage preserves that mutually exclusive contract.
Keep onSelect and className available on both variants while rejecting call
sites that provide both content props.

In `@src/components/npm-stats/PackagePills.tsx`:
- Around line 243-254: Update the sub-package menu structure around the remove
button so the remove action is rendered as its own sibling Menu.Item rather than
a nested button inside another menuitem. Preserve the existing onRemoveFromGroup
arguments and stopPropagation behavior while ensuring the action is
independently keyboard reachable.
- Around line 197-204: Update the Menu.Item onClick handler around onColorClick
to use the actual Base UI event parameter type, removing the unknown
React.MouseEvent cast and passing e directly. Preserve the existing
preventDefault call and arguments to onColorClick.

In `@src/components/SearchModal.tsx`:
- Around line 3368-3370: Update isSearchModalPortalTarget to detect Base UI menu
popups via their semantic selector, such as [role="menu"], instead of relying
only on the project-specific .dropdown-content class. Preserve the existing
Element/null safety and ensure migrated Menu.Popup instances are recognized so
inside-menu presses do not close SearchModal.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 646d5086-9917-4679-8e27-613f02b78fea

📥 Commits

Reviewing files that changed from the base of the PR and between ece8227 and 269aa2a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • package.json
  • src/components/AuthenticatedUserMenu.tsx
  • src/components/AvatarCropModal.tsx
  • src/components/BlogAuthorFilter.tsx
  • src/components/BrandContextMenu.tsx
  • src/components/Breadcrumbs.tsx
  • src/components/CopyPageDropdown.tsx
  • src/components/Dropdown.tsx
  • src/components/LibrariesBrowser.tsx
  • src/components/LibrariesOverlay.tsx
  • src/components/LibraryLayout.tsx
  • src/components/LoginModal.tsx
  • src/components/Navbar.tsx
  • src/components/SearchModal.tsx
  • src/components/Select.tsx
  • src/components/application-builder/parts.tsx
  • src/components/charts/ChartControls.tsx
  • src/components/ds/BrandAssets.tsx
  • src/components/ds/ui/index.tsx
  • src/components/npm-stats/BaselineSection.tsx
  • src/components/npm-stats/ChartControls.tsx
  • src/components/npm-stats/LatestBucketNavigator.tsx
  • src/components/npm-stats/NPMStatsChart.tsx
  • src/components/npm-stats/PackagePills.tsx
  • src/components/shop/CartDrawer.tsx
  • src/routes/ds.buttons.tsx
  • src/routes/ds.cards.tsx
  • src/routes/ds.dropdown.tsx
  • src/routes/ds.iconography.lazy.tsx
  • src/routes/stats/npm/index.tsx
  • src/styles/app.css
  • src/ui/Tooltip.tsx

Comment on lines +80 to +82
<span
dangerouslySetInnerHTML={{ __html: heading.text }}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Trace where MarkdownHeading values are produced.
rg -n 'MarkdownHeading' --type=ts --type=tsx src | head -30
rg -nP -C 4 '\bheadings\s*[:=]' --type=ts --type=tsx src | head -60

Repository: TanStack/tanstack.com

Length of output: 223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect repository files and locate the Breadcrumbs component plus heading usage.
printf 'Tracked files matching Breadcrumbs: '; git ls-files | grep -E '(^|/)Breadcrumbs\.tsx$' || true
echo
printf 'Tracked TypeScript/TSX files containing MarkdownHeading/headings: '; git ls-files '*.{ts,tsx}' | xargs rg -n 'MarkdownHeading|headings' || true
echo
printf 'Breadcrumbs relevant section: '; wc -l src/components/Breadcrumbs.tsx
sed -n '1,140p' src/components/Breadcrumbs.tsx

Repository: TanStack/tanstack.com

Length of output: 13039


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Collect MarkdownHeading producers:\n'
sed -n '1,320p' src/utils/markdown/processor.ts

printf '\nBreadcrumbs callers:\n'
rg -n -C 5 'Breadcrumbs|headings=\{markdown\.headings\}|headings=\w+' src/components src/routes | sed -n '1,220p'

printf '\nDoc and blog routes around markdown parsing:\n'
sed -n '1,90p' src/components/Doc.tsx
sed -n '60,180p' src/routes/blog.$.tsx

Repository: TanStack/tanstack.com

Length of output: 23830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repo/package markdown dependencies:\n'
git ls-files | grep -E '(^|/)(package\.json)$' | xargs -r rg -n '"`@tanstack/markdown`"|markdown|allowHtml|inlineHtml' || true

printf '\nBlog loader and post source data:\n'
rg -n -C 8 'function\s*\(|export async function|fetchBlogPost|files|read.*blog|markdown|content|content:' src/routes/src/blog src/utils src/components src | sed -n '1,220p'

printf '\nDoc loader content source:\n'
rg -n -C 8 'function\s*\(|export async function|content:|markdown|fetch|read|filePath' src/routes src/components/Doc.tsx src/utils/markdown src/utils | sed -n '1,240p'

Repository: TanStack/tanstack.com

Length of output: 14878


🌐 Web query:

TanStack Markdown 0.0.11 allowHtml inlineHtml parser heading

💡 Result:

In TanStack Markdown, raw HTML support is an opt-in feature controlled by the allowHtml option [1][2]. When allowHtml: true is configured in your ParseOptions or RenderOptions, the parser recognizes and includes raw block and inline HTML nodes in the output [3][4]. Regarding your specific areas of interest: 1. allowHtml: This is a boolean option [3][4]. By default, it is set to false, meaning raw HTML is escaped [3][5]. Enabling it requires an explicit trust boundary, as TanStack Markdown does not act as a general HTML sanitizer [5][2]. 2. inlineHtml: Inline HTML is supported only when allowHtml is enabled [1]. The parser produces an HtmlInlineNode type, which contains the raw inline HTML value [4]. 3. Parser and Headings: The parser handles ATX headings (levels 1-6) and automatically generates duplicate-safe IDs for them [6][1]. You can configure this behavior using the headingIds option [3][4]. Setting headingIds to false disables ID generation, or you can provide a custom function to implement your own ID logic [6]. For security, the library advises keeping allowHtml disabled for untrusted user-generated content [5]. When enabled, renderers use dangerouslySetInnerHTML for the HTML nodes [5]. If you need to render HTML, ensure you have an independent defense layer or sanitization policy in place if the source is not trusted [5].

Citations:


Sanitize or avoid dangerouslySetInnerHTML for heading text.

allowHtml: true is enabled in parseSiteMarkdown, so parsed headings can include raw inline HTML. inlineText ignores HtmlInlineNode, but this should not be relied on once raw HTML support is opted in; render headings safely before passing them to dangerouslySetInnerHTML.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 80-80: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(react-unsafe-html-injection)

🪛 React Doctor (0.9.3)

[error] 81-81: dangerouslySetInnerHTML is an XSS hole that runs attacker-controlled HTML in your users' browsers.

Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.

(no-danger)

🤖 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 `@src/components/Breadcrumbs.tsx` around lines 80 - 82, Update the heading
rendering in the Breadcrumbs component to sanitize heading.text before passing
it to dangerouslySetInnerHTML, preserving supported inline markup while removing
unsafe content. Keep the existing heading output behavior and ensure the
sanitized value is used in the rendered span.

Source: Linters/SAST tools

Comment on lines 60 to +70
export function DropdownTrigger({
children,
render,
className,
asChild = true,
nativeButton,
}: DropdownTriggerProps) {
return (
<DropdownMenu.Trigger asChild={asChild} className={className}>
{children}
</DropdownMenu.Trigger>
<Menu.Trigger
className={className}
render={render}
nativeButton={nativeButton}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List every DropdownTrigger call site and the element passed to render.
rg -nP -A 6 '<DropdownTrigger\b' --type=tsx --type=ts src

Repository: TanStack/tanstack.com

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)Dropdown\.tsx$|Dropdown' || true

echo
echo "Call sites:"
rg -n -A 8 '<DropdownTrigger\b' -g '*.tsx' -g '*.ts' src || true

echo
echo "Trigger implementation:"
fd -a 'Dropdown\.tsx' src | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,140p' "$f"
done

Repository: TanStack/tanstack.com

Length of output: 26493


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Dropdown imports:"
fd -a 'Dropdown\.tsx$|AuthenticatedUserMenu\.tsx$|BrandContextMenu\.tsx$' src | while read -r f; do
  echo "--- $f"
  sed -n '1,80p' "$f"
done

echo
echo "Non-button/non-explicit nativeButton call sites:"
rg -n -B 3 -A 10 '<DropdownTrigger\b' -g '*.tsx' -g '*.ts' src \
  | rg -v 'nativeButton|type="button"|<button|<Button' \
  || true

echo
echo "Node package metadata for Base UI version:"
for f in package.json src/**/package.json; do
  [ -f "$f" ] && { echo "--- $f"; rg -n '"`@base-ui/react`"|"`@base-ui`|`@base-ui/react/menu`' "$f" || true; }
done

Repository: TanStack/tanstack.com

Length of output: 32396


🌐 Web query:

Base UI React Menu Trigger nativeButton default false props

💡 Result:

In the Base UI React Menu component, the Menu.Trigger component has a nativeButton prop, which defaults to true, not false [1]. When nativeButton is set to true (the default), the Menu.Trigger renders as a native HTML element [1][2]. This provides standard button behavior, including focus management and appropriate ARIA attributes for accessibility. If you explicitly set nativeButton to false, the component will not automatically render as a element, allowing for greater customization of the underlying rendered element [1]. However, in most standard use cases, leaving it at its default value of true is recommended to ensure accessibility and expected interaction patterns [3][2].

Citations:


Derive nativeButton from the trigger element.

Menu.Trigger defaults nativeButton to true, so AuthenticatedUserMenu’s <div> trigger gets button semantics. Preserve non-native triggers by setting nativeButton in DropdownTrigger whenever render is not a native button, and keep BrandContextMenu’s explicit nativeButton={false}.

🤖 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 `@src/components/Dropdown.tsx` around lines 60 - 70, Update DropdownTrigger to
derive nativeButton from render, passing false whenever the trigger is not a
native button while preserving explicit nativeButton values such as
BrandContextMenu’s false setting. Keep native button triggers using the existing
true/default behavior and continue forwarding className and render to
Menu.Trigger.

Comment on lines 110 to 136
export function DropdownItem({
children,
className,
onSelect,
asChild,
render,
}: DropdownItemProps) {
const itemClassName = twMerge(
'flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 outline-none',
'text-sm text-gray-700 dark:text-gray-300',
'hover:bg-gray-100 dark:hover:bg-gray-700/50',
'data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-700/50',
'transition-colors duration-150',
className,
)

if (render) {
return (
<Menu.Item onClick={onSelect} className={itemClassName} render={render} />
)
}

return (
<DropdownMenu.Item
asChild={asChild}
onSelect={onSelect}
className={twMerge(
'flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 outline-none',
'text-sm text-gray-700 dark:text-gray-300',
'hover:bg-gray-100 dark:hover:bg-gray-700/50',
'focus:bg-gray-100 dark:focus:bg-gray-700/50',
'transition-colors duration-150',
className,
)}
>
<Menu.Item onClick={onSelect} className={itemClassName}>
{children}
</DropdownMenu.Item>
</Menu.Item>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the installed Base UI Menu.Item typings for selection-related props.
fd -t d -H 'base-ui*' node_modules 2>/dev/null | head
fd -t f 'index.d.ts' node_modules/@base-ui 2>/dev/null | head -20
rg -n 'onClick|closeOnClick|nativeButton|MenuItemProps' node_modules/@base-ui --glob '*.d.ts' -m 40 2>/dev/null | head -40

Repository: TanStack/tanstack.com

Length of output: 159


🌐 Web query:

Base UI Menu.Item keyboard Enter selection onClick prop documentation

💡 Result:

In Base UI, the Menu.Item component provides an onClick prop that serves as the primary click handler for the item [1][2]. When a Menu.Item is selected via keyboard interaction—specifically by pressing the Enter key—it is expected to trigger this onClick handler [1]. However, developers have reported inconsistencies where the onClick event may not fire as expected when using custom render overrides or specific underlying elements (such as links) [3][4]. Key technical considerations include: 1. Standard Behavior: By default, Menu.Item renders an element that manages interaction states [2]. When the item is focused and Enter is pressed, the component handles the activation, which should trigger the onClick event [1]. 2. Troubleshooting Custom Rendering: If you are using the render prop to replace the default element, you must ensure that the underlying component correctly propagates event handlers [3]. In some configurations, particularly when overriding the default element, you may need to explicitly ensure that the Enter key correctly triggers the component's internal activation logic, or verify that the native event propagation is not being blocked [3][4]. 3. Link Navigation: When rendering a link (e.g., an tag) as a Menu.Item, pressing Enter may sometimes close the menu without triggering the navigation [4]. This can occur if the internal activation logic suppresses default browser behaviors or if the event handling requires manual intervention to bridge the gap between Base UI's activation and the link's standard behavior [4][5]. 4. Disabled State: Disabled items are focusable via keyboard navigation but cannot be activated, meaning the onClick handler will not be called [6][7]. If your onClick handler is not firing on Enter, ensure that your custom component is correctly forwarding events and that you are not inadvertently intercepting the activation sequence [3]. For advanced cases involving complex navigation or custom components, you may need to use a ref to manually trigger actions or handle specific keydown events to ensure accessibility and expected behavior [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package references =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
  [ -e "$f" ] && { echo "--- $f"; rg -n '"`@base-ui/react`"|`@base-ui/react`' "$f" | head -20 || true; }
done

echo "== installed package manifests, if present =="
fd -t f 'package.json' node_modules/@base-ui 2>/dev/null | while read -r f; do
  echo "--- $f"
  sed -n '1,100p' "$f"
done

echo "== installed types/tests if present =="
rg -n 'onClick|onActivate|activate|keyboard|Enter|Space|onKeyDown' node_modules/@base-ui 2>/dev/null --glob '*.{js,jsx,ts,tsx,d.ts}' -m 80 || true

echo "== relevant source references in repo =="
fd -t f 'Dropdown.tsx|package.json|pnpm-lock.yaml' . | sed -n '1,120p'
rg -n 'DropdownItem|Menu\.Item|Dropdown' src --glob '*.{ts,tsx}' -m 50 || true

Repository: TanStack/tanstack.com

Length of output: 457


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== base-ui package manifests if present =="
fd -t f 'package.json' node_modules/@base-ui 2>/dev/null | while read -r f; do
  echo "--- $f"
  sed -n '1,120p' "$f"
done

echo "== package location and installed files =="
fd -a 'base-ui|`@base`+ui|*.d.ts' node_modules 2>/dev/null | sed -n '1,120p' || true

echo "== installed type/check tests mentioning click/key activation =="
rg -n 'onClick|onActivate|activate|keyboard|Enter|Space|onKeyDown|MenuItemProps' node_modules/@base-ui 2>/dev/null --glob '*.{js,jsx,ts,tsx,d.ts}' -m 120 || true

echo "== repo package references and Dropdown file =="
sed -n '1,80p' package.json
sed -n '90,145p' src/components/Dropdown.tsx

Repository: TanStack/tanstack.com

Length of output: 202


🌐 Web query:

Base UI Menu.Item onClick fires Enter activation default behavior @base-ui/react``

💡 Result:

In @base-ui/react, the Menu.Item component is designed to trigger its onClick handler when activated via the Enter key (or Space, depending on the configuration and element type) [1][2]. However, users have reported issues where the onClick handler is not correctly fired for keyboard users in certain scenarios, particularly when using the render prop to customize the underlying element [3][4]. If you are encountering this issue, please consider the following points based on the current state of Base UI: 1. Native Element Behavior: By default, Menu.Item renders a div element. If you replace the default element with a button-like component using the render prop, you may need to ensure proper handling of native button behaviors [3]. Some issues related to Enter key activation have been traced back to event.preventDefault calls within internal hooks like useButton, which can inadvertently block the activation of custom elements or links [4]. 2. Known Issues: There are active GitHub issues tracking inconsistent behavior when rendering custom components (such as links) or when the internal button logic interacts poorly with specific rendered elements [3][4][1]. 3. Debugging: If your onClick handler is not firing, verify if your custom component requires additional props or if the default event propagation is being interrupted [3]. You can also test with the default element to confirm if the issue is specific to your custom rendering logic [3]. For the most reliable behavior, ensure that any custom component passed to the render prop correctly manages its own interactive state, and check the official Base UI repository for recent fixes or status updates on these specific keyboard navigation issues [3][4].

Citations:


Keep keyboard item activation on onSelect.

@base-ui/react 1.7.0 does not expose a supported MenuItemProps.onSelect replacement, but relying on onClick makes keyboard selection depend on synthesized click behavior. Preserve keyboard Enter/Space activation explicitly rather than replacing Radix’s documented selection prop.

🤖 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 `@src/components/Dropdown.tsx` around lines 110 - 136, Update DropdownItem to
preserve explicit keyboard Enter/Space activation through the supported
Menu.Item selection mechanism rather than relying solely on onClick. Keep
onSelect wired to the documented item-selection prop, and ensure the render and
non-render branches both preserve that keyboard behavior.

Comment on lines +827 to +857
<DropdownTrigger
render={
<button
className={twMerge(
hiddenClass,
'inline-flex cursor-pointer items-center gap-1 whitespace-nowrap text-text-muted transition-colors hover:text-text-primary',
)}
>
<span>On this page</span>
<CaretDownIcon className="h-3.5 w-3.5" />
</button>
}
/>
<DropdownContent align="end" sideOffset={8} className={hiddenClass}>
{headings.map((heading) => (
<DropdownItem key={`breadcrumb-toc-${heading.id}`} asChild>
<Link
to="."
hash={heading.id}
style={{
paddingLeft: `${(heading.level - 2) * 0.5 + 0.5}rem`,
}}
resetScroll={false}
hashScrollIntoView={{ behavior: 'smooth' }}
>
<span dangerouslySetInnerHTML={{ __html: heading.text }} />
</Link>
</DropdownItem>
<DropdownItem
key={`breadcrumb-toc-${heading.id}`}
render={
<Link
to="."
hash={heading.id}
style={{
paddingLeft: `${(heading.level - 2) * 0.5 + 0.5}rem`,
}}
resetScroll={false}
hashScrollIntoView={{ behavior: 'smooth' }}
>
<span dangerouslySetInnerHTML={{ __html: heading.text }} />
</Link>
}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/components/ds/ui/index.tsx --match Breadcrumbs --view expanded
rg -n -C 4 --type ts --type tsx \
  '\bMarkdownHeading\b|dangerouslySetInnerHTML|heading\.text' src

Repository: TanStack/tanstack.com

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect relevant components without relying on rg file-type syntax.
printf '--- Breadcrumbs outline ---\n'
ast-grep outline src/components/ds/ui/index.tsx --match Breadcrumbs --view expanded || true

printf '\n--- Relevant symbol matches ---\n'
grep -RInE --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' 'MarkdownHeading|dangerouslySetInnerHTML|heading\.text' src || true

printf '\n--- Candidate file sections ---\n'
python3 - <<'PY'
from pathlib import Path
for p in Path('src').rglob('*'):
    if p.is_file() and p.suffix in {'.ts','.tsx','.js','.jsx'}:
        try:
            text = p.read_text(encoding='utf-8')
        except UnicodeDecodeError:
            continue
        lines = text.splitlines()
        hits=None
        for i,l in enumerate(lines):
            if 'MarkdownHeading' in l or 'dangerouslySetInnerHTML' in l or 'heading.text' in l:
                if hits is None: hits=[]
                hits.append(i)
        if hits:
            print(f'\n--- {p} ---')
            for i,h in enumerate(hits):
                a=max(0,h-6); b=min(len(lines),h+8)
                print(f'@@ lines {a+1}-{b}')
                for n in range(a,b):
                    print(f'{n+1}: {lines[n]}')
PY

Repository: TanStack/tanstack.com

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect relevant components without relying on rg file-type syntax.
echo '--- Breadcrumbs outline ---'
ast-grep outline src/components/ds/ui/index.tsx --match Breadcrumbs --view expanded || true

echo
echo '--- Relevant symbol matches ---'
grep -RInE --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' 'MarkdownHeading|dangerouslySetInnerHTML|heading\.text' src || true

echo
echo '--- Candidate file sections ---'
python3 - <<'PY'
from pathlib import Path
for p in Path('src').rglob('*'):
    if p.is_file() and p.suffix in {'.ts','.tsx','.js','.jsx'}:
        try:
            text = p.read_text(encoding='utf-8')
        except UnicodeDecodeError:
            continue
        lines = text.splitlines()
        hits = None
        for i, l in enumerate(lines):
            if 'MarkdownHeading' in l or 'dangerouslySetInnerHTML' in l or 'heading.text' in l:
                if hits is None:
                    hits = []
                hits.append(i)
        if hits:
            print(f'\n--- {p} ---')
            for h in hits:
                a = max(0, h - 6)
                b = min(len(lines), h + 8)
                print(f'@@ lines {a+1}-{b}')
                for n in range(a, b):
                    print(f'{n+1}: {lines[n]}')
PY

Repository: TanStack/tanstack.com

Length of output: 25917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package references for `@tanstack/markdown` ---'
grep -RInE '`@tanstack/markdown`' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true

echo
echo '--- markdown processor relevant sections ---'
sed -n '1,260p' src/utils/markdown/processor.ts

echo
echo '--- docs markdown extensions references ---'
find . -path './node_modules' -prune -o \( -type f \( -name '*.ts' -o -name '*.tsx' \) \) -print | xargs grep -lE 'docsMarkdownExtensions|sanitize|rehype|remark|html' 2>/dev/null | head -50 || true

Repository: TanStack/tanstack.com

Length of output: 8734


🌐 Web query:

@tanstack/markdown 0.0.11 parseMarkdown allowHtml extensions sanitize headings text

💡 Result:

In @tanstack/markdown (version 0.0.11), the handling of HTML, extensions, and heading-related text is governed by the core parser and renderer architecture as follows: Allowing HTML: By default, raw HTML is escaped for security. To enable raw block and inline HTML, you must set the allowHtml option to true in the ParseOptions passed to parseMarkdown or the RenderOptions passed to renderHtml [1][2][3]. Even when enabled, the library automatically strips executable URL protocols (such as javascript:) from links and images [4][1][3]. Extensions: Extensions are synchronous hooks used to implement profile-specific syntax [5][6]. You can define them as objects with optional hooks including parseBlock, transformDocument, transformInline, and renderHtml [5][7][6]. Extensions are passed as an array to the extensions option in ParseOptions [2][7]. The renderHtml hook allows you to provide custom HTML for nodes; note that HTML returned by these custom hooks is trusted and not automatically sanitized by the library [5]. Headings: The parser supports ATX headings (levels 1-6) [3]. By default, the parser generates lowercase, duplicate-safe IDs for headings [2][6]. You can customize this behavior via the headingIds option in ParseOptions, which accepts either a boolean (to disable) or a custom function that receives the heading text and the normalized line index to return a unique ID string [2][6]. When rendering, you can optionally enable anchor links by setting headingAnchors: true in the render options [8]. Sanitization: TanStack Markdown does not include a general-purpose HTML sanitizer [4][3]. Its security model relies on escaping raw HTML by default and stripping unsafe URL schemes [4][1]. Because the library prioritizes a lightweight, synchronous architecture, it assumes that any raw HTML or custom extension-generated HTML provided to the renderer is trusted [4][5]. [4][1][2][5][6][3]

Citations:


Sanitize MarkdownHeading.text before rendering it as HTML.

parseSiteMarkdown enables allowHtml, and collectHeadingsFromBlocks saves inline markdown as heading.text; Breadcrumbs later passes that value through dangerouslySetInnerHTML. Keep this rendering only if MarkdownHeading stores sanitized text.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 853-853: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(react-unsafe-html-injection)

🪛 React Doctor (0.9.3)

[error] 854-854: dangerouslySetInnerHTML is an XSS hole that runs attacker-controlled HTML in your users' browsers.

Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.

(no-danger)

🤖 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 `@src/components/ds/ui/index.tsx` around lines 827 - 857, Sanitize heading
content before Breadcrumbs renders it through dangerouslySetInnerHTML. Update
collectHeadingsFromBlocks or the MarkdownHeading construction path to sanitize
MarkdownHeading.text while preserving the existing heading markup and navigation
behavior; ensure Breadcrumbs only receives sanitized HTML.

Source: Linters/SAST tools

Comment on lines +1737 to +1755
<Menu.Popup className="z-50 w-[min(420px,calc(100vw-2rem))] rounded-md bg-white p-3 text-gray-900 shadow-lg dark:bg-gray-800 dark:text-gray-100">
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-medium">Embed chart</div>
<label className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300">
<span>Iframe height</span>
<input
className="w-16 rounded border border-gray-500/20 bg-gray-50 px-1.5 py-1 text-right font-mono text-[11px] outline-none focus:border-blue-500 dark:bg-gray-900"
max={1200}
min={240}
onChange={(event) => {
const nextHeight = Number(event.currentTarget.value)
if (!Number.isFinite(nextHeight)) return
setIframeHeight(Math.max(240, Math.min(1200, nextHeight)))
}}
type="number"
value={iframeHeight}
/>
</label>

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

Text entry inside the menu popup can be captured by menu typeahead.

Menu.Popup applies composite navigation and printable-character typeahead to its subtree. The iframe-height <input> here does not stop key events. The same pattern in src/components/npm-stats/PackagePills.tsx at lines 169-174 calls event.stopPropagation() on onKeyDown for exactly this reason. Apply the same guard to the number input, and to the read-only <input> and <textarea> below, so that arrow keys, Escape, and digits reach the fields.

🐛 Proposed fix
                   <input
                     className="w-16 rounded border border-gray-500/20 bg-gray-50 px-1.5 py-1 text-right font-mono text-[11px] outline-none focus:border-blue-500 dark:bg-gray-900"
                     max={1200}
                     min={240}
                     onChange={(event) => {
                       const nextHeight = Number(event.currentTarget.value)
                       if (!Number.isFinite(nextHeight)) return
                       setIframeHeight(Math.max(240, Math.min(1200, nextHeight)))
                     }}
+                    onClick={(event) => event.stopPropagation()}
+                    onKeyDown={(event) => event.stopPropagation()}
                     type="number"
                     value={iframeHeight}
                   />
🧰 Tools
🪛 React Doctor (0.9.3)

[error] 1748-1748: Coercing an input's value with this parse stores 0 for a cleared field and NaN for partial input, which then flows into state or a request body; guard the empty and NaN cases (for example value ? Number(value) : undefined) before using it.

Guard Number(e.target.value) / parseInt(e.target.value) against empty and NaN before storing it. Number('') is 0 and Number('abc') is NaN, both of which silently ship a wrong value.

(no-unguarded-numeric-input-parse)

🤖 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 `@src/components/npm-stats/NPMStatsChart.tsx` around lines 1737 - 1755, Update
the iframe-height input and the read-only input and textarea in the Menu.Popup
content to add an onKeyDown handler that stops propagation, matching the
existing PackagePills pattern, so field keyboard interactions—including arrow
keys, Escape, and digits—are not captured by menu typeahead.

Comment on lines +1747 to +1751
onChange={(event) => {
const nextHeight = Number(event.currentTarget.value)
if (!Number.isFinite(nextHeight)) return
setIframeHeight(Math.max(240, Math.min(1200, nextHeight)))
}}

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

A cleared height field snaps to 240 and blocks further editing.

Number('') is 0, and Number.isFinite(0) is true. The clamp then stores 240. The user cannot clear the field to type a new value, because the controlled value immediately becomes 240.

Keep the raw text in state, or clamp only on blur.

🐛 Proposed fix
                     onChange={(event) => {
-                      const nextHeight = Number(event.currentTarget.value)
-                      if (!Number.isFinite(nextHeight)) return
-                      setIframeHeight(Math.max(240, Math.min(1200, nextHeight)))
+                      const raw = event.currentTarget.value
+                      if (raw === '') return
+                      const nextHeight = Number(raw)
+                      if (!Number.isFinite(nextHeight)) return
+                      setIframeHeight(nextHeight)
                     }}
+                    onBlur={() =>
+                      setIframeHeight((current) =>
+                        Math.max(240, Math.min(1200, current)),
+                      )
+                    }
🧰 Tools
🪛 React Doctor (0.9.3)

[error] 1748-1748: Coercing an input's value with this parse stores 0 for a cleared field and NaN for partial input, which then flows into state or a request body; guard the empty and NaN cases (for example value ? Number(value) : undefined) before using it.

Guard Number(e.target.value) / parseInt(e.target.value) against empty and NaN before storing it. Number('') is 0 and Number('abc') is NaN, both of which silently ship a wrong value.

(no-unguarded-numeric-input-parse)

🤖 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 `@src/components/npm-stats/NPMStatsChart.tsx` around lines 1747 - 1751, Update
the height input handling around the onChange callback to preserve an empty raw
value while the user edits, instead of converting it to 0 and clamping it to
240. Keep the existing 240–1200 bounds for valid numeric input, and apply the
minimum or other normalization on blur so clearing the controlled field remains
possible.

Source: Linters/SAST tools

Comment thread src/components/npm-stats/PackagePills.tsx
Comment thread src/components/Select.tsx
Comment on lines +66 to 74
<span className="truncate font-medium">
{selectedOption.label}
</span>
) : selectedOption.logo ? (
<span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20">
<img
height={16}
width={16}
src={selectedOption.logo}
alt={`${selectedOption.label} logo`}
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<CaretUpDownIcon
className="h-4 w-4 opacity-40"
aria-hidden="true"
/>
</span>

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

Make the selected label shrink and reserve caret space.

The label can overlap the caret or overflow the trigger for a long option label. Add min-w-0 flex-1 and right padding to the label.

Proposed fix
-              <span className="truncate font-medium">
+              <span className="min-w-0 flex-1 truncate pr-6 font-medium">
                 {selectedOption.label}
               </span>
📝 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
<span className="truncate font-medium">
{selectedOption.label}
</span>
) : selectedOption.logo ? (
<span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20">
<img
height={16}
width={16}
src={selectedOption.logo}
alt={`${selectedOption.label} logo`}
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<CaretUpDownIcon
className="h-4 w-4 opacity-40"
aria-hidden="true"
/>
</span>
<span className="min-w-0 flex-1 truncate pr-6 font-medium">
{selectedOption.label}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<CaretUpDownIcon
className="h-4 w-4 opacity-40"
aria-hidden="true"
/>
</span>
🤖 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 `@src/components/Select.tsx` around lines 66 - 74, Update the selected option
label span in Select to include min-w-0 and flex-1 so long labels can shrink,
and add right padding to reserve space for the CaretUpDownIcon. Keep the
existing truncation styling and caret positioning unchanged.

Comment thread src/routes/ds.dropdown.tsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant