@intlayer/design-system 8.4.5 → 8.4.7

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.
Files changed (44) hide show
  1. package/dist/esm/components/ContentEditor/ContentEditorTextArea.mjs +1 -1
  2. package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs +2 -2
  3. package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/DictionaryCreationForm.mjs +1 -1
  4. package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs +3 -3
  5. package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs +1 -1
  6. package/dist/esm/components/DictionaryFieldEditor/NavigationView/NavigationViewNode.mjs +1 -1
  7. package/dist/esm/components/DictionaryFieldEditor/SaveForm/SaveForm.mjs +2 -2
  8. package/dist/esm/components/DictionaryFieldEditor/StructureView/StructureView.mjs +1 -1
  9. package/dist/esm/components/Form/elements/OTPElement.mjs +1 -1
  10. package/dist/esm/components/IDE/CodeBlockShiki.mjs +12 -0
  11. package/dist/esm/components/IDE/CodeBlockShiki.mjs.map +1 -1
  12. package/dist/esm/components/LocaleSwitcherContentDropDown/LocaleSwitcherContent.mjs +1 -1
  13. package/dist/esm/components/MarkDownRender/MarkDownRender.mjs +23 -19
  14. package/dist/esm/components/MarkDownRender/MarkDownRender.mjs.map +1 -1
  15. package/dist/esm/components/Modal/Modal.mjs +2 -2
  16. package/dist/esm/components/Navbar/MobileNavbar.mjs +1 -1
  17. package/dist/esm/components/Pagination/Pagination.mjs +1 -1
  18. package/dist/esm/components/RightDrawer/RightDrawer.mjs +3 -3
  19. package/dist/esm/components/TextArea/AutocompleteTextArea.mjs +60 -225
  20. package/dist/esm/components/TextArea/AutocompleteTextArea.mjs.map +1 -1
  21. package/dist/esm/components/TextArea/ContentEditableTextArea.mjs +444 -0
  22. package/dist/esm/components/TextArea/ContentEditableTextArea.mjs.map +1 -0
  23. package/dist/esm/components/TextArea/index.mjs +3 -2
  24. package/dist/esm/components/index.mjs +3 -2
  25. package/dist/esm/hooks/index.mjs +9 -9
  26. package/dist/types/components/Badge/index.d.ts +1 -1
  27. package/dist/types/components/Button/Button.d.ts +3 -3
  28. package/dist/types/components/CollapsibleTable/CollapsibleTable.d.ts +1 -1
  29. package/dist/types/components/Container/index.d.ts +6 -6
  30. package/dist/types/components/IDE/CodeBlockShiki.d.ts.map +1 -1
  31. package/dist/types/components/Input/Checkbox.d.ts +1 -1
  32. package/dist/types/components/Link/Link.d.ts +3 -3
  33. package/dist/types/components/MarkDownRender/MarkDownRender.d.ts.map +1 -1
  34. package/dist/types/components/Pagination/Pagination.d.ts +1 -1
  35. package/dist/types/components/SwitchSelector/index.d.ts +1 -1
  36. package/dist/types/components/TabSelector/TabSelector.d.ts +1 -1
  37. package/dist/types/components/Tag/index.d.ts +2 -2
  38. package/dist/types/components/TextArea/AutocompleteTextArea.d.ts +14 -120
  39. package/dist/types/components/TextArea/AutocompleteTextArea.d.ts.map +1 -1
  40. package/dist/types/components/TextArea/ContentEditableTextArea.d.ts +65 -0
  41. package/dist/types/components/TextArea/ContentEditableTextArea.d.ts.map +1 -0
  42. package/dist/types/components/TextArea/index.d.ts +3 -2
  43. package/dist/types/components/index.d.ts +3 -2
  44. package/package.json +17 -17
@@ -1,258 +1,93 @@
1
1
  'use client';
2
2
 
3
- import { useAutocomplete } from "../../hooks/reactQuery.mjs";
4
- import { AutoSizedTextArea } from "./AutoSizeTextArea.mjs";
3
+ import { ContentEditableTextArea } from "./ContentEditableTextArea.mjs";
5
4
  import { useEffect, useRef, useState } from "react";
6
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
- import { useConfiguration } from "@intlayer/editor-react";
5
+ import { jsx } from "react/jsx-runtime";
8
6
 
9
7
  //#region src/components/TextArea/AutocompleteTextArea.tsx
10
8
  /**
11
- * Custom hook for debouncing values to prevent excessive API calls.
12
- *
13
- * Delays updating the returned value until the input value has stopped changing
14
- * for the specified delay period.
15
- *
16
- * @param value - The value to debounce
17
- * @param delay - Delay in milliseconds before updating the debounced value
18
- * @returns The debounced value that only updates after the delay period
19
- *
20
- * @example
21
- * ```tsx
22
- * const [searchTerm, setSearchTerm] = useState('');
23
- * const debouncedSearchTerm = useDebounce(searchTerm, 300);
24
- *
25
- * useEffect(() => {
26
- * if (debouncedSearchTerm) {
27
- * performSearch(debouncedSearchTerm);
28
- * }
29
- * }, [debouncedSearchTerm]);
30
- * ```
31
- */
32
- const useDebounce = (value, delay) => {
33
- const [debouncedValue, setDebouncedValue] = useState(value);
34
- useEffect(() => {
35
- const timer = setTimeout(() => {
36
- setDebouncedValue(value);
37
- }, delay);
38
- return () => clearTimeout(timer);
39
- }, [value, delay]);
40
- return debouncedValue;
41
- };
42
- /**
43
9
  * AutoCompleteTextarea Component
44
10
  *
45
- * An intelligent textarea that provides AI-powered autocomplete suggestions as users type,
46
- * combining auto-sizing functionality with contextual text completion.
47
- *
48
- * ## Features
49
- * - **AI-Powered Suggestions**: Context-aware autocomplete using configured AI models
50
- * - **Debounced API Calls**: Efficient suggestion fetching with 200ms debounce
51
- * - **Visual Suggestions**: Inline preview of suggested completions
52
- * - **Keyboard Navigation**: Tab key to accept suggestions
53
- * - **Context Analysis**: Uses surrounding text for better suggestions
54
- * - **Auto-Sizing**: Inherits all AutoSizedTextArea capabilities
55
- * - **Performance Optimized**: Smart caching and minimal re-renders
56
- *
57
- * ## Technical Implementation
58
- * - **Debounce Strategy**: 200ms delay before fetching suggestions
59
- * - **Context Window**: 5 lines before/after cursor for context
60
- * - **Minimum Trigger**: Requires 3+ characters before suggesting
61
- * - **Position Tracking**: Ghost layer for accurate suggestion positioning
62
- * - **Cursor Management**: Tracks cursor position during suggestion fetch
11
+ * A textarea with inline autocomplete ghost text, built on a contentEditable div
12
+ * instead of a native `<textarea>`. Ghost text (suggestions) is rendered inline
13
+ * at the cursor position and can be accepted with the Tab key.
63
14
  *
64
- * ## AI Integration
65
- * - Uses configured AI model (OpenAI, Anthropic, etc.)
66
- * - Sends context-aware prompts for relevant suggestions
67
- * - Respects temperature and model settings from configuration
68
- * - Handles API errors gracefully without interrupting user flow
69
- *
70
- * ## Use Cases
71
- * - **Content Creation**: Blog posts, articles, documentation
72
- * - **Code Comments**: Intelligent code documentation assistance
73
- * - **Email Composition**: Professional email writing assistance
74
- * - **Creative Writing**: Story and narrative completion
75
- * - **Technical Documentation**: API docs, README files
76
- * - **Social Media**: Post creation with engagement optimization
15
+ * The component wraps `ContentEditableTextArea` and manages suggestion state.
16
+ * When `suggestion` prop is provided it is shown as ghost text at the end of the
17
+ * current text. When `isActive` is false, ghost text is hidden.
77
18
  *
78
19
  * @example
79
20
  * ```tsx
80
- * // Blog writing assistant
81
- * const [blogPost, setBlogPost] = useState('');
82
- * const [isAiEnabled, setIsAiEnabled] = useState(true);
83
- *
84
- * <div className="space-y-4">
85
- * <div className="flex items-center gap-2">
86
- * <Switch
87
- * checked={isAiEnabled}
88
- * onChange={setIsAiEnabled}
89
- * />
90
- * <label>AI Writing Assistant</label>
91
- * </div>
92
- *
93
- * <AutoCompleteTextarea
94
- * value={blogPost}
95
- * onChange={(e) => setBlogPost(e.target.value)}
96
- * placeholder="Start writing your blog post..."
97
- * isActive={isAiEnabled}
98
- * autoSize={true}
99
- * maxRows={15}
100
- * className="min-h-[200px] font-serif text-lg leading-relaxed"
101
- * />
102
- * </div>
103
- *
104
- * // Code documentation assistant
105
- * <AutoCompleteTextarea
106
- * value={docComment}
107
- * onChange={handleDocChange}
108
- * placeholder="/** Describe this function... *\/"
109
- * isActive={true}
110
- * autoSize={true}
111
- * maxRows={8}
112
- * className="font-mono text-sm"
113
- * />
114
- *
115
- * // Email composition with templates
116
21
  * <AutoCompleteTextarea
117
- * defaultValue="Dear "
118
- * placeholder="AI will help complete your email..."
22
+ * value={content}
23
+ * onChange={handleChange}
24
+ * suggestion="suggested completion..."
119
25
  * isActive={true}
120
26
  * autoSize={true}
121
- * maxRows={12}
122
- * variant={InputVariant.DEFAULT}
123
27
  * />
124
28
  * ```
125
- *
126
- * ## Accessibility
127
- * - Ghost layer is properly hidden from screen readers
128
- * - Maintains focus management during suggestion acceptance
129
- * - Preserves keyboard navigation patterns
130
- * - Respects reduced motion preferences
131
29
  */
132
30
  const AutoCompleteTextarea = ({ isActive = true, suggestion: suggestionProp, ...props }) => {
133
- const defaultValue = String(props.value ?? props.defaultValue ?? "");
134
- const { mutate: autocomplete } = useAutocomplete();
135
- const configuration = useConfiguration();
136
- const [isTyped, setIsTyped] = useState(false);
137
- const [text, setText] = useState(defaultValue);
31
+ const [text, setText] = useState(String(props.value ?? props.defaultValue ?? ""));
138
32
  const [suggestion, setSuggestion] = useState("");
139
- const textareaRef = useRef(null);
140
- const placeholderRef = useRef(null);
141
- const ghostLayerRef = useRef(null);
142
- const [suggestionPosition, setSuggestionPosition] = useState(null);
143
- const [cursorAtFetch, setCursorAtFetch] = useState(-1);
144
- const debouncedText = useDebounce(text, 200);
33
+ const editorRef = useRef(null);
145
34
  useEffect(() => {
146
35
  if (typeof props.value === "undefined") return;
147
- setText(defaultValue);
36
+ setText(String(props.value ?? props.defaultValue ?? ""));
148
37
  }, [props.value, props.defaultValue]);
149
- useEffect(() => {
150
- if (!isActive) return;
151
- if (!isTyped) return;
152
- if (debouncedText.length > 3) setSuggestion("");
153
- else setSuggestion("");
154
- }, [
155
- debouncedText,
156
- isActive,
157
- autocomplete,
158
- configuration
159
- ]);
160
- useEffect(() => {
161
- if (!suggestion || cursorAtFetch === -1 || !placeholderRef.current || !ghostLayerRef.current) {
162
- setSuggestionPosition(null);
163
- return;
164
- }
165
- const rect = placeholderRef.current.getBoundingClientRect();
166
- const parentRect = ghostLayerRef.current.getBoundingClientRect();
167
- setSuggestionPosition({
168
- left: rect.left - parentRect.left,
169
- top: rect.top - parentRect.top
170
- });
171
- }, [
172
- suggestion,
173
- cursorAtFetch,
174
- text
175
- ]);
176
38
  const acceptSuggestion = () => {
177
- const currentCursor = textareaRef.current?.selectionStart ?? cursorAtFetch;
178
- if (currentCursor !== cursorAtFetch) return;
179
- setText(text.slice(0, currentCursor) + suggestion + text.slice(currentCursor));
39
+ const active = suggestionProp ?? suggestion;
40
+ if (!active) return;
41
+ const cursor = editorRef.current?.getCursorOffset() ?? text.length;
42
+ setText(text.slice(0, cursor) + active + text.slice(cursor));
180
43
  setSuggestion("");
181
- setCursorAtFetch(-1);
182
44
  setTimeout(() => {
183
- textareaRef.current?.focus();
184
- const newCursorPos = currentCursor + suggestion.length;
185
- textareaRef.current?.setSelectionRange(newCursorPos, newCursorPos);
45
+ editorRef.current?.focus();
46
+ editorRef.current?.setCursorAtOffset(cursor + active.length);
186
47
  }, 0);
187
48
  };
188
- return /* @__PURE__ */ jsxs("div", {
189
- className: "relative w-full",
190
- children: [
191
- /* @__PURE__ */ jsxs("div", {
192
- ref: ghostLayerRef,
193
- className: "pointer-events-none absolute inset-0 whitespace-pre-wrap break-words px-1 py-3 text-base leading-[1.45rem] md:py-1 md:text-sm md:leading-[1.23rem]",
194
- "aria-hidden": "true",
195
- children: [suggestion && cursorAtFetch !== -1 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
196
- /* @__PURE__ */ jsx("span", {
197
- className: "align-text-top text-transparent",
198
- children: text.slice(0, cursorAtFetch)
199
- }),
200
- /* @__PURE__ */ jsx("span", {
201
- ref: placeholderRef,
202
- style: { visibility: "hidden" },
203
- "aria-hidden": "true",
204
- children: suggestion
205
- }),
206
- /* @__PURE__ */ jsx("span", {
207
- className: "align-text-top text-transparent",
208
- children: text.slice(cursorAtFetch)
209
- })
210
- ] }) : /* @__PURE__ */ jsx("span", {
211
- className: "align-text-top text-transparent",
212
- children: text
213
- }), suggestionProp && /* @__PURE__ */ jsx("span", {
214
- className: "align-text-top text-neutral",
215
- children: suggestionProp
216
- })]
217
- }),
218
- suggestion && suggestionPosition && /* @__PURE__ */ jsx("div", {
219
- className: "pointer-events-none whitespace-pre-wrap break-words text-base text-neutral leading-[1.45rem] md:text-sm md:leading-[1.23rem]",
220
- style: {
221
- position: "absolute",
222
- left: suggestionPosition.left,
223
- top: suggestionPosition.top
224
- },
225
- children: suggestion
226
- }),
227
- /* @__PURE__ */ jsx(AutoSizedTextArea, {
228
- ...props,
229
- ref: textareaRef,
230
- value: text,
231
- onChange: (e) => {
232
- setIsTyped(true);
233
- setText(e.target.value);
234
- setSuggestion("");
235
- props.onChange?.(e);
236
- },
237
- onKeyDown: (e) => {
238
- if (e.key === "Tab" && suggestion) {
239
- e.preventDefault();
240
- acceptSuggestion();
241
- }
242
- props.onKeyDown?.(e);
243
- },
244
- onSelect: (e) => {
245
- if (suggestion && e.target.selectionStart !== cursorAtFetch) {
246
- setSuggestion("");
247
- setCursorAtFetch(-1);
248
- }
249
- props.onSelect?.(e);
250
- }
251
- })
252
- ]
49
+ const activeGhost = isActive ? suggestionProp ?? (suggestion || void 0) : void 0;
50
+ const textLines = text.split("\n");
51
+ return /* @__PURE__ */ jsx(ContentEditableTextArea, {
52
+ ref: editorRef,
53
+ value: text,
54
+ onChange: (val) => {
55
+ setText(val);
56
+ setSuggestion("");
57
+ if (props.onChange) {
58
+ const evt = {
59
+ target: { value: val },
60
+ currentTarget: { value: val }
61
+ };
62
+ props.onChange(evt);
63
+ }
64
+ },
65
+ onKeyDown: (e) => {
66
+ if (e.key === "Tab" && (suggestionProp ?? suggestion)) {
67
+ e.preventDefault();
68
+ acceptSuggestion();
69
+ }
70
+ props.onKeyDown?.(e);
71
+ },
72
+ ghostText: activeGhost,
73
+ ghostLine: suggestionProp ? textLines.length - 1 : void 0,
74
+ ghostOffset: suggestionProp ? textLines[textLines.length - 1]?.length ?? 0 : void 0,
75
+ placeholder: props.placeholder,
76
+ disabled: props.disabled,
77
+ autoSize: props.autoSize,
78
+ maxRows: props.maxRows,
79
+ minRows: props.rows,
80
+ variant: props.variant,
81
+ validationStyleEnabled: props.validationStyleEnabled,
82
+ className: props.className,
83
+ dir: props.dir,
84
+ "aria-label": props["aria-label"],
85
+ "aria-invalid": props["aria-invalid"],
86
+ "aria-describedby": props["aria-describedby"],
87
+ "data-testid": props["data-testid"]
253
88
  });
254
89
  };
255
90
 
256
91
  //#endregion
257
- export { AutoCompleteTextarea, useDebounce };
92
+ export { AutoCompleteTextarea };
258
93
  //# sourceMappingURL=AutocompleteTextArea.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"AutocompleteTextArea.mjs","names":[],"sources":["../../../../src/components/TextArea/AutocompleteTextArea.tsx"],"sourcesContent":["'use client';\n\nimport { useAutocomplete } from '@hooks/reactQuery';\nimport type { AutocompleteResponse } from '@intlayer/backend';\nimport { useConfiguration } from '@intlayer/editor-react';\nimport { type FC, useEffect, useRef, useState } from 'react';\nimport {\n AutoSizedTextArea,\n type AutoSizedTextAreaProps,\n} from './AutoSizeTextArea';\n\n/**\n * Custom hook for debouncing values to prevent excessive API calls.\n *\n * Delays updating the returned value until the input value has stopped changing\n * for the specified delay period.\n *\n * @param value - The value to debounce\n * @param delay - Delay in milliseconds before updating the debounced value\n * @returns The debounced value that only updates after the delay period\n *\n * @example\n * ```tsx\n * const [searchTerm, setSearchTerm] = useState('');\n * const debouncedSearchTerm = useDebounce(searchTerm, 300);\n *\n * useEffect(() => {\n * if (debouncedSearchTerm) {\n * performSearch(debouncedSearchTerm);\n * }\n * }, [debouncedSearchTerm]);\n * ```\n */\nexport const useDebounce = <T,>(value: T, delay: number): T => {\n const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n // Cleanup the timer if value changes before 'delay' ms\n return () => clearTimeout(timer);\n }, [value, delay]);\n\n return debouncedValue;\n};\n\n/**\n * Props for the AutocompleteTextArea component.\n *\n * Extends AutoSizedTextAreaProps with AI-powered autocomplete functionality.\n *\n * @example\n * ```tsx\n * // AI-powered autocomplete textarea\n * <AutoCompleteTextarea\n * placeholder=\"Start typing for AI suggestions...\"\n * isActive={true}\n * autoSize={true}\n * maxRows={10}\n * />\n *\n * // Manual suggestion mode\n * <AutoCompleteTextarea\n * value={content}\n * onChange={handleChange}\n * suggestion=\"Consider adding more details about...\"\n * isActive={false}\n * />\n *\n * // Disabled autocomplete for sensitive content\n * <AutoCompleteTextarea\n * placeholder=\"Private notes (no AI assistance)\"\n * isActive={false}\n * autoSize={true}\n * />\n * ```\n */\nexport type AutocompleteTextAreaProps = AutoSizedTextAreaProps & {\n /** Whether AI autocomplete is active and should fetch suggestions */\n isActive?: boolean;\n /** Manual suggestion text to display (overrides AI suggestions) */\n suggestion?: string;\n};\n\n/**\n * AutoCompleteTextarea Component\n *\n * An intelligent textarea that provides AI-powered autocomplete suggestions as users type,\n * combining auto-sizing functionality with contextual text completion.\n *\n * ## Features\n * - **AI-Powered Suggestions**: Context-aware autocomplete using configured AI models\n * - **Debounced API Calls**: Efficient suggestion fetching with 200ms debounce\n * - **Visual Suggestions**: Inline preview of suggested completions\n * - **Keyboard Navigation**: Tab key to accept suggestions\n * - **Context Analysis**: Uses surrounding text for better suggestions\n * - **Auto-Sizing**: Inherits all AutoSizedTextArea capabilities\n * - **Performance Optimized**: Smart caching and minimal re-renders\n *\n * ## Technical Implementation\n * - **Debounce Strategy**: 200ms delay before fetching suggestions\n * - **Context Window**: 5 lines before/after cursor for context\n * - **Minimum Trigger**: Requires 3+ characters before suggesting\n * - **Position Tracking**: Ghost layer for accurate suggestion positioning\n * - **Cursor Management**: Tracks cursor position during suggestion fetch\n *\n * ## AI Integration\n * - Uses configured AI model (OpenAI, Anthropic, etc.)\n * - Sends context-aware prompts for relevant suggestions\n * - Respects temperature and model settings from configuration\n * - Handles API errors gracefully without interrupting user flow\n *\n * ## Use Cases\n * - **Content Creation**: Blog posts, articles, documentation\n * - **Code Comments**: Intelligent code documentation assistance\n * - **Email Composition**: Professional email writing assistance\n * - **Creative Writing**: Story and narrative completion\n * - **Technical Documentation**: API docs, README files\n * - **Social Media**: Post creation with engagement optimization\n *\n * @example\n * ```tsx\n * // Blog writing assistant\n * const [blogPost, setBlogPost] = useState('');\n * const [isAiEnabled, setIsAiEnabled] = useState(true);\n *\n * <div className=\"space-y-4\">\n * <div className=\"flex items-center gap-2\">\n * <Switch\n * checked={isAiEnabled}\n * onChange={setIsAiEnabled}\n * />\n * <label>AI Writing Assistant</label>\n * </div>\n *\n * <AutoCompleteTextarea\n * value={blogPost}\n * onChange={(e) => setBlogPost(e.target.value)}\n * placeholder=\"Start writing your blog post...\"\n * isActive={isAiEnabled}\n * autoSize={true}\n * maxRows={15}\n * className=\"min-h-[200px] font-serif text-lg leading-relaxed\"\n * />\n * </div>\n *\n * // Code documentation assistant\n * <AutoCompleteTextarea\n * value={docComment}\n * onChange={handleDocChange}\n * placeholder=\"/** Describe this function... *\\/\"\n * isActive={true}\n * autoSize={true}\n * maxRows={8}\n * className=\"font-mono text-sm\"\n * />\n *\n * // Email composition with templates\n * <AutoCompleteTextarea\n * defaultValue=\"Dear \"\n * placeholder=\"AI will help complete your email...\"\n * isActive={true}\n * autoSize={true}\n * maxRows={12}\n * variant={InputVariant.DEFAULT}\n * />\n * ```\n *\n * ## Accessibility\n * - Ghost layer is properly hidden from screen readers\n * - Maintains focus management during suggestion acceptance\n * - Preserves keyboard navigation patterns\n * - Respects reduced motion preferences\n */\nexport const AutoCompleteTextarea: FC<AutocompleteTextAreaProps> = ({\n isActive = true,\n suggestion: suggestionProp,\n ...props\n}) => {\n const defaultValue = String(props.value ?? props.defaultValue ?? '');\n const { mutate: autocomplete } = useAutocomplete();\n const configuration = useConfiguration();\n const [isTyped, setIsTyped] = useState(false);\n const [text, setText] = useState(defaultValue);\n const [suggestion, setSuggestion] = useState('');\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const placeholderRef = useRef<HTMLSpanElement>(null);\n const ghostLayerRef = useRef<HTMLDivElement>(null);\n const [suggestionPosition, setSuggestionPosition] = useState<{\n left: number;\n top: number;\n } | null>(null);\n const [cursorAtFetch, setCursorAtFetch] = useState(-1);\n\n // Only update this “debouncedText” after the user stops typing for 200ms\n const debouncedText = useDebounce(text, 200);\n\n useEffect(() => {\n if (typeof props.value === 'undefined') return;\n setText(defaultValue);\n }, [props.value, props.defaultValue]);\n\n useEffect(() => {\n if (!isActive) return;\n if (!isTyped) return;\n\n const fetchSuggestion = async () => {\n try {\n const cursor =\n textareaRef.current?.selectionStart ?? debouncedText.length;\n const before = debouncedText.slice(0, cursor);\n const after = debouncedText.slice(cursor);\n const numLines = 5;\n const beforeLines = before.split('\\n');\n const contextBeforeLines = beforeLines.slice(\n Math.max(0, beforeLines.length - numLines - 1),\n -1\n );\n const contextBefore = contextBeforeLines.join('\\n');\n const currentLine = beforeLines[beforeLines.length - 1] ?? '';\n const afterLines = after.split('\\n');\n const contextAfter = afterLines.slice(1, numLines + 1).join('\\n');\n\n autocomplete(\n {\n text: before,\n contextBefore,\n currentLine,\n contextAfter,\n aiOptions: {\n apiKey: configuration.ai?.apiKey,\n model: configuration.ai?.model,\n temperature: configuration.ai?.temperature,\n },\n },\n {\n onSuccess: (data: AutocompleteResponse) => {\n setSuggestion(data.data?.autocompletion ?? '');\n setCursorAtFetch(cursor);\n },\n }\n );\n } catch (err) {\n console.error('Autocomplete error:', err);\n }\n };\n\n if (debouncedText.length > 3) {\n // Only fetch if user typed more than 3 chars and has paused\n setSuggestion('');\n // TODO: Uncomment this when the autocomplete works well enough\n // fetchSuggestion();\n } else {\n // If typed less than threshold, clear the suggestion\n setSuggestion('');\n }\n }, [debouncedText, isActive, autocomplete, configuration]);\n\n useEffect(() => {\n if (\n !suggestion ||\n cursorAtFetch === -1 ||\n !placeholderRef.current ||\n !ghostLayerRef.current\n ) {\n setSuggestionPosition(null);\n return;\n }\n\n const rect = placeholderRef.current.getBoundingClientRect();\n const parentRect = ghostLayerRef.current.getBoundingClientRect();\n setSuggestionPosition({\n left: rect.left - parentRect.left,\n top: rect.top - parentRect.top,\n });\n }, [suggestion, cursorAtFetch, text]);\n\n const acceptSuggestion = () => {\n const currentCursor = textareaRef.current?.selectionStart ?? cursorAtFetch;\n if (currentCursor !== cursorAtFetch) return;\n const newText =\n text.slice(0, currentCursor) + suggestion + text.slice(currentCursor);\n setText(newText);\n setSuggestion('');\n setCursorAtFetch(-1);\n setTimeout(() => {\n textareaRef.current?.focus();\n const newCursorPos = currentCursor + suggestion.length;\n textareaRef.current?.setSelectionRange(newCursorPos, newCursorPos);\n }, 0);\n };\n\n return (\n <div className=\"relative w-full\">\n <div\n ref={ghostLayerRef}\n className=\"pointer-events-none absolute inset-0 whitespace-pre-wrap break-words px-1 py-3 text-base leading-[1.45rem] md:py-1 md:text-sm md:leading-[1.23rem]\"\n aria-hidden=\"true\"\n >\n {suggestion && cursorAtFetch !== -1 ? (\n <>\n <span className=\"align-text-top text-transparent\">\n {text.slice(0, cursorAtFetch)}\n </span>\n <span\n ref={placeholderRef}\n style={{ visibility: 'hidden' }}\n aria-hidden=\"true\"\n >\n {suggestion}\n </span>\n <span className=\"align-text-top text-transparent\">\n {text.slice(cursorAtFetch)}\n </span>\n </>\n ) : (\n <span className=\"align-text-top text-transparent\">{text}</span>\n )}\n {suggestionProp && (\n <span className=\"align-text-top text-neutral\">{suggestionProp}</span>\n )}\n </div>\n {suggestion && suggestionPosition && (\n <div\n className=\"pointer-events-none whitespace-pre-wrap break-words text-base text-neutral leading-[1.45rem] md:text-sm md:leading-[1.23rem]\"\n style={{\n position: 'absolute',\n left: suggestionPosition.left,\n top: suggestionPosition.top,\n }}\n >\n {suggestion}\n </div>\n )}\n <AutoSizedTextArea\n {...props}\n ref={textareaRef}\n value={text}\n onChange={(e) => {\n setIsTyped(true);\n setText(e.target.value);\n setSuggestion('');\n props.onChange?.(e);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Tab' && suggestion) {\n e.preventDefault();\n acceptSuggestion();\n }\n props.onKeyDown?.(e);\n }}\n onSelect={(e) => {\n if (\n suggestion &&\n (e.target as HTMLTextAreaElement).selectionStart !== cursorAtFetch\n ) {\n setSuggestion('');\n setCursorAtFetch(-1);\n }\n props.onSelect?.(e);\n }}\n />\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AutocompleteTextArea.mjs","names":[],"sources":["../../../../src/components/TextArea/AutocompleteTextArea.tsx"],"sourcesContent":["'use client';\n\nimport {\n type ChangeEvent,\n type FC,\n type KeyboardEvent,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport type { AutoSizedTextAreaProps } from './AutoSizeTextArea';\nimport {\n ContentEditableTextArea,\n type ContentEditableTextAreaHandle,\n} from './ContentEditableTextArea';\n\n/**\n * Props for the AutocompleteTextArea component.\n *\n * Extends AutoSizedTextAreaProps with inline autocomplete functionality\n * using a contentEditable-based textarea.\n *\n * @example\n * ```tsx\n * <AutoCompleteTextarea\n * placeholder=\"Start typing...\"\n * isActive={true}\n * autoSize={true}\n * maxRows={10}\n * />\n * ```\n */\nexport type AutocompleteTextAreaProps = AutoSizedTextAreaProps & {\n /** Whether inline autocomplete ghost text is active */\n isActive?: boolean;\n /** Manual suggestion text to display as ghost text after the cursor */\n suggestion?: string;\n};\n\n/**\n * AutoCompleteTextarea Component\n *\n * A textarea with inline autocomplete ghost text, built on a contentEditable div\n * instead of a native `<textarea>`. Ghost text (suggestions) is rendered inline\n * at the cursor position and can be accepted with the Tab key.\n *\n * The component wraps `ContentEditableTextArea` and manages suggestion state.\n * When `suggestion` prop is provided it is shown as ghost text at the end of the\n * current text. When `isActive` is false, ghost text is hidden.\n *\n * @example\n * ```tsx\n * <AutoCompleteTextarea\n * value={content}\n * onChange={handleChange}\n * suggestion=\"suggested completion...\"\n * isActive={true}\n * autoSize={true}\n * />\n * ```\n */\nexport const AutoCompleteTextarea: FC<AutocompleteTextAreaProps> = ({\n isActive = true,\n suggestion: suggestionProp,\n ...props\n}) => {\n const defaultValue = String(props.value ?? props.defaultValue ?? '');\n const [text, setText] = useState(defaultValue);\n const [suggestion, setSuggestion] = useState('');\n const editorRef = useRef<ContentEditableTextAreaHandle>(null);\n\n useEffect(() => {\n if (typeof props.value === 'undefined') return;\n setText(String(props.value ?? props.defaultValue ?? ''));\n }, [props.value, props.defaultValue]);\n\n const acceptSuggestion = () => {\n const active = suggestionProp ?? suggestion;\n if (!active) return;\n\n const cursor = editorRef.current?.getCursorOffset() ?? text.length;\n const next = text.slice(0, cursor) + active + text.slice(cursor);\n setText(next);\n setSuggestion('');\n\n setTimeout(() => {\n editorRef.current?.focus();\n editorRef.current?.setCursorAtOffset(cursor + active.length);\n }, 0);\n };\n\n const activeGhost = isActive\n ? (suggestionProp ?? (suggestion || undefined))\n : undefined;\n const textLines = text.split('\\n');\n const activeLine = suggestionProp ? textLines.length - 1 : undefined;\n const activeOffset = suggestionProp\n ? (textLines[textLines.length - 1]?.length ?? 0)\n : undefined;\n\n return (\n <ContentEditableTextArea\n ref={editorRef}\n value={text}\n onChange={(val) => {\n setText(val);\n setSuggestion('');\n\n if (props.onChange) {\n const evt = {\n target: { value: val },\n currentTarget: { value: val },\n } as ChangeEvent<HTMLTextAreaElement>;\n props.onChange(evt);\n }\n }}\n onKeyDown={(e) => {\n if (e.key === 'Tab' && (suggestionProp ?? suggestion)) {\n e.preventDefault();\n acceptSuggestion();\n }\n props.onKeyDown?.(e as unknown as KeyboardEvent<HTMLTextAreaElement>);\n }}\n ghostText={activeGhost}\n ghostLine={activeLine}\n ghostOffset={activeOffset}\n placeholder={props.placeholder}\n disabled={props.disabled}\n autoSize={props.autoSize}\n maxRows={props.maxRows}\n minRows={props.rows}\n variant={props.variant}\n validationStyleEnabled={props.validationStyleEnabled}\n className={props.className}\n dir={props.dir as 'ltr' | 'rtl' | 'auto'}\n aria-label={props['aria-label']}\n aria-invalid={props['aria-invalid']}\n aria-describedby={props['aria-describedby']}\n data-testid={(props as Record<string, unknown>)['data-testid'] as string}\n />\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,wBAAuD,EAClE,WAAW,MACX,YAAY,gBACZ,GAAG,YACC;CAEJ,MAAM,CAAC,MAAM,WAAW,SADH,OAAO,MAAM,SAAS,MAAM,gBAAgB,GAAG,CACtB;CAC9C,MAAM,CAAC,YAAY,iBAAiB,SAAS,GAAG;CAChD,MAAM,YAAY,OAAsC,KAAK;AAE7D,iBAAgB;AACd,MAAI,OAAO,MAAM,UAAU,YAAa;AACxC,UAAQ,OAAO,MAAM,SAAS,MAAM,gBAAgB,GAAG,CAAC;IACvD,CAAC,MAAM,OAAO,MAAM,aAAa,CAAC;CAErC,MAAM,yBAAyB;EAC7B,MAAM,SAAS,kBAAkB;AACjC,MAAI,CAAC,OAAQ;EAEb,MAAM,SAAS,UAAU,SAAS,iBAAiB,IAAI,KAAK;AAE5D,UADa,KAAK,MAAM,GAAG,OAAO,GAAG,SAAS,KAAK,MAAM,OAAO,CACnD;AACb,gBAAc,GAAG;AAEjB,mBAAiB;AACf,aAAU,SAAS,OAAO;AAC1B,aAAU,SAAS,kBAAkB,SAAS,OAAO,OAAO;KAC3D,EAAE;;CAGP,MAAM,cAAc,WACf,mBAAmB,cAAc,UAClC;CACJ,MAAM,YAAY,KAAK,MAAM,KAAK;AAMlC,QACE,oBAAC,yBAAD;EACE,KAAK;EACL,OAAO;EACP,WAAW,QAAQ;AACjB,WAAQ,IAAI;AACZ,iBAAc,GAAG;AAEjB,OAAI,MAAM,UAAU;IAClB,MAAM,MAAM;KACV,QAAQ,EAAE,OAAO,KAAK;KACtB,eAAe,EAAE,OAAO,KAAK;KAC9B;AACD,UAAM,SAAS,IAAI;;;EAGvB,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,UAAU,kBAAkB,aAAa;AACrD,MAAE,gBAAgB;AAClB,sBAAkB;;AAEpB,SAAM,YAAY,EAAmD;;EAEvE,WAAW;EACX,WA7Be,iBAAiB,UAAU,SAAS,IAAI;EA8BvD,aA7BiB,iBAChB,UAAU,UAAU,SAAS,IAAI,UAAU,IAC5C;EA4BA,aAAa,MAAM;EACnB,UAAU,MAAM;EAChB,UAAU,MAAM;EAChB,SAAS,MAAM;EACf,SAAS,MAAM;EACf,SAAS,MAAM;EACf,wBAAwB,MAAM;EAC9B,WAAW,MAAM;EACjB,KAAK,MAAM;EACX,cAAY,MAAM;EAClB,gBAAc,MAAM;EACpB,oBAAkB,MAAM;EACxB,eAAc,MAAkC;EAChD"}