@intlayer/design-system 8.4.5 → 8.4.6
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/dist/esm/components/DictionaryFieldEditor/NavigationView/NavigationViewNode.mjs +1 -1
- package/dist/esm/components/IDE/CodeBlockShiki.mjs +12 -0
- package/dist/esm/components/IDE/CodeBlockShiki.mjs.map +1 -1
- package/dist/esm/components/TextArea/AutocompleteTextArea.mjs +60 -225
- package/dist/esm/components/TextArea/AutocompleteTextArea.mjs.map +1 -1
- package/dist/esm/components/TextArea/ContentEditableTextArea.mjs +444 -0
- package/dist/esm/components/TextArea/ContentEditableTextArea.mjs.map +1 -0
- package/dist/esm/components/TextArea/index.mjs +3 -2
- package/dist/esm/components/index.mjs +3 -2
- package/dist/esm/hooks/useAuth/useOAuth2.mjs +1 -1
- package/dist/esm/hooks/useAuth/useSession.mjs +1 -1
- package/dist/types/components/Badge/index.d.ts +2 -2
- package/dist/types/components/Button/Button.d.ts +4 -4
- package/dist/types/components/CollapsibleTable/CollapsibleTable.d.ts +2 -2
- package/dist/types/components/Container/index.d.ts +4 -4
- package/dist/types/components/IDE/CodeBlockShiki.d.ts.map +1 -1
- package/dist/types/components/Input/Checkbox.d.ts +2 -2
- package/dist/types/components/Input/Input.d.ts +1 -1
- package/dist/types/components/Input/OTPInput.d.ts +1 -1
- package/dist/types/components/Link/Link.d.ts +4 -4
- package/dist/types/components/Pagination/Pagination.d.ts +2 -2
- package/dist/types/components/SwitchSelector/index.d.ts +2 -2
- package/dist/types/components/TabSelector/TabSelector.d.ts +1 -1
- package/dist/types/components/Tag/index.d.ts +3 -3
- package/dist/types/components/TextArea/AutocompleteTextArea.d.ts +14 -120
- package/dist/types/components/TextArea/AutocompleteTextArea.d.ts.map +1 -1
- package/dist/types/components/TextArea/ContentEditableTextArea.d.ts +65 -0
- package/dist/types/components/TextArea/ContentEditableTextArea.d.ts.map +1 -0
- package/dist/types/components/TextArea/index.d.ts +3 -2
- package/dist/types/components/index.d.ts +3 -2
- package/package.json +17 -17
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Button, ButtonColor, ButtonTextAlign, ButtonVariant } from "../../Button/Button.mjs";
|
|
2
2
|
import { Accordion } from "../../Accordion/Accordion.mjs";
|
|
3
3
|
import { getIsEditableSection } from "../getIsEditableSection.mjs";
|
|
4
|
-
import configuration from "@intlayer/config/built";
|
|
5
4
|
import { ChevronRight, Plus } from "lucide-react";
|
|
6
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import configuration from "@intlayer/config/built";
|
|
7
7
|
import { useEditedContentActions, useEditorLocale, useFocusUnmergedDictionary } from "@intlayer/editor-react";
|
|
8
8
|
import { useIntlayer } from "react-intlayer";
|
|
9
9
|
import { getContentNodeByKeyPath, getEmptyNode, getNodeType } from "@intlayer/core/dictionaryManipulator";
|
|
@@ -11,12 +11,23 @@ const loadLanguage = async (lang) => {
|
|
|
11
11
|
if (languageCache.has(lang)) return languageCache.get(lang);
|
|
12
12
|
let languageModule;
|
|
13
13
|
switch (lang) {
|
|
14
|
+
case "angular-html":
|
|
15
|
+
languageModule = await import("shiki/langs/angular-html.mjs");
|
|
16
|
+
break;
|
|
17
|
+
case "angular-ts":
|
|
18
|
+
languageModule = await import("shiki/langs/angular-ts.mjs");
|
|
19
|
+
break;
|
|
20
|
+
case "astro":
|
|
21
|
+
languageModule = await import("shiki/langs/astro.mjs");
|
|
22
|
+
break;
|
|
14
23
|
case "typescript":
|
|
15
24
|
case "ts":
|
|
16
25
|
languageModule = await import("shiki/langs/typescript.mjs");
|
|
17
26
|
break;
|
|
18
27
|
case "javascript":
|
|
19
28
|
case "js":
|
|
29
|
+
case "cjs":
|
|
30
|
+
case "mjs":
|
|
20
31
|
languageModule = await import("shiki/langs/javascript.mjs");
|
|
21
32
|
break;
|
|
22
33
|
case "bash":
|
|
@@ -43,6 +54,7 @@ const loadLanguage = async (lang) => {
|
|
|
43
54
|
case "xml":
|
|
44
55
|
languageModule = await import("shiki/langs/xml.mjs");
|
|
45
56
|
break;
|
|
57
|
+
case "yml":
|
|
46
58
|
case "yaml":
|
|
47
59
|
languageModule = await import("shiki/langs/yaml.mjs");
|
|
48
60
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeBlockShiki.mjs","names":[],"sources":["../../../../src/components/IDE/CodeBlockShiki.tsx"],"sourcesContent":["'use client';\n\nimport { type FC, useEffect, useState } from 'react';\nimport type {\n BundledLanguage,\n BundledTheme,\n HighlighterGeneric,\n} from 'shiki/bundle/web';\nimport { CodeDefault } from './CodeBlockClient';\n\n// Map of loaded modules to avoid re-importing\nconst languageCache = new Map<BundledLanguage, any>();\nconst themeCache = new Map<BundledTheme, any>();\n\n// Lazy load language modules\nconst loadLanguage = async (lang: BundledLanguage): Promise<any> => {\n if (languageCache.has(lang)) return languageCache.get(lang);\n\n let languageModule: any;\n switch (lang) {\n case 'typescript':\n case 'ts':\n languageModule = await import('shiki/langs/typescript.mjs');\n break;\n case 'javascript':\n case 'js':\n languageModule = await import('shiki/langs/javascript.mjs');\n break;\n case 'bash':\n case 'sh':\n case 'shell':\n languageModule = await import('shiki/langs/bash.mjs');\n break;\n case 'json':\n languageModule = await import('shiki/langs/json.mjs');\n break;\n case 'jsonc':\n case 'json5':\n languageModule = await import('shiki/langs/json5.mjs');\n break;\n case 'tsx':\n languageModule = await import('shiki/langs/tsx.mjs');\n break;\n case 'vue':\n languageModule = await import('shiki/langs/vue.mjs');\n break;\n case 'html':\n languageModule = await import('shiki/langs/html.mjs');\n break;\n case 'xml':\n languageModule = await import('shiki/langs/xml.mjs');\n break;\n case 'yaml':\n languageModule = await import('shiki/langs/yaml.mjs');\n break;\n default:\n languageModule = await import('shiki/langs/typescript.mjs');\n break;\n }\n\n const language = languageModule.default;\n languageCache.set(lang, language);\n return language;\n};\n\n// Lazy load theme modules\nconst loadTheme = async (themeName: BundledTheme): Promise<any> => {\n if (themeCache.has(themeName)) return themeCache.get(themeName);\n\n let themeModule: any;\n switch (themeName) {\n case 'github-dark':\n themeModule = await import('shiki/themes/github-dark.mjs');\n break;\n case 'github-light':\n default:\n themeModule = await import('shiki/themes/github-light.mjs');\n break;\n }\n\n const theme = themeModule.default;\n themeCache.set(themeName, theme);\n return theme;\n};\n\n// Singleton Highlighter Instance\nlet highlighterPromise: Promise<HighlighterGeneric<any, any>> | null = null;\n\nconst getHighlighterInstance = async () => {\n if (!highlighterPromise) {\n highlighterPromise = import('shiki/bundle/web').then(\n ({ createHighlighter }) =>\n createHighlighter({\n langs: [],\n themes: [],\n })\n );\n }\n return highlighterPromise;\n};\n\n// Create a promise for highlighting\nconst highlightCode = async (\n code: string,\n lang: BundledLanguage,\n isDarkMode?: boolean\n): Promise<string> => {\n const themeName: BundledTheme = isDarkMode ? 'github-dark' : 'github-light';\n\n // Load highlighter, language, and theme in parallel\n const [highlighter, languageModule, themeModule] = await Promise.all([\n getHighlighterInstance(),\n loadLanguage(lang),\n loadTheme(themeName),\n ]);\n\n // Load into the singleton instance if not already loaded\n if (!highlighter.getLoadedLanguages().includes(lang)) {\n await highlighter.loadLanguage(languageModule);\n }\n if (!highlighter.getLoadedThemes().includes(themeName)) {\n await highlighter.loadTheme(themeModule);\n }\n\n return highlighter.codeToHtml(code, {\n lang,\n theme: themeName,\n });\n};\n\nexport type CodeBlockShikiProps = {\n children: string;\n lang: BundledLanguage;\n isDarkMode?: boolean;\n};\n\nexport const CodeBlockShiki: FC<CodeBlockShikiProps> = ({\n children,\n lang,\n isDarkMode,\n}) => {\n const [html, setHtml] = useState<string | null>(null);\n\n useEffect(() => {\n let isCancelled = false;\n\n highlightCode(children, lang, isDarkMode)\n .then((result) => {\n if (!isCancelled) setHtml(result);\n })\n .catch((error) => {\n console.error('Failed to highlight code:', error);\n if (!isCancelled && html === null) setHtml('');\n });\n\n return () => {\n isCancelled = true;\n };\n }, [children, lang, isDarkMode]);\n\n return (\n <div className=\"min-w-0 max-w-full overflow-auto bg-transparent [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&_pre::-webkit-scrollbar]:hidden [&_pre]:[-ms-overflow-style:none] [&_pre]:[scrollbar-width:none]\">\n {html ? (\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki generates safe HTML for code highlighting\n <div dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n <CodeDefault>{children}</CodeDefault>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;AAWA,MAAM,gCAAgB,IAAI,KAA2B;AACrD,MAAM,6BAAa,IAAI,KAAwB;AAG/C,MAAM,eAAe,OAAO,SAAwC;AAClE,KAAI,cAAc,IAAI,KAAK,CAAE,QAAO,cAAc,IAAI,KAAK;CAE3D,IAAI;AACJ,SAAQ,MAAR;EACE,KAAK;EACL,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;EACL,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;EACL,KAAK;EACL,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;EACL,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF,KAAK;AACH,oBAAiB,MAAM,OAAO;AAC9B;EACF;AACE,oBAAiB,MAAM,OAAO;AAC9B;;CAGJ,MAAM,WAAW,eAAe;AAChC,eAAc,IAAI,MAAM,SAAS;AACjC,QAAO;;AAIT,MAAM,YAAY,OAAO,cAA0C;AACjE,KAAI,WAAW,IAAI,UAAU,CAAE,QAAO,WAAW,IAAI,UAAU;CAE/D,IAAI;AACJ,SAAQ,WAAR;EACE,KAAK;AACH,iBAAc,MAAM,OAAO;AAC3B;EAEF;AACE,iBAAc,MAAM,OAAO;AAC3B;;CAGJ,MAAM,QAAQ,YAAY;AAC1B,YAAW,IAAI,WAAW,MAAM;AAChC,QAAO;;AAIT,IAAI,qBAAmE;AAEvE,MAAM,yBAAyB,YAAY;AACzC,KAAI,CAAC,mBACH,sBAAqB,OAAO,oBAAoB,MAC7C,EAAE,wBACD,kBAAkB;EAChB,OAAO,EAAE;EACT,QAAQ,EAAE;EACX,CAAC,CACL;AAEH,QAAO;;AAIT,MAAM,gBAAgB,OACpB,MACA,MACA,eACoB;CACpB,MAAM,YAA0B,aAAa,gBAAgB;CAG7D,MAAM,CAAC,aAAa,gBAAgB,eAAe,MAAM,QAAQ,IAAI;EACnE,wBAAwB;EACxB,aAAa,KAAK;EAClB,UAAU,UAAU;EACrB,CAAC;AAGF,KAAI,CAAC,YAAY,oBAAoB,CAAC,SAAS,KAAK,CAClD,OAAM,YAAY,aAAa,eAAe;AAEhD,KAAI,CAAC,YAAY,iBAAiB,CAAC,SAAS,UAAU,CACpD,OAAM,YAAY,UAAU,YAAY;AAG1C,QAAO,YAAY,WAAW,MAAM;EAClC;EACA,OAAO;EACR,CAAC;;AASJ,MAAa,kBAA2C,EACtD,UACA,MACA,iBACI;CACJ,MAAM,CAAC,MAAM,WAAW,SAAwB,KAAK;AAErD,iBAAgB;EACd,IAAI,cAAc;AAElB,gBAAc,UAAU,MAAM,WAAW,CACtC,MAAM,WAAW;AAChB,OAAI,CAAC,YAAa,SAAQ,OAAO;IACjC,CACD,OAAO,UAAU;AAChB,WAAQ,MAAM,6BAA6B,MAAM;AACjD,OAAI,CAAC,eAAe,SAAS,KAAM,SAAQ,GAAG;IAC9C;AAEJ,eAAa;AACX,iBAAc;;IAEf;EAAC;EAAU;EAAM;EAAW,CAAC;AAEhC,QACE,oBAAC,OAAD;EAAK,WAAU;YACZ,OAEC,oBAAC,OAAD,EAAK,yBAAyB,EAAE,QAAQ,MAAM,EAAI,IAElD,oBAAC,aAAD,EAAc,UAAuB;EAEnC"}
|
|
1
|
+
{"version":3,"file":"CodeBlockShiki.mjs","names":[],"sources":["../../../../src/components/IDE/CodeBlockShiki.tsx"],"sourcesContent":["'use client';\n\nimport { type FC, useEffect, useState } from 'react';\nimport type {\n BundledLanguage,\n BundledTheme,\n HighlighterGeneric,\n} from 'shiki/bundle/web';\nimport { CodeDefault } from './CodeBlockClient';\n\n// Map of loaded modules to avoid re-importing\nconst languageCache = new Map<BundledLanguage, any>();\nconst themeCache = new Map<BundledTheme, any>();\n\n// Lazy load language modules\nconst loadLanguage = async (lang: BundledLanguage): Promise<any> => {\n if (languageCache.has(lang)) return languageCache.get(lang);\n\n let languageModule: any;\n switch (lang) {\n case 'angular-html':\n languageModule = await import('shiki/langs/angular-html.mjs');\n break;\n case 'angular-ts':\n languageModule = await import('shiki/langs/angular-ts.mjs');\n break;\n case 'astro':\n languageModule = await import('shiki/langs/astro.mjs');\n break;\n case 'typescript':\n case 'ts':\n languageModule = await import('shiki/langs/typescript.mjs');\n break;\n case 'javascript':\n case 'js':\n case 'cjs':\n case 'mjs':\n languageModule = await import('shiki/langs/javascript.mjs');\n break;\n case 'bash':\n case 'sh':\n case 'shell':\n languageModule = await import('shiki/langs/bash.mjs');\n break;\n case 'json':\n languageModule = await import('shiki/langs/json.mjs');\n break;\n case 'jsonc':\n case 'json5':\n languageModule = await import('shiki/langs/json5.mjs');\n break;\n case 'tsx':\n languageModule = await import('shiki/langs/tsx.mjs');\n break;\n case 'vue':\n languageModule = await import('shiki/langs/vue.mjs');\n break;\n case 'html':\n languageModule = await import('shiki/langs/html.mjs');\n break;\n case 'xml':\n languageModule = await import('shiki/langs/xml.mjs');\n break;\n case 'yml':\n case 'yaml':\n languageModule = await import('shiki/langs/yaml.mjs');\n break;\n default:\n languageModule = await import('shiki/langs/typescript.mjs');\n break;\n }\n\n const language = languageModule.default;\n languageCache.set(lang, language);\n return language;\n};\n\n// Lazy load theme modules\nconst loadTheme = async (themeName: BundledTheme): Promise<any> => {\n if (themeCache.has(themeName)) return themeCache.get(themeName);\n\n let themeModule: any;\n switch (themeName) {\n case 'github-dark':\n themeModule = await import('shiki/themes/github-dark.mjs');\n break;\n case 'github-light':\n default:\n themeModule = await import('shiki/themes/github-light.mjs');\n break;\n }\n\n const theme = themeModule.default;\n themeCache.set(themeName, theme);\n return theme;\n};\n\n// Singleton Highlighter Instance\nlet highlighterPromise: Promise<HighlighterGeneric<any, any>> | null = null;\n\nconst getHighlighterInstance = async () => {\n if (!highlighterPromise) {\n highlighterPromise = import('shiki/bundle/web').then(\n ({ createHighlighter }) =>\n createHighlighter({\n langs: [],\n themes: [],\n })\n );\n }\n return highlighterPromise;\n};\n\n// Create a promise for highlighting\nconst highlightCode = async (\n code: string,\n lang: BundledLanguage,\n isDarkMode?: boolean\n): Promise<string> => {\n const themeName: BundledTheme = isDarkMode ? 'github-dark' : 'github-light';\n\n // Load highlighter, language, and theme in parallel\n const [highlighter, languageModule, themeModule] = await Promise.all([\n getHighlighterInstance(),\n loadLanguage(lang),\n loadTheme(themeName),\n ]);\n\n // Load into the singleton instance if not already loaded\n if (!highlighter.getLoadedLanguages().includes(lang)) {\n await highlighter.loadLanguage(languageModule);\n }\n if (!highlighter.getLoadedThemes().includes(themeName)) {\n await highlighter.loadTheme(themeModule);\n }\n\n return highlighter.codeToHtml(code, {\n lang,\n theme: themeName,\n });\n};\n\nexport type CodeBlockShikiProps = {\n children: string;\n lang: BundledLanguage;\n isDarkMode?: boolean;\n};\n\nexport const CodeBlockShiki: FC<CodeBlockShikiProps> = ({\n children,\n lang,\n isDarkMode,\n}) => {\n const [html, setHtml] = useState<string | null>(null);\n\n useEffect(() => {\n let isCancelled = false;\n\n highlightCode(children, lang, isDarkMode)\n .then((result) => {\n if (!isCancelled) setHtml(result);\n })\n .catch((error) => {\n console.error('Failed to highlight code:', error);\n if (!isCancelled && html === null) setHtml('');\n });\n\n return () => {\n isCancelled = true;\n };\n }, [children, lang, isDarkMode]);\n\n return (\n <div className=\"min-w-0 max-w-full overflow-auto bg-transparent [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&_pre::-webkit-scrollbar]:hidden [&_pre]:[-ms-overflow-style:none] [&_pre]:[scrollbar-width:none]\">\n {html ? (\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki generates safe HTML for code highlighting\n <div dangerouslySetInnerHTML={{ __html: html }} />\n ) : (\n <CodeDefault>{children}</CodeDefault>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,258 +1,93 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { AutoSizedTextArea } from "./AutoSizeTextArea.mjs";
|
|
3
|
+
import { ContentEditableTextArea } from "./ContentEditableTextArea.mjs";
|
|
5
4
|
import { useEffect, useRef, useState } from "react";
|
|
6
|
-
import {
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
*
|
|
118
|
-
*
|
|
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
|
|
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
|
|
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
|
|
178
|
-
if (
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
textareaRef.current?.setSelectionRange(newCursorPos, newCursorPos);
|
|
45
|
+
editorRef.current?.focus();
|
|
46
|
+
editorRef.current?.setCursorAtOffset(cursor + active.length);
|
|
186
47
|
}, 0);
|
|
187
48
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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"}
|