dyelight 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ragaeeb Haq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # DyeLight
2
+
3
+ A lightweight TypeScript React component for highlighting characters in textareas with powerful text annotation capabilities.
4
+
5
+ [![wakatime](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/897368ef-62b9-48be-bba9-7c530f10e3da.svg)](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/897368ef-62b9-48be-bba9-7c530f10e3da)
6
+ ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
7
+ [![Node.js CI](https://github.com/ragaeeb/dyelight/actions/workflows/build.yml/badge.svg)](https://github.com/ragaeeb/dyelight/actions/workflows/build.yml)
8
+ ![GitHub License](https://img.shields.io/github/license/ragaeeb/dyelight)
9
+ ![GitHub Release](https://img.shields.io/github/v/release/ragaeeb/dyelight)
10
+ [![Size](https://deno.bundlejs.com/badge?q=dyelight@latest&badge=detailed)](https://bundlejs.com/?q=dyelight%40latest)
11
+ ![typescript](https://badgen.net/badge/icon/typescript?icon=typescript&label&color=blue)
12
+ ![npm](https://img.shields.io/npm/dm/dyelight)
13
+ ![GitHub issues](https://img.shields.io/github/issues/ragaeeb/dyelight)
14
+ ![GitHub stars](https://img.shields.io/github/stars/ragaeeb/dyelight?style=social)
15
+ [![NPM Version](https://img.shields.io/npm/v/dyelight)](https://www.npmjs.com/package/dyelight)
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
+
18
+ ## Features
19
+
20
+ - **Absolute Position Highlighting**: Highlight text using simple start/end positions across the entire text
21
+ - **Multi-line Support**: Highlights automatically span across line breaks
22
+ - **Line-level Highlighting**: Optional background highlighting for entire lines
23
+ - **Pattern Matching**: Built-in support for regex and keyword highlighting
24
+ - **Auto-resize**: Automatic textarea height adjustment based on content
25
+ - **TypeScript**: Full TypeScript support with comprehensive type definitions
26
+ - **Lightweight**: Minimal dependencies, optimized for performance
27
+ - **Flexible Styling**: Support for CSS classes and inline styles
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install dyelight
33
+ ```
34
+
35
+ ```bash
36
+ yarn add dyelight
37
+ ```
38
+
39
+ ```bash
40
+ pnpm add dyelight
41
+ ```
42
+
43
+ ## Basic Usage
44
+
45
+ ```tsx
46
+ import React, { useState } from 'react';
47
+ import { DyeLight, HighlightBuilder } from 'dyelight';
48
+
49
+ function MyEditor() {
50
+ const [text, setText] = useState('Hello world\nThis is line 2');
51
+
52
+ // Highlight characters 0-5 (absolute positions)
53
+ const highlights = HighlightBuilder.ranges([
54
+ { start: 0, end: 5, className: 'bg-yellow-200' },
55
+ { start: 12, end: 24, className: 'bg-blue-200' },
56
+ ]);
57
+
58
+ return (
59
+ <DyeLight
60
+ value={text}
61
+ onChange={setText}
62
+ highlights={highlights}
63
+ className="w-full p-2 border rounded"
64
+ rows={4}
65
+ />
66
+ );
67
+ }
68
+ ```
69
+
70
+ ## Advanced Usage
71
+
72
+ ### Pattern-based Highlighting
73
+
74
+ ```tsx
75
+ import { DyeLight, HighlightBuilder } from 'dyelight';
76
+
77
+ function CodeEditor() {
78
+ const [code, setCode] = useState(`
79
+ function hello() {
80
+ const message = "Hello World";
81
+ console.log(message);
82
+ }
83
+ `);
84
+
85
+ // Highlight JavaScript keywords
86
+ const keywordHighlights = HighlightBuilder.pattern(
87
+ code,
88
+ /\b(function|const|let|var|if|else|for|while)\b/g,
89
+ 'text-blue-600 font-semibold',
90
+ );
91
+
92
+ // Highlight strings
93
+ const stringHighlights = HighlightBuilder.pattern(code, /"[^"]*"/g, 'text-green-600');
94
+
95
+ // Highlight specific words
96
+ const wordHighlights = HighlightBuilder.words(code, ['console', 'log'], 'text-purple-600');
97
+
98
+ return (
99
+ <DyeLight
100
+ value={code}
101
+ onChange={setCode}
102
+ highlights={[...keywordHighlights, ...stringHighlights, ...wordHighlights]}
103
+ className="font-mono text-sm"
104
+ enableAutoResize
105
+ />
106
+ );
107
+ }
108
+ ```
109
+
110
+ ### Line Highlighting
111
+
112
+ ```tsx
113
+ function ErrorHighlighter() {
114
+ const [text, setText] = useState('Line 1\nLine 2 with error\nLine 3');
115
+
116
+ // Highlight line 1 (0-indexed) with error styling
117
+ const lineHighlights = HighlightBuilder.lines([
118
+ { line: 1, className: 'bg-red-100' },
119
+ { line: 2, color: '#e8f5e8' }, // Or use direct color
120
+ ]);
121
+
122
+ return <DyeLight value={text} onChange={setText} lineHighlights={lineHighlights} />;
123
+ }
124
+ ```
125
+
126
+ ### Character-level Highlighting
127
+
128
+ ```tsx
129
+ function CharacterHighlighter() {
130
+ const [text, setText] = useState('Hello World');
131
+
132
+ // Highlight individual characters
133
+ const characterHighlights = HighlightBuilder.characters([
134
+ { index: 0, className: 'bg-red-200' }, // 'H'
135
+ { index: 6, className: 'bg-blue-200' }, // 'W'
136
+ { index: 10, style: { backgroundColor: 'yellow' } }, // 'd'
137
+ ]);
138
+
139
+ return <DyeLight value={text} onChange={setText} highlights={characterHighlights} />;
140
+ }
141
+ ```
142
+
143
+ ### Using Refs
144
+
145
+ ```tsx
146
+ function RefExample() {
147
+ const dyeLightRef = useRef<DyeLightRef>(null);
148
+ const [text, setText] = useState('');
149
+
150
+ const handleFocus = () => {
151
+ dyeLightRef.current?.focus();
152
+ };
153
+
154
+ const handleSelectAll = () => {
155
+ dyeLightRef.current?.select();
156
+ };
157
+
158
+ const handleGetValue = () => {
159
+ const value = dyeLightRef.current?.getValue();
160
+ console.log('Current value:', value);
161
+ };
162
+
163
+ return (
164
+ <div>
165
+ <DyeLight ref={dyeLightRef} value={text} onChange={setText} />
166
+ <button onClick={handleFocus}>Focus</button>
167
+ <button onClick={handleSelectAll}>Select All</button>
168
+ <button onClick={handleGetValue}>Get Value</button>
169
+ </div>
170
+ );
171
+ }
172
+ ```
173
+
174
+ ## API Reference
175
+
176
+ ### DyeLight Props
177
+
178
+ | Prop | Type | Default | Description |
179
+ | ------------------ | ---------------------------------- | ----------- | ------------------------------------ |
180
+ | `value` | `string` | `undefined` | Controlled value |
181
+ | `defaultValue` | `string` | `''` | Default value for uncontrolled usage |
182
+ | `onChange` | `(value: string) => void` | `undefined` | Callback when value changes |
183
+ | `highlights` | `CharacterRange[]` | `[]` | Character range highlights |
184
+ | `lineHighlights` | `{ [lineNumber: number]: string }` | `{}` | Line-level highlights |
185
+ | `enableAutoResize` | `boolean` | `true` | Enable auto-height adjustment |
186
+ | `className` | `string` | `''` | CSS class name |
187
+ | `dir` | `'ltr' \| 'rtl'` | `'ltr'` | Text direction |
188
+ | `rows` | `number` | `4` | Number of visible rows |
189
+
190
+ All standard textarea HTML attributes are also supported.
191
+
192
+ ### CharacterRange
193
+
194
+ ```typescript
195
+ type CharacterRange = {
196
+ start: number; // Start position (inclusive)
197
+ end: number; // End position (exclusive)
198
+ className?: string; // CSS class name
199
+ style?: React.CSSProperties; // Inline styles
200
+ };
201
+ ```
202
+
203
+ ### DyeLightRef Methods
204
+
205
+ | Method | Description |
206
+ | ------------------------------- | -------------------------- |
207
+ | `focus()` | Focus the textarea |
208
+ | `blur()` | Blur the textarea |
209
+ | `select()` | Select all text |
210
+ | `setSelectionRange(start, end)` | Set text selection |
211
+ | `getValue()` | Get current value |
212
+ | `setValue(value)` | Set value programmatically |
213
+
214
+ ## HighlightBuilder Utilities
215
+
216
+ ### `HighlightBuilder.ranges(ranges)`
217
+
218
+ Create highlights from character ranges.
219
+
220
+ ### `HighlightBuilder.pattern(text, pattern, className?, style?)`
221
+
222
+ Highlight text matching a regex pattern.
223
+
224
+ ### `HighlightBuilder.words(text, words, className?, style?)`
225
+
226
+ Highlight specific words.
227
+
228
+ ### `HighlightBuilder.characters(chars)`
229
+
230
+ Highlight individual characters by index.
231
+
232
+ ### `HighlightBuilder.selection(start, end, className?, style?)`
233
+
234
+ Create a single selection highlight.
235
+
236
+ ### `HighlightBuilder.lines(lines)`
237
+
238
+ Create line-level highlights.
239
+
240
+ ## Styling
241
+
242
+ DyeLight uses CSS-in-JS for core functionality but allows complete customization through CSS classes and inline styles.
243
+
244
+ ### Example CSS Classes
245
+
246
+ ```css
247
+ .highlight-keyword {
248
+ background-color: #e3f2fd;
249
+ color: #1976d2;
250
+ font-weight: 600;
251
+ }
252
+
253
+ .highlight-error {
254
+ background-color: #ffebee;
255
+ color: #c62828;
256
+ text-decoration: underline wavy red;
257
+ }
258
+
259
+ .highlight-string {
260
+ background-color: #e8f5e8;
261
+ color: #2e7d32;
262
+ }
263
+ ```
264
+
265
+ ## Browser Support
266
+
267
+ DyeLight supports all modern browsers including:
268
+
269
+ - Chrome 60+
270
+ - Firefox 60+
271
+ - Safari 12+
272
+ - Edge 79+
273
+
274
+ ## Contributing
275
+
276
+ Contributions are welcome! Please feel free to submit a Pull Request.
277
+
278
+ ## License
279
+
280
+ MIT © [Ragaeeb Haq](https://github.com/ragaeeb)
@@ -0,0 +1,132 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * Represents a character range to highlight in the entire text
5
+ */
6
+ type CharacterRange = {
7
+ /** Optional CSS class name to apply to the highlighted range */
8
+ className?: string;
9
+ /** Zero-based end index in the entire text (exclusive) */
10
+ end: number;
11
+ /** Zero-based start index in the entire text (inclusive) */
12
+ start: number;
13
+ /** Optional inline styles to apply to the highlighted range */
14
+ style?: React.CSSProperties;
15
+ };
16
+ /**
17
+ * Methods exposed by the HighlightableTextarea component through its ref
18
+ */
19
+ type DyeLightRef = {
20
+ blur: () => void;
21
+ focus: () => void;
22
+ getValue: () => string;
23
+ select: () => void;
24
+ setSelectionRange: (start: number, end: number) => void;
25
+ setValue: (value: string) => void;
26
+ };
27
+ /**
28
+ * Props for the DyeLight component
29
+ */
30
+ interface DyeLightProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
31
+ /** CSS class name for the component */
32
+ className?: string;
33
+ /** Default value for uncontrolled usage */
34
+ defaultValue?: string;
35
+ /** Text direction */
36
+ dir?: 'ltr' | 'rtl';
37
+ /** Enable automatic height adjustment based on content */
38
+ enableAutoResize?: boolean;
39
+ /** Character range highlights using absolute positions in the entire text */
40
+ highlights?: CharacterRange[];
41
+ /** Line-level highlights mapped by line number (0-based) to CSS color/class */
42
+ lineHighlights?: {
43
+ [lineNumber: number]: string;
44
+ };
45
+ /** Callback fired when the textarea value changes */
46
+ onChange?: (value: string) => void;
47
+ /** Number of visible text lines */
48
+ rows?: number;
49
+ /** Controlled value */
50
+ value?: string;
51
+ }
52
+
53
+ /**
54
+ * Automatically resizes a textarea element to fit its content
55
+ * Sets the height to 'auto' first, then adjusts to the scroll height
56
+ * Prevents textarea from having scrollbars when content fits
57
+ *
58
+ * @param textArea - The HTML textarea element to resize
59
+ */
60
+ declare const autoResize: (textArea: HTMLTextAreaElement) => void;
61
+
62
+ /**
63
+ * Utility functions for building highlight objects
64
+ */
65
+ declare const HighlightBuilder: {
66
+ /**
67
+ * Creates highlights for individual characters using absolute positions
68
+ */
69
+ characters: (chars: Array<{
70
+ className?: string;
71
+ index: number;
72
+ style?: React.CSSProperties;
73
+ }>) => {
74
+ className: string | undefined;
75
+ end: number;
76
+ start: number;
77
+ style: react.CSSProperties | undefined;
78
+ }[];
79
+ /**
80
+ * Creates line highlights
81
+ */
82
+ lines: (lines: Array<{
83
+ className?: string;
84
+ color?: string;
85
+ line: number;
86
+ }>) => {
87
+ [lineNumber: number]: string;
88
+ };
89
+ /**
90
+ * Highlights text matching a pattern using absolute positions
91
+ */
92
+ pattern: (text: string, pattern: RegExp | string, className?: string, style?: React.CSSProperties) => {
93
+ className: string | undefined;
94
+ end: number;
95
+ start: number;
96
+ style: react.CSSProperties | undefined;
97
+ }[];
98
+ /**
99
+ * Creates character range highlights using absolute positions
100
+ */
101
+ ranges: (ranges: Array<{
102
+ className?: string;
103
+ end: number;
104
+ start: number;
105
+ style?: React.CSSProperties;
106
+ }>) => {
107
+ className: string | undefined;
108
+ end: number;
109
+ start: number;
110
+ style: react.CSSProperties | undefined;
111
+ }[];
112
+ /**
113
+ * Highlights text between specific start and end positions
114
+ */
115
+ selection: (start: number, end: number, className?: string, style?: React.CSSProperties) => {
116
+ className: string | undefined;
117
+ end: number;
118
+ start: number;
119
+ style: react.CSSProperties | undefined;
120
+ }[];
121
+ /**
122
+ * Highlights entire words that match a pattern
123
+ */
124
+ words: (text: string, words: string[], className?: string, style?: React.CSSProperties) => {
125
+ className: string | undefined;
126
+ end: number;
127
+ start: number;
128
+ style: react.CSSProperties | undefined;
129
+ }[];
130
+ };
131
+
132
+ export { type CharacterRange, type DyeLightProps, type DyeLightRef, HighlightBuilder, autoResize };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import{forwardRef as J,useCallback as T,useEffect as K,useImperativeHandle as Q,useMemo as X,useRef as w,useState as $}from"react";var I=e=>{e.style.height="auto",e.style.height=`${e.scrollHeight}px`};var F=e=>{let r=e.split(`
2
+ `),t=[],n=0;return r.forEach((a,l)=>{t.push(n),n+=a.length+(l<r.length-1?1:0)}),{lines:r,lineStarts:t}},P=(e,r)=>{for(let t=r.length-1;t>=0;t--)if(e>=r[t])return{char:e-r[t],line:t};return{char:0,line:0}},k=e=>/^(#|rgb|hsl|var\(--.*?\)|transparent|currentColor|inherit|initial|unset)/i.test(e)||/^[a-z]+$/i.test(e);var z={display:"block",position:"relative",width:"100%"},Y={background:"transparent",boxSizing:"border-box",caretColor:"#000000",color:"transparent",minHeight:"auto",position:"relative",width:"100%",zIndex:2},V={border:"transparent solid 1px",bottom:0,boxSizing:"border-box",color:"inherit",fontFamily:"inherit",fontSize:"inherit",left:0,lineHeight:"inherit",margin:0,overflow:"hidden",pointerEvents:"none",position:"absolute",right:0,top:0,whiteSpace:"pre-wrap",wordWrap:"break-word",zIndex:1};import{jsx as L,jsxs as te}from"react/jsx-runtime";var U=(e,r,t)=>{if(!t)return L("div",{children:e},r);let n=k(t);return L("div",{className:n?void 0:t,style:n?{backgroundColor:t}:void 0,children:e},r)},Z=(e,r,t,n)=>{if(t.length===0)return U(e||"\xA0",r,n);let a=t.toSorted((p,u)=>p.start-u.start),l=[],c=0;if(a.forEach((p,u)=>{let{className:A,end:H,start:o,style:g}=p,f=Math.max(0,Math.min(o,e.length)),x=Math.max(f,Math.min(H,e.length));if(f>c){let h=e.slice(c,f);h&&l.push(h)}if(x>f){let h=e.slice(f,x);l.push(L("span",{className:A,style:g,children:h},`highlight-${r}-${u}`))}c=Math.max(c,x)}),c<e.length){let p=e.slice(c);p&&l.push(p)}let C=l.length===0?"\xA0":l;return U(C,r,n)},ee=J(({className:e="",defaultValue:r="",dir:t="ltr",enableAutoResize:n=!0,highlights:a=[],lineHighlights:l={},onChange:c,rows:C=4,style:p,value:u,...A},H)=>{let o=w(null),g=w(null),f=w(null),[x,h]=$(u??r),[b,B]=$(),S=u!==void 0?u:x,y=T(s=>{n&&(I(s),B(s.scrollHeight))},[n]),G=T(s=>{let i=s.target.value;u===void 0&&h(i),c?.(i),y(s.target)},[u,c,y]),O=T(()=>{if(o.current&&g.current){let{scrollLeft:s,scrollTop:i}=o.current;g.current.scrollTop=i,g.current.scrollLeft=s}},[]),v=T(()=>{if(!o.current||!g.current)return;let s=getComputedStyle(o.current),i=g.current;i.style.padding=s.padding,i.style.fontSize=s.fontSize,i.style.fontFamily=s.fontFamily,i.style.lineHeight=s.lineHeight,i.style.letterSpacing=s.letterSpacing,i.style.wordSpacing=s.wordSpacing,i.style.textIndent=s.textIndent},[]);Q(H,()=>({blur:()=>o.current?.blur(),focus:()=>o.current?.focus(),getValue:()=>S,select:()=>o.current?.select(),setSelectionRange:(s,i)=>o.current?.setSelectionRange(s,i),setValue:s=>{u===void 0&&h(s),o.current&&(o.current.value=s,y(o.current))}}),[S,u,y]),K(()=>{o.current&&n&&y(o.current),v()},[S,y,n,v]);let W=X(()=>{let{lines:s,lineStarts:i}=F(S),R={};return a.forEach(m=>{let E=P(m.start,i),N=P(m.end-1,i);for(let d=E.line;d<=N.line;d++){R[d]||(R[d]=[]);let D=i[d],M=Math.max(m.start-D,0),_=Math.min(m.end-D,s[d].length);_>M&&R[d].push({className:m.className,end:_,start:M,style:m.style})}}),s.map((m,E)=>{let N=l[E],d=R[E]||[];return Z(m,E,d,N)})},[S,a,l]),j={...Y,height:b?`${b}px`:void 0,resize:n?"none":"vertical"},q={...V,direction:t,height:b?`${b}px`:void 0,padding:o.current?getComputedStyle(o.current).padding:"8px 12px"};return te("div",{ref:f,style:{...z,...p},children:[L("div",{"aria-hidden":"true",ref:g,style:q,children:W}),L("textarea",{className:e,dir:t,onChange:G,onScroll:O,ref:o,rows:C,style:j,value:S,...A})]})});ee.displayName="DyeLight";var re={characters:e=>e.map(({className:r,index:t,style:n})=>({className:r,end:t+1,start:t,style:n})),lines:e=>{let r={};return e.forEach(({className:t,color:n,line:a})=>{r[a]=t||n||""}),r},pattern:(e,r,t,n)=>{let a=typeof r=="string"?new RegExp(r,"g"):new RegExp(r.source,"g");return Array.from(e.matchAll(a)).map(c=>({className:t,end:c.index+c[0].length,start:c.index,style:n}))},ranges:e=>e.map(({className:r,end:t,start:n,style:a})=>({className:r,end:t,start:n,style:a})),selection:(e,r,t,n)=>[{className:t,end:r,start:e,style:n}],words:(e,r,t,n)=>{let a=new RegExp(`\\b(${r.join("|")})\\b`,"g");return re.pattern(e,a,t,n)}};export{re as HighlightBuilder,I as autoResize};
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/DyeLight.tsx","../src/domUtils.ts","../src/textUtils.ts","../src/styles.ts","../src/builder.ts"],"sourcesContent":["import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\n\nimport { autoResize } from './domUtils';\nimport type { DyeLightProps, DyeLightRef } from './types';\nimport { absoluteToLinePos, getLinePositions, isColorValue } from './textUtils';\nimport { DEFAULT_BASE_STYLE, DEFAULT_CONTAINER_STYLE, DEFAULT_HIGHLIGHT_LAYER_STYLE } from './styles';\n\nconst createLineElement = (content: React.ReactNode, lineIndex: number, lineHighlight?: string): React.ReactElement => {\n if (!lineHighlight) {\n return <div key={lineIndex}>{content}</div>;\n }\n\n const isColor = isColorValue(lineHighlight);\n return (\n <div\n className={isColor ? undefined : lineHighlight}\n key={lineIndex}\n style={isColor ? { backgroundColor: lineHighlight } : undefined}\n >\n {content}\n </div>\n );\n};\n\nconst renderHighlightedLine = (\n line: string,\n lineIndex: number,\n ranges: Array<{ className?: string; end: number; start: number; style?: React.CSSProperties }>,\n lineHighlight?: string,\n): React.ReactElement => {\n if (ranges.length === 0) {\n const content = line || '\\u00A0';\n return createLineElement(content, lineIndex, lineHighlight);\n }\n\n // Sort ranges by start position\n const sortedRanges = ranges.toSorted((a, b) => a.start - b.start);\n\n const result: React.ReactNode[] = [];\n let lastIndex = 0;\n\n sortedRanges.forEach((range, idx) => {\n const { className, end, start, style: rangeStyle } = range;\n\n // Clamp range to line bounds\n const clampedStart = Math.max(0, Math.min(start, line.length));\n const clampedEnd = Math.max(clampedStart, Math.min(end, line.length));\n\n // Add text before highlight\n if (clampedStart > lastIndex) {\n const textBefore = line.slice(lastIndex, clampedStart);\n if (textBefore) {\n result.push(textBefore);\n }\n }\n\n // Add highlighted text\n if (clampedEnd > clampedStart) {\n const highlightedText = line.slice(clampedStart, clampedEnd);\n result.push(\n <span className={className} key={`highlight-${lineIndex}-${idx}`} style={rangeStyle}>\n {highlightedText}\n </span>,\n );\n }\n\n lastIndex = Math.max(lastIndex, clampedEnd);\n });\n\n // Add remaining text\n if (lastIndex < line.length) {\n const textAfter = line.slice(lastIndex);\n if (textAfter) {\n result.push(textAfter);\n }\n }\n\n const content = result.length === 0 ? '\\u00A0' : result;\n return createLineElement(content, lineIndex, lineHighlight);\n};\n\n/**\n * A textarea component with support for highlighting character ranges using absolute positions\n * and optional line-level highlighting. Perfect for syntax highlighting, error indication,\n * and text annotation without the complexity of line-based positioning.\n *\n * @example\n * ```tsx\n * const MyComponent = () => {\n * const [text, setText] = useState('Hello world\\nSecond line');\n *\n * // Highlight characters 0-5 (absolute positions)\n * const highlights = HighlightBuilder.ranges([\n * { start: 0, end: 5, className: 'bg-yellow-200' },\n * { start: 12, end: 18, className: 'bg-blue-200' }\n * ]);\n *\n * // Highlight keywords automatically\n * const keywordHighlights = HighlightBuilder.pattern(\n * text,\n * /\\b(function|const|let)\\b/g,\n * 'text-blue-600'\n * );\n *\n * // Optional line highlights\n * const lineHighlights = HighlightBuilder.lines([\n * { line: 1, className: 'bg-red-100' }\n * ]);\n *\n * return (\n * <DyeLight\n * value={text}\n * onChange={setText}\n * highlights={[...highlights, ...keywordHighlights]}\n * lineHighlights={lineHighlights}\n * />\n * );\n * };\n * ```\n */\nconst DyeLight = forwardRef<DyeLightRef, DyeLightProps>(\n (\n {\n className = '',\n defaultValue = '',\n dir = 'ltr',\n enableAutoResize = true,\n highlights = [],\n lineHighlights = {},\n onChange,\n rows = 4,\n style,\n value,\n ...props\n },\n ref,\n ) => {\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const highlightLayerRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const [internalValue, setInternalValue] = useState(value ?? defaultValue);\n const [textareaHeight, setTextareaHeight] = useState<number | undefined>();\n\n const currentValue = value !== undefined ? value : internalValue;\n\n const handleAutoResize = useCallback(\n (element: HTMLTextAreaElement) => {\n if (!enableAutoResize) return;\n\n autoResize(element);\n setTextareaHeight(element.scrollHeight);\n },\n [enableAutoResize],\n );\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const newValue = e.target.value;\n\n if (value === undefined) {\n setInternalValue(newValue);\n }\n\n onChange?.(newValue);\n handleAutoResize(e.target);\n },\n [value, onChange, handleAutoResize],\n );\n\n const syncScroll = useCallback(() => {\n if (textareaRef.current && highlightLayerRef.current) {\n const { scrollLeft, scrollTop } = textareaRef.current;\n highlightLayerRef.current.scrollTop = scrollTop;\n highlightLayerRef.current.scrollLeft = scrollLeft;\n }\n }, []);\n\n const syncStyles = useCallback(() => {\n if (!textareaRef.current || !highlightLayerRef.current) return;\n\n const computedStyle = getComputedStyle(textareaRef.current);\n const highlightLayer = highlightLayerRef.current;\n\n highlightLayer.style.padding = computedStyle.padding;\n highlightLayer.style.fontSize = computedStyle.fontSize;\n highlightLayer.style.fontFamily = computedStyle.fontFamily;\n highlightLayer.style.lineHeight = computedStyle.lineHeight;\n highlightLayer.style.letterSpacing = computedStyle.letterSpacing;\n highlightLayer.style.wordSpacing = computedStyle.wordSpacing;\n highlightLayer.style.textIndent = computedStyle.textIndent;\n }, []);\n\n useImperativeHandle(\n ref,\n () => ({\n blur: () => textareaRef.current?.blur(),\n focus: () => textareaRef.current?.focus(),\n getValue: () => currentValue,\n select: () => textareaRef.current?.select(),\n setSelectionRange: (start: number, end: number) => textareaRef.current?.setSelectionRange(start, end),\n setValue: (newValue: string) => {\n if (value === undefined) {\n setInternalValue(newValue);\n }\n if (textareaRef.current) {\n textareaRef.current.value = newValue;\n handleAutoResize(textareaRef.current);\n }\n },\n }),\n [currentValue, value, handleAutoResize],\n );\n\n useEffect(() => {\n if (textareaRef.current && enableAutoResize) {\n handleAutoResize(textareaRef.current);\n }\n syncStyles();\n }, [currentValue, handleAutoResize, enableAutoResize, syncStyles]);\n\n const highlightedContent = useMemo(() => {\n const { lines, lineStarts } = getLinePositions(currentValue);\n\n // Group highlights by line\n const highlightsByLine: {\n [lineIndex: number]: Array<{\n className?: string;\n end: number;\n start: number;\n style?: React.CSSProperties;\n }>;\n } = {};\n\n highlights.forEach((highlight) => {\n const startPos = absoluteToLinePos(highlight.start, lineStarts);\n const endPos = absoluteToLinePos(highlight.end - 1, lineStarts);\n\n // Handle highlights that span multiple lines\n for (let lineIndex = startPos.line; lineIndex <= endPos.line; lineIndex++) {\n if (!highlightsByLine[lineIndex]) {\n highlightsByLine[lineIndex] = [];\n }\n\n const lineStart = lineStarts[lineIndex];\n\n // Calculate highlight range within this line\n const rangeStart = Math.max(highlight.start - lineStart, 0);\n const rangeEnd = Math.min(highlight.end - lineStart, lines[lineIndex].length);\n\n if (rangeEnd > rangeStart) {\n highlightsByLine[lineIndex].push({\n className: highlight.className,\n end: rangeEnd,\n start: rangeStart,\n style: highlight.style,\n });\n }\n }\n });\n\n return lines.map((line, lineIndex) => {\n const lineHighlight = lineHighlights[lineIndex];\n const ranges = highlightsByLine[lineIndex] || [];\n\n return renderHighlightedLine(line, lineIndex, ranges, lineHighlight);\n });\n }, [currentValue, highlights, lineHighlights]);\n\n const baseTextareaStyle: React.CSSProperties = {\n ...DEFAULT_BASE_STYLE,\n height: textareaHeight ? `${textareaHeight}px` : undefined,\n resize: enableAutoResize ? 'none' : 'vertical',\n };\n\n const highlightLayerStyle: React.CSSProperties = {\n ...DEFAULT_HIGHLIGHT_LAYER_STYLE,\n direction: dir,\n height: textareaHeight ? `${textareaHeight}px` : undefined,\n padding: textareaRef.current ? getComputedStyle(textareaRef.current).padding : '8px 12px',\n };\n\n return (\n <div ref={containerRef} style={{ ...DEFAULT_CONTAINER_STYLE, ...style }}>\n <div aria-hidden=\"true\" ref={highlightLayerRef} style={highlightLayerStyle}>\n {highlightedContent}\n </div>\n\n <textarea\n className={className}\n dir={dir}\n onChange={handleChange}\n onScroll={syncScroll}\n ref={textareaRef}\n rows={rows}\n style={baseTextareaStyle}\n value={currentValue}\n {...props}\n />\n </div>\n );\n },\n);\n\nDyeLight.displayName = 'DyeLight';\n\nexport default DyeLight;\n","/**\n * Automatically resizes a textarea element to fit its content\n * Sets the height to 'auto' first, then adjusts to the scroll height\n * Prevents textarea from having scrollbars when content fits\n *\n * @param textArea - The HTML textarea element to resize\n */\nexport const autoResize = (textArea: HTMLTextAreaElement) => {\n textArea.style.height = 'auto';\n textArea.style.height = `${textArea.scrollHeight}px`;\n};\n","/**\n * Converts absolute text positions to line-relative positions\n */\nexport const getLinePositions = (text: string) => {\n const lines = text.split('\\n');\n const lineStarts: number[] = [];\n let position = 0;\n\n lines.forEach((line, index) => {\n lineStarts.push(position);\n position += line.length + (index < lines.length - 1 ? 1 : 0); // +1 for \\n except last line\n });\n\n return { lines, lineStarts };\n};\n\n/**\n * Converts absolute position to line and character index\n */\nexport const absoluteToLinePos = (absolutePos: number, lineStarts: number[]) => {\n for (let i = lineStarts.length - 1; i >= 0; i--) {\n if (absolutePos >= lineStarts[i]) {\n return {\n char: absolutePos - lineStarts[i],\n line: i,\n };\n }\n }\n return { char: 0, line: 0 };\n};\n\nexport const isColorValue = (value: string): boolean => {\n return (\n /^(#|rgb|hsl|var\\(--.*?\\)|transparent|currentColor|inherit|initial|unset)/i.test(value) ||\n /^[a-z]+$/i.test(value)\n );\n};\n","export const DEFAULT_CONTAINER_STYLE: React.CSSProperties = {\n display: 'block',\n position: 'relative',\n width: '100%',\n};\n\nexport const DEFAULT_BASE_STYLE: React.CSSProperties = {\n background: 'transparent',\n boxSizing: 'border-box',\n caretColor: '#000000',\n color: 'transparent',\n minHeight: 'auto',\n position: 'relative',\n width: '100%',\n zIndex: 2,\n};\n\nexport const DEFAULT_HIGHLIGHT_LAYER_STYLE: React.CSSProperties = {\n border: 'transparent solid 1px',\n bottom: 0,\n boxSizing: 'border-box',\n color: 'inherit',\n fontFamily: 'inherit',\n fontSize: 'inherit',\n left: 0,\n lineHeight: 'inherit',\n margin: 0,\n overflow: 'hidden',\n pointerEvents: 'none',\n position: 'absolute',\n right: 0,\n top: 0,\n whiteSpace: 'pre-wrap',\n wordWrap: 'break-word',\n zIndex: 1,\n};\n","/**\n * Utility functions for building highlight objects\n */\nexport const HighlightBuilder = {\n /**\n * Creates highlights for individual characters using absolute positions\n */\n characters: (chars: Array<{ className?: string; index: number; style?: React.CSSProperties }>) => {\n return chars.map(({ className, index, style }) => ({\n className,\n end: index + 1,\n start: index,\n style,\n }));\n },\n\n /**\n * Creates line highlights\n */\n lines: (lines: Array<{ className?: string; color?: string; line: number }>) => {\n const result: { [lineNumber: number]: string } = {};\n lines.forEach(({ className, color, line }) => {\n result[line] = className || color || '';\n });\n return result;\n },\n\n /**\n * Highlights text matching a pattern using absolute positions\n */\n pattern: (text: string, pattern: RegExp | string, className?: string, style?: React.CSSProperties) => {\n const regex = typeof pattern === 'string' ? new RegExp(pattern, 'g') : new RegExp(pattern.source, 'g');\n const matches = Array.from(text.matchAll(regex));\n\n return matches.map((match) => ({\n className,\n end: match.index! + match[0].length,\n start: match.index!,\n style,\n }));\n },\n\n /**\n * Creates character range highlights using absolute positions\n */\n ranges: (ranges: Array<{ className?: string; end: number; start: number; style?: React.CSSProperties }>) => {\n return ranges.map(({ className, end, start, style }) => ({\n className,\n end,\n start,\n style,\n }));\n },\n\n /**\n * Highlights text between specific start and end positions\n */\n selection: (start: number, end: number, className?: string, style?: React.CSSProperties) => {\n return [{ className, end, start, style }];\n },\n\n /**\n * Highlights entire words that match a pattern\n */\n words: (text: string, words: string[], className?: string, style?: React.CSSProperties) => {\n const pattern = new RegExp(`\\\\b(${words.join('|')})\\\\b`, 'g');\n return HighlightBuilder.pattern(text, pattern, className, style);\n },\n};\n"],"mappings":"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","names":["forwardRef","useCallback","useEffect","useImperativeHandle","useMemo","useRef","useState","autoResize","textArea","getLinePositions","text","lines","lineStarts","position","line","index","absoluteToLinePos","absolutePos","i","isColorValue","value","DEFAULT_CONTAINER_STYLE","DEFAULT_BASE_STYLE","DEFAULT_HIGHLIGHT_LAYER_STYLE","jsx","jsxs","createLineElement","content","lineIndex","lineHighlight","isColor","isColorValue","renderHighlightedLine","line","ranges","sortedRanges","a","b","result","lastIndex","range","idx","className","end","start","rangeStyle","clampedStart","clampedEnd","textBefore","highlightedText","textAfter","DyeLight","forwardRef","defaultValue","dir","enableAutoResize","highlights","lineHighlights","onChange","rows","style","value","props","ref","textareaRef","useRef","highlightLayerRef","containerRef","internalValue","setInternalValue","useState","textareaHeight","setTextareaHeight","currentValue","handleAutoResize","useCallback","element","autoResize","handleChange","e","newValue","syncScroll","scrollLeft","scrollTop","syncStyles","computedStyle","highlightLayer","useImperativeHandle","useEffect","highlightedContent","useMemo","lines","lineStarts","getLinePositions","highlightsByLine","highlight","startPos","absoluteToLinePos","endPos","lineStart","rangeStart","rangeEnd","baseTextareaStyle","DEFAULT_BASE_STYLE","highlightLayerStyle","DEFAULT_HIGHLIGHT_LAYER_STYLE","DEFAULT_CONTAINER_STYLE","HighlightBuilder","chars","className","index","style","lines","result","color","line","text","pattern","regex","match","ranges","end","start","words"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "dyelight",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "https://github.com/ragaeeb/dyelight.git"
6
+ },
7
+ "module": "./dist/index.js",
8
+ "version": "1.0.0",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.30.1",
21
+ "@types/bun": "^1.2.17",
22
+ "@types/react": "^19.1.8",
23
+ "@types/react-dom": "^19.1.6",
24
+ "eslint": "^9.30.1",
25
+ "eslint-config-prettier": "^10.1.5",
26
+ "eslint-plugin-perfectionist": "^4.15.0",
27
+ "eslint-plugin-prettier": "^5.5.1",
28
+ "eslint-plugin-react": "^7.37.5",
29
+ "globals": "^16.3.0",
30
+ "prettier": "^3.6.2",
31
+ "semantic-release": "^24.2.6",
32
+ "tsup": "^8.5.0",
33
+ "typescript-eslint": "^8.35.1"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^19.1.0",
37
+ "react-dom": "^19.1.0"
38
+ },
39
+ "type": "module",
40
+ "bugs": {
41
+ "url": "https://github.com/ragaeeb/dyelight/issues"
42
+ },
43
+ "description": "A lightweight TypeScript React component to allow highlighting characters in textareas.",
44
+ "homepage": "https://github.com/ragaeeb/dyelight",
45
+ "keywords": [
46
+ "highlight",
47
+ "textarea",
48
+ "multiline",
49
+ "input",
50
+ "forms",
51
+ "styling"
52
+ ],
53
+ "license": "MIT",
54
+ "scripts": {
55
+ "build": "tsup"
56
+ }
57
+ }