dyelight 1.0.3 → 1.1.1

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A lightweight TypeScript React component for highlighting characters in textareas with powerful text annotation capabilities.
4
4
 
5
+ [Demo](https://dyelight.vercel.app)
6
+
5
7
  [![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
8
  [![codecov](https://codecov.io/gh/ragaeeb/dyelight/graph/badge.svg?token=7VETE1WNMP)](https://codecov.io/gh/ragaeeb/dyelight)
7
9
  ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
@@ -14,7 +16,7 @@ A lightweight TypeScript React component for highlighting characters in textarea
14
16
  ![GitHub issues](https://img.shields.io/github/issues/ragaeeb/dyelight)
15
17
  ![GitHub stars](https://img.shields.io/github/stars/ragaeeb/dyelight?style=social)
16
18
  [![NPM Version](https://img.shields.io/npm/v/dyelight)](https://www.npmjs.com/package/dyelight)
17
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
19
+ [![Vercel Deploy](https://deploy-badge.vercel.app/vercel/dyelight)](https://dyelight.vercel.app)
18
20
 
19
21
  ## Features
20
22
 
@@ -25,7 +27,35 @@ A lightweight TypeScript React component for highlighting characters in textarea
25
27
  - **Auto-resize**: Automatic textarea height adjustment based on content
26
28
  - **TypeScript**: Full TypeScript support with comprehensive type definitions
27
29
  - **Lightweight**: Minimal dependencies, optimized for performance
28
- - **Flexible Styling**: Support for CSS classes and inline styles
30
+ - **Flexible Styling**: Support for CSS classes on both the textarea (`className`) and the wrapper container (`containerClassName`)
31
+ - **Modern UI Friendly**: Optimized for integration with Tailwind CSS and UI libraries like shadcn/ui
32
+ - **Smart Placeholder**: Placeholders remain visible even with the transparent character-highlighting overlay
33
+ - **Storybook Playground**: Explore the component interactively with the bundled Storybook setup
34
+
35
+ ## Development
36
+
37
+ This project uses [Bun](https://bun.sh/) for dependency management and scripts.
38
+
39
+ ```bash
40
+ bun install
41
+ ```
42
+
43
+ ### Tooling
44
+
45
+ - **Biome** – linting and formatting use the latest Biome-recommended rules defined in `biome.json`.
46
+ - **tsdown** – builds rely on the upstream `tsdown` CLI configured in `tsdown.config.ts`.
47
+ - **Storybook** – interactive documentation lives under `.storybook` with stories in `src/**/*.stories.tsx`.
48
+ - **Testing Library** – unit tests rely on the latest `@testing-library/react` and `@testing-library/dom` helpers.
49
+
50
+ ### Available scripts
51
+
52
+ - `bun run build` – bundle the library with `tsdown` and emit declarations.
53
+ - `bun run test` – execute the Bun-powered unit tests with coverage for every hook and the `DyeLight` component.
54
+ - `bun run storybook` – start Storybook locally at <http://localhost:6006> to demo the component.
55
+ - `bun run storybook:build` – produce a static Storybook build.
56
+ - `bun run lint` / `bun run format` – lint and format the project with Biome.
57
+
58
+ Storybook ships with example stories under `src/DyeLight.stories.tsx` that showcase auto-resize, line-level highlights, and interactive editing.
29
59
 
30
60
  ## Installation
31
61
 
@@ -184,7 +214,8 @@ function RefExample() {
184
214
  | `highlights` | `CharacterRange[]` | `[]` | Character range highlights |
185
215
  | `lineHighlights` | `{ [lineNumber: number]: string }` | `{}` | Line-level highlights |
186
216
  | `enableAutoResize` | `boolean` | `true` | Enable auto-height adjustment |
187
- | `className` | `string` | `''` | CSS class name |
217
+ | `className` | `string` | `''` | CSS class for the textarea element |
218
+ | `containerClassName` | `string` | `''` | CSS class for the wrapper container |
188
219
  | `dir` | `'ltr' \| 'rtl'` | `'ltr'` | Text direction |
189
220
  | `rows` | `number` | `4` | Number of visible rows |
190
221
 
@@ -238,9 +269,25 @@ Create a single selection highlight.
238
269
 
239
270
  Create line-level highlights.
240
271
 
241
- ## Styling
272
+ DyeLight uses CSS-in-JS for core functionality but allows complete customization through CSS classes.
273
+
274
+ ### Modern Layout & UI (Tailwind CSS)
242
275
 
243
- DyeLight uses CSS-in-JS for core functionality but allows complete customization through CSS classes and inline styles.
276
+ To achieve modern UI effects like focus rings or shadow-depth, apply the layout and border styles to the `containerClassName`. Apply the inner spacing and typography to the `className`.
277
+
278
+ ```tsx
279
+ <DyeLight
280
+ // Outer shell: handles layout, borders, and focus rings
281
+ containerClassName={cn(
282
+ "flex-1 min-h-0 rounded-md border border-input bg-background shadow-xs",
283
+ "focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50"
284
+ )}
285
+ // Inner textarea: handles text color, spacing, and resizing
286
+ className="h-full w-full bg-transparent px-3 py-2 text-base outline-none"
287
+ placeholder="Start typing..."
288
+ enableAutoResize={false}
289
+ />
290
+ ```
244
291
 
245
292
  ### Example CSS Classes
246
293
 
@@ -276,6 +323,10 @@ DyeLight supports all modern browsers including:
276
323
 
277
324
  Contributions are welcome! Please feel free to submit a Pull Request.
278
325
 
326
+ 1. Install dependencies with `bun install`.
327
+ 2. Run `bun run lint` and `bun test` to ensure code style and tests pass.
328
+ 3. Use `bun run storybook` to verify UI changes where relevant.
329
+
279
330
  ## License
280
331
 
281
332
  MIT © [Ragaeeb Haq](https://github.com/ragaeeb)
@@ -0,0 +1,348 @@
1
+ import React from "react";
2
+
3
+ //#region src/builder.d.ts
4
+ /**
5
+ * Utility functions for building highlight objects for the DyeLight component
6
+ * Provides a fluent API for creating various types of text highlights
7
+ */
8
+ declare const HighlightBuilder: {
9
+ /**
10
+ * Creates highlights for individual characters using absolute positions
11
+ * @param chars - Array of character highlight configurations
12
+ * @param chars[].index - Zero-based character index in the text
13
+ * @param chars[].className - Optional CSS class name to apply
14
+ * @param chars[].style - Optional inline styles to apply
15
+ * @returns Array of character range highlights
16
+ * @example
17
+ * ```tsx
18
+ * // Highlight characters at positions 5, 10, and 15
19
+ * const highlights = HighlightBuilder.characters([
20
+ * { index: 5, className: 'highlight-error' },
21
+ * { index: 10, className: 'highlight-warning' },
22
+ * { index: 15, style: { backgroundColor: 'yellow' } }
23
+ * ]);
24
+ * ```
25
+ */
26
+ characters: (chars: Array<{
27
+ className?: string;
28
+ index: number;
29
+ style?: React.CSSProperties;
30
+ }>) => {
31
+ className: string | undefined;
32
+ end: number;
33
+ start: number;
34
+ style: React.CSSProperties | undefined;
35
+ }[];
36
+ /**
37
+ * Creates line highlights for entire lines
38
+ * @param lines - Array of line highlight configurations
39
+ * @param lines[].line - Zero-based line number
40
+ * @param lines[].className - Optional CSS class name to apply to the line
41
+ * @param lines[].color - Optional color value (CSS color, hex, rgb, etc.)
42
+ * @returns Object mapping line numbers to highlight values
43
+ * @example
44
+ * ```tsx
45
+ * // Highlight lines 0 and 2 with different styles
46
+ * const lineHighlights = HighlightBuilder.lines([
47
+ * { line: 0, className: 'error-line' },
48
+ * { line: 2, color: '#ffff00' }
49
+ * ]);
50
+ * ```
51
+ */
52
+ lines: (lines: Array<{
53
+ className?: string;
54
+ color?: string;
55
+ line: number;
56
+ }>) => {
57
+ [lineNumber: number]: string;
58
+ };
59
+ /**
60
+ * Highlights text matching a pattern using absolute positions
61
+ * @param text - The text to search within
62
+ * @param pattern - Regular expression or string pattern to match
63
+ * @param className - Optional CSS class name to apply to matches
64
+ * @param style - Optional inline styles to apply to matches
65
+ * @returns Array of character range highlights for all matches
66
+ * @example
67
+ * ```tsx
68
+ * // Highlight all JavaScript keywords
69
+ * const highlights = HighlightBuilder.pattern(
70
+ * code,
71
+ * /\b(function|const|let|var|if|else|for|while)\b/g,
72
+ * 'keyword-highlight'
73
+ * );
74
+ *
75
+ * // Highlight all email addresses
76
+ * const emailHighlights = HighlightBuilder.pattern(
77
+ * text,
78
+ * /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
79
+ * 'email-highlight'
80
+ * );
81
+ * ```
82
+ */
83
+ pattern: (text: string, pattern: RegExp | string, className?: string, style?: React.CSSProperties) => {
84
+ className: string | undefined;
85
+ end: number;
86
+ start: number;
87
+ style: React.CSSProperties | undefined;
88
+ }[];
89
+ /**
90
+ * Creates character range highlights using absolute positions
91
+ * @param ranges - Array of character range configurations
92
+ * @param ranges[].start - Zero-based start index (inclusive)
93
+ * @param ranges[].end - Zero-based end index (exclusive)
94
+ * @param ranges[].className - Optional CSS class name to apply
95
+ * @param ranges[].style - Optional inline styles to apply
96
+ * @returns Array of character range highlights
97
+ * @example
98
+ * ```tsx
99
+ * // Highlight specific ranges in the text
100
+ * const highlights = HighlightBuilder.ranges([
101
+ * { start: 0, end: 5, className: 'title-highlight' },
102
+ * { start: 10, end: 20, style: { backgroundColor: 'yellow' } },
103
+ * { start: 25, end: 30, className: 'error-highlight' }
104
+ * ]);
105
+ * ```
106
+ */
107
+ ranges: (ranges: Array<{
108
+ className?: string;
109
+ end: number;
110
+ start: number;
111
+ style?: React.CSSProperties;
112
+ }>) => {
113
+ className: string | undefined;
114
+ end: number;
115
+ start: number;
116
+ style: React.CSSProperties | undefined;
117
+ }[];
118
+ /**
119
+ * Highlights text between specific start and end positions
120
+ * Convenience method for highlighting a single selection range
121
+ * @param start - Zero-based start index (inclusive)
122
+ * @param end - Zero-based end index (exclusive)
123
+ * @param className - Optional CSS class name to apply
124
+ * @param style - Optional inline styles to apply
125
+ * @returns Array containing a single character range highlight
126
+ * @example
127
+ * ```tsx
128
+ * // Highlight a selection from position 10 to 25
129
+ * const selectionHighlight = HighlightBuilder.selection(
130
+ * 10,
131
+ * 25,
132
+ * 'selection-highlight'
133
+ * );
134
+ * ```
135
+ */
136
+ selection: (start: number, end: number, className?: string, style?: React.CSSProperties) => {
137
+ className: string | undefined;
138
+ end: number;
139
+ start: number;
140
+ style: React.CSSProperties | undefined;
141
+ }[];
142
+ /**
143
+ * Highlights entire words that match specific terms
144
+ * @param text - The text to search within
145
+ * @param words - Array of words to highlight
146
+ * @param className - Optional CSS class name to apply to matched words
147
+ * @param style - Optional inline styles to apply to matched words
148
+ * @returns Array of character range highlights for all matched words
149
+ * @example
150
+ * ```tsx
151
+ * // Highlight specific programming keywords
152
+ * const highlights = HighlightBuilder.words(
153
+ * sourceCode,
154
+ * ['function', 'const', 'let', 'var', 'return'],
155
+ * 'keyword'
156
+ * );
157
+ *
158
+ * // Highlight important terms with custom styling
159
+ * const termHighlights = HighlightBuilder.words(
160
+ * document,
161
+ * ['TODO', 'FIXME', 'NOTE'],
162
+ * undefined,
163
+ * { backgroundColor: 'orange', fontWeight: 'bold' }
164
+ * );
165
+ * ```
166
+ */
167
+ words: (text: string, words: string[], className?: string, style?: React.CSSProperties) => {
168
+ className: string | undefined;
169
+ end: number;
170
+ start: number;
171
+ style: React.CSSProperties | undefined;
172
+ }[];
173
+ };
174
+ //#endregion
175
+ //#region src/types.d.ts
176
+ /**
177
+ * Represents a character range to highlight in the entire text using absolute positions
178
+ * @example
179
+ * ```tsx
180
+ * const highlight: CharacterRange = {
181
+ * start: 0,
182
+ * end: 5,
183
+ * className: 'highlight-keyword',
184
+ * style: { backgroundColor: 'yellow', fontWeight: 'bold' }
185
+ * };
186
+ * ```
187
+ */
188
+ type CharacterRange = {
189
+ /** Optional CSS class name to apply to the highlighted range */className?: string; /** Zero-based end index in the entire text (exclusive) */
190
+ end: number; /** Zero-based start index in the entire text (inclusive) */
191
+ start: number; /** Optional inline styles to apply to the highlighted range */
192
+ style?: React.CSSProperties;
193
+ };
194
+ /**
195
+ * Props for the DyeLight component
196
+ * Extends standard textarea props while replacing onChange with a simplified version
197
+ * @example
198
+ * ```tsx
199
+ * const MyEditor = () => {
200
+ * const [code, setCode] = useState('const x = 1;');
201
+ *
202
+ * const highlights = HighlightBuilder.pattern(
203
+ * code,
204
+ * /\b(const|let|var)\b/g,
205
+ * 'keyword-highlight'
206
+ * );
207
+ *
208
+ * const lineHighlights = HighlightBuilder.lines([
209
+ * { line: 0, className: 'current-line' }
210
+ * ]);
211
+ *
212
+ * return (
213
+ * <DyeLight
214
+ * value={code}
215
+ * onChange={setCode}
216
+ * highlights={highlights}
217
+ * lineHighlights={lineHighlights}
218
+ * enableAutoResize={true}
219
+ * rows={10}
220
+ * className="my-editor"
221
+ * placeholder="Enter your code here..."
222
+ * />
223
+ * );
224
+ * };
225
+ * ```
226
+ */
227
+ interface DyeLightProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
228
+ /** CSS class name for the textarea element */
229
+ className?: string;
230
+ /** CSS class name for the container wrapper element */
231
+ containerClassName?: string;
232
+ /** Default value for uncontrolled usage */
233
+ defaultValue?: string;
234
+ /** Text direction - supports left-to-right and right-to-left */
235
+ dir?: 'ltr' | 'rtl';
236
+ /** Enable automatic height adjustment based on content */
237
+ enableAutoResize?: boolean;
238
+ /** Character range highlights using absolute positions in the entire text */
239
+ highlights?: CharacterRange[];
240
+ /** Line-level highlights mapped by line number (0-based) to CSS color/class */
241
+ lineHighlights?: {
242
+ [lineNumber: number]: string;
243
+ };
244
+ /** Callback fired when the textarea value changes */
245
+ onChange?: (value: string) => void;
246
+ /** Number of visible text lines */
247
+ rows?: number;
248
+ /** Controlled value */
249
+ value?: string;
250
+ }
251
+ /**
252
+ * Methods exposed by the DyeLight component through its ref
253
+ * Provides programmatic access to common textarea operations
254
+ * @example
255
+ * ```tsx
256
+ * const MyComponent = () => {
257
+ * const dyeLightRef = useRef<DyeLightRef>(null);
258
+ *
259
+ * const handleFocus = () => {
260
+ * dyeLightRef.current?.focus();
261
+ * };
262
+ *
263
+ * const handleGetValue = () => {
264
+ * const value = dyeLightRef.current?.getValue();
265
+ * console.log('Current value:', value);
266
+ * };
267
+ *
268
+ * return <DyeLight ref={dyeLightRef} />;
269
+ * };
270
+ * ```
271
+ */
272
+ type DyeLightRef = {
273
+ /** Removes focus from the textarea */blur: () => void; /** Sets focus to the textarea */
274
+ focus: () => void; /** Gets the current value of the textarea */
275
+ getValue: () => string; /** Selects all text in the textarea */
276
+ select: () => void; /** Sets the selection range in the textarea */
277
+ setSelectionRange: (start: number, end: number) => void; /** Sets the value of the textarea programmatically */
278
+ setValue: (value: string) => void; /** Scrolls the character position into view with an optional pixel offset */
279
+ scrollToPosition: (pos: number, offset?: number, behavior?: ScrollBehavior) => void;
280
+ };
281
+ //#endregion
282
+ //#region src/DyeLight.d.ts
283
+ /**
284
+ * @fileoverview DyeLight - A React textarea component with advanced text highlighting capabilities
285
+ *
286
+ * This component provides a textarea with overlay-based text highlighting that supports:
287
+ * - Character-level highlighting using absolute text positions
288
+ * - Line-level highlighting with CSS classes or color values
289
+ * - Automatic height adjustment based on content
290
+ * - Synchronized scrolling between textarea and highlight layer
291
+ * - Both controlled and uncontrolled usage patterns
292
+ * - RTL text direction support
293
+ */
294
+ /**
295
+ * Creates a line element with optional highlighting
296
+ */
297
+ declare const createLineElement: (content: React.ReactNode, lineIndex: number, lineHighlight?: string) => React.ReactElement;
298
+ /**
299
+ * Renders a single line with character-level highlights and optional line-level highlighting
300
+ */
301
+ declare const renderHighlightedLine: (line: string, lineIndex: number, ranges: Array<{
302
+ absoluteStart: number;
303
+ className?: string;
304
+ end: number;
305
+ start: number;
306
+ style?: React.CSSProperties;
307
+ }>, lineHighlight?: string) => React.ReactElement;
308
+ /**
309
+ * A textarea component with support for highlighting character ranges using absolute positions
310
+ * and optional line-level highlighting. Perfect for syntax highlighting, error indication,
311
+ * and text annotation without the complexity of line-based positioning.
312
+ */
313
+ declare const DyeLight: React.ForwardRefExoticComponent<DyeLightProps & React.RefAttributes<DyeLightRef>>;
314
+ //#endregion
315
+ //#region src/domUtils.d.ts
316
+ /**
317
+ * @fileoverview DOM utility functions for textarea manipulation
318
+ *
319
+ * This module provides utility functions for working with DOM elements,
320
+ * specifically focused on textarea auto-resizing functionality used by
321
+ * the DyeLight component.
322
+ */
323
+ /**
324
+ * Automatically resizes a textarea element to fit its content
325
+ *
326
+ * This function adjusts the textarea height to match its scroll height,
327
+ * effectively removing scrollbars when the content fits and expanding
328
+ * the textarea as content is added. The height is first set to 'auto'
329
+ * to allow the element to shrink if content is removed.
330
+ *
331
+ * @param textArea - The HTML textarea element to resize
332
+ * @example
333
+ * ```ts
334
+ * const textarea = document.querySelector('textarea');
335
+ * if (textarea) {
336
+ * autoResize(textarea);
337
+ * }
338
+ *
339
+ * // Or in an event handler:
340
+ * const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
341
+ * autoResize(e.target);
342
+ * };
343
+ * ```
344
+ */
345
+ declare const autoResize: (textArea: HTMLTextAreaElement) => void;
346
+ //#endregion
347
+ export { CharacterRange, DyeLight, DyeLightProps, DyeLightRef, HighlightBuilder, autoResize, createLineElement, renderHighlightedLine };
348
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/builder.ts","../src/types.ts","../src/DyeLight.tsx","../src/domUtils.ts"],"mappings":";;;;AAcA;;;cAAa,gBAAA;EAAA;;;;;;;;;;;;;;;;;EAAA,UAAA,GAAA,KAAA,EAkBW,KAAA;IAAA,SAAA;IAAA,KAAA;IAAA,KAAA,GAAmD,KAAA,CAAM,aAAA;EAAA;IAAA,SAAA;IAAA,GAAA;IAAA,KAAA;IAAA,KAAA;;;;;;;;;;;;;;;;;;iBAoB9D,KAAA;IAAA,SAAA;IAAA,KAAA;IAAA,IAAA;EAAA;IAAA,CAAA,UAAA;EAAA;EAAA;;;;;;;;;;;;;;;AC9BnB;AA4CA;;;;;;AA4CA;;ED1DmB,OAAA,GAAA,IAAA,UAAA,OAAA,EAgCkB,MAAA,WAAA,SAAA,WAAA,KAAA,GAA6C,KAAA,CAAM,aAAA;IAAA,SAAA;IAAA,GAAA;IAAA,KAAA;IAAA,KAAA;;;;;;;;;;;;;;AC9DxF;AA4CA;;;;;mBD2CqB,KAAA;IAAA,SAAA;IAAA,GAAA;IAAA,KAAA;IAAA,KAAA,GAAgE,KAAA,CAAM,aAAA;EAAA;IAAA,SAAA;IAAA,GAAA;IAAA,KAAA;IAAA,KAAA;;;;;;;;;;;ACvF3F;AA4CA;;;;;;AA4CA;;sEDqBwE,KAAA,CAAM,aAAA;IAAA,SAAA;IAAA,GAAA;IAAA,KAAA;IAAA,KAAA;;;;;;;;;AC7G9E;AA4CA;;;;;;AA4CA;;;;ACrFA;AAwBA;;;;;AAyEA;qEFsCuE,KAAA,CAAM,aAAA;IAAA,SAAA;IAAA,GAAA;IAAA,KAAA;IAAA,KAAA;;;;;;AC1I7E;AA4CA;;;;;;AA4CA;;;;KAxFY,cAAA;EAAA,gEAAA,SAAA;EAAA,GAAA;EAAA,KAAA;EAAA,KAAA,GAQA,KAAA,CAAM,aAAA;AAAA;AAAA;AAoClB;;;;;;AA4CA;;;;ACrFA;AAwBA;;;;;AAyEA;;;;;;;;;AC5FA;;;;;;;AFAkB,UAoCD,aAAA,SAAsB,IAAA,CAAK,KAAA,CAAM,sBAAA,CAAuB,mBAAA;EAAA;EAAA,SAAA;EAAA;EAAA,kBAAA;EAAA;EAAA,YAAA;EAAA;EAAA,GAAA;EAAA;EAAA,gBAAA;EAAA;EAAA,UAAA,GAYxD,cAAA;EAAA;EAAA,cAAA;IAAA,CAAA,UAAA;EAAA;EAAA;EAAA,QAAA,IAAA,KAAA;EAAA;EAAA,IAAA;EAAA;EAAA,KAAA;AAAA;AAAA;;;AAgCjB;;;;ACrFA;AAwBA;;;;;AAyEA;;;;;;;;AD5CiB,KAgCL,WAAA;EAAA,sCAAA,IAAA;EAAA,KAAA;EAAA,QAAA;EAAA,MAAA;EAAA,iBAAA,GAAA,KAAA,UAAA,GAAA;EAAA,QAAA,GAAA,KAAA;EAAA,gBAAA,GAAA,GAAA,UAAA,MAAA,WAAA,QAAA,GAcoD,cAAA;AAAA;;;;ACnGhE;AAwBA;;;;;AAyEA;;;;;AAjGA;AAwBA;cAxBa,iBAAA,GAAA,OAAA,EACA,KAAA,CAAM,SAAA,EAAA,SAAA,UAAA,aAAA,cAGhB,KAAA,CAAM,YAAA;AAAA;AAoBT;;AApBS,cAoBI,qBAAA,GAAA,IAAA,UAAA,SAAA,UAAA,MAAA,EAGD,KAAA;EAAA,aAAA;EAAA,SAAA;EAAA,GAAA;EAAA,KAAA;EAAA,KAAA,GAKI,KAAA,CAAM,aAAA;AAAA,IAAA,aAAA,cAGnB,KAAA,CAAM,YAAA;AAAA;;AA8DT;;;AA9DS,cA8DI,QAAA,EAAQ,KAAA,CAAA,yBAAA,CAAA,aAAA,GAAA,KAAA,CAAA,aAAA,CAAA,WAAA;;;;AC5FrB;;;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;cAAa,UAAA,GAAA,QAAA,EAAwB,mBAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import{forwardRef as e,useCallback as t,useEffect as n,useImperativeHandle as r,useMemo as i,useRef as a,useState as o}from"react";import{jsx as s,jsxs as c}from"react/jsx-runtime";const l={characters:e=>e.map(({className:e,index:t,style:n})=>({className:e,end:t+1,start:t,style:n})),lines:e=>{let t={};return e.forEach(({className:e,color:n,line:r})=>{t[r]=e||n||``}),t},pattern:(e,t,n,r)=>{let i=typeof t==`string`?new RegExp(t,`g`):new RegExp(t.source,`g`);return Array.from(e.matchAll(i)).map(e=>({className:n,end:e.index+e[0].length,start:e.index,style:r}))},ranges:e=>e.map(({className:e,end:t,start:n,style:r})=>({className:e,end:t,start:n,style:r})),selection:(e,t,n,r)=>[{className:n,end:t,start:e,style:r}],words:(e,t,n,r)=>{let i=RegExp(`\\b(${t.join(`|`)})\\b`,`g`);return l.pattern(e,i,n,r)}},u=e=>{e.style.height=`auto`,e.style.height=`${e.scrollHeight}px`},d=e=>{let[n,r]=o();return{handleAutoResize:t(t=>{e&&(u(t),r(t.scrollHeight))},[e]),textareaHeight:n}},f=e=>{let t=e.split(`
2
+ `),n=[],r=0;return t.forEach((e,i)=>{n.push(r),r+=e.length+(i<t.length-1?1:0)}),{lineStarts:n,lines:t}},p=(e,t)=>{for(let n=t.length-1;n>=0;n--)if(e>=t[n])return{char:e-t[n],line:n};return{char:0,line:0}},m=e=>/^(#|rgb|hsl|var\(--.*?\)|transparent|currentColor|inherit|initial|unset)/i.test(e)||/^[a-z]+$/i.test(e),h=(e,t,n,r)=>{let{lines:i,lineStarts:a}=f(e),o={};return t.forEach(e=>{let t=p(e.start,a),n=p(e.end-1,a);for(let r=t.line;r<=n.line;r++){o[r]||(o[r]=[]);let t=a[r],n=Math.max(e.start-t,0),s=Math.min(e.end-t,i[r].length);s>n&&o[r].push({absoluteStart:e.start,className:e.className,end:s,start:n,style:e.style})}}),i.map((e,t)=>{let i=n[t];return r(e,t,o[t]||[],i)})},g=(e,t,n,r)=>i(()=>h(e,t,n,r),[e,t,n,r]),_=(e,t)=>{if(!e||!t)return;let{scrollLeft:n,scrollTop:r}=e;t.scrollTop=r,t.scrollLeft=n},v=(e,t,n=getComputedStyle)=>{if(!e||!t)return;let r=n(e);t.style.padding=r.padding,t.style.fontSize=r.fontSize,t.style.fontFamily=r.fontFamily,t.style.lineHeight=r.lineHeight,t.style.letterSpacing=r.letterSpacing,t.style.wordSpacing=r.wordSpacing,t.style.textIndent=r.textIndent},y=()=>{let e=a(null);return{highlightLayerRef:e,syncScroll:t(t=>{_(t.current,e.current)},[]),syncStyles:t(t=>{v(t.current,e.current)},[])}},b=(e,t,n,r,i)=>{if(!e)return;let a=e.value;a!==t&&(n||r(a),i?.(a))},x=(e,t,n,r)=>{t||n(e),r?.(e)},S=(e,t,n,r,i)=>{e!==t&&(n||r(e),i?.(e))},C=(e,t,n,r,i)=>{e&&(e.value=t,n||r(t),i?.(t))},w=(e,r=``,i)=>{let s=a(null),[c,l]=o(e??r),u=e!==void 0,d=u?e:c,f=t(()=>{b(s.current,d,u,l,i)},[d,u,i]),p=t(e=>{x(e.target.value,u,l,i)},[u,i]),m=t(e=>{S(e.currentTarget.value,d,u,l,i)},[d,u,i]),h=t(e=>{C(s.current,e,u,l,i)},[u,i]);return n(()=>{f()},[f]),n(()=>{u&&s.current&&s.current.value!==e&&(s.current.value=e)},[u,e]),n(()=>{let e=s.current;if(!e)return;let t=new MutationObserver(()=>{f()});t.observe(e,{attributeFilter:[`value`],attributes:!0});let n=setInterval(()=>{e.value!==d&&f()},100);return()=>{t.disconnect(),clearInterval(n)}},[d,f]),{currentValue:d,handleChange:p,handleInput:m,setValue:h,textareaRef:s}},T={display:`block`,position:`relative`,width:`100%`},E={background:`transparent`,boxSizing:`border-box`,caretColor:`#000000`,color:`transparent`,minHeight:`auto`,position:`relative`,width:`100%`,zIndex:2},D={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},O=(e,t,n)=>{if(!n)return s(`div`,{children:e},t);let r=m(n);return s(`div`,{className:r?void 0:n,style:r?{backgroundColor:n}:void 0,children:e},t)},k=(e,t,n,r)=>{if(n.length===0)return O(e||`\xA0`,t,r);let i=n.toSorted((e,t)=>e.start-t.start),a=[],o=0;if(i.forEach((n,r)=>{let{className:i,end:c,start:l,style:u}=n,d=Math.max(0,Math.min(l,e.length)),f=Math.max(d,Math.min(c,e.length));if(d>o){let t=e.slice(o,d);t&&a.push(t)}if(f>d){let o=e.slice(d,f);a.push(s(`span`,{className:i,style:u,"data-range-start":n.absoluteStart,children:o},`highlight-${t}-${r.toString()}`))}o=Math.max(o,f)}),o<e.length){let t=e.slice(o);t&&a.push(t)}return O(a.length===0?`\xA0`:a,t,r)},A=e(({className:e=``,containerClassName:i=``,defaultValue:o=``,dir:l=`ltr`,enableAutoResize:u=!0,highlights:f=[],lineHighlights:p={},onChange:m,rows:h=4,style:_,value:v,...b},x)=>{let S=a(null),{currentValue:C,handleChange:O,handleInput:A,setValue:j,textareaRef:M}=w(v,o,m),{handleAutoResize:N,textareaHeight:P}=d(u),{highlightLayerRef:F,syncScroll:I,syncStyles:L}=y(),R=g(C,f,p,k),z=t(e=>{O(e),N(e.target)},[O,N]),B=t(e=>{A(e),N(e.currentTarget)},[A,N]),V=t(e=>{j(e),M.current&&N(M.current)},[j,N,M]);n(()=>{},[]);let H=t(()=>{I(M)},[I,M]);r(x,()=>({blur:()=>M.current?.blur(),focus:()=>M.current?.focus(),getValue:()=>C,scrollToPosition:(e,t=40,n=`auto`)=>{if(F.current&&M.current){let r=F.current.querySelector(`[data-range-start="${e.toString()}"]`);if(r instanceof HTMLElement){let e=M.current,i=r.offsetTop;r.offsetHeight;let a=e.scrollTop,o=e.clientHeight;i>=a&&a+o-t,n===`smooth`?e.scrollTo({behavior:`smooth`,top:i-t}):e.scrollTop=i-t}}},select:()=>M.current?.select(),setSelectionRange:(e,t)=>M.current?.setSelectionRange(e,t),setValue:V}),[C,V,F,M]),n(()=>{M.current&&u&&N(M.current),L(M)},[C,N,u,L,M]);let U={...E,color:C?`transparent`:`inherit`,height:P?`${P}px`:void 0,resize:u?`none`:`vertical`},W={...D,direction:l,height:P?`${P}px`:void 0,padding:M.current?getComputedStyle(M.current).padding:`8px 12px`};return c(`div`,{className:i,ref:S,style:{...T,..._},children:[s(`div`,{"aria-hidden":`true`,ref:F,style:W,children:R}),s(`textarea`,{className:e,dir:l,onChange:z,onInput:B,onScroll:H,ref:M,rows:h,style:U,value:C,...b})]})});A.displayName=`DyeLight`;export{A as DyeLight,l as HighlightBuilder,u as autoResize,O as createLineElement,k as renderHighlightedLine};
3
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["renderHighlightedLine"],"sources":["../src/builder.ts","../src/domUtils.ts","../src/hooks/useAutoResize.ts","../src/textUtils.ts","../src/hooks/useHighlightedContent.ts","../src/hooks/useHighlightSync.ts","../src/hooks/useTextareaValue.ts","../src/styles.ts","../src/DyeLight.tsx"],"sourcesContent":["/**\n * @fileoverview HighlightBuilder - Utility functions for creating highlight objects\n *\n * This module provides a convenient API for building highlight configurations\n * for the DyeLight component. It includes methods for creating character-level\n * highlights, line-level highlights, pattern-based highlights, and more.\n */\n\nimport type React from 'react';\n\n/**\n * Utility functions for building highlight objects for the DyeLight component\n * Provides a fluent API for creating various types of text highlights\n */\nexport const HighlightBuilder = {\n /**\n * Creates highlights for individual characters using absolute positions\n * @param chars - Array of character highlight configurations\n * @param chars[].index - Zero-based character index in the text\n * @param chars[].className - Optional CSS class name to apply\n * @param chars[].style - Optional inline styles to apply\n * @returns Array of character range highlights\n * @example\n * ```tsx\n * // Highlight characters at positions 5, 10, and 15\n * const highlights = HighlightBuilder.characters([\n * { index: 5, className: 'highlight-error' },\n * { index: 10, className: 'highlight-warning' },\n * { index: 15, style: { backgroundColor: 'yellow' } }\n * ]);\n * ```\n */\n characters: (chars: Array<{ className?: string; index: number; style?: React.CSSProperties }>) => {\n return chars.map(({ className, index, style }) => ({ className, end: index + 1, start: index, style }));\n },\n\n /**\n * Creates line highlights for entire lines\n * @param lines - Array of line highlight configurations\n * @param lines[].line - Zero-based line number\n * @param lines[].className - Optional CSS class name to apply to the line\n * @param lines[].color - Optional color value (CSS color, hex, rgb, etc.)\n * @returns Object mapping line numbers to highlight values\n * @example\n * ```tsx\n * // Highlight lines 0 and 2 with different styles\n * const lineHighlights = HighlightBuilder.lines([\n * { line: 0, className: 'error-line' },\n * { line: 2, color: '#ffff00' }\n * ]);\n * ```\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 * @param text - The text to search within\n * @param pattern - Regular expression or string pattern to match\n * @param className - Optional CSS class name to apply to matches\n * @param style - Optional inline styles to apply to matches\n * @returns Array of character range highlights for all matches\n * @example\n * ```tsx\n * // Highlight all JavaScript keywords\n * const highlights = HighlightBuilder.pattern(\n * code,\n * /\\b(function|const|let|var|if|else|for|while)\\b/g,\n * 'keyword-highlight'\n * );\n *\n * // Highlight all email addresses\n * const emailHighlights = HighlightBuilder.pattern(\n * text,\n * /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b/g,\n * 'email-highlight'\n * );\n * ```\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) => ({ className, end: match.index! + match[0].length, start: match.index!, style }));\n },\n\n /**\n * Creates character range highlights using absolute positions\n * @param ranges - Array of character range configurations\n * @param ranges[].start - Zero-based start index (inclusive)\n * @param ranges[].end - Zero-based end index (exclusive)\n * @param ranges[].className - Optional CSS class name to apply\n * @param ranges[].style - Optional inline styles to apply\n * @returns Array of character range highlights\n * @example\n * ```tsx\n * // Highlight specific ranges in the text\n * const highlights = HighlightBuilder.ranges([\n * { start: 0, end: 5, className: 'title-highlight' },\n * { start: 10, end: 20, style: { backgroundColor: 'yellow' } },\n * { start: 25, end: 30, className: 'error-highlight' }\n * ]);\n * ```\n */\n ranges: (ranges: Array<{ className?: string; end: number; start: number; style?: React.CSSProperties }>) => {\n return ranges.map(({ className, end, start, style }) => ({ className, end, start, style }));\n },\n\n /**\n * Highlights text between specific start and end positions\n * Convenience method for highlighting a single selection range\n * @param start - Zero-based start index (inclusive)\n * @param end - Zero-based end index (exclusive)\n * @param className - Optional CSS class name to apply\n * @param style - Optional inline styles to apply\n * @returns Array containing a single character range highlight\n * @example\n * ```tsx\n * // Highlight a selection from position 10 to 25\n * const selectionHighlight = HighlightBuilder.selection(\n * 10,\n * 25,\n * 'selection-highlight'\n * );\n * ```\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 specific terms\n * @param text - The text to search within\n * @param words - Array of words to highlight\n * @param className - Optional CSS class name to apply to matched words\n * @param style - Optional inline styles to apply to matched words\n * @returns Array of character range highlights for all matched words\n * @example\n * ```tsx\n * // Highlight specific programming keywords\n * const highlights = HighlightBuilder.words(\n * sourceCode,\n * ['function', 'const', 'let', 'var', 'return'],\n * 'keyword'\n * );\n *\n * // Highlight important terms with custom styling\n * const termHighlights = HighlightBuilder.words(\n * document,\n * ['TODO', 'FIXME', 'NOTE'],\n * undefined,\n * { backgroundColor: 'orange', fontWeight: 'bold' }\n * );\n * ```\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","/**\n * @fileoverview DOM utility functions for textarea manipulation\n *\n * This module provides utility functions for working with DOM elements,\n * specifically focused on textarea auto-resizing functionality used by\n * the DyeLight component.\n */\n\n/**\n * Automatically resizes a textarea element to fit its content\n *\n * This function adjusts the textarea height to match its scroll height,\n * effectively removing scrollbars when the content fits and expanding\n * the textarea as content is added. The height is first set to 'auto'\n * to allow the element to shrink if content is removed.\n *\n * @param textArea - The HTML textarea element to resize\n * @example\n * ```ts\n * const textarea = document.querySelector('textarea');\n * if (textarea) {\n * autoResize(textarea);\n * }\n *\n * // Or in an event handler:\n * const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n * autoResize(e.target);\n * };\n * ```\n */\nexport const autoResize = (textArea: HTMLTextAreaElement) => {\n // Reset height to auto to allow shrinking\n textArea.style.height = 'auto';\n // Set height to scroll height to fit content\n textArea.style.height = `${textArea.scrollHeight}px`;\n};\n","import { useCallback, useState } from 'react';\n\nimport { autoResize } from '@/domUtils';\n\nexport const createAutoResizeHandler = (\n enableAutoResize: boolean,\n setTextareaHeight: (height: number | undefined) => void,\n resize: (element: HTMLTextAreaElement) => void = autoResize,\n) => {\n return (element: HTMLTextAreaElement) => {\n if (!enableAutoResize) {\n return;\n }\n\n resize(element);\n setTextareaHeight(element.scrollHeight);\n };\n};\n\n/**\n * Hook for managing textarea auto-resize functionality\n */\nexport const useAutoResize = (enableAutoResize: boolean) => {\n const [textareaHeight, setTextareaHeight] = useState<number | undefined>();\n\n /**\n * Handles automatic resizing of the textarea based on content\n */\n const handleAutoResize = useCallback(\n (element: HTMLTextAreaElement) => {\n if (!enableAutoResize) {\n return;\n }\n autoResize(element);\n setTextareaHeight(element.scrollHeight);\n },\n [enableAutoResize],\n );\n\n return { handleAutoResize, textareaHeight };\n};\n","/**\n * @fileoverview Text utility functions for position calculations and text processing\n *\n * This module provides utilities for converting between absolute text positions\n * and line-relative positions, as well as other text processing functions used\n * by the DyeLight component for highlight positioning.\n */\n\n/**\n * Analyzes text content and returns line information with position mappings\n * @param text - The text content to analyze\n * @returns Object containing lines array and line start positions\n * @returns returns.lines - Array of individual lines (without newline characters)\n * @returns returns.lineStarts - Array of absolute positions where each line starts\n * @example\n * ```ts\n * const text = \"Hello\\nWorld\\nTest\";\n * const { lines, lineStarts } = getLinePositions(text);\n * // lines: [\"Hello\", \"World\", \"Test\"]\n * // lineStarts: [0, 6, 12]\n * ```\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 { lineStarts, lines };\n};\n\n/**\n * Converts an absolute text position to line-relative coordinates\n * @param absolutePos - Zero-based absolute position in the entire text\n * @param lineStarts - Array of line start positions (from getLinePositions)\n * @returns Object containing line number and character position within that line\n * @returns returns.line - Zero-based line number\n * @returns returns.char - Zero-based character position within the line\n * @example\n * ```ts\n * const text = \"Hello\\nWorld\\nTest\";\n * const { lineStarts } = getLinePositions(text);\n * const pos = absoluteToLinePos(8, lineStarts);\n * // pos: { line: 1, char: 2 } (the 'r' in \"World\")\n * ```\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 { char: absolutePos - lineStarts[i], line: i };\n }\n }\n return { char: 0, line: 0 };\n};\n\n/**\n * Determines if a string value represents a CSS color value\n * @param value - The string value to test\n * @returns True if the value appears to be a color value, false otherwise\n * @example\n * ```ts\n * isColorValue('#ff0000'); // true\n * isColorValue('rgb(255, 0, 0)'); // true\n * isColorValue('red'); // true\n * isColorValue('my-css-class'); // false (contains hyphens)\n * isColorValue('transparent'); // true\n * isColorValue('var(--primary-color)'); // true\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","import { useMemo } from 'react';\nimport { absoluteToLinePos, getLinePositions } from '@/textUtils';\nimport type { CharacterRange } from '@/types';\n\nexport const computeHighlightedContent = (\n text: string,\n highlights: CharacterRange[],\n lineHighlights: { [lineNumber: number]: string },\n renderHighlightedLine: (\n line: string,\n lineIndex: number,\n ranges: Array<{\n absoluteStart: number;\n className?: string;\n end: number;\n start: number;\n style?: React.CSSProperties;\n }>,\n lineHighlight?: string,\n ) => React.ReactElement,\n) => {\n const { lines, lineStarts } = getLinePositions(text);\n\n const highlightsByLine: {\n [lineIndex: number]: Array<{\n absoluteStart: number;\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 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 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 absoluteStart: highlight.start,\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};\n\n/**\n * Hook for computing highlighted content from text and highlight ranges\n */\nexport const useHighlightedContent = (\n text: string,\n highlights: CharacterRange[],\n lineHighlights: { [lineNumber: number]: string },\n renderHighlightedLine: (\n line: string,\n lineIndex: number,\n ranges: Array<{\n absoluteStart: number;\n className?: string;\n end: number;\n start: number;\n style?: React.CSSProperties;\n }>,\n lineHighlight?: string,\n ) => React.ReactElement,\n) => {\n /**\n * Computes the highlighted content by processing text and highlight ranges\n * Groups highlights by line and renders each line with appropriate highlighting\n */\n const highlightedContent = useMemo(\n () => computeHighlightedContent(text, highlights, lineHighlights, renderHighlightedLine),\n [text, highlights, lineHighlights, renderHighlightedLine],\n );\n\n return highlightedContent;\n};\n","import { useCallback, useRef } from 'react';\n\nexport const syncScrollPositions = (\n textarea: Pick<HTMLTextAreaElement, 'scrollLeft' | 'scrollTop'> | null,\n highlightLayer: Pick<HTMLDivElement, 'scrollLeft' | 'scrollTop'> | null,\n) => {\n if (!textarea || !highlightLayer) {\n return;\n }\n\n const { scrollLeft, scrollTop } = textarea;\n highlightLayer.scrollTop = scrollTop;\n highlightLayer.scrollLeft = scrollLeft;\n};\n\nexport const syncHighlightStyles = (\n textarea: HTMLTextAreaElement | null,\n highlightLayer: HTMLDivElement | null,\n computeStyle: (element: Element) => CSSStyleDeclaration = getComputedStyle,\n) => {\n if (!textarea || !highlightLayer) {\n return;\n }\n\n const computedStyle = computeStyle(textarea);\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/**\n * Hook for managing highlight layer synchronization\n */\nexport const useHighlightSync = () => {\n const highlightLayerRef = useRef<HTMLDivElement>(null);\n\n const syncScroll = useCallback((textareaRef: React.RefObject<HTMLTextAreaElement | null>) => {\n syncScrollPositions(textareaRef.current, highlightLayerRef.current);\n }, []);\n\n const syncStyles = useCallback((textareaRef: React.RefObject<HTMLTextAreaElement | null>) => {\n syncHighlightStyles(textareaRef.current, highlightLayerRef.current);\n }, []);\n\n return { highlightLayerRef, syncScroll, syncStyles };\n};\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport const syncValueWithDOM = (\n textarea: HTMLTextAreaElement | null,\n currentValue: string,\n isControlled: boolean,\n setInternalValue: (value: string) => void,\n onChange?: (value: string) => void,\n) => {\n if (!textarea) {\n return;\n }\n\n const domValue = textarea.value;\n if (domValue !== currentValue) {\n if (!isControlled) {\n setInternalValue(domValue);\n }\n onChange?.(domValue);\n }\n};\n\nexport const handleChangeValue = (\n newValue: string,\n isControlled: boolean,\n setInternalValue: (value: string) => void,\n onChange?: (value: string) => void,\n) => {\n if (!isControlled) {\n setInternalValue(newValue);\n }\n\n onChange?.(newValue);\n};\n\nexport const handleInputValue = (\n newValue: string,\n currentValue: string,\n isControlled: boolean,\n setInternalValue: (value: string) => void,\n onChange?: (value: string) => void,\n) => {\n if (newValue === currentValue) {\n return;\n }\n\n if (!isControlled) {\n setInternalValue(newValue);\n }\n\n onChange?.(newValue);\n};\n\nexport const applySetValue = (\n textarea: HTMLTextAreaElement | null,\n newValue: string,\n isControlled: boolean,\n setInternalValue: (value: string) => void,\n onChange?: (value: string) => void,\n) => {\n if (!textarea) {\n return;\n }\n\n textarea.value = newValue;\n\n if (!isControlled) {\n setInternalValue(newValue);\n }\n\n onChange?.(newValue);\n};\n\n/**\n * Hook for managing textarea value state and synchronization\n * Handles both controlled and uncontrolled modes, plus programmatic changes\n */\nexport const useTextareaValue = (value?: string, defaultValue = '', onChange?: (value: string) => void) => {\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = useState(value ?? defaultValue);\n\n const isControlled = value !== undefined;\n const currentValue = isControlled ? value : internalValue;\n\n const syncValueWithDOMCallback = useCallback(() => {\n syncValueWithDOM(textareaRef.current, currentValue, isControlled, setInternalValue, onChange);\n }, [currentValue, isControlled, onChange]);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n handleChangeValue(e.target.value, isControlled, setInternalValue, onChange);\n },\n [isControlled, onChange],\n );\n\n const handleInput = useCallback(\n (e: React.FormEvent<HTMLTextAreaElement>) => {\n handleInputValue(e.currentTarget.value, currentValue, isControlled, setInternalValue, onChange);\n },\n [currentValue, isControlled, onChange],\n );\n\n const setValue = useCallback(\n (newValue: string) => {\n applySetValue(textareaRef.current, newValue, isControlled, setInternalValue, onChange);\n },\n [isControlled, onChange],\n );\n\n useEffect(() => {\n syncValueWithDOMCallback();\n }, [syncValueWithDOMCallback]);\n\n useEffect(() => {\n if (isControlled && textareaRef.current && textareaRef.current.value !== value) {\n textareaRef.current.value = value;\n }\n }, [isControlled, value]);\n\n useEffect(() => {\n const textarea = textareaRef.current;\n if (!textarea) {\n return;\n }\n\n const observer = new MutationObserver(() => {\n syncValueWithDOMCallback();\n });\n\n observer.observe(textarea, { attributeFilter: ['value'], attributes: true });\n\n const intervalId = setInterval(() => {\n if (textarea.value !== currentValue) {\n syncValueWithDOMCallback();\n }\n }, 100);\n\n return () => {\n observer.disconnect();\n clearInterval(intervalId);\n };\n }, [currentValue, syncValueWithDOMCallback]);\n\n return { currentValue: currentValue, handleChange, handleInput, setValue, textareaRef };\n};\n","/**\n * @fileoverview Default styles for DyeLight component elements\n *\n * This module contains the default CSS-in-JS styles used by the DyeLight component\n * for its container, textarea, and highlight layer elements. These styles ensure\n * proper layering, positioning, and visual alignment between the textarea and\n * its highlight overlay.\n */\n\nimport type React from 'react';\n\n/**\n * Default styles for the main container element that wraps the entire component\n * Creates a positioned container that serves as the reference for absolutely positioned children\n */\nexport const DEFAULT_CONTAINER_STYLE: React.CSSProperties = {\n /** Ensures the container behaves as a block-level element */\n display: 'block',\n /** Enables absolute positioning for child elements */\n position: 'relative',\n /** Takes full width of parent container */\n width: '100%',\n};\n\n/**\n * Default styles for the textarea element\n * Makes the textarea transparent while maintaining its functionality and positioning\n */\nexport const DEFAULT_BASE_STYLE: React.CSSProperties = {\n /** Transparent background to show highlights underneath */\n background: 'transparent',\n /** Ensures consistent box model calculations */\n boxSizing: 'border-box',\n /** Maintains visible text cursor */\n caretColor: '#000000',\n /** Transparent text to reveal highlights while keeping cursor and selection */\n color: 'transparent',\n /** Prevents unwanted height constraints */\n minHeight: 'auto',\n /** Positioned above the highlight layer */\n position: 'relative',\n /** Takes full width of container */\n width: '100%',\n /** Ensures textarea appears above highlight layer */\n zIndex: 2,\n};\n\n/**\n * Default styles for the highlight layer that renders behind the textarea\n * Positioned absolutely to overlay perfectly with the textarea content\n */\nexport const DEFAULT_HIGHLIGHT_LAYER_STYLE: React.CSSProperties = {\n /** Transparent border to match textarea default border */\n border: 'transparent solid 1px',\n /** Stretch to fill container bottom */\n bottom: 0,\n /** Consistent box model with textarea */\n boxSizing: 'border-box',\n /** Inherit text color from parent */\n color: 'inherit',\n /** Match textarea font family */\n fontFamily: 'inherit',\n /** Match textarea font size */\n fontSize: 'inherit',\n /** Stretch to fill container left */\n left: 0,\n /** Match textarea line height */\n lineHeight: 'inherit',\n /** Remove default margins */\n margin: 0,\n /** Hide scrollbars on highlight layer */\n overflow: 'hidden',\n /** Prevent highlight layer from capturing mouse events */\n pointerEvents: 'none',\n /** Positioned absolutely within container */\n position: 'absolute',\n /** Stretch to fill container right */\n right: 0,\n /** Stretch to fill container top */\n top: 0,\n /** Preserve whitespace and line breaks like textarea */\n whiteSpace: 'pre-wrap',\n /** Break long words like textarea */\n wordWrap: 'break-word',\n /** Ensure highlight layer appears behind textarea */\n zIndex: 1,\n};\n","import type React from 'react';\nimport { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';\nimport { useAutoResize } from './hooks/useAutoResize';\nimport { useHighlightedContent } from './hooks/useHighlightedContent';\nimport { useHighlightSync } from './hooks/useHighlightSync';\nimport { useTextareaValue } from './hooks/useTextareaValue';\nimport { DEFAULT_BASE_STYLE, DEFAULT_CONTAINER_STYLE, DEFAULT_HIGHLIGHT_LAYER_STYLE } from './styles';\nimport { isColorValue } from './textUtils';\nimport type { DyeLightProps, DyeLightRef } from './types';\n\n/**\n * @fileoverview DyeLight - A React textarea component with advanced text highlighting capabilities\n *\n * This component provides a textarea with overlay-based text highlighting that supports:\n * - Character-level highlighting using absolute text positions\n * - Line-level highlighting with CSS classes or color values\n * - Automatic height adjustment based on content\n * - Synchronized scrolling between textarea and highlight layer\n * - Both controlled and uncontrolled usage patterns\n * - RTL text direction support\n */\n\n/**\n * Creates a line element with optional highlighting\n */\nexport const createLineElement = (\n content: React.ReactNode,\n lineIndex: number,\n lineHighlight?: string,\n): 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\n/**\n * Renders a single line with character-level highlights and optional line-level highlighting\n */\nexport const renderHighlightedLine = (\n line: string,\n lineIndex: number,\n ranges: Array<{\n absoluteStart: number;\n className?: string;\n end: number;\n start: number;\n style?: React.CSSProperties;\n }>,\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\n className={className}\n key={`highlight-${lineIndex}-${idx.toString()}`}\n style={rangeStyle}\n data-range-start={range.absoluteStart}\n >\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 */\nexport const DyeLight = forwardRef<DyeLightRef, DyeLightProps>(\n (\n {\n className = '',\n containerClassName = '',\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 containerRef = useRef<HTMLDivElement>(null);\n\n // Custom hooks for managing component logic\n const { currentValue, handleChange, handleInput, setValue, textareaRef } = useTextareaValue(\n value,\n defaultValue,\n onChange,\n );\n\n const { handleAutoResize, textareaHeight } = useAutoResize(enableAutoResize);\n\n const { highlightLayerRef, syncScroll, syncStyles } = useHighlightSync();\n\n const highlightedContent = useHighlightedContent(\n currentValue,\n highlights,\n lineHighlights,\n renderHighlightedLine,\n );\n\n // Enhanced change handler that includes auto-resize\n const handleChangeWithResize = useCallback(\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n handleChange(e);\n handleAutoResize(e.target);\n },\n [handleChange, handleAutoResize],\n );\n\n // Enhanced input handler that includes auto-resize\n const handleInputWithResize = useCallback(\n (e: React.FormEvent<HTMLTextAreaElement>) => {\n handleInput(e);\n handleAutoResize(e.currentTarget);\n },\n [handleInput, handleAutoResize],\n );\n\n // Enhanced setValue that includes auto-resize\n const setValueWithResize = useCallback(\n (newValue: string) => {\n setValue(newValue);\n if (textareaRef.current) {\n handleAutoResize(textareaRef.current);\n }\n },\n [setValue, handleAutoResize, textareaRef],\n );\n\n // Scroll handler with ref binding\n useEffect(() => {}, []);\n\n const handleScroll = useCallback(() => {\n syncScroll(textareaRef);\n }, [syncScroll, textareaRef]);\n\n // Expose ref methods\n useImperativeHandle(\n ref,\n () => ({\n blur: () => textareaRef.current?.blur(),\n focus: () => textareaRef.current?.focus(),\n getValue: () => currentValue,\n scrollToPosition: (pos: number, offset = 40, behavior: ScrollBehavior = 'auto') => {\n if (highlightLayerRef.current && textareaRef.current) {\n const span = highlightLayerRef.current.querySelector(`[data-range-start=\"${pos.toString()}\"]`);\n if (span instanceof HTMLElement) {\n const textarea = textareaRef.current;\n const spanTop = span.offsetTop;\n const spanHeight = span.offsetHeight;\n const scrollTop = textarea.scrollTop;\n const clientHeight = textarea.clientHeight;\n\n // Check if the element is already comfortably visible\n // We define \"comfortably visible\" as the start being in the viewport\n // and not too close to the edge (unless it's top/bot).\n const isStartVisible = spanTop >= scrollTop && spanTop <= scrollTop + clientHeight - offset;\n\n if (behavior === 'smooth') {\n textarea.scrollTo({ behavior: 'smooth', top: spanTop - offset });\n } else {\n textarea.scrollTop = spanTop - offset;\n }\n }\n }\n },\n select: () => textareaRef.current?.select(),\n setSelectionRange: (start: number, end: number) => textareaRef.current?.setSelectionRange(start, end),\n setValue: setValueWithResize,\n }),\n [currentValue, setValueWithResize, highlightLayerRef, textareaRef],\n );\n\n // Sync styles and handle auto-resize on value changes\n useEffect(() => {\n if (textareaRef.current && enableAutoResize) {\n handleAutoResize(textareaRef.current);\n }\n syncStyles(textareaRef);\n }, [currentValue, handleAutoResize, enableAutoResize, syncStyles, textareaRef]);\n\n // Compute styles\n const baseTextareaStyle: React.CSSProperties = {\n ...DEFAULT_BASE_STYLE,\n color: currentValue ? 'transparent' : 'inherit',\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 className={containerClassName} 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={handleChangeWithResize}\n onInput={handleInputWithResize}\n onScroll={handleScroll}\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"],"mappings":"qLAcA,MAAa,EAAmB,CAkB5B,WAAa,GACF,EAAM,KAAK,CAAE,YAAW,QAAO,YAAa,CAAE,YAAW,IAAK,EAAQ,EAAG,MAAO,EAAO,QAAO,EAAE,CAmB3G,MAAQ,GAAuE,CAC3E,IAAM,EAA2C,EAAE,CAInD,OAHA,EAAM,SAAS,CAAE,YAAW,QAAO,UAAW,CAC1C,EAAO,GAAQ,GAAa,GAAS,IACvC,CACK,GA2BX,SAAU,EAAc,EAA0B,EAAoB,IAAgC,CAClG,IAAM,EAAQ,OAAO,GAAY,SAAW,IAAI,OAAO,EAAS,IAAI,CAAG,IAAI,OAAO,EAAQ,OAAQ,IAAI,CAGtG,OAFgB,MAAM,KAAK,EAAK,SAAS,EAAM,CAAC,CAEjC,IAAK,IAAW,CAAE,YAAW,IAAK,EAAM,MAAS,EAAM,GAAG,OAAQ,MAAO,EAAM,MAAQ,QAAO,EAAE,EAqBnH,OAAS,GACE,EAAO,KAAK,CAAE,YAAW,MAAK,QAAO,YAAa,CAAE,YAAW,MAAK,QAAO,QAAO,EAAE,CAqB/F,WAAY,EAAe,EAAa,EAAoB,IACjD,CAAC,CAAE,YAAW,MAAK,QAAO,QAAO,CAAC,CA4B7C,OAAQ,EAAc,EAAiB,EAAoB,IAAgC,CACvF,IAAM,EAAc,OAAO,OAAO,EAAM,KAAK,IAAI,CAAC,MAAO,IAAI,CAC7D,OAAO,EAAiB,QAAQ,EAAM,EAAS,EAAW,EAAM,EAEvE,CCtIY,EAAc,GAAkC,CAEzD,EAAS,MAAM,OAAS,OAExB,EAAS,MAAM,OAAS,GAAG,EAAS,aAAa,KCZxC,EAAiB,GAA8B,CACxD,GAAM,CAAC,EAAgB,GAAqB,GAA8B,CAgB1E,MAAO,CAAE,iBAXgB,EACpB,GAAiC,CACzB,IAGL,EAAW,EAAQ,CACnB,EAAkB,EAAQ,aAAa,GAE3C,CAAC,EAAiB,CACrB,CAE0B,iBAAgB,ECjBlC,EAAoB,GAAiB,CAC9C,IAAM,EAAQ,EAAK,MAAM;EAAK,CACxB,EAAuB,EAAE,CAC3B,EAAW,EAOf,OALA,EAAM,SAAS,EAAM,IAAU,CAC3B,EAAW,KAAK,EAAS,CACzB,GAAY,EAAK,QAAU,EAAQ,EAAM,OAAS,EAAI,EAAI,IAC5D,CAEK,CAAE,aAAY,QAAO,EAkBnB,GAAqB,EAAqB,IAAyB,CAC5E,IAAK,IAAI,EAAI,EAAW,OAAS,EAAG,GAAK,EAAG,IACxC,GAAI,GAAe,EAAW,GAC1B,MAAO,CAAE,KAAM,EAAc,EAAW,GAAI,KAAM,EAAG,CAG7D,MAAO,CAAE,KAAM,EAAG,KAAM,EAAG,EAiBlB,EAAgB,GAErB,4EAA4E,KAAK,EAAM,EACvF,YAAY,KAAK,EAAM,CCxElB,GACT,EACA,EACA,EACA,IAYC,CACD,GAAM,CAAE,QAAO,cAAe,EAAiB,EAAK,CAE9C,EAQF,EAAE,CA4BN,OA1BA,EAAW,QAAS,GAAc,CAC9B,IAAM,EAAW,EAAkB,EAAU,MAAO,EAAW,CACzD,EAAS,EAAkB,EAAU,IAAM,EAAG,EAAW,CAE/D,IAAK,IAAI,EAAY,EAAS,KAAM,GAAa,EAAO,KAAM,IAAa,CAClE,EAAiB,KAClB,EAAiB,GAAa,EAAE,EAGpC,IAAM,EAAY,EAAW,GAEvB,EAAa,KAAK,IAAI,EAAU,MAAQ,EAAW,EAAE,CACrD,EAAW,KAAK,IAAI,EAAU,IAAM,EAAW,EAAM,GAAW,OAAO,CAEzE,EAAW,GACX,EAAiB,GAAW,KAAK,CAC7B,cAAe,EAAU,MACzB,UAAW,EAAU,UACrB,IAAK,EACL,MAAO,EACP,MAAO,EAAU,MACpB,CAAC,GAGZ,CAEK,EAAM,KAAK,EAAM,IAAc,CAClC,IAAM,EAAgB,EAAe,GAGrC,OAAOA,EAAsB,EAAM,EAFpB,EAAiB,IAAc,EAAE,CAEM,EAAc,EACtE,EAMO,GACT,EACA,EACA,EACA,IAiB2B,MACjB,EAA0B,EAAM,EAAY,EAAgBA,EAAsB,CACxF,CAAC,EAAM,EAAY,EAAgBA,EAAsB,CAC5D,CC5FQ,GACT,EACA,IACC,CACD,GAAI,CAAC,GAAY,CAAC,EACd,OAGJ,GAAM,CAAE,aAAY,aAAc,EAClC,EAAe,UAAY,EAC3B,EAAe,WAAa,GAGnB,GACT,EACA,EACA,EAA0D,mBACzD,CACD,GAAI,CAAC,GAAY,CAAC,EACd,OAGJ,IAAM,EAAgB,EAAa,EAAS,CAE5C,EAAe,MAAM,QAAU,EAAc,QAC7C,EAAe,MAAM,SAAW,EAAc,SAC9C,EAAe,MAAM,WAAa,EAAc,WAChD,EAAe,MAAM,WAAa,EAAc,WAChD,EAAe,MAAM,cAAgB,EAAc,cACnD,EAAe,MAAM,YAAc,EAAc,YACjD,EAAe,MAAM,WAAa,EAAc,YAMvC,MAAyB,CAClC,IAAM,EAAoB,EAAuB,KAAK,CAUtD,MAAO,CAAE,oBAAmB,WART,EAAa,GAA6D,CACzF,EAAoB,EAAY,QAAS,EAAkB,QAAQ,EACpE,EAAE,CAAC,CAMkC,WAJrB,EAAa,GAA6D,CACzF,EAAoB,EAAY,QAAS,EAAkB,QAAQ,EACpE,EAAE,CAAC,CAE8C,EC/C3C,GACT,EACA,EACA,EACA,EACA,IACC,CACD,GAAI,CAAC,EACD,OAGJ,IAAM,EAAW,EAAS,MACtB,IAAa,IACR,GACD,EAAiB,EAAS,CAE9B,IAAW,EAAS,GAIf,GACT,EACA,EACA,EACA,IACC,CACI,GACD,EAAiB,EAAS,CAG9B,IAAW,EAAS,EAGX,GACT,EACA,EACA,EACA,EACA,IACC,CACG,IAAa,IAIZ,GACD,EAAiB,EAAS,CAG9B,IAAW,EAAS,GAGX,GACT,EACA,EACA,EACA,EACA,IACC,CACI,IAIL,EAAS,MAAQ,EAEZ,GACD,EAAiB,EAAS,CAG9B,IAAW,EAAS,GAOX,GAAoB,EAAgB,EAAe,GAAI,IAAuC,CACvG,IAAM,EAAc,EAA4B,KAAK,CAC/C,CAAC,EAAe,GAAoB,EAAS,GAAS,EAAa,CAEnE,EAAe,IAAU,IAAA,GACzB,EAAe,EAAe,EAAQ,EAEtC,EAA2B,MAAkB,CAC/C,EAAiB,EAAY,QAAS,EAAc,EAAc,EAAkB,EAAS,EAC9F,CAAC,EAAc,EAAc,EAAS,CAAC,CAEpC,EAAe,EAChB,GAA8C,CAC3C,EAAkB,EAAE,OAAO,MAAO,EAAc,EAAkB,EAAS,EAE/E,CAAC,EAAc,EAAS,CAC3B,CAEK,EAAc,EACf,GAA4C,CACzC,EAAiB,EAAE,cAAc,MAAO,EAAc,EAAc,EAAkB,EAAS,EAEnG,CAAC,EAAc,EAAc,EAAS,CACzC,CAEK,EAAW,EACZ,GAAqB,CAClB,EAAc,EAAY,QAAS,EAAU,EAAc,EAAkB,EAAS,EAE1F,CAAC,EAAc,EAAS,CAC3B,CAoCD,OAlCA,MAAgB,CACZ,GAA0B,EAC3B,CAAC,EAAyB,CAAC,CAE9B,MAAgB,CACR,GAAgB,EAAY,SAAW,EAAY,QAAQ,QAAU,IACrE,EAAY,QAAQ,MAAQ,IAEjC,CAAC,EAAc,EAAM,CAAC,CAEzB,MAAgB,CACZ,IAAM,EAAW,EAAY,QAC7B,GAAI,CAAC,EACD,OAGJ,IAAM,EAAW,IAAI,qBAAuB,CACxC,GAA0B,EAC5B,CAEF,EAAS,QAAQ,EAAU,CAAE,gBAAiB,CAAC,QAAQ,CAAE,WAAY,GAAM,CAAC,CAE5E,IAAM,EAAa,gBAAkB,CAC7B,EAAS,QAAU,GACnB,GAA0B,EAE/B,IAAI,CAEP,UAAa,CACT,EAAS,YAAY,CACrB,cAAc,EAAW,GAE9B,CAAC,EAAc,EAAyB,CAAC,CAErC,CAAgB,eAAc,eAAc,cAAa,WAAU,cAAa,EChI9E,EAA+C,CAExD,QAAS,QAET,SAAU,WAEV,MAAO,OACV,CAMY,EAA0C,CAEnD,WAAY,cAEZ,UAAW,aAEX,WAAY,UAEZ,MAAO,cAEP,UAAW,OAEX,SAAU,WAEV,MAAO,OAEP,OAAQ,EACX,CAMY,EAAqD,CAE9D,OAAQ,wBAER,OAAQ,EAER,UAAW,aAEX,MAAO,UAEP,WAAY,UAEZ,SAAU,UAEV,KAAM,EAEN,WAAY,UAEZ,OAAQ,EAER,SAAU,SAEV,cAAe,OAEf,SAAU,WAEV,MAAO,EAEP,IAAK,EAEL,WAAY,WAEZ,SAAU,aAEV,OAAQ,EACX,CC7DY,GACT,EACA,EACA,IACqB,CACrB,GAAI,CAAC,EACD,OAAO,EAAC,MAAA,CAAA,SAAqB,EAAA,CAAZ,EAA0B,CAG/C,IAAM,EAAU,EAAa,EAAc,CAC3C,OACI,EAAC,MAAA,CACG,UAAW,EAAU,IAAA,GAAY,EAEjC,MAAO,EAAU,CAAE,gBAAiB,EAAe,CAAG,IAAA,YAErD,GAHI,EAIH,EAOD,GACT,EACA,EACA,EAOA,IACqB,CACrB,GAAI,EAAO,SAAW,EAElB,OAAO,EADS,GAAQ,OACU,EAAW,EAAc,CAI/D,IAAM,EAAe,EAAO,UAAU,EAAG,IAAM,EAAE,MAAQ,EAAE,MAAM,CAE3D,EAA4B,EAAE,CAChC,EAAY,EAoChB,GAlCA,EAAa,SAAS,EAAO,IAAQ,CACjC,GAAM,CAAE,YAAW,MAAK,QAAO,MAAO,GAAe,EAG/C,EAAe,KAAK,IAAI,EAAG,KAAK,IAAI,EAAO,EAAK,OAAO,CAAC,CACxD,EAAa,KAAK,IAAI,EAAc,KAAK,IAAI,EAAK,EAAK,OAAO,CAAC,CAGrE,GAAI,EAAe,EAAW,CAC1B,IAAM,EAAa,EAAK,MAAM,EAAW,EAAa,CAClD,GACA,EAAO,KAAK,EAAW,CAK/B,GAAI,EAAa,EAAc,CAC3B,IAAM,EAAkB,EAAK,MAAM,EAAc,EAAW,CAC5D,EAAO,KACH,EAAC,OAAA,CACc,YAEX,MAAO,EACP,mBAAkB,EAAM,uBAEvB,GAJI,aAAa,EAAU,GAAG,EAAI,UAAU,GAK1C,CACV,CAGL,EAAY,KAAK,IAAI,EAAW,EAAW,EAC7C,CAGE,EAAY,EAAK,OAAQ,CACzB,IAAM,EAAY,EAAK,MAAM,EAAU,CACnC,GACA,EAAO,KAAK,EAAU,CAK9B,OAAO,EADS,EAAO,SAAW,EAAI,OAAW,EACf,EAAW,EAAc,EAQlD,EAAW,GAEhB,CACI,YAAY,GACZ,qBAAqB,GACrB,eAAe,GACf,MAAM,MACN,mBAAmB,GACnB,aAAa,EAAE,CACf,iBAAiB,EAAE,CACnB,WACA,OAAO,EACP,QACA,QACA,GAAG,GAEP,IACC,CACD,IAAM,EAAe,EAAuB,KAAK,CAG3C,CAAE,eAAc,eAAc,cAAa,WAAU,eAAgB,EACvE,EACA,EACA,EACH,CAEK,CAAE,mBAAkB,kBAAmB,EAAc,EAAiB,CAEtE,CAAE,oBAAmB,aAAY,cAAe,GAAkB,CAElE,EAAqB,EACvB,EACA,EACA,EACA,EACH,CAGK,EAAyB,EAC1B,GAA8C,CAC3C,EAAa,EAAE,CACf,EAAiB,EAAE,OAAO,EAE9B,CAAC,EAAc,EAAiB,CACnC,CAGK,EAAwB,EACzB,GAA4C,CACzC,EAAY,EAAE,CACd,EAAiB,EAAE,cAAc,EAErC,CAAC,EAAa,EAAiB,CAClC,CAGK,EAAqB,EACtB,GAAqB,CAClB,EAAS,EAAS,CACd,EAAY,SACZ,EAAiB,EAAY,QAAQ,EAG7C,CAAC,EAAU,EAAkB,EAAY,CAC5C,CAGD,MAAgB,GAAI,EAAE,CAAC,CAEvB,IAAM,EAAe,MAAkB,CACnC,EAAW,EAAY,EACxB,CAAC,EAAY,EAAY,CAAC,CAG7B,EACI,OACO,CACH,SAAY,EAAY,SAAS,MAAM,CACvC,UAAa,EAAY,SAAS,OAAO,CACzC,aAAgB,EAChB,kBAAmB,EAAa,EAAS,GAAI,EAA2B,SAAW,CAC/E,GAAI,EAAkB,SAAW,EAAY,QAAS,CAClD,IAAM,EAAO,EAAkB,QAAQ,cAAc,sBAAsB,EAAI,UAAU,CAAC,IAAI,CAC9F,GAAI,aAAgB,YAAa,CAC7B,IAAM,EAAW,EAAY,QACvB,EAAU,EAAK,UACF,EAAK,aACxB,IAAM,EAAY,EAAS,UACrB,EAAe,EAAS,aAKP,GAAW,GAAwB,EAAY,EAAe,EAEjF,IAAa,SACb,EAAS,SAAS,CAAE,SAAU,SAAU,IAAK,EAAU,EAAQ,CAAC,CAEhE,EAAS,UAAY,EAAU,KAK/C,WAAc,EAAY,SAAS,QAAQ,CAC3C,mBAAoB,EAAe,IAAgB,EAAY,SAAS,kBAAkB,EAAO,EAAI,CACrG,SAAU,EACb,EACD,CAAC,EAAc,EAAoB,EAAmB,EAAY,CACrE,CAGD,MAAgB,CACR,EAAY,SAAW,GACvB,EAAiB,EAAY,QAAQ,CAEzC,EAAW,EAAY,EACxB,CAAC,EAAc,EAAkB,EAAkB,EAAY,EAAY,CAAC,CAG/E,IAAM,EAAyC,CAC3C,GAAG,EACH,MAAO,EAAe,cAAgB,UACtC,OAAQ,EAAiB,GAAG,EAAe,IAAM,IAAA,GACjD,OAAQ,EAAmB,OAAS,WACvC,CAEK,EAA2C,CAC7C,GAAG,EACH,UAAW,EACX,OAAQ,EAAiB,GAAG,EAAe,IAAM,IAAA,GACjD,QAAS,EAAY,QAAU,iBAAiB,EAAY,QAAQ,CAAC,QAAU,WAClF,CAED,OACI,EAAC,MAAA,CAAI,UAAW,EAAoB,IAAK,EAAc,MAAO,CAAE,GAAG,EAAyB,GAAG,EAAO,WAClG,EAAC,MAAA,CAAI,cAAY,OAAO,IAAK,EAAmB,MAAO,WAClD,GACC,CAEN,EAAC,WAAA,CACc,YACN,MACL,SAAU,EACV,QAAS,EACT,SAAU,EACV,IAAK,EACC,OACN,MAAO,EACP,MAAO,EACP,GAAI,GACN,CAAA,EACA,EAGjB,CAED,EAAS,YAAc"}
package/package.json CHANGED
@@ -1,46 +1,35 @@
1
1
  {
2
- "name": "dyelight",
3
- "repository": {
4
- "type": "git",
5
- "url": "https://github.com/ragaeeb/dyelight.git"
2
+ "bugs": {
3
+ "url": "https://github.com/ragaeeb/dyelight/issues"
4
+ },
5
+ "description": "A lightweight TypeScript React component to allow highlighting characters in textareas.",
6
+ "devDependencies": {
7
+ "@biomejs/biome": "^2.3.11",
8
+ "@storybook/addon-docs": "^10.1.11",
9
+ "@storybook/react-vite": "^10.1.11",
10
+ "@testing-library/dom": "^10.4.1",
11
+ "@testing-library/react": "^16.3.1",
12
+ "@types/bun": "^1.3.6",
13
+ "@types/react": "^19.2.8",
14
+ "@types/react-dom": "^19.2.3",
15
+ "semantic-release": "^25.0.2",
16
+ "storybook": "^10.1.11",
17
+ "tsdown": "^0.20.0-beta.3",
18
+ "vite-tsconfig-paths": "^6.0.4"
19
+ },
20
+ "engines": {
21
+ "bun": ">=1.3.6",
22
+ "node": ">=24.x"
6
23
  },
7
- "module": "./dist/index.js",
8
- "version": "1.0.3",
9
- "types": "./dist/index.d.ts",
10
24
  "exports": {
11
25
  ".": {
12
- "import": "./dist/index.js",
13
- "types": "./dist/index.d.ts"
26
+ "import": "./dist/index.mjs",
27
+ "types": "./dist/index.d.mts"
14
28
  }
15
29
  },
16
30
  "files": [
17
31
  "dist"
18
32
  ],
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
33
  "homepage": "https://github.com/ragaeeb/dyelight",
45
34
  "keywords": [
46
35
  "highlight",
@@ -51,8 +40,26 @@
51
40
  "styling"
52
41
  ],
53
42
  "license": "MIT",
43
+ "main": "./dist/index.mjs",
44
+ "module": "./dist/index.mjs",
45
+ "name": "dyelight",
46
+ "packageManager": "bun@1.3.6",
47
+ "peerDependencies": {
48
+ "react": "^19.2.3",
49
+ "react-dom": "^19.2.3"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/ragaeeb/dyelight.git"
54
+ },
54
55
  "scripts": {
55
- "build": "tsup",
56
- "test": "bun test --coverage --coverage-reporter=lcov"
57
- }
56
+ "build": "tsdown",
57
+ "format": "biome format --write .",
58
+ "lint": "biome lint .",
59
+ "storybook": "storybook dev -p 6006 --no-open",
60
+ "storybook:build": "storybook build"
61
+ },
62
+ "type": "module",
63
+ "types": "./dist/index.d.mts",
64
+ "version": "1.1.1"
58
65
  }
package/dist/index.d.ts DELETED
@@ -1,341 +0,0 @@
1
- import React from 'react';
2
-
3
- /**
4
- * @fileoverview HighlightBuilder - Utility functions for creating highlight objects
5
- *
6
- * This module provides a convenient API for building highlight configurations
7
- * for the DyeLight component. It includes methods for creating character-level
8
- * highlights, line-level highlights, pattern-based highlights, and more.
9
- */
10
-
11
- /**
12
- * Utility functions for building highlight objects for the DyeLight component
13
- * Provides a fluent API for creating various types of text highlights
14
- */
15
- declare const HighlightBuilder: {
16
- /**
17
- * Creates highlights for individual characters using absolute positions
18
- * @param chars - Array of character highlight configurations
19
- * @param chars[].index - Zero-based character index in the text
20
- * @param chars[].className - Optional CSS class name to apply
21
- * @param chars[].style - Optional inline styles to apply
22
- * @returns Array of character range highlights
23
- * @example
24
- * ```tsx
25
- * // Highlight characters at positions 5, 10, and 15
26
- * const highlights = HighlightBuilder.characters([
27
- * { index: 5, className: 'highlight-error' },
28
- * { index: 10, className: 'highlight-warning' },
29
- * { index: 15, style: { backgroundColor: 'yellow' } }
30
- * ]);
31
- * ```
32
- */
33
- characters: (chars: Array<{
34
- className?: string;
35
- index: number;
36
- style?: React.CSSProperties;
37
- }>) => {
38
- className: string | undefined;
39
- end: number;
40
- start: number;
41
- style: React.CSSProperties | undefined;
42
- }[];
43
- /**
44
- * Creates line highlights for entire lines
45
- * @param lines - Array of line highlight configurations
46
- * @param lines[].line - Zero-based line number
47
- * @param lines[].className - Optional CSS class name to apply to the line
48
- * @param lines[].color - Optional color value (CSS color, hex, rgb, etc.)
49
- * @returns Object mapping line numbers to highlight values
50
- * @example
51
- * ```tsx
52
- * // Highlight lines 0 and 2 with different styles
53
- * const lineHighlights = HighlightBuilder.lines([
54
- * { line: 0, className: 'error-line' },
55
- * { line: 2, color: '#ffff00' }
56
- * ]);
57
- * ```
58
- */
59
- lines: (lines: Array<{
60
- className?: string;
61
- color?: string;
62
- line: number;
63
- }>) => {
64
- [lineNumber: number]: string;
65
- };
66
- /**
67
- * Highlights text matching a pattern using absolute positions
68
- * @param text - The text to search within
69
- * @param pattern - Regular expression or string pattern to match
70
- * @param className - Optional CSS class name to apply to matches
71
- * @param style - Optional inline styles to apply to matches
72
- * @returns Array of character range highlights for all matches
73
- * @example
74
- * ```tsx
75
- * // Highlight all JavaScript keywords
76
- * const highlights = HighlightBuilder.pattern(
77
- * code,
78
- * /\b(function|const|let|var|if|else|for|while)\b/g,
79
- * 'keyword-highlight'
80
- * );
81
- *
82
- * // Highlight all email addresses
83
- * const emailHighlights = HighlightBuilder.pattern(
84
- * text,
85
- * /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
86
- * 'email-highlight'
87
- * );
88
- * ```
89
- */
90
- pattern: (text: string, pattern: RegExp | string, className?: string, style?: React.CSSProperties) => {
91
- className: string | undefined;
92
- end: number;
93
- start: number;
94
- style: React.CSSProperties | undefined;
95
- }[];
96
- /**
97
- * Creates character range highlights using absolute positions
98
- * @param ranges - Array of character range configurations
99
- * @param ranges[].start - Zero-based start index (inclusive)
100
- * @param ranges[].end - Zero-based end index (exclusive)
101
- * @param ranges[].className - Optional CSS class name to apply
102
- * @param ranges[].style - Optional inline styles to apply
103
- * @returns Array of character range highlights
104
- * @example
105
- * ```tsx
106
- * // Highlight specific ranges in the text
107
- * const highlights = HighlightBuilder.ranges([
108
- * { start: 0, end: 5, className: 'title-highlight' },
109
- * { start: 10, end: 20, style: { backgroundColor: 'yellow' } },
110
- * { start: 25, end: 30, className: 'error-highlight' }
111
- * ]);
112
- * ```
113
- */
114
- ranges: (ranges: Array<{
115
- className?: string;
116
- end: number;
117
- start: number;
118
- style?: React.CSSProperties;
119
- }>) => {
120
- className: string | undefined;
121
- end: number;
122
- start: number;
123
- style: React.CSSProperties | undefined;
124
- }[];
125
- /**
126
- * Highlights text between specific start and end positions
127
- * Convenience method for highlighting a single selection range
128
- * @param start - Zero-based start index (inclusive)
129
- * @param end - Zero-based end index (exclusive)
130
- * @param className - Optional CSS class name to apply
131
- * @param style - Optional inline styles to apply
132
- * @returns Array containing a single character range highlight
133
- * @example
134
- * ```tsx
135
- * // Highlight a selection from position 10 to 25
136
- * const selectionHighlight = HighlightBuilder.selection(
137
- * 10,
138
- * 25,
139
- * 'selection-highlight'
140
- * );
141
- * ```
142
- */
143
- selection: (start: number, end: number, className?: string, style?: React.CSSProperties) => {
144
- className: string | undefined;
145
- end: number;
146
- start: number;
147
- style: React.CSSProperties | undefined;
148
- }[];
149
- /**
150
- * Highlights entire words that match specific terms
151
- * @param text - The text to search within
152
- * @param words - Array of words to highlight
153
- * @param className - Optional CSS class name to apply to matched words
154
- * @param style - Optional inline styles to apply to matched words
155
- * @returns Array of character range highlights for all matched words
156
- * @example
157
- * ```tsx
158
- * // Highlight specific programming keywords
159
- * const highlights = HighlightBuilder.words(
160
- * sourceCode,
161
- * ['function', 'const', 'let', 'var', 'return'],
162
- * 'keyword'
163
- * );
164
- *
165
- * // Highlight important terms with custom styling
166
- * const termHighlights = HighlightBuilder.words(
167
- * document,
168
- * ['TODO', 'FIXME', 'NOTE'],
169
- * undefined,
170
- * { backgroundColor: 'orange', fontWeight: 'bold' }
171
- * );
172
- * ```
173
- */
174
- words: (text: string, words: string[], className?: string, style?: React.CSSProperties) => {
175
- className: string | undefined;
176
- end: number;
177
- start: number;
178
- style: React.CSSProperties | undefined;
179
- }[];
180
- };
181
-
182
- /**
183
- * @fileoverview DOM utility functions for textarea manipulation
184
- *
185
- * This module provides utility functions for working with DOM elements,
186
- * specifically focused on textarea auto-resizing functionality used by
187
- * the DyeLight component.
188
- */
189
- /**
190
- * Automatically resizes a textarea element to fit its content
191
- *
192
- * This function adjusts the textarea height to match its scroll height,
193
- * effectively removing scrollbars when the content fits and expanding
194
- * the textarea as content is added. The height is first set to 'auto'
195
- * to allow the element to shrink if content is removed.
196
- *
197
- * @param textArea - The HTML textarea element to resize
198
- * @example
199
- * ```ts
200
- * const textarea = document.querySelector('textarea');
201
- * if (textarea) {
202
- * autoResize(textarea);
203
- * }
204
- *
205
- * // Or in an event handler:
206
- * const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
207
- * autoResize(e.target);
208
- * };
209
- * ```
210
- */
211
- declare const autoResize: (textArea: HTMLTextAreaElement) => void;
212
-
213
- /**
214
- * @fileoverview Type definitions for the DyeLight component
215
- *
216
- * This module contains all TypeScript type definitions used by the DyeLight
217
- * component, including props interfaces, ref methods, and highlight configurations.
218
- * These types provide comprehensive type safety and IntelliSense support.
219
- */
220
-
221
- /**
222
- * Represents a character range to highlight in the entire text using absolute positions
223
- * @example
224
- * ```tsx
225
- * const highlight: CharacterRange = {
226
- * start: 0,
227
- * end: 5,
228
- * className: 'highlight-keyword',
229
- * style: { backgroundColor: 'yellow', fontWeight: 'bold' }
230
- * };
231
- * ```
232
- */
233
- type CharacterRange = {
234
- /** Optional CSS class name to apply to the highlighted range */
235
- className?: string;
236
- /** Zero-based end index in the entire text (exclusive) */
237
- end: number;
238
- /** Zero-based start index in the entire text (inclusive) */
239
- start: number;
240
- /** Optional inline styles to apply to the highlighted range */
241
- style?: React.CSSProperties;
242
- };
243
- /**
244
- * Props for the DyeLight component
245
- * Extends standard textarea props while replacing onChange with a simplified version
246
- * @example
247
- * ```tsx
248
- * const MyEditor = () => {
249
- * const [code, setCode] = useState('const x = 1;');
250
- *
251
- * const highlights = HighlightBuilder.pattern(
252
- * code,
253
- * /\b(const|let|var)\b/g,
254
- * 'keyword-highlight'
255
- * );
256
- *
257
- * const lineHighlights = HighlightBuilder.lines([
258
- * { line: 0, className: 'current-line' }
259
- * ]);
260
- *
261
- * return (
262
- * <DyeLight
263
- * value={code}
264
- * onChange={setCode}
265
- * highlights={highlights}
266
- * lineHighlights={lineHighlights}
267
- * enableAutoResize={true}
268
- * rows={10}
269
- * className="my-editor"
270
- * placeholder="Enter your code here..."
271
- * />
272
- * );
273
- * };
274
- * ```
275
- */
276
- interface DyeLightProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
277
- /** CSS class name for the textarea element */
278
- className?: string;
279
- /** Default value for uncontrolled usage */
280
- defaultValue?: string;
281
- /** Text direction - supports left-to-right and right-to-left */
282
- dir?: 'ltr' | 'rtl';
283
- /** Enable automatic height adjustment based on content */
284
- enableAutoResize?: boolean;
285
- /** Character range highlights using absolute positions in the entire text */
286
- highlights?: CharacterRange[];
287
- /** Line-level highlights mapped by line number (0-based) to CSS color/class */
288
- lineHighlights?: {
289
- [lineNumber: number]: string;
290
- };
291
- /** Callback fired when the textarea value changes */
292
- onChange?: (value: string) => void;
293
- /** Number of visible text lines */
294
- rows?: number;
295
- /** Controlled value */
296
- value?: string;
297
- }
298
- /**
299
- * Methods exposed by the DyeLight component through its ref
300
- * Provides programmatic access to common textarea operations
301
- * @example
302
- * ```tsx
303
- * const MyComponent = () => {
304
- * const dyeLightRef = useRef<DyeLightRef>(null);
305
- *
306
- * const handleFocus = () => {
307
- * dyeLightRef.current?.focus();
308
- * };
309
- *
310
- * const handleGetValue = () => {
311
- * const value = dyeLightRef.current?.getValue();
312
- * console.log('Current value:', value);
313
- * };
314
- *
315
- * return <DyeLight ref={dyeLightRef} />;
316
- * };
317
- * ```
318
- */
319
- type DyeLightRef = {
320
- /** Removes focus from the textarea */
321
- blur: () => void;
322
- /** Sets focus to the textarea */
323
- focus: () => void;
324
- /** Gets the current value of the textarea */
325
- getValue: () => string;
326
- /** Selects all text in the textarea */
327
- select: () => void;
328
- /** Sets the selection range in the textarea */
329
- setSelectionRange: (start: number, end: number) => void;
330
- /** Sets the value of the textarea programmatically */
331
- setValue: (value: string) => void;
332
- };
333
-
334
- /**
335
- * A textarea component with support for highlighting character ranges using absolute positions
336
- * and optional line-level highlighting. Perfect for syntax highlighting, error indication,
337
- * and text annotation without the complexity of line-based positioning.
338
- */
339
- declare const DyeLight: React.ForwardRefExoticComponent<DyeLightProps & React.RefAttributes<DyeLightRef>>;
340
-
341
- export { type CharacterRange, DyeLight, type DyeLightProps, type DyeLightRef, HighlightBuilder, autoResize };
package/dist/index.js DELETED
@@ -1,3 +0,0 @@
1
- import"react";var J={characters:e=>e.map(({className:n,index:t,style:r})=>({className:n,end:t+1,start:t,style:r})),lines:e=>{let n={};return e.forEach(({className:t,color:r,line:s})=>{n[s]=t||r||""}),n},pattern:(e,n,t,r)=>{let s=typeof n=="string"?new RegExp(n,"g"):new RegExp(n.source,"g");return Array.from(e.matchAll(s)).map(a=>({className:t,end:a.index+a[0].length,start:a.index,style:r}))},ranges:e=>e.map(({className:n,end:t,start:r,style:s})=>({className:n,end:t,start:r,style:s})),selection:(e,n,t,r)=>[{className:t,end:n,start:e,style:r}],words:(e,n,t,r)=>{let s=new RegExp(`\\b(${n.join("|")})\\b`,"g");return J.pattern(e,s,t,r)}};var N=e=>{e.style.height="auto",e.style.height=`${e.scrollHeight}px`};import{forwardRef as re,useCallback as T,useEffect as ne,useImperativeHandle as se,useRef as oe}from"react";import{useCallback as K,useState as Q}from"react";var M=e=>{let[n,t]=Q();return{handleAutoResize:K(s=>{e&&(N(s),t(s.scrollHeight))},[e]),textareaHeight:n}};import{useMemo as X}from"react";var P=e=>{let n=e.split(`
2
- `),t=[],r=0;return n.forEach((s,i)=>{t.push(r),r+=s.length+(i<n.length-1?1:0)}),{lines:n,lineStarts:t}},L=(e,n)=>{for(let t=n.length-1;t>=0;t--)if(e>=n[t])return{char:e-n[t],line:t};return{char:0,line:0}},I=e=>/^(#|rgb|hsl|var\(--.*?\)|transparent|currentColor|inherit|initial|unset)/i.test(e)||/^[a-z]+$/i.test(e);var V=(e,n,t,r)=>X(()=>{let{lines:i,lineStarts:a}=P(e),m={};return n.forEach(c=>{let g=L(c.start,a),h=L(c.end-1,a);for(let o=g.line;o<=h.line;o++){m[o]||(m[o]=[]);let l=a[o],p=Math.max(c.start-l,0),d=Math.min(c.end-l,i[o].length);d>p&&m[o].push({className:c.className,end:d,start:p,style:c.style})}}),i.map((c,g)=>{let h=t[g],o=m[g]||[];return r(c,g,o,h)})},[e,n,t,r]);import{useCallback as w,useRef as Z}from"react";var D=()=>{let e=Z(null),n=w(r=>{if(r.current&&e.current){let{scrollLeft:s,scrollTop:i}=r.current;e.current.scrollTop=i,e.current.scrollLeft=s}},[]),t=w(r=>{if(!r.current||!e.current)return;let s=getComputedStyle(r.current),i=e.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},[]);return{highlightLayerRef:e,syncScroll:n,syncStyles:t}};import{useCallback as E,useEffect as C,useRef as ee,useState as te}from"react";var z=(e,n="",t)=>{let r=ee(null),[s,i]=te(e??n),a=e!==void 0?e:s,m=E(()=>{if(r.current){let o=r.current.value;o!==a&&(e===void 0&&i(o),t?.(o))}},[a,e,t]),c=E(o=>{let l=o.target.value;e===void 0&&i(l),t?.(l)},[e,t]),g=E(o=>{let l=o.currentTarget.value;l!==a&&(e===void 0&&i(l),t?.(l))},[a,e,t]),h=E(o=>{r.current&&(r.current.value=o,e===void 0&&i(o),t?.(o))},[e,t]);return C(()=>{m()},[m]),C(()=>{e!==void 0&&r.current&&r.current.value!==e&&(r.current.value=e)},[e]),C(()=>{let o=r.current;if(!o)return;let l=new MutationObserver(()=>{m()});l.observe(o,{attributeFilter:["value"],attributes:!0});let p=setInterval(()=>{o.value!==a&&m()},100);return()=>{l.disconnect(),clearInterval(p)}},[a,m]),{currentValue:a,handleChange:c,handleInput:g,setValue:h,textareaRef:r}};import"react";var _={display:"block",position:"relative",width:"100%"},F={background:"transparent",boxSizing:"border-box",caretColor:"#000000",color:"transparent",minHeight:"auto",position:"relative",width:"100%",zIndex:2},k={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 b,jsxs as ce}from"react/jsx-runtime";var Y=(e,n,t)=>{if(!t)return b("div",{children:e},n);let r=I(t);return b("div",{className:r?void 0:t,style:r?{backgroundColor:t}:void 0,children:e},n)},ie=(e,n,t,r)=>{if(t.length===0)return Y(e||"\xA0",n,r);let s=t.toSorted((c,g)=>c.start-g.start),i=[],a=0;if(s.forEach((c,g)=>{let{className:h,end:o,start:l,style:p}=c,d=Math.max(0,Math.min(l,e.length)),S=Math.max(d,Math.min(o,e.length));if(d>a){let R=e.slice(a,d);R&&i.push(R)}if(S>d){let R=e.slice(d,S);i.push(b("span",{className:h,style:p,children:R},`highlight-${n}-${g}`))}a=Math.max(a,S)}),a<e.length){let c=e.slice(a);c&&i.push(c)}let m=i.length===0?"\xA0":i;return Y(m,n,r)},ae=re(({className:e="",defaultValue:n="",dir:t="ltr",enableAutoResize:r=!0,highlights:s=[],lineHighlights:i={},onChange:a,rows:m=4,style:c,value:g,...h},o)=>{let l=oe(null),{currentValue:p,handleChange:d,handleInput:S,setValue:R,textareaRef:u}=z(g,n,a),{handleAutoResize:y,textareaHeight:x}=M(r),{highlightLayerRef:O,syncScroll:A,syncStyles:v}=D(),$=V(p,s,i,ie),U=T(f=>{d(f),y(f.target)},[d,y]),B=T(f=>{S(f),y(f.currentTarget)},[S,y]),H=T(f=>{R(f),u.current&&y(u.current)},[R,y,u]),W=T(()=>{A(u)},[A,u]);se(o,()=>({blur:()=>u.current?.blur(),focus:()=>u.current?.focus(),getValue:()=>p,select:()=>u.current?.select(),setSelectionRange:(f,q)=>u.current?.setSelectionRange(f,q),setValue:H}),[p,H]),ne(()=>{u.current&&r&&y(u.current),v(u)},[p,y,r,v,u]);let G={...F,height:x?`${x}px`:void 0,resize:r?"none":"vertical"},j={...k,direction:t,height:x?`${x}px`:void 0,padding:u.current?getComputedStyle(u.current).padding:"8px 12px"};return ce("div",{ref:l,style:{..._,...c},children:[b("div",{"aria-hidden":"true",ref:O,style:j,children:$}),b("textarea",{className:e,dir:t,onChange:U,onInput:B,onScroll:W,ref:u,rows:m,style:G,value:p,...h})]})});ae.displayName="DyeLight";import"react";export{ae as DyeLight,J as HighlightBuilder,N as autoResize};
3
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/builder.ts","../src/domUtils.ts","../src/DyeLight.tsx","../src/hooks/useAutoResize.ts","../src/hooks/useHighlightedContent.ts","../src/textUtils.ts","../src/hooks/useHighlightSync.ts","../src/hooks/useTextareaValue.ts","../src/styles.ts","../src/types.ts"],"sourcesContent":["/**\n * @fileoverview HighlightBuilder - Utility functions for creating highlight objects\n *\n * This module provides a convenient API for building highlight configurations\n * for the DyeLight component. It includes methods for creating character-level\n * highlights, line-level highlights, pattern-based highlights, and more.\n */\n\nimport React from 'react';\n\n/**\n * Utility functions for building highlight objects for the DyeLight component\n * Provides a fluent API for creating various types of text highlights\n */\nexport const HighlightBuilder = {\n /**\n * Creates highlights for individual characters using absolute positions\n * @param chars - Array of character highlight configurations\n * @param chars[].index - Zero-based character index in the text\n * @param chars[].className - Optional CSS class name to apply\n * @param chars[].style - Optional inline styles to apply\n * @returns Array of character range highlights\n * @example\n * ```tsx\n * // Highlight characters at positions 5, 10, and 15\n * const highlights = HighlightBuilder.characters([\n * { index: 5, className: 'highlight-error' },\n * { index: 10, className: 'highlight-warning' },\n * { index: 15, style: { backgroundColor: 'yellow' } }\n * ]);\n * ```\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 for entire lines\n * @param lines - Array of line highlight configurations\n * @param lines[].line - Zero-based line number\n * @param lines[].className - Optional CSS class name to apply to the line\n * @param lines[].color - Optional color value (CSS color, hex, rgb, etc.)\n * @returns Object mapping line numbers to highlight values\n * @example\n * ```tsx\n * // Highlight lines 0 and 2 with different styles\n * const lineHighlights = HighlightBuilder.lines([\n * { line: 0, className: 'error-line' },\n * { line: 2, color: '#ffff00' }\n * ]);\n * ```\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 * @param text - The text to search within\n * @param pattern - Regular expression or string pattern to match\n * @param className - Optional CSS class name to apply to matches\n * @param style - Optional inline styles to apply to matches\n * @returns Array of character range highlights for all matches\n * @example\n * ```tsx\n * // Highlight all JavaScript keywords\n * const highlights = HighlightBuilder.pattern(\n * code,\n * /\\b(function|const|let|var|if|else|for|while)\\b/g,\n * 'keyword-highlight'\n * );\n *\n * // Highlight all email addresses\n * const emailHighlights = HighlightBuilder.pattern(\n * text,\n * /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b/g,\n * 'email-highlight'\n * );\n * ```\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 * @param ranges - Array of character range configurations\n * @param ranges[].start - Zero-based start index (inclusive)\n * @param ranges[].end - Zero-based end index (exclusive)\n * @param ranges[].className - Optional CSS class name to apply\n * @param ranges[].style - Optional inline styles to apply\n * @returns Array of character range highlights\n * @example\n * ```tsx\n * // Highlight specific ranges in the text\n * const highlights = HighlightBuilder.ranges([\n * { start: 0, end: 5, className: 'title-highlight' },\n * { start: 10, end: 20, style: { backgroundColor: 'yellow' } },\n * { start: 25, end: 30, className: 'error-highlight' }\n * ]);\n * ```\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 * Convenience method for highlighting a single selection range\n * @param start - Zero-based start index (inclusive)\n * @param end - Zero-based end index (exclusive)\n * @param className - Optional CSS class name to apply\n * @param style - Optional inline styles to apply\n * @returns Array containing a single character range highlight\n * @example\n * ```tsx\n * // Highlight a selection from position 10 to 25\n * const selectionHighlight = HighlightBuilder.selection(\n * 10,\n * 25,\n * 'selection-highlight'\n * );\n * ```\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 specific terms\n * @param text - The text to search within\n * @param words - Array of words to highlight\n * @param className - Optional CSS class name to apply to matched words\n * @param style - Optional inline styles to apply to matched words\n * @returns Array of character range highlights for all matched words\n * @example\n * ```tsx\n * // Highlight specific programming keywords\n * const highlights = HighlightBuilder.words(\n * sourceCode,\n * ['function', 'const', 'let', 'var', 'return'],\n * 'keyword'\n * );\n *\n * // Highlight important terms with custom styling\n * const termHighlights = HighlightBuilder.words(\n * document,\n * ['TODO', 'FIXME', 'NOTE'],\n * undefined,\n * { backgroundColor: 'orange', fontWeight: 'bold' }\n * );\n * ```\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","/**\n * @fileoverview DOM utility functions for textarea manipulation\n *\n * This module provides utility functions for working with DOM elements,\n * specifically focused on textarea auto-resizing functionality used by\n * the DyeLight component.\n */\n\n/**\n * Automatically resizes a textarea element to fit its content\n *\n * This function adjusts the textarea height to match its scroll height,\n * effectively removing scrollbars when the content fits and expanding\n * the textarea as content is added. The height is first set to 'auto'\n * to allow the element to shrink if content is removed.\n *\n * @param textArea - The HTML textarea element to resize\n * @example\n * ```ts\n * const textarea = document.querySelector('textarea');\n * if (textarea) {\n * autoResize(textarea);\n * }\n *\n * // Or in an event handler:\n * const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n * autoResize(e.target);\n * };\n * ```\n */\nexport const autoResize = (textArea: HTMLTextAreaElement) => {\n // Reset height to auto to allow shrinking\n textArea.style.height = 'auto';\n // Set height to scroll height to fit content\n textArea.style.height = `${textArea.scrollHeight}px`;\n};\n","import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';\n\nimport type { DyeLightProps, DyeLightRef } from './types';\n\nimport { useAutoResize } from './hooks/useAutoResize';\nimport { useHighlightedContent } from './hooks/useHighlightedContent';\nimport { useHighlightSync } from './hooks/useHighlightSync';\nimport { useTextareaValue } from './hooks/useTextareaValue';\nimport { DEFAULT_BASE_STYLE, DEFAULT_CONTAINER_STYLE, DEFAULT_HIGHLIGHT_LAYER_STYLE } from './styles';\nimport { isColorValue } from './textUtils';\n\n/**\n * @fileoverview DyeLight - A React textarea component with advanced text highlighting capabilities\n *\n * This component provides a textarea with overlay-based text highlighting that supports:\n * - Character-level highlighting using absolute text positions\n * - Line-level highlighting with CSS classes or color values\n * - Automatic height adjustment based on content\n * - Synchronized scrolling between textarea and highlight layer\n * - Both controlled and uncontrolled usage patterns\n * - RTL text direction support\n */\n\n/**\n * Creates a line element with optional highlighting\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\n/**\n * Renders a single line with character-level highlights and optional line-level highlighting\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 */\nexport const 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 containerRef = useRef<HTMLDivElement>(null);\n\n // Custom hooks for managing component logic\n const { currentValue, handleChange, handleInput, setValue, textareaRef } = useTextareaValue(\n value,\n defaultValue,\n onChange,\n );\n\n const { handleAutoResize, textareaHeight } = useAutoResize(enableAutoResize);\n\n const { highlightLayerRef, syncScroll, syncStyles } = useHighlightSync();\n\n const highlightedContent = useHighlightedContent(\n currentValue,\n highlights,\n lineHighlights,\n renderHighlightedLine,\n );\n\n // Enhanced change handler that includes auto-resize\n const handleChangeWithResize = useCallback(\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n handleChange(e);\n handleAutoResize(e.target);\n },\n [handleChange, handleAutoResize],\n );\n\n // Enhanced input handler that includes auto-resize\n const handleInputWithResize = useCallback(\n (e: React.FormEvent<HTMLTextAreaElement>) => {\n handleInput(e);\n handleAutoResize(e.currentTarget);\n },\n [handleInput, handleAutoResize],\n );\n\n // Enhanced setValue that includes auto-resize\n const setValueWithResize = useCallback(\n (newValue: string) => {\n setValue(newValue);\n if (textareaRef.current) {\n handleAutoResize(textareaRef.current);\n }\n },\n [setValue, handleAutoResize, textareaRef],\n );\n\n // Scroll handler with ref binding\n const handleScroll = useCallback(() => {\n syncScroll(textareaRef);\n }, [syncScroll, textareaRef]);\n\n // Expose ref methods\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: setValueWithResize,\n }),\n [currentValue, setValueWithResize],\n );\n\n // Sync styles and handle auto-resize on value changes\n useEffect(() => {\n if (textareaRef.current && enableAutoResize) {\n handleAutoResize(textareaRef.current);\n }\n syncStyles(textareaRef);\n }, [currentValue, handleAutoResize, enableAutoResize, syncStyles, textareaRef]);\n\n // Compute styles\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={handleChangeWithResize}\n onInput={handleInputWithResize}\n onScroll={handleScroll}\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","import { useCallback, useState } from 'react';\n\nimport { autoResize } from '@/domUtils';\n\n/**\n * Hook for managing textarea auto-resize functionality\n */\nexport const useAutoResize = (enableAutoResize: boolean) => {\n const [textareaHeight, setTextareaHeight] = useState<number | undefined>();\n\n /**\n * Handles automatic resizing of the textarea based on content\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 return {\n handleAutoResize,\n textareaHeight,\n };\n};\n","import { useMemo } from 'react';\n\nimport type { CharacterRange } from '@/types';\n\nimport { absoluteToLinePos, getLinePositions } from '@/textUtils';\n\n/**\n * Hook for computing highlighted content from text and highlight ranges\n */\nexport const useHighlightedContent = (\n text: string,\n highlights: CharacterRange[],\n lineHighlights: { [lineNumber: number]: string },\n 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) => {\n /**\n * Computes the highlighted content by processing text and highlight ranges\n * Groups highlights by line and renders each line with appropriate highlighting\n */\n const highlightedContent = useMemo(() => {\n const { lines, lineStarts } = getLinePositions(text);\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 }, [text, highlights, lineHighlights, renderHighlightedLine]);\n\n return highlightedContent;\n};\n","/**\n * @fileoverview Text utility functions for position calculations and text processing\n *\n * This module provides utilities for converting between absolute text positions\n * and line-relative positions, as well as other text processing functions used\n * by the DyeLight component for highlight positioning.\n */\n\n/**\n * Analyzes text content and returns line information with position mappings\n * @param text - The text content to analyze\n * @returns Object containing lines array and line start positions\n * @returns returns.lines - Array of individual lines (without newline characters)\n * @returns returns.lineStarts - Array of absolute positions where each line starts\n * @example\n * ```ts\n * const text = \"Hello\\nWorld\\nTest\";\n * const { lines, lineStarts } = getLinePositions(text);\n * // lines: [\"Hello\", \"World\", \"Test\"]\n * // lineStarts: [0, 6, 12]\n * ```\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 an absolute text position to line-relative coordinates\n * @param absolutePos - Zero-based absolute position in the entire text\n * @param lineStarts - Array of line start positions (from getLinePositions)\n * @returns Object containing line number and character position within that line\n * @returns returns.line - Zero-based line number\n * @returns returns.char - Zero-based character position within the line\n * @example\n * ```ts\n * const text = \"Hello\\nWorld\\nTest\";\n * const { lineStarts } = getLinePositions(text);\n * const pos = absoluteToLinePos(8, lineStarts);\n * // pos: { line: 1, char: 2 } (the 'r' in \"World\")\n * ```\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\n/**\n * Determines if a string value represents a CSS color value\n * @param value - The string value to test\n * @returns True if the value appears to be a color value, false otherwise\n * @example\n * ```ts\n * isColorValue('#ff0000'); // true\n * isColorValue('rgb(255, 0, 0)'); // true\n * isColorValue('red'); // true\n * isColorValue('my-css-class'); // false (contains hyphens)\n * isColorValue('transparent'); // true\n * isColorValue('var(--primary-color)'); // true\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","import { useCallback, useRef } from 'react';\n\n/**\n * Hook for managing highlight layer synchronization\n */\nexport const useHighlightSync = () => {\n const highlightLayerRef = useRef<HTMLDivElement>(null);\n\n /**\n * Synchronizes scroll position between textarea and highlight layer\n */\n const syncScroll = useCallback((textareaRef: React.RefObject<HTMLTextAreaElement | null>) => {\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 /**\n * Synchronizes computed styles from textarea to highlight layer\n */\n const syncStyles = useCallback((textareaRef: React.RefObject<HTMLTextAreaElement | null>) => {\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 return {\n highlightLayerRef,\n syncScroll,\n syncStyles,\n };\n};\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Hook for managing textarea value state and synchronization\n * Handles both controlled and uncontrolled modes, plus programmatic changes\n */\nexport const useTextareaValue = (value?: string, defaultValue = '', onChange?: (value: string) => void) => {\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const [internalValue, setInternalValue] = useState(value ?? defaultValue);\n\n const currentValue = value !== undefined ? value : internalValue;\n\n /**\n * Syncs the current value with the actual DOM value\n * This is crucial for handling programmatic value changes\n */\n const syncValueWithDOM = useCallback(() => {\n if (textareaRef.current) {\n const domValue = textareaRef.current.value;\n if (domValue !== currentValue) {\n if (value === undefined) {\n // Uncontrolled mode - update internal state\n setInternalValue(domValue);\n }\n // Notify parent of the change\n onChange?.(domValue);\n }\n }\n }, [currentValue, value, onChange]);\n\n /**\n * Handles textarea value changes\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 },\n [value, onChange],\n );\n\n /**\n * Handles input events to catch programmatic changes\n */\n const handleInput = useCallback(\n (e: React.FormEvent<HTMLTextAreaElement>) => {\n const newValue = e.currentTarget.value;\n\n // Check if this is a programmatic change (value differs from our state)\n if (newValue !== currentValue) {\n if (value === undefined) {\n setInternalValue(newValue);\n }\n onChange?.(newValue);\n }\n },\n [currentValue, value, onChange],\n );\n\n /**\n * Programmatically sets the textarea value\n */\n const setValue = useCallback(\n (newValue: string) => {\n if (textareaRef.current) {\n textareaRef.current.value = newValue;\n\n // Update internal state\n if (value === undefined) {\n setInternalValue(newValue);\n }\n\n // Notify parent\n onChange?.(newValue);\n }\n },\n [value, onChange],\n );\n\n // Sync with DOM value on mount and when external changes occur\n useEffect(() => {\n syncValueWithDOM();\n }, [syncValueWithDOM]);\n\n // Handle controlled value changes\n useEffect(() => {\n if (value !== undefined && textareaRef.current && textareaRef.current.value !== value) {\n textareaRef.current.value = value;\n }\n }, [value]);\n\n // Use a MutationObserver to detect programmatic value changes\n useEffect(() => {\n const textarea = textareaRef.current;\n if (!textarea) return;\n\n const observer = new MutationObserver(() => {\n syncValueWithDOM();\n });\n\n // Watch for attribute changes (like value attribute)\n observer.observe(textarea, {\n attributeFilter: ['value'],\n attributes: true,\n });\n\n // Also use a polling mechanism as a fallback\n const intervalId = setInterval(() => {\n if (textarea.value !== currentValue) {\n syncValueWithDOM();\n }\n }, 100);\n\n return () => {\n observer.disconnect();\n clearInterval(intervalId);\n };\n }, [currentValue, syncValueWithDOM]);\n\n return {\n currentValue,\n handleChange,\n handleInput,\n setValue,\n textareaRef,\n };\n};\n","/**\n * @fileoverview Default styles for DyeLight component elements\n *\n * This module contains the default CSS-in-JS styles used by the DyeLight component\n * for its container, textarea, and highlight layer elements. These styles ensure\n * proper layering, positioning, and visual alignment between the textarea and\n * its highlight overlay.\n */\n\nimport React from 'react';\n\n/**\n * Default styles for the main container element that wraps the entire component\n * Creates a positioned container that serves as the reference for absolutely positioned children\n */\nexport const DEFAULT_CONTAINER_STYLE: React.CSSProperties = {\n /** Ensures the container behaves as a block-level element */\n display: 'block',\n /** Enables absolute positioning for child elements */\n position: 'relative',\n /** Takes full width of parent container */\n width: '100%',\n};\n\n/**\n * Default styles for the textarea element\n * Makes the textarea transparent while maintaining its functionality and positioning\n */\nexport const DEFAULT_BASE_STYLE: React.CSSProperties = {\n /** Transparent background to show highlights underneath */\n background: 'transparent',\n /** Ensures consistent box model calculations */\n boxSizing: 'border-box',\n /** Maintains visible text cursor */\n caretColor: '#000000',\n /** Transparent text to reveal highlights while keeping cursor and selection */\n color: 'transparent',\n /** Prevents unwanted height constraints */\n minHeight: 'auto',\n /** Positioned above the highlight layer */\n position: 'relative',\n /** Takes full width of container */\n width: '100%',\n /** Ensures textarea appears above highlight layer */\n zIndex: 2,\n};\n\n/**\n * Default styles for the highlight layer that renders behind the textarea\n * Positioned absolutely to overlay perfectly with the textarea content\n */\nexport const DEFAULT_HIGHLIGHT_LAYER_STYLE: React.CSSProperties = {\n /** Transparent border to match textarea default border */\n border: 'transparent solid 1px',\n /** Stretch to fill container bottom */\n bottom: 0,\n /** Consistent box model with textarea */\n boxSizing: 'border-box',\n /** Inherit text color from parent */\n color: 'inherit',\n /** Match textarea font family */\n fontFamily: 'inherit',\n /** Match textarea font size */\n fontSize: 'inherit',\n /** Stretch to fill container left */\n left: 0,\n /** Match textarea line height */\n lineHeight: 'inherit',\n /** Remove default margins */\n margin: 0,\n /** Hide scrollbars on highlight layer */\n overflow: 'hidden',\n /** Prevent highlight layer from capturing mouse events */\n pointerEvents: 'none',\n /** Positioned absolutely within container */\n position: 'absolute',\n /** Stretch to fill container right */\n right: 0,\n /** Stretch to fill container top */\n top: 0,\n /** Preserve whitespace and line breaks like textarea */\n whiteSpace: 'pre-wrap',\n /** Break long words like textarea */\n wordWrap: 'break-word',\n /** Ensure highlight layer appears behind textarea */\n zIndex: 1,\n};\n","/**\n * @fileoverview Type definitions for the DyeLight component\n *\n * This module contains all TypeScript type definitions used by the DyeLight\n * component, including props interfaces, ref methods, and highlight configurations.\n * These types provide comprehensive type safety and IntelliSense support.\n */\n\nimport React from 'react';\n\n/**\n * Represents a character range to highlight in the entire text using absolute positions\n * @example\n * ```tsx\n * const highlight: CharacterRange = {\n * start: 0,\n * end: 5,\n * className: 'highlight-keyword',\n * style: { backgroundColor: 'yellow', fontWeight: 'bold' }\n * };\n * ```\n */\nexport type CharacterRange = {\n /** Optional CSS class name to apply to the highlighted range */\n className?: string;\n /** Zero-based end index in the entire text (exclusive) */\n end: number;\n /** Zero-based start index in the entire text (inclusive) */\n start: number;\n /** Optional inline styles to apply to the highlighted range */\n style?: React.CSSProperties;\n};\n\n/**\n * Props for the DyeLight component\n * Extends standard textarea props while replacing onChange with a simplified version\n * @example\n * ```tsx\n * const MyEditor = () => {\n * const [code, setCode] = useState('const x = 1;');\n *\n * const highlights = HighlightBuilder.pattern(\n * code,\n * /\\b(const|let|var)\\b/g,\n * 'keyword-highlight'\n * );\n *\n * const lineHighlights = HighlightBuilder.lines([\n * { line: 0, className: 'current-line' }\n * ]);\n *\n * return (\n * <DyeLight\n * value={code}\n * onChange={setCode}\n * highlights={highlights}\n * lineHighlights={lineHighlights}\n * enableAutoResize={true}\n * rows={10}\n * className=\"my-editor\"\n * placeholder=\"Enter your code here...\"\n * />\n * );\n * };\n * ```\n */\nexport interface DyeLightProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {\n /** CSS class name for the textarea element */\n className?: string;\n /** Default value for uncontrolled usage */\n defaultValue?: string;\n /** Text direction - supports left-to-right and right-to-left */\n dir?: 'ltr' | 'rtl';\n /** Enable automatic height adjustment based on content */\n enableAutoResize?: boolean;\n /** Character range highlights using absolute positions in the entire text */\n highlights?: CharacterRange[];\n /** Line-level highlights mapped by line number (0-based) to CSS color/class */\n lineHighlights?: { [lineNumber: number]: string };\n /** Callback fired when the textarea value changes */\n onChange?: (value: string) => void;\n /** Number of visible text lines */\n rows?: number;\n /** Controlled value */\n value?: string;\n}\n\n/**\n * Methods exposed by the DyeLight component through its ref\n * Provides programmatic access to common textarea operations\n * @example\n * ```tsx\n * const MyComponent = () => {\n * const dyeLightRef = useRef<DyeLightRef>(null);\n *\n * const handleFocus = () => {\n * dyeLightRef.current?.focus();\n * };\n *\n * const handleGetValue = () => {\n * const value = dyeLightRef.current?.getValue();\n * console.log('Current value:', value);\n * };\n *\n * return <DyeLight ref={dyeLightRef} />;\n * };\n * ```\n */\nexport type DyeLightRef = {\n /** Removes focus from the textarea */\n blur: () => void;\n /** Sets focus to the textarea */\n focus: () => void;\n /** Gets the current value of the textarea */\n getValue: () => string;\n /** Selects all text in the textarea */\n select: () => void;\n /** Sets the selection range in the textarea */\n setSelectionRange: (start: number, end: number) => void;\n /** Sets the value of the textarea programmatically */\n setValue: (value: string) => void;\n};\n"],"mappings":"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","names":["HighlightBuilder","chars","className","index","style","lines","result","color","line","text","pattern","regex","match","ranges","end","start","words","autoResize","textArea","forwardRef","useCallback","useEffect","useImperativeHandle","useRef","useCallback","useState","useAutoResize","enableAutoResize","textareaHeight","setTextareaHeight","useState","useCallback","element","autoResize","useMemo","getLinePositions","text","lines","lineStarts","position","line","index","absoluteToLinePos","absolutePos","i","isColorValue","value","useHighlightedContent","text","highlights","lineHighlights","renderHighlightedLine","useMemo","lines","lineStarts","getLinePositions","highlightsByLine","highlight","startPos","absoluteToLinePos","endPos","lineIndex","lineStart","rangeStart","rangeEnd","line","lineHighlight","ranges","useCallback","useRef","useHighlightSync","highlightLayerRef","syncScroll","textareaRef","scrollLeft","scrollTop","syncStyles","computedStyle","highlightLayer","useCallback","useEffect","useRef","useState","useTextareaValue","value","defaultValue","onChange","textareaRef","internalValue","setInternalValue","currentValue","syncValueWithDOM","domValue","handleChange","e","newValue","handleInput","setValue","textarea","observer","intervalId","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","containerRef","useRef","currentValue","handleChange","handleInput","setValue","textareaRef","useTextareaValue","handleAutoResize","textareaHeight","useAutoResize","highlightLayerRef","syncScroll","syncStyles","useHighlightSync","highlightedContent","useHighlightedContent","handleChangeWithResize","useCallback","e","handleInputWithResize","setValueWithResize","newValue","handleScroll","useImperativeHandle","useEffect","baseTextareaStyle","DEFAULT_BASE_STYLE","highlightLayerStyle","DEFAULT_HIGHLIGHT_LAYER_STYLE","DEFAULT_CONTAINER_STYLE"]}