Skip to content

additive react-mentions-ts/core entry while keeping the root entry st…#194

Draft
hbmartin wants to merge 1 commit into
masterfrom
factor-out-core
Draft

additive react-mentions-ts/core entry while keeping the root entry st…#194
hbmartin wants to merge 1 commit into
masterfrom
factor-out-core

Conversation

@hbmartin
Copy link
Copy Markdown
Owner

@hbmartin hbmartin commented Apr 19, 2026

…yled/backward-compatible. The split is now explicit: core uses dependency-free structural styles and class concatenation, while styled root keeps cva, clsx, and tailwind-merge in src/styles/styled.ts (line 42).

Updated packaging/build exports in package.json (line 11) and tsdown.config.ts (line 4), added core entry tests in src/coreEntry.spec.tsx (line 1), and removed the stale src/utils/cn.ts helper.

Summary by CodeRabbit

  • New Features

    • Added a new /core entry point providing an unstyled component API.
    • New export path ./styles/tailwind.css for explicit styling imports.
  • Documentation

    • Updated README to document both styled (default) and unstyled (core) usage paths.
  • Chores

    • Bumped package version to 6.0.0-next.1.
    • Marked styling dependencies as optional peer dependencies.
    • Updated build configuration to support multiple entry points.

Note

Add a dependency-free react-mentions-ts/core entry point alongside the existing styled entry

  • Extracts Base variants of all major components (MentionsInputBase, HighlighterBase, SuggestionsOverlayBase, SuggestionBase, MentionBase, LoadingIndicatorBase) that accept an injected style config instead of hard-coded Tailwind classes.
  • Adds a ./core subpath export in package.json and a new src/core.ts entry built by tsdown.config.ts, re-exporting core components with plain CSS class-based styles from src/styles/core.ts.
  • The existing default entry (react-mentions-ts) continues to use Tailwind-based styledStyles; class-variance-authority, clsx, and tailwind-merge are now optional peer dependencies.
  • Removes the cn utility from src/utils/index.ts and replaces barrel imports throughout with direct module imports.
  • Risk: any consumer importing cn from the utils barrel will break; the Suggestion and SuggestionsOverlay components now compose class names differently via the style config, which may change rendered class names.

Macroscope summarized bbcdaaf.

…yled/backward-compatible. The split is now explicit: core uses dependency-free structural styles and class concatenation, while styled root keeps cva, clsx, and tailwind-merge in src/styles/styled.ts (line 42).

Updated packaging/build exports in package.json (line 11) and tsdown.config.ts (line 4), added core entry tests in src/coreEntry.spec.tsx (line 1), and removed the stale src/utils/cn.ts helper.
Copilot AI review requested due to automatic review settings April 19, 2026 15:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This PR refactors the mentions input library to separate styled and unstyled implementations. It extracts component logic into reusable base classes, introduces a centralized styling system with optional Tailwind support, adds a new /core entry point for minimal dependencies, reorganizes utilities to use individual module exports, and updates package exports and build configuration accordingly.

Changes

Cohort / File(s) Summary
Base Component Extraction
src/HighlighterBase.tsx, src/LoadingIndicatorBase.tsx, src/MentionBase.tsx, src/SuggestionBase.tsx, src/SuggestionsOverlayBase.tsx, src/MentionsInputBase.tsx
New unstyled base components implementing core logic for rendering and interaction, with styles injected via required styles prop. Interfaces extracted/defined for each base component's props.
Styled Component Wrappers
src/Highlighter.tsx, src/LoadingIndicator.tsx, src/Mention.tsx, src/Suggestion.tsx, src/SuggestionsOverlay.tsx, src/MentionsInput.tsx
Refactored from full implementations to thin wrappers that delegate to base components while injecting styledStyles (Tailwind-merged) styling configuration. Props exclude styles (injected internally).
Core Unstyled Entry
src/core.ts, src/core/MentionsInput.tsx, src/core/Mention.tsx
New public entry point exporting unstyled MentionsInput/Mention components with coreStyles (zero-dependency, layout-only styling) and core utilities/types for use without Tailwind/styling dependencies.
Styling Infrastructure
src/styles/types.ts, src/styles/core.ts, src/styles/styled.ts, src/styles/joinClassNames.ts, src/styles/mergeStyles.ts, src/styles/tailwindMergeClassNames.ts
New style configuration system defining MentionsInputStyleConfig interfaces for all UI sections, with coreStyles (minimal/layout-only) and styledStyles (Tailwind-merged via CVA variants). Utilities for class/style merging.
Utility Module Reorganization
src/utils/cn.ts, src/utils/index.ts, src/MentionsInputDerived.ts, src/MentionsInputEditing.ts, src/MentionsInputQueryState.ts, src/MentionsInputSelectors.ts
Updated imports to use default exports from individual utility modules (./utils/getMentionsAndPlainText, ./utils/getPlainText, etc.) instead of named exports from ./utils/index. Removed cn utility export.
Package & Build Configuration
.attw.json, package.json, tsdown.config.ts
Added .attw.json to exclude Tailwind CSS. Expanded package.json exports to include /core subpath (ESM/CJS) and /styles/tailwind.css. Changed sideEffects to array including CSS file. Added typesVersions and peerDependenciesMeta for optional peers. Bumped version to 6.0.0-next.1. Updated tsdown.config.ts to build dual entry points (index and core).
Documentation & Tests
README.md, src/coreEntry.spec.tsx, src/MentionsInput.spec.tsx, src/MentionsInput.performance.spec.tsx, src/MentionsInputView.tsx
Updated README distinguishing default styled entry from /core unstyled entry. Added coreEntry.spec.tsx validating core imports lack styled/Tailwind dependencies and basic functionality. Updated test spies to match module restructuring. Added optional controlStyle prop to MentionsInputView.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #174 — Implements hybrid refactor extracting layout/measurement logic and centralizing view-sync via requestViewSync/flushPendingViewSync into base component.
  • PR #22 — Refactors styling system to use clsx, class-variance-authority, and tailwind-merge for Tailwind-based class merging.
  • PR #7 — Refactors core components (Highlighter.tsx, MentionsInput.tsx, Suggestion.tsx) and utility module organization.

Suggested labels

Review effort 4/5

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: introducing a core entry point while maintaining the root entry. It accurately summarizes the primary objective of the PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch factor-out-core

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

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new 'core' entrypoint that provides a dependency-free version of the library, removing the requirement for Tailwind CSS, class-variance-authority, and tailwind-merge. The implementation refactors existing components into base versions that accept a style configuration object, enabling both the default styled entry and the new core entry to share logic. A review comment suggests improving the mergeStyles utility by ensuring it always returns a shallow copy of style objects to prevent accidental mutation of the source configuration.

Comment thread src/styles/mergeStyles.ts
continue
}

merged = merged === undefined ? style : { ...merged, ...style }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Assigning merged = style directly creates a reference to the input object. If the resulting merged style object is later mutated (though unlikely in standard React usage), it could inadvertently modify the source style object from the configuration. It is safer to always create a shallow copy.

Suggested change
merged = merged === undefined ? style : { ...merged, ...style }
merged = merged === undefined ? { ...style } : { ...merged, ...style }

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an additive react-mentions-ts/core entrypoint that avoids styled/Tailwind helper dependencies, while keeping the existing root entry styled and backward-compatible via an explicit split between core structural styles and styled Tailwind defaults.

Changes:

  • Add a new core build entry + package export (react-mentions-ts/core) and update packaging metadata accordingly.
  • Refactor styling/rendering into “Base” components and MentionsInputBase, with style behavior injected via src/styles/core.ts (dependency-free) or src/styles/styled.ts (Tailwind/CVA).
  • Add tests validating the core entry renders without Tailwind defaults and does not pull styled-only dependencies into the core source graph; remove the stale cn helper.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tsdown.config.ts Adds a second build entry for core alongside index.
src/utils/index.ts Removes the cn re-export from the utils barrel.
src/utils/cn.ts Deletes the Tailwind/clsx-based cn helper.
src/styles/types.ts Introduces shared style-config types (class merge + inline style hooks).
src/styles/tailwindMergeClassNames.ts Adds Tailwind-aware class merger used by the styled entry.
src/styles/styled.ts Defines the styled/Tailwind defaults style config (uses CVA + twMerge).
src/styles/mergeStyles.ts Adds a small helper to merge optional inline style objects.
src/styles/joinClassNames.ts Adds dependency-free class concatenation used by core.
src/styles/core.ts Defines dependency-free “structural” inline styles and empty class defaults for core.
src/coreEntry.spec.tsx Adds tests for the new core entry behavior and dependency isolation.
src/core/MentionsInput.tsx Adds a core MentionsInput that uses MentionsInputBase + core styles.
src/core/Mention.tsx Adds a core Mention wrapper using MentionBase + core styles.
src/core.ts Adds the public react-mentions-ts/core entry exports (components/types/helpers).
src/SuggestionsOverlayBase.tsx Extracts overlay logic into a style-injected base component.
src/SuggestionsOverlay.tsx Converts to a thin styled wrapper over SuggestionsOverlayBase.
src/SuggestionBase.tsx Extracts suggestion rendering into a style-injected base component.
src/Suggestion.tsx Converts to a thin styled wrapper over SuggestionBase.
src/MentionsInputView.tsx Adds controlStyle support so core can provide structural positioning via inline styles.
src/MentionsInputSelectors.ts Switches to direct utility imports (avoids barrel coupling).
src/MentionsInputQueryState.ts Switches to direct utility imports.
src/MentionsInputEditing.ts Switches to direct utility imports.
src/MentionsInputDerived.ts Switches to direct utility imports.
src/MentionsInputBase.tsx Introduces MentionsInputBase that drives behavior and consumes an injected style config.
src/MentionsInput.tsx Converts to a thin styled wrapper class over MentionsInputBase.
src/MentionsInput.spec.tsx Updates spies/imports to reflect direct utility imports and new module structure.
src/MentionsInput.performance.spec.tsx Updates perf spies and thresholds to match refactor behavior.
src/MentionBase.tsx Extracts mention rendering into a style-injected base component.
src/Mention.tsx Converts to a thin styled wrapper over MentionBase.
src/LoadingIndicatorBase.tsx Extracts loading indicator into a style-injected base component.
src/LoadingIndicator.tsx Converts to a thin styled wrapper over LoadingIndicatorBase.
src/HighlighterBase.tsx Extracts highlighter logic into a style-injected base component.
src/Highlighter.tsx Converts to a thin styled wrapper over HighlighterBase.
package.json Adds ./core export + typesVersions mapping, adjusts sideEffects, and makes styling peers optional.
README.md Documents styled vs core entry usage and Tailwind helper expectations.
.attw.json Excludes styles/tailwind.css from arethetypeswrong entrypoint checks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 35 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/HighlighterBase.tsx">

<violation number="1" location="src/HighlighterBase.tsx:220">
P2: Caret marker is not rendered when the caret is at the end of a value that ends with a mention, causing missing/stale caret position updates.</violation>
</file>

<file name="package.json">

<violation number="1" location="package.json:175">
P2: Do not mark `class-variance-authority`, `clsx`, and `tailwind-merge` as optional peers while the default root export still requires them.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/HighlighterBase.tsx
// append a span containing a space, to ensure the last text line has the correct height
components.push(' ')

if (components !== resultComponents) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 19, 2026

Choose a reason for hiding this comment

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

P2: Caret marker is not rendered when the caret is at the end of a value that ends with a mention, causing missing/stale caret position updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/HighlighterBase.tsx, line 220:

<comment>Caret marker is not rendered when the caret is at the end of a value that ends with a mention, causing missing/stale caret position updates.</comment>

<file context>
@@ -0,0 +1,253 @@
+  // append a span containing a space, to ensure the last text line has the correct height
+  components.push(' ')
+
+  if (components !== resultComponents) {
+    resultComponents.push(renderCaretMarker(), ...components)
+  }
</file context>
Fix with Cubic

Comment thread package.json
"react-dom": ">=19.0.0",
"tailwind-merge": ">=3.0.0"
},
"peerDependenciesMeta": {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 19, 2026

Choose a reason for hiding this comment

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

P2: Do not mark class-variance-authority, clsx, and tailwind-merge as optional peers while the default root export still requires them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.json, line 175:

<comment>Do not mark `class-variance-authority`, `clsx`, and `tailwind-merge` as optional peers while the default root export still requires them.</comment>

<file context>
@@ -152,6 +172,17 @@
     "react-dom": ">=19.0.0",
     "tailwind-merge": ">=3.0.0"
   },
+  "peerDependenciesMeta": {
+    "class-variance-authority": {
+      "optional": true
</file context>
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.attw.json:
- Line 2: Update the excludeEntrypoints entry to use the exact export key format
by adding the missing "./" prefix: change the string in the excludeEntrypoints
array from "styles/tailwind.css" to "./styles/tailwind.css" so it matches
AreTheTypesWrong's expected keys (refer to the excludeEntrypoints property in
.attw.json).

In `@package.json`:
- Around line 175-185: The peerDependenciesMeta currently marks
class-variance-authority, clsx, and tailwind-merge as optional which breaks the
default entry that imports src/styles/styled.ts; change package.json so those
three remain required peerDependencies for the default export (i.e., remove them
from peerDependenciesMeta optional list) OR refactor the styled runtime into its
own export/package that imports src/styles/styled.ts and declare those three as
peers only on that new export; keep react-mentions-ts/core referencing
src/styles/core.ts as optional-peer-safe.

In `@README.md`:
- Line 412: Replace the smart quotes in the `@source` directive example in
README.md for the optional helper (react-mentions-ts/styles/tailwind.css) with
plain ASCII double quotes so the snippet reads `@source` "../dist";; locate the
example showing the `@source` “../dist” directive and update the quotation
characters to plain quotes to ensure valid CSS when copied.

In `@src/core.ts`:
- Around line 1-23: The core index is missing a re-export for the MentionProps
type; add MentionProps to the exported type list in src/core.ts so consumers can
import { MentionProps } from './core' instead of deep-importing ./core/Mention;
update the type export block (the list containing MentionsInputProps,
MentionComponentProps, etc.) to include MentionProps alongside those existing
type names to expose the public prop type.

In `@src/coreEntry.spec.tsx`:
- Around line 91-102: The test currently only examines collectCoreSourceGraph()
output (graphText) which misses regressions in the built /core artifacts and
only matches exact specifiers; update the test to either read and assert the
emitted core bundle(s) (e.g., the built dist/core.* files) for absence of
'tailwind-merge' and 'class-variance-authority' and imports to styled helpers,
or normalize import specifiers before matching by resolving/normalizing paths
(so checks like "from './styles/styled'" also match "../styles/styled" and
deeper variants); specifically modify the test around
collectCoreSourceGraph/graphText and the expect checks to (1) load the emitted
core artifact content and assert it does not contain the helper names/specifier
patterns, or (2) transform each specifier (e.g., path.normalize or a regex)
before using expect(...).not.toContain for 'tailwind-merge',
'class-variance-authority', "from './styles/styled'" and "from './utils/cn'".

In `@src/HighlighterBase.tsx`:
- Around line 180-222: The caret marker is only injected inside textIteratee, so
collapsed caret positions immediately before/after mentions are missed; update
mentionIteratee (used by iterateMentionsMarkup) to detect when
caretPositionInMarkup falls at the mention boundary (use plainTextIndex and the
mention's display length/indices) and push renderCaretMarker() into the correct
components array (or split the mention into prefix/suffix similar to
textIteratee) before/after calling getMentionComponentForMatch; ensure the same
componentKey handling and final splice into resultComponents is preserved so
caret placement is consistent with the existing logic around components,
resultComponents, renderSubstring, and renderCaretMarker.

In `@src/MentionsInput.performance.spec.tsx`:
- Line 99: Add a short inline comment next to the performance ceiling assertions
(e.g., the expect(metrics.getPlainTextCalls).toBeLessThanOrEqual(2) at the
current location and the similar assertions at the other two spots) documenting
why the allowed ceiling was raised: describe the fixture/behavior change that
causes the extra getPlainText calls, what the new expected upper bound
represents, and that these constants act as regression guards for the fixtures
so future readers understand the rationale for the higher threshold.

In `@src/MentionsInputBase.tsx`:
- Around line 1858-1874: The debounce callback currently calls
provideData(queryInfo.query) directly so any synchronous throw escapes and
leaves the query loading; change executeQuery to evaluate provideData inside a
try/catch and convert its result into a promise before calling
updateSuggestions: e.g. wrap provideData(queryInfo.query) with
Promise.resolve(...) in the try branch and on catch create a rejected promise
(Promise.reject(err)) so that updateSuggestions(queryId, childIndex, queryInfo,
resultPromise, controller) receives a promise in all cases and can route errors
through the existing query error path; use the existing symbols executeQuery,
provideData, updateSuggestions, getDataProvider, controller, queryId,
childIndex, and queryInfo to locate and modify the code.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: d4fa2867-da24-4ab1-bb5b-24710f6b698d

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2888a and bbcdaaf.

📒 Files selected for processing (35)
  • .attw.json
  • README.md
  • package.json
  • src/Highlighter.tsx
  • src/HighlighterBase.tsx
  • src/LoadingIndicator.tsx
  • src/LoadingIndicatorBase.tsx
  • src/Mention.tsx
  • src/MentionBase.tsx
  • src/MentionsInput.performance.spec.tsx
  • src/MentionsInput.spec.tsx
  • src/MentionsInput.tsx
  • src/MentionsInputBase.tsx
  • src/MentionsInputDerived.ts
  • src/MentionsInputEditing.ts
  • src/MentionsInputQueryState.ts
  • src/MentionsInputSelectors.ts
  • src/MentionsInputView.tsx
  • src/Suggestion.tsx
  • src/SuggestionBase.tsx
  • src/SuggestionsOverlay.tsx
  • src/SuggestionsOverlayBase.tsx
  • src/core.ts
  • src/core/Mention.tsx
  • src/core/MentionsInput.tsx
  • src/coreEntry.spec.tsx
  • src/styles/core.ts
  • src/styles/joinClassNames.ts
  • src/styles/mergeStyles.ts
  • src/styles/styled.ts
  • src/styles/tailwindMergeClassNames.ts
  • src/styles/types.ts
  • src/utils/cn.ts
  • src/utils/index.ts
  • tsdown.config.ts
💤 Files with no reviewable changes (2)
  • src/utils/cn.ts
  • src/utils/index.ts

Comment thread .attw.json
@@ -0,0 +1,3 @@
{
"excludeEntrypoints": ["styles/tailwind.css"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In AreTheTypesWrong .attw.json, should excludeEntrypointsvalues for subpath exports use the exact export key (e.g.,"./styles/tailwind.css") or omit "./"(e.g.,"styles/tailwind.css")?

💡 Result:

In AreTheTypesWrong's .attw.json, excludeEntrypoints values for subpath exports should use the exact export key including the "./" prefix, such as "./styles/tailwind.css". The official CLI documentation and example configurations consistently show entrypoint paths with the ./ prefix. For example: - CLI usage: attw --pack . --exclude-entrypoints styles.css comments explain this excludes "./styles.css". - .attw.json example: "excludeEntrypoints": ["./internal"]. - --entrypoints . one two three maps to [".", "./one", "./two", "./three"]. Subpath exports in package.json are defined with ./ (e.g., "./styles/tailwind.css": "./dist/styles/tailwind.css"), and attw discovers/matches them as full subpaths from the package root, requiring the ./ prefix in excludeEntrypoints for exact matching.

Citations:


Use "./styles/tailwind.css" in excludeEntrypoints.

The current value "styles/tailwind.css" is missing the ./ prefix. AreTheTypesWrong requires excludeEntrypoints values to match the exact export key format, which includes the ./ prefix. Update to "./styles/tailwind.css".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.attw.json at line 2, Update the excludeEntrypoints entry to use the exact
export key format by adding the missing "./" prefix: change the string in the
excludeEntrypoints array from "styles/tailwind.css" to "./styles/tailwind.css"
so it matches AreTheTypesWrong's expected keys (refer to the excludeEntrypoints
property in .attw.json).

Comment thread package.json
Comment on lines +175 to +185
"peerDependenciesMeta": {
"class-variance-authority": {
"optional": true
},
"clsx": {
"optional": true
},
"tailwind-merge": {
"optional": true
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n package.json | head -200

Repository: hbmartin/react-mentions-ts

Length of output: 7227


🏁 Script executed:

# Check the exports structure and dependencies
cat package.json | jq '.exports, .dependencies, .peerDependencies, .peerDependenciesMeta' 2>/dev/null || cat package.json | grep -A 50 '"exports"' | head -60

Repository: hbmartin/react-mentions-ts

Length of output: 909


🏁 Script executed:

# Find the main entry point and check its imports
git ls-files | grep -E '(src/|lib/)' | head -20

Repository: hbmartin/react-mentions-ts

Length of output: 807


🏁 Script executed:

# Look for the styled.ts file mentioned in the comment
fd -t f 'styled\.ts' 2>/dev/null || find . -name 'styled.ts' -type f 2>/dev/null

Repository: hbmartin/react-mentions-ts

Length of output: 90


🏁 Script executed:

cat -n src/styles/styled.ts

Repository: hbmartin/react-mentions-ts

Length of output: 4273


🏁 Script executed:

# Check the main index entry point
fd -t f 'index\.ts' -path 'src/*' | head -5

Repository: hbmartin/react-mentions-ts

Length of output: 238


🏁 Script executed:

# Check if there's a core.ts file
fd -t f 'core\.ts' -path 'src/*' | head -5

Repository: hbmartin/react-mentions-ts

Length of output: 238


🏁 Script executed:

# Check imports in the main index file
cat src/index.ts 2>/dev/null | head -50

Repository: hbmartin/react-mentions-ts

Length of output: 821


🏁 Script executed:

# Search for imports of styled.ts
rg -n "from.*styled\'" --type ts --type tsx

Repository: hbmartin/react-mentions-ts

Length of output: 162


🏁 Script executed:

# Check MentionsInput imports
rg -n "import.*styled" src/MentionsInput.tsx -A 2 -B 2

Repository: hbmartin/react-mentions-ts

Length of output: 209


🏁 Script executed:

# Look for core.ts file
fd -type f 'core.ts'

Repository: hbmartin/react-mentions-ts

Length of output: 240


🏁 Script executed:

# Check what gets exported from the main components
rg -n "styledStyles\|styled\|tailwindMerge\|cva" src/MentionsInput.tsx | head -20

Repository: hbmartin/react-mentions-ts

Length of output: 52


🏁 Script executed:

# Check what tailwindMergeClassNames imports
cat -n src/styles/tailwindMergeClassNames.ts

Repository: hbmartin/react-mentions-ts

Length of output: 432


🏁 Script executed:

# Search for imports of styledStyles
rg -n "styledStyles" src/

Repository: hbmartin/react-mentions-ts

Length of output: 1195


🏁 Script executed:

# Check if there's a core entry point file
fd 'core\.ts$' src/

Repository: hbmartin/react-mentions-ts

Length of output: 100


🏁 Script executed:

# Look for what the build system exports for core and main
cat tsdown.config.ts 2>/dev/null || find . -name "tsdown.config*" -type f

Repository: hbmartin/react-mentions-ts

Length of output: 504


🏁 Script executed:

# Check what's in the core entry point
cat -n src/core.ts

Repository: hbmartin/react-mentions-ts

Length of output: 992


🏁 Script executed:

# Check what's in styles/core.ts
cat -n src/styles/core.ts

Repository: hbmartin/react-mentions-ts

Length of output: 5730


🏁 Script executed:

# Check if core/MentionsInput imports styled or core styles
cat -n src/core/MentionsInput.tsx | head -50

Repository: hbmartin/react-mentions-ts

Length of output: 402


🏁 Script executed:

# List what's in the core directory
find src/core -type f -name "*.tsx" | head -20

Repository: hbmartin/react-mentions-ts

Length of output: 117


🏁 Script executed:

# Check if any core components import the styled.ts (which uses optional deps)
rg -l "from.*styles/styled" src/core/

Repository: hbmartin/react-mentions-ts

Length of output: 52


🏁 Script executed:

# Verify if default MentionsInput uses styled
head -20 src/MentionsInput.tsx

Repository: hbmartin/react-mentions-ts

Length of output: 350


Don't make the styled runtime helpers optional peers.

The default entry imports class-variance-authority, clsx, and tailwind-merge directly through src/styles/styled.ts, making them hard runtime dependencies. The react-mentions-ts/core entry uses src/styles/core.ts instead, which has no dependency on these packages and can safely treat them as optional. However, marking all three as optional peer dependencies removes the install-time signal for the default entry, creating a runtime risk for consumers whose package managers do not auto-install optional peers.

Either keep these as required peer dependencies for the default entry, or split the styled entry behind a separate package/export that explicitly declares the dependency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 175 - 185, The peerDependenciesMeta currently
marks class-variance-authority, clsx, and tailwind-merge as optional which
breaks the default entry that imports src/styles/styled.ts; change package.json
so those three remain required peerDependencies for the default export (i.e.,
remove them from peerDependenciesMeta optional list) OR refactor the styled
runtime into its own export/package that imports src/styles/styled.ts and
declare those three as peers only on that new export; keep
react-mentions-ts/core referencing src/styles/core.ts as optional-peer-safe.

Comment thread README.md
```

The optional helper `react-mentions-ts/styles/tailwind.css` only declares an `@source “../dist”;` directive so Tailwind v4 can detect the library's utility classes inside `node_modules/react-mentions-ts/dist`. Including it keeps your Tailwind config clean and avoids adding explicit `content` globs for the package.
The optional helper `react-mentions-ts/styles/tailwind.css` is for the default styled entry. It only declares an `@source “../dist”;` directive so Tailwind v4 can detect the library's utility classes inside `node_modules/react-mentions-ts/dist`. Including it keeps your Tailwind config clean and avoids adding explicit `content` globs for the package.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use plain quotes in the @source example.

The inline directive currently shows smart quotes around ../dist, so a copy/paste into CSS will be invalid.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 412, Replace the smart quotes in the `@source` directive
example in README.md for the optional helper
(react-mentions-ts/styles/tailwind.css) with plain ASCII double quotes so the
snippet reads `@source` "../dist";; locate the example showing the `@source`
“../dist” directive and update the quotation characters to plain quotes to
ensure valid CSS when copied.

Comment thread src/core.ts
Comment on lines +1 to +23
export { default as MentionsInput } from './core/MentionsInput'
export { default as Mention } from './core/Mention'
export type {
MentionsInputProps,
MentionsInputClassNames,
MentionComponentProps,
MentionDataItem,
MentionRenderEmpty,
MentionRenderError,
MentionSearchContext,
MentionsInputChangeEvent,
MentionsInputChangeHandler,
MentionsInputChangeTrigger,
MentionsInputChangeTriggerType,
MentionSerializer,
MentionSerializerMatch,
MentionSelection,
MentionSelectionState,
SuggestionQueryState,
} from './types'
export { default as createMarkupSerializer } from './utils/createMarkupSerializer'
export { makeTriggerRegex } from './utils/makeTriggerRegex'
export { default as getSubstringIndex } from './utils/getSubstringIndex'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Re-export MentionProps from the public /core entry.

src/core/Mention.tsx already exposes the prop type, but /core drops it. That forces TypeScript consumers to deep-import an internal file just to type a <Mention /> wrapper.

Proposed fix
 export { default as MentionsInput } from './core/MentionsInput'
 export { default as Mention } from './core/Mention'
+export type { MentionProps } from './core/Mention'
 export type {
   MentionsInputProps,
   MentionsInputClassNames,
   MentionComponentProps,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core.ts` around lines 1 - 23, The core index is missing a re-export for
the MentionProps type; add MentionProps to the exported type list in src/core.ts
so consumers can import { MentionProps } from './core' instead of deep-importing
./core/Mention; update the type export block (the list containing
MentionsInputProps, MentionComponentProps, etc.) to include MentionProps
alongside those existing type names to expose the public prop type.

Comment thread src/coreEntry.spec.tsx
Comment on lines +91 to +102
it('keeps styled-only helpers out of the core source graph', () => {
const graph = collectCoreSourceGraph()
const graphText = [...graph]
// eslint-disable-next-line security/detect-non-literal-fs-filename -- source graph traversal only reads discovered repo source files.
.map((file) => `${file}\n${readFileSync(file, 'utf8')}`)
.join('\n')

expect(graphText).not.toContain('tailwind-merge')
expect(graphText).not.toContain('class-variance-authority')
expect(graphText).not.toContain("from './styles/styled'")
expect(graphText).not.toContain("from './utils/cn'")
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This only guards the source graph, not the shipped /core entry.

A tsdown/exports regression can still leak styled helpers into dist/core.* while this passes, and the path checks only match ./styles/styled / ./utils/cn even though nested core files would import them as ../.... Please assert on the emitted core artifact, or at least normalize specifiers before matching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coreEntry.spec.tsx` around lines 91 - 102, The test currently only
examines collectCoreSourceGraph() output (graphText) which misses regressions in
the built /core artifacts and only matches exact specifiers; update the test to
either read and assert the emitted core bundle(s) (e.g., the built dist/core.*
files) for absence of 'tailwind-merge' and 'class-variance-authority' and
imports to styled helpers, or normalize import specifiers before matching by
resolving/normalizing paths (so checks like "from './styles/styled'" also match
"../styles/styled" and deeper variants); specifically modify the test around
collectCoreSourceGraph/graphText and the expect checks to (1) load the emitted
core artifact content and assert it does not contain the helper names/specifier
patterns, or (2) transform each specifier (e.g., path.normalize or a regex)
before using expect(...).not.toContain for 'tailwind-merge',
'class-variance-authority', "from './styles/styled'" and "from './utils/cn'".

Comment thread src/HighlighterBase.tsx
Comment on lines +180 to +222
const textIteratee = (substr: string, index: number, _substrPlainTextIndex: number) => {
if (
isNumber(caretPositionInMarkup) &&
caretPositionInMarkup >= index &&
caretPositionInMarkup <= index + substr.length
) {
const splitIndex = caretPositionInMarkup - index
// Before the reassignment, components still points at resultComponents, so the push stores
// the substring that comes before the caret in the final output.
components.push(renderSubstring(substr.slice(0, splitIndex), substringComponentKey))
substringComponentKey += 1
// Reassigning component just switches the working array to a fresh list
// for the text after the caret; later we splice that array back in
// Without the initial push, the prefix would not land in resultComponents.
components = [renderSubstring(substr.slice(splitIndex), substringComponentKey)]
} else {
components.push(renderSubstring(substr, substringComponentKey))
}
substringComponentKey += 1
}

const mentionIteratee = (
_markup: string,
_index: number,
plainTextIndex: number,
id: string,
display: string,
mentionChildIndex: number
) => {
const key = generateComponentKey(componentKeys, id)
components.push(
getMentionComponentForMatch(id, display, mentionChildIndex, key, plainTextIndex)
)
}

iterateMentionsMarkup(value, config, mentionIteratee, textIteratee)

// append a span containing a space, to ensure the last text line has the correct height
components.push(' ')

if (components !== resultComponents) {
resultComponents.push(renderCaretMarker(), ...components)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Render the caret marker at mention boundaries too.

The caret is only inserted from textIteratee(). A collapsed selection immediately before or after a mention never creates data-mentions-caret, so caret measurement and suggestion positioning break at those boundaries.

Proposed fix
+  let caretRendered = false
+
   const textIteratee = (substr: string, index: number, _substrPlainTextIndex: number) => {
     if (
       isNumber(caretPositionInMarkup) &&
       caretPositionInMarkup >= index &&
       caretPositionInMarkup <= index + substr.length
     ) {
       const splitIndex = caretPositionInMarkup - index
       components.push(renderSubstring(substr.slice(0, splitIndex), substringComponentKey))
       substringComponentKey += 1
+      caretRendered = true
       components = [renderSubstring(substr.slice(splitIndex), substringComponentKey)]
     } else {
       components.push(renderSubstring(substr, substringComponentKey))
     }
     substringComponentKey += 1
   }
 
   const mentionIteratee = (
-    _markup: string,
-    _index: number,
+    markup: string,
+    index: number,
     plainTextIndex: number,
     id: string,
     display: string,
     mentionChildIndex: number
   ) => {
+    if (caretPositionInMarkup === index) {
+      components.push(renderCaretMarker())
+      caretRendered = true
+    }
     const key = generateComponentKey(componentKeys, id)
     components.push(
       getMentionComponentForMatch(id, display, mentionChildIndex, key, plainTextIndex)
     )
+    if (caretPositionInMarkup === index + markup.length) {
+      components.push(renderCaretMarker())
+      caretRendered = true
+    }
   }
 
   iterateMentionsMarkup(value, config, mentionIteratee, textIteratee)
+
+  if (!caretRendered && caretPositionInMarkup === value.length) {
+    components.push(renderCaretMarker())
+  }
Based on learnings: Keep paint-sensitive measurement and overlay math inside `MentionsInputLayout`, `MeasurementBridge`, and minimal `useLayoutEffect` usage; preserve portal, scroll, and auto-resize behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HighlighterBase.tsx` around lines 180 - 222, The caret marker is only
injected inside textIteratee, so collapsed caret positions immediately
before/after mentions are missed; update mentionIteratee (used by
iterateMentionsMarkup) to detect when caretPositionInMarkup falls at the mention
boundary (use plainTextIndex and the mention's display length/indices) and push
renderCaretMarker() into the correct components array (or split the mention into
prefix/suffix similar to textIteratee) before/after calling
getMentionComponentForMatch; ensure the same componentKey handling and final
splice into resultComponents is preserved so caret placement is consistent with
the existing logic around components, resultComponents, renderSubstring, and
renderCaretMarker.

emitPerformanceMetric('controlled-keystroke', metrics)

expect(metrics.getPlainTextCalls).toBe(0)
expect(metrics.getPlainTextCalls).toBeLessThanOrEqual(2)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Document why these performance ceilings increased.

These constants are now the regression guard for the fixtures. A short inline note about the extra expected calls would make future perf regressions much easier to interpret.

Also applies to: 425-425, 511-511

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MentionsInput.performance.spec.tsx` at line 99, Add a short inline
comment next to the performance ceiling assertions (e.g., the
expect(metrics.getPlainTextCalls).toBeLessThanOrEqual(2) at the current location
and the similar assertions at the other two spots) documenting why the allowed
ceiling was raised: describe the fixture/behavior change that causes the extra
getPlainText calls, what the new expected upper bound represents, and that these
constants act as regression guards for the fixtures so future readers understand
the rationale for the higher threshold.

Comment thread src/MentionsInputBase.tsx
Comment on lines +1858 to +1874
const executeQuery = () => {
this._queryDebounceTimers.delete(childIndex)

const provideData = getDataProvider<Extra>(mentionChild.props.data, {
ignoreAccents,
maxSuggestions,
signal: controller.signal,
getSubstringIndex,
})

void this.updateSuggestions(
queryId,
childIndex,
queryInfo,
provideData(queryInfo.query),
controller
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Route synchronous provider failures through the existing query error path.

provideData(queryInfo.query) is evaluated before updateSuggestions() starts. If a consumer's data provider throws synchronously, the exception escapes the debounce callback and the query stays stuck in the loading state.

Proposed fix
     const executeQuery = () => {
       this._queryDebounceTimers.delete(childIndex)
 
-      const provideData = getDataProvider<Extra>(mentionChild.props.data, {
-        ignoreAccents,
-        maxSuggestions,
-        signal: controller.signal,
-        getSubstringIndex,
-      })
-
-      void this.updateSuggestions(
-        queryId,
-        childIndex,
-        queryInfo,
-        provideData(queryInfo.query),
-        controller
-      )
+      const results = Promise.resolve().then(() => {
+        const provideData = getDataProvider<Extra>(mentionChild.props.data, {
+          ignoreAccents,
+          maxSuggestions,
+          signal: controller.signal,
+          getSubstringIndex,
+        })
+
+        return provideData(queryInfo.query)
+      })
+
+      void this.updateSuggestions(queryId, childIndex, queryInfo, results, controller)
     }
Based on learnings: Keep async suggestion flows race-safe: debounce side effects rather than controlled input updates, abort or ignore stale requests, and preserve existing suggestions while a same-trigger refresh is loading.
📝 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 executeQuery = () => {
this._queryDebounceTimers.delete(childIndex)
const provideData = getDataProvider<Extra>(mentionChild.props.data, {
ignoreAccents,
maxSuggestions,
signal: controller.signal,
getSubstringIndex,
})
void this.updateSuggestions(
queryId,
childIndex,
queryInfo,
provideData(queryInfo.query),
controller
)
const executeQuery = () => {
this._queryDebounceTimers.delete(childIndex)
const results = Promise.resolve().then(() => {
const provideData = getDataProvider<Extra>(mentionChild.props.data, {
ignoreAccents,
maxSuggestions,
signal: controller.signal,
getSubstringIndex,
})
return provideData(queryInfo.query)
})
void this.updateSuggestions(queryId, childIndex, queryInfo, results, controller)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MentionsInputBase.tsx` around lines 1858 - 1874, The debounce callback
currently calls provideData(queryInfo.query) directly so any synchronous throw
escapes and leaves the query loading; change executeQuery to evaluate
provideData inside a try/catch and convert its result into a promise before
calling updateSuggestions: e.g. wrap provideData(queryInfo.query) with
Promise.resolve(...) in the try branch and on catch create a rejected promise
(Promise.reject(err)) so that updateSuggestions(queryId, childIndex, queryInfo,
resultPromise, controller) receives a promise in all cases and can route errors
through the existing query error path; use the existing symbols executeQuery,
provideData, updateSuggestions, getDataProvider, controller, queryId,
childIndex, and queryInfo to locate and modify the code.

@hbmartin hbmartin added this to the 6.1.0 milestone Apr 20, 2026
@hbmartin hbmartin marked this pull request as draft April 22, 2026 15:26
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.

2 participants