@vectara/vectara-ui 18.0.1 → 18.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/lib/components/form/codeEditor/CodeEditor.d.ts +9 -1
- package/lib/components/form/codeEditor/CodeEditor.js +60 -41
- package/lib/components/form/input/NumberInput.js +43 -34
- package/lib/components/typography/_text.scss +9 -0
- package/lib/styles/index.css +8 -0
- package/package.json +1 -1
- package/src/docs/pages/codeEditor/CodeEditor.tsx +62 -11
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as monacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
|
1
2
|
import { generateTokensProvider } from "./generateTokensProvider";
|
|
2
3
|
export type CodeEditorColorConfig = {
|
|
3
4
|
token: string;
|
|
@@ -24,11 +25,18 @@ interface Props {
|
|
|
24
25
|
isReadOnly?: boolean;
|
|
25
26
|
height?: string;
|
|
26
27
|
resizable?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Provides autocomplete suggestions. The provider is invoked on trigger characters and on Ctrl+Space,
|
|
30
|
+
* and additionally as the user types when `quickSuggestions` is enabled.
|
|
31
|
+
*/
|
|
32
|
+
completionItemProvider?: monacoTypes.languages.CompletionItemProvider;
|
|
33
|
+
/** When true, suggestions appear as the user types. Defaults to false to match the previous behavior. */
|
|
34
|
+
quickSuggestions?: boolean;
|
|
27
35
|
"data-testid"?: string;
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
38
|
* Generic (to console) code editor that wraps React Monaco Editor.
|
|
31
39
|
* As the need arises, this component can accept props to customize the internal Monaco editor options.
|
|
32
40
|
*/
|
|
33
|
-
export declare const VuiCodeEditor: ({ language, TokensProvider, onChange, placeholder, onError, colorConfig, value, defaultValue, error, isReadOnly, height, resizable, "data-testid": testId }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare const VuiCodeEditor: ({ language: languageProp, TokensProvider, onChange, placeholder, onError, colorConfig, value, defaultValue, error, isReadOnly, height, resizable, completionItemProvider, quickSuggestions, "data-testid": testId }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
34
42
|
export {};
|
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import Editor from "@monaco-editor/react";
|
|
3
3
|
import * as monacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
|
4
|
-
import { useRef } from "react";
|
|
4
|
+
import { useEffect, useId, useRef } from "react";
|
|
5
|
+
import { VuiText } from "../../typography/Text";
|
|
6
|
+
import { VuiTextColor } from "../../typography/TextColor";
|
|
7
|
+
import { VuiSpacer } from "../../spacer/Spacer";
|
|
8
|
+
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
9
|
+
// Mac vs Linux/Windows
|
|
10
|
+
const SUGGEST_SHORTCUT_LABEL = isMac ? "⌃ Space" : "Ctrl + Space";
|
|
5
11
|
/**
|
|
6
12
|
* Generic (to console) code editor that wraps React Monaco Editor.
|
|
7
13
|
* As the need arises, this component can accept props to customize the internal Monaco editor options.
|
|
8
14
|
*/
|
|
9
|
-
export const VuiCodeEditor = ({ language, TokensProvider, onChange, placeholder, onError = (errors) => {
|
|
15
|
+
export const VuiCodeEditor = ({ language: languageProp, TokensProvider, onChange, placeholder, onError = (errors) => {
|
|
10
16
|
/*noop*/
|
|
11
|
-
}, colorConfig, value, defaultValue, error, isReadOnly, height = "300px", resizable = false, "data-testid": testId }) => {
|
|
17
|
+
}, colorConfig, value, defaultValue, error, isReadOnly, height = "300px", resizable = false, completionItemProvider, quickSuggestions = false, "data-testid": testId }) => {
|
|
18
|
+
const instanceId = useId().replace(/:/g, "");
|
|
19
|
+
const language = `${languageProp}-${instanceId}`;
|
|
12
20
|
const tokensProvider = TokensProvider ? new TokensProvider() : null;
|
|
13
21
|
const monacoRef = useRef(null);
|
|
14
22
|
const modelRef = useRef(null);
|
|
23
|
+
const completionProviderRef = useRef(completionItemProvider);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
completionProviderRef.current = completionItemProvider;
|
|
26
|
+
}, [completionItemProvider]);
|
|
15
27
|
if (modelRef.current && monacoRef.current) {
|
|
16
28
|
if (error) {
|
|
17
29
|
const markers = [
|
|
@@ -36,6 +48,13 @@ export const VuiCodeEditor = ({ language, TokensProvider, onChange, placeholder,
|
|
|
36
48
|
if (tokensProvider) {
|
|
37
49
|
monaco.languages.setTokensProvider(language, tokensProvider);
|
|
38
50
|
}
|
|
51
|
+
if (completionProviderRef.current) {
|
|
52
|
+
monaco.languages.registerCompletionItemProvider(language, {
|
|
53
|
+
triggerCharacters: completionProviderRef.current.triggerCharacters,
|
|
54
|
+
provideCompletionItems: (model, position, context, token) => { var _a, _b; return (_b = (_a = completionProviderRef.current) === null || _a === void 0 ? void 0 : _a.provideCompletionItems(model, position, context, token)) !== null && _b !== void 0 ? _b : { suggestions: [] }; },
|
|
55
|
+
resolveCompletionItem: (item, token) => { var _a, _b, _c; return (_c = (_b = (_a = completionProviderRef.current) === null || _a === void 0 ? void 0 : _a.resolveCompletionItem) === null || _b === void 0 ? void 0 : _b.call(_a, item, token)) !== null && _c !== void 0 ? _c : item; }
|
|
56
|
+
});
|
|
57
|
+
}
|
|
39
58
|
monaco.editor.defineTheme("vectaraEditor", {
|
|
40
59
|
base: "vs",
|
|
41
60
|
inherit: true,
|
|
@@ -58,41 +77,41 @@ export const VuiCodeEditor = ({ language, TokensProvider, onChange, placeholder,
|
|
|
58
77
|
monaco.editor.onDidCreateModel((model) => (modelRef.current = model));
|
|
59
78
|
};
|
|
60
79
|
const shouldShowPlaceholder = placeholder && (!value || value.trim() === "") && (!defaultValue || defaultValue.trim() === "");
|
|
61
|
-
return (_jsxs("div", Object.assign({ className: "vuiCodeEditor", "data-testid": testId, style: resizable ? { minHeight: height, height, resize: "vertical", overflow: "auto" } : undefined }, { children: [shouldShowPlaceholder && (_jsx("div", Object.assign({ className: "vuiCodeEditor-placeholder", "aria-hidden": "true" }, { children: placeholder }))), _jsx(Editor, { beforeMount: init, height: resizable ? "100%" : height, width: "100%", defaultLanguage: language, defaultValue: defaultValue, value: value, onChange: onChange, onValidate: (markers) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
80
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", Object.assign({ className: "vuiCodeEditor", "data-testid": testId, style: resizable ? { minHeight: height, height, resize: "vertical", overflow: "auto" } : undefined }, { children: [shouldShowPlaceholder && (_jsx("div", Object.assign({ className: "vuiCodeEditor-placeholder", "aria-hidden": "true" }, { children: placeholder }))), _jsx(Editor, { beforeMount: init, height: resizable ? "100%" : height, width: "100%", defaultLanguage: language, defaultValue: defaultValue, value: value, onChange: onChange, onValidate: (markers) => {
|
|
81
|
+
const errors = markers.reduce((acc, marker) => {
|
|
82
|
+
if (marker.severity === monacoTypes.MarkerSeverity.Error) {
|
|
83
|
+
acc.push({
|
|
84
|
+
startLine: marker.startLineNumber,
|
|
85
|
+
endLine: marker.endLineNumber,
|
|
86
|
+
startColumn: marker.startColumn,
|
|
87
|
+
endColumn: marker.endColumn,
|
|
88
|
+
message: marker.message
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return acc;
|
|
92
|
+
}, []);
|
|
93
|
+
if (errors.length) {
|
|
94
|
+
onError(errors);
|
|
95
|
+
}
|
|
96
|
+
}, theme: "vectaraEditor", options: {
|
|
97
|
+
readOnly: isReadOnly,
|
|
98
|
+
fontSize: 12,
|
|
99
|
+
automaticLayout: true,
|
|
100
|
+
renderLineHighlight: "none",
|
|
101
|
+
lineNumbers: "off",
|
|
102
|
+
glyphMargin: false,
|
|
103
|
+
folding: false,
|
|
104
|
+
lineDecorationsWidth: 2,
|
|
105
|
+
fixedOverflowWidgets: true,
|
|
106
|
+
quickSuggestions: {
|
|
107
|
+
comments: false,
|
|
108
|
+
strings: false,
|
|
109
|
+
other: quickSuggestions
|
|
110
|
+
},
|
|
111
|
+
suggestOnTriggerCharacters: true,
|
|
112
|
+
minimap: {
|
|
113
|
+
enabled: false
|
|
114
|
+
},
|
|
115
|
+
wordWrap: "on"
|
|
116
|
+
} })] })), completionItemProvider && (_jsxs(_Fragment, { children: [_jsx(VuiSpacer, { size: "xs" }), _jsx(VuiText, Object.assign({ size: "xs" }, { children: _jsx("p", { children: _jsxs(VuiTextColor, Object.assign({ color: "subdued" }, { children: [_jsx("kbd", { children: SUGGEST_SHORTCUT_LABEL }), " shows suggestions."] })) }) }))] }))] }));
|
|
98
117
|
};
|
|
@@ -10,52 +10,61 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
-
import { forwardRef, useEffect, useState } from "react";
|
|
13
|
+
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
14
14
|
import { VuiBasicInput } from "./BasicInput";
|
|
15
15
|
export const VuiNumberInput = forwardRef((_a, ref) => {
|
|
16
16
|
var { value, onChange, max, min, step, allowUndefined } = _a, rest = __rest(_a, ["value", "onChange", "max", "min", "step", "allowUndefined"]);
|
|
17
|
+
// localValue (rather than binding to `value` directly) sidesteps a
|
|
18
|
+
// Firefox quirk: `<input type="number">` reports "" mid-decimal when the
|
|
19
|
+
// the user types "1,0", which would round-trip through the parent and
|
|
20
|
+
// erase the user's input.
|
|
17
21
|
const [localValue, setLocalValue] = useState(value);
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
// 2. Types 1.0
|
|
23
|
-
// will have the input show "0" as soon as they enter a decimal point.
|
|
24
|
-
// When that character is entered, onChange is called with undefined.
|
|
25
|
-
// This value gets stored in the value state, which resets the value to 0.
|
|
26
|
-
// For some reason, using a useState hook to store the value doesn't have
|
|
27
|
-
// this problem.
|
|
22
|
+
// Last value exchanged with the parent. The resync effect ignores echoes
|
|
23
|
+
// of our own emits — without it, a stale prop ("6" arriving after the
|
|
24
|
+
// user has already typed "65") would clobber the in-flight edit.
|
|
25
|
+
const lastSyncedRef = useRef(value);
|
|
28
26
|
useEffect(() => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// When allowUndefined is on, also ignore undefined — otherwise the
|
|
32
|
-
// parent reflecting undefined back would clear the input mid-typing.
|
|
33
|
-
const isUndefined = !(allowUndefined && value === undefined);
|
|
34
|
-
if (value !== 0 && isUndefined) {
|
|
27
|
+
if (value !== lastSyncedRef.current) {
|
|
28
|
+
lastSyncedRef.current = value;
|
|
35
29
|
setLocalValue(value);
|
|
36
30
|
}
|
|
37
31
|
}, [value]);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
onChange(allowUndefined ? localValue : localValue !== null && localValue !== void 0 ? localValue : 0);
|
|
44
|
-
}, [localValue]);
|
|
32
|
+
const emit = (next) => {
|
|
33
|
+
const outgoing = allowUndefined ? next : next !== null && next !== void 0 ? next : 0;
|
|
34
|
+
lastSyncedRef.current = outgoing;
|
|
35
|
+
onChange(outgoing);
|
|
36
|
+
};
|
|
45
37
|
const onChangeValue = (e) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
if (e.target.value === "") {
|
|
39
|
+
setLocalValue(undefined);
|
|
40
|
+
emit(undefined);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
49
43
|
const numberValue = Number(e.target.value);
|
|
50
|
-
if (isNaN(numberValue))
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
if (isNaN(numberValue)) {
|
|
45
|
+
setLocalValue(undefined);
|
|
46
|
+
emit(undefined);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setLocalValue(numberValue);
|
|
50
|
+
emit(numberValue);
|
|
53
51
|
};
|
|
54
52
|
const onBlur = () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
// Clamp against the effective emitted value so empty + !allowUndefined
|
|
54
|
+
// (which emits 0) still clamps to min.
|
|
55
|
+
const current = allowUndefined ? localValue : localValue !== null && localValue !== void 0 ? localValue : 0;
|
|
56
|
+
if (current === undefined)
|
|
57
|
+
return;
|
|
58
|
+
if (min !== undefined && current < min) {
|
|
59
|
+
// Clamp min.
|
|
60
|
+
setLocalValue(min);
|
|
61
|
+
emit(min);
|
|
62
|
+
}
|
|
63
|
+
else if (max !== undefined && current > max) {
|
|
64
|
+
// Clamp max.
|
|
65
|
+
setLocalValue(max);
|
|
66
|
+
emit(max);
|
|
67
|
+
}
|
|
59
68
|
};
|
|
60
69
|
const props = Object.assign({ type: "number", value: localValue !== null && localValue !== void 0 ? localValue : "", onChange: onChangeValue, onBlur,
|
|
61
70
|
max,
|
|
@@ -70,6 +70,15 @@ $textRhythm: $sizeM;
|
|
|
70
70
|
background-color: transparent;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
kbd {
|
|
74
|
+
background-color: var(--vui-color-light-shade);
|
|
75
|
+
border: 1px solid var(--vui-color-border-medium);
|
|
76
|
+
border-radius: $sizeXxs;
|
|
77
|
+
padding: 0 $sizeXxs;
|
|
78
|
+
font-family: var(--vui-font-family-monospace);
|
|
79
|
+
font-size: 0.9em;
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
// Inline styles when nested in text-like elements.
|
|
74
83
|
strong,
|
|
75
84
|
b,
|
package/lib/styles/index.css
CHANGED
|
@@ -6132,6 +6132,14 @@ h2.react-datepicker__current-month {
|
|
|
6132
6132
|
padding: 0;
|
|
6133
6133
|
background-color: transparent;
|
|
6134
6134
|
}
|
|
6135
|
+
.vuiText kbd {
|
|
6136
|
+
background-color: var(--vui-color-light-shade);
|
|
6137
|
+
border: 1px solid var(--vui-color-border-medium);
|
|
6138
|
+
border-radius: 4px;
|
|
6139
|
+
padding: 0 4px;
|
|
6140
|
+
font-family: var(--vui-font-family-monospace);
|
|
6141
|
+
font-size: 0.9em;
|
|
6142
|
+
}
|
|
6135
6143
|
.vuiText strong > pre,
|
|
6136
6144
|
.vuiText strong > code,
|
|
6137
6145
|
.vuiText b > pre,
|
package/package.json
CHANGED
|
@@ -1,23 +1,74 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import
|
|
2
|
+
import * as monacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
|
3
|
+
import { VuiCodeEditor, VuiSpacer } from "../../../lib";
|
|
3
4
|
|
|
4
5
|
const placeholder = `{
|
|
5
6
|
"team": "engineering"
|
|
6
7
|
}`;
|
|
7
8
|
|
|
9
|
+
const FIELDS = ["doc.title", "doc.author", "doc.date", "doc.tags"];
|
|
10
|
+
const OPERATORS = ["=", "!=", ">", "<", "LIKE", "AND", "OR"];
|
|
11
|
+
|
|
12
|
+
const completionItemProvider: monacoTypes.languages.CompletionItemProvider = {
|
|
13
|
+
triggerCharacters: ["."],
|
|
14
|
+
provideCompletionItems: (model, position) => {
|
|
15
|
+
const word = model.getWordUntilPosition(position);
|
|
16
|
+
const range = {
|
|
17
|
+
startLineNumber: position.lineNumber,
|
|
18
|
+
endLineNumber: position.lineNumber,
|
|
19
|
+
startColumn: word.startColumn,
|
|
20
|
+
endColumn: word.endColumn
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const suggestions: Array<monacoTypes.languages.CompletionItem> = [
|
|
24
|
+
...FIELDS.map((field) => ({
|
|
25
|
+
label: field,
|
|
26
|
+
kind: monacoTypes.languages.CompletionItemKind.Field,
|
|
27
|
+
insertText: field,
|
|
28
|
+
range
|
|
29
|
+
})),
|
|
30
|
+
...OPERATORS.map((operator) => ({
|
|
31
|
+
label: operator,
|
|
32
|
+
kind: monacoTypes.languages.CompletionItemKind.Operator,
|
|
33
|
+
insertText: operator,
|
|
34
|
+
range
|
|
35
|
+
}))
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
return { suggestions };
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
8
42
|
export const CodeEditor = () => {
|
|
9
43
|
const [value, setValue] = useState<string>("");
|
|
44
|
+
const [filter, setFilter] = useState<string>("");
|
|
10
45
|
|
|
11
46
|
return (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
47
|
+
<>
|
|
48
|
+
<VuiCodeEditor
|
|
49
|
+
resizable
|
|
50
|
+
language="json"
|
|
51
|
+
onChange={(newValue?: string) => {
|
|
52
|
+
setValue(newValue ?? "");
|
|
53
|
+
}}
|
|
54
|
+
placeholder={placeholder}
|
|
55
|
+
value={value}
|
|
56
|
+
data-testid="codeEditor"
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<VuiSpacer size="m" />
|
|
60
|
+
|
|
61
|
+
<VuiCodeEditor
|
|
62
|
+
language="filter"
|
|
63
|
+
onChange={(newValue?: string) => {
|
|
64
|
+
setFilter(newValue ?? "");
|
|
65
|
+
}}
|
|
66
|
+
placeholder='doc.title LIKE "%intro%"'
|
|
67
|
+
value={filter}
|
|
68
|
+
completionItemProvider={completionItemProvider}
|
|
69
|
+
quickSuggestions
|
|
70
|
+
data-testid="filterEditor"
|
|
71
|
+
/>
|
|
72
|
+
</>
|
|
22
73
|
);
|
|
23
74
|
};
|