-
Notifications
You must be signed in to change notification settings - Fork 3
additive react-mentions-ts/core entry while keeping the root entry st… #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "excludeEntrypoints": ["styles/tailwind.css"] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,7 @@ yarn add react-mentions-ts | |
| pnpm add react-mentions-ts | ||
| ``` | ||
|
|
||
| React Mentions TS uses peer dependencies for its styling helpers and React runtime. Ensure these are installed in your application (skip any you already have): | ||
| The default `react-mentions-ts` entry is Tailwind-ready and uses optional styling peer dependencies for class composition. Install them when using the default styled entry: | ||
|
|
||
| ```bash | ||
| # npm | ||
|
|
@@ -53,6 +53,12 @@ pnpm add class-variance-authority clsx react react-dom tailwind-merge | |
|
|
||
| Check `package.json` for the latest peer dependency version ranges. | ||
|
|
||
| If you want the smaller dependency-free rendering path, import from `react-mentions-ts/core` instead. The core entry only needs the React peers and leaves all visual styling to your app: | ||
|
|
||
| ```tsx | ||
| import { MentionsInput, Mention } from 'react-mentions-ts/core' | ||
| ``` | ||
|
|
||
| ## 🚀 Quick Start | ||
|
|
||
| ### Add a MentionsInput with Mention children | ||
|
|
@@ -382,7 +388,7 @@ No dynamic imports or `next/dynamic` wrappers are needed. | |
|
|
||
| ## 🎨 Styling | ||
|
|
||
| React Mentions ships its markup with **Tailwind utility classes**. Consumers should have Tailwind configured in their application build so these classes compile to real CSS. If you do not use Tailwind you can still provide your own styles via `className`, CSS modules, or inline styles. | ||
| The default `react-mentions-ts` entry ships its markup with **Tailwind utility classes**. Consumers should have Tailwind configured in their application build so these classes compile to real CSS. If you do not use Tailwind, import from `react-mentions-ts/core` and provide your own styles via `className`, CSS modules, or inline styles. | ||
|
|
||
| ### Tailwind CSS | ||
|
|
||
|
|
@@ -403,10 +409,22 @@ module.exports = { | |
| @import 'react-mentions-ts/styles/tailwind.css'; | ||
| ``` | ||
|
|
||
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use plain quotes in the The inline directive currently shows smart quotes around 🤖 Prompt for AI Agents |
||
|
|
||
| If you are still on Tailwind v3, add `./node_modules/react-mentions-ts/dist/**/*.{js,jsx,ts,tsx}` to the `content` array instead of importing the helper file. | ||
|
|
||
| ### Core Entry | ||
|
|
||
| Use `react-mentions-ts/core` when you want the behavior without Tailwind helper dependencies: | ||
|
|
||
| ```tsx | ||
| import { MentionsInput, Mention } from 'react-mentions-ts/core' | ||
| ``` | ||
|
|
||
| The core entry exposes the same primary components and types as the default entry. It keeps the structural inline styles required for caret measurement, highlighter alignment, suggestions positioning, and single-line/multi-line behavior, but it does not provide visual Tailwind defaults. | ||
|
|
||
| The only intentional class behavior difference is merging: the styled root entry collapses conflicting Tailwind utility classes with `tailwind-merge`; the core entry concatenates classes as provided. | ||
|
|
||
| ### CSS Class Names | ||
|
|
||
| Assign a `className` prop to `MentionsInput`. All DOM nodes will receive derived class names: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,20 @@ | ||
| { | ||
| "name": "react-mentions-ts", | ||
| "private": false, | ||
| "version": "6.0.0-next.0", | ||
| "version": "6.0.0-next.1", | ||
| "description": "A React component that enables Facebook/Twitter-style @mentions and tagging in textarea inputs with full TypeScript support.", | ||
| "type": "module", | ||
| "packageManager": "pnpm@10.33.0", | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.cts", | ||
| "typesVersions": { | ||
| "*": { | ||
| "core": [ | ||
| "./dist/core.d.cts" | ||
| ] | ||
| } | ||
| }, | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
|
|
@@ -19,6 +26,17 @@ | |
| "default": "./dist/index.cjs" | ||
| } | ||
| }, | ||
| "./core": { | ||
| "import": { | ||
| "types": "./dist/core.d.ts", | ||
| "default": "./dist/core.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/core.d.cts", | ||
| "default": "./dist/core.cjs" | ||
| } | ||
| }, | ||
| "./styles/tailwind.css": "./styles/tailwind.css", | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "files": [ | ||
|
|
@@ -29,7 +47,9 @@ | |
| "LICENSE", | ||
| "package.json" | ||
| ], | ||
| "sideEffects": false, | ||
| "sideEffects": [ | ||
| "./styles/tailwind.css" | ||
| ], | ||
| "scripts": { | ||
| "clean": "rimraf dist", | ||
| "build": "pnpm clean && tsdown --config tsdown.config.ts", | ||
|
|
@@ -152,6 +172,17 @@ | |
| "react-dom": ">=19.0.0", | ||
| "tailwind-merge": ">=3.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Do not mark Prompt for AI agents |
||
| "class-variance-authority": { | ||
| "optional": true | ||
| }, | ||
| "clsx": { | ||
| "optional": true | ||
| }, | ||
| "tailwind-merge": { | ||
| "optional": true | ||
| } | ||
| }, | ||
|
Comment on lines
+175
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n package.json | head -200Repository: 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 -60Repository: 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 -20Repository: 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/nullRepository: hbmartin/react-mentions-ts Length of output: 90 🏁 Script executed: cat -n src/styles/styled.tsRepository: hbmartin/react-mentions-ts Length of output: 4273 🏁 Script executed: # Check the main index entry point
fd -t f 'index\.ts' -path 'src/*' | head -5Repository: 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 -5Repository: 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 -50Repository: hbmartin/react-mentions-ts Length of output: 821 🏁 Script executed: # Search for imports of styled.ts
rg -n "from.*styled\'" --type ts --type tsxRepository: hbmartin/react-mentions-ts Length of output: 162 🏁 Script executed: # Check MentionsInput imports
rg -n "import.*styled" src/MentionsInput.tsx -A 2 -B 2Repository: 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 -20Repository: hbmartin/react-mentions-ts Length of output: 52 🏁 Script executed: # Check what tailwindMergeClassNames imports
cat -n src/styles/tailwindMergeClassNames.tsRepository: 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 fRepository: hbmartin/react-mentions-ts Length of output: 504 🏁 Script executed: # Check what's in the core entry point
cat -n src/core.tsRepository: hbmartin/react-mentions-ts Length of output: 992 🏁 Script executed: # Check what's in styles/core.ts
cat -n src/styles/core.tsRepository: 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 -50Repository: 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 -20Repository: 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.tsxRepository: hbmartin/react-mentions-ts Length of output: 350 Don't make the styled runtime helpers optional peers. The default entry imports 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 |
||
| "engines": { | ||
| "node": ">=22" | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,263 +1,9 @@ | ||
| import React, { useLayoutEffect, useMemo, useState } from 'react' | ||
| import type { CSSProperties } from 'react' | ||
| import { cva } from 'class-variance-authority' | ||
| import { iterateMentionsMarkup, mapPlainTextIndex, isNumber, cn } from './utils' | ||
| import readConfigFromChildren, { collectMentionElements } from './utils/readConfigFromChildren' | ||
| import { useEventCallback } from './utils/useEventCallback' | ||
| import type { | ||
| CaretCoordinates, | ||
| MentionChildConfig, | ||
| MentionComponentProps, | ||
| MentionSelectionState, | ||
| } from './types' | ||
| import HighlighterBase from './HighlighterBase' | ||
| import type { HighlighterProps } from './HighlighterBase' | ||
| import styledStyles from './styles/styled' | ||
|
|
||
| const generateComponentKey = (usedKeys: Record<string, number>, id: string) => { | ||
| if (Object.hasOwn(usedKeys, id)) { | ||
| usedKeys[id] += 1 | ||
| } else { | ||
| usedKeys[id] = 0 | ||
| } | ||
| return `${id}_${usedKeys[id].toString()}` | ||
| export default function Highlighter< | ||
| Extra extends Record<string, unknown> = Record<string, unknown>, | ||
| >(props: Readonly<Omit<HighlighterProps<Extra>, 'styles'>>) { | ||
| return <HighlighterBase {...props} styles={styledStyles.highlighter} /> | ||
| } | ||
|
|
||
| interface HighlighterProps<Extra extends Record<string, unknown> = Record<string, unknown>> { | ||
| readonly selectionStart: number | null | ||
| readonly selectionEnd: number | null | ||
| readonly value?: string | ||
| readonly onCaretPositionChange: (position: CaretCoordinates) => void | ||
| readonly containerRef?: (node: HTMLDivElement | null) => void | ||
| readonly children: React.ReactNode | ||
| readonly mentionChildren?: React.ReactElement<MentionComponentProps<Extra>>[] | ||
| readonly config?: MentionChildConfig<Extra>[] | ||
| readonly singleLine: boolean | ||
| readonly className?: string | ||
| readonly substringClassName?: string | ||
| readonly caretClassName?: string | ||
| readonly recomputeVersion?: number | ||
| readonly mentionSelectionMap?: Record<string, MentionSelectionState> | ||
| } | ||
|
|
||
| // Note: singleLine intentionally overrides whitespace/break behavior | ||
| const highlighterStyles = cva( | ||
| 'box-border w-full overflow-hidden text-start text-transparent pointer-events-none [font-family:inherit] [font-size:inherit] [line-height:inherit]', | ||
| { | ||
| variants: { | ||
| singleLine: { | ||
| true: 'whitespace-pre break-normal', | ||
| false: 'whitespace-pre-wrap break-words', | ||
| }, | ||
| }, | ||
| } | ||
| ) | ||
|
|
||
| const substringStyles = 'text-transparent inline [white-space:inherit]' | ||
| const caretStyles = 'relative inline-block h-0 w-0 align-baseline' | ||
| const singleLineContentWrapperStyle: CSSProperties = { | ||
| display: 'inline-block', | ||
| whiteSpace: 'inherit', | ||
| width: 'max-content', | ||
| minWidth: '100%', | ||
| } | ||
|
|
||
| function Highlighter<Extra extends Record<string, unknown> = Record<string, unknown>>({ | ||
| selectionStart, | ||
| selectionEnd, | ||
| value = '', | ||
| onCaretPositionChange, | ||
| containerRef, | ||
| children, | ||
| mentionChildren: mentionChildrenProp, | ||
| config: configProp, | ||
| singleLine, | ||
| className, | ||
| substringClassName, | ||
| caretClassName, | ||
| recomputeVersion, | ||
| mentionSelectionMap, | ||
| }: HighlighterProps<Extra>) { | ||
| const [position, setPosition] = useState<CaretCoordinates | null>(null) | ||
| const [caretElement, setCaretElement] = useState<HTMLSpanElement | null>(null) | ||
|
|
||
| const updatePosition = useEventCallback((offsetLeft: number, offsetTop: number) => { | ||
| if (position?.left === offsetLeft && position.top === offsetTop) { | ||
| return | ||
| } | ||
|
|
||
| const newPosition: CaretCoordinates = { left: offsetLeft, top: offsetTop } | ||
| setPosition(newPosition) | ||
| onCaretPositionChange(newPosition) | ||
| }) | ||
|
|
||
| // Ensure caret position updates whenever content/selection affects layout. | ||
| useLayoutEffect(() => { | ||
| if (caretElement === null) { | ||
| return undefined | ||
| } | ||
|
|
||
| const measure = () => { | ||
| const offsetLeft = caretElement.offsetLeft | ||
| const offsetTop = | ||
| caretElement.previousElementSibling === null | ||
| ? caretElement.offsetTop | ||
| : (caretElement.previousElementSibling as HTMLSpanElement).offsetTop + | ||
| (caretElement.previousElementSibling as HTMLSpanElement).offsetHeight | ||
|
|
||
| updatePosition(offsetLeft, offsetTop) | ||
| } | ||
|
|
||
| const rafId = | ||
| typeof globalThis.requestAnimationFrame === 'function' | ||
| ? globalThis.requestAnimationFrame(measure) | ||
| : undefined | ||
|
|
||
| if (rafId === undefined) { | ||
| measure() | ||
| } | ||
|
|
||
| return () => { | ||
| if (rafId !== undefined && typeof globalThis.cancelAnimationFrame === 'function') { | ||
| globalThis.cancelAnimationFrame(rafId) | ||
| } | ||
| } | ||
| // value/selection/singleLine impact layout/position | ||
| }, [ | ||
| caretElement, | ||
| recomputeVersion, | ||
| selectionEnd, | ||
| selectionStart, | ||
| singleLine, | ||
| updatePosition, | ||
| value, | ||
| ]) | ||
|
|
||
| const mentionChildren = useMemo( | ||
| () => mentionChildrenProp ?? collectMentionElements(children), | ||
| [children, mentionChildrenProp] | ||
| ) | ||
| const config: MentionChildConfig<Extra>[] = useMemo( | ||
| () => configProp ?? readConfigFromChildren<Extra>(mentionChildren), | ||
| [configProp, mentionChildren] | ||
| ) | ||
| let caretPositionInMarkup: number | null | undefined | ||
|
|
||
| const rootClassName = cn(highlighterStyles({ singleLine }), className) | ||
| const substringClass = cn(substringStyles, substringClassName) | ||
| const caretClass = cn(caretStyles, caretClassName) | ||
|
|
||
| if (selectionEnd === selectionStart) { | ||
| caretPositionInMarkup = mapPlainTextIndex(value, config, selectionStart, 'START') as | ||
| | number | ||
| | undefined | ||
| } | ||
| const selectionMap = mentionSelectionMap ?? {} | ||
|
|
||
| const resultComponents: React.ReactNode[] = [] | ||
| const componentKeys: Record<string, number> = {} | ||
| let components: React.ReactNode[] = resultComponents | ||
| let substringComponentKey = 0 | ||
|
|
||
| const renderSubstring = (substringValue: string, key: number) => ( | ||
| // set substring span to hidden, so that Emojis are not shown double in Mobile Safari | ||
| <span className={substringClass} key={key}> | ||
| {substringValue} | ||
| </span> | ||
| ) | ||
|
|
||
| const getMentionComponentForMatch = ( | ||
| id: string, | ||
| display: string, | ||
| mentionChildIndex: number, | ||
| key: string, | ||
| plainTextIndex: number | ||
| ) => { | ||
| const selectionKey = `${mentionChildIndex}:${plainTextIndex}` | ||
| const selectionState = selectionMap[selectionKey] | ||
| const props = { id, display, key, selectionState } | ||
| const child = mentionChildren[mentionChildIndex] as React.ReactElement<MentionComponentProps> | ||
| return React.cloneElement(child, props) | ||
| } | ||
|
|
||
| const renderCaretMarker = () => ( | ||
| <span | ||
| className={caretClass} | ||
| data-mentions-caret | ||
| ref={setCaretElement} | ||
| key="caret" | ||
| aria-hidden="true" | ||
| /> | ||
| ) | ||
|
|
||
| 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) | ||
| } | ||
| const content = singleLine ? ( | ||
| <div style={singleLineContentWrapperStyle}>{resultComponents}</div> | ||
| ) : ( | ||
| resultComponents | ||
| ) | ||
|
|
||
| return ( | ||
| <div | ||
| className={rootClassName} | ||
| data-slot="highlighter" | ||
| data-single-line={singleLine ? 'true' : undefined} | ||
| data-multi-line={singleLine ? undefined : 'true'} | ||
| style={HIGHLIGHTER_OVERLAY_STYLE} | ||
| ref={containerRef} | ||
| > | ||
| {content} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| const HIGHLIGHTER_OVERLAY_STYLE: CSSProperties = { | ||
| position: 'absolute', | ||
| top: 0, | ||
| left: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| pointerEvents: 'none', | ||
| zIndex: 0, | ||
| } | ||
|
|
||
| export default Highlighter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In AreTheTypesWrong.attw.json, shouldexcludeEntrypointsvalues 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"inexcludeEntrypoints.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