@gooddata/sdk-ui-kit 11.52.0 → 11.53.0-alpha.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.
Files changed (67) hide show
  1. package/esm/@ui/UiConfigEditor/ConfigEditorToolbar.d.ts +26 -0
  2. package/esm/@ui/UiConfigEditor/ConfigEditorToolbar.d.ts.map +1 -0
  3. package/esm/@ui/UiConfigEditor/ConfigEditorToolbar.js +38 -0
  4. package/esm/@ui/UiConfigEditor/UiConfigEditor.d.ts +20 -0
  5. package/esm/@ui/UiConfigEditor/UiConfigEditor.d.ts.map +1 -0
  6. package/esm/@ui/UiConfigEditor/UiConfigEditor.js +229 -0
  7. package/esm/@ui/UiConfigEditor/configEditorGrammars.d.ts +15 -0
  8. package/esm/@ui/UiConfigEditor/configEditorGrammars.d.ts.map +1 -0
  9. package/esm/@ui/UiConfigEditor/configEditorGrammars.js +50 -0
  10. package/esm/@ui/UiConfigEditor/configEditorHighlighting.d.ts +6 -0
  11. package/esm/@ui/UiConfigEditor/configEditorHighlighting.d.ts.map +1 -0
  12. package/esm/@ui/UiConfigEditor/configEditorHighlighting.js +31 -0
  13. package/esm/@ui/UiConfigEditor/configEditorLanguage.d.ts +43 -0
  14. package/esm/@ui/UiConfigEditor/configEditorLanguage.d.ts.map +1 -0
  15. package/esm/@ui/UiConfigEditor/configEditorLanguage.js +179 -0
  16. package/esm/@ui/UiConfigEditor/types.d.ts +135 -0
  17. package/esm/@ui/UiConfigEditor/types.d.ts.map +1 -0
  18. package/esm/@ui/UiConfigEditor/types.js +2 -0
  19. package/esm/@ui/UiMenu/components/defaults/DefaultUiMenuHeader.d.ts.map +1 -1
  20. package/esm/@ui/UiMenu/components/defaults/DefaultUiMenuHeader.js +5 -2
  21. package/esm/@ui/UiSubmenuHeader/UiSubmenuHeader.d.ts +2 -2
  22. package/esm/@ui/UiSubmenuHeader/UiSubmenuHeader.d.ts.map +1 -1
  23. package/esm/@ui/UiSubmenuHeader/UiSubmenuHeader.js +1 -0
  24. package/esm/Dialog/StylingEditorDialog/StylingEditorDialog.d.ts +13 -0
  25. package/esm/Dialog/StylingEditorDialog/StylingEditorDialog.d.ts.map +1 -1
  26. package/esm/Dialog/StylingEditorDialog/StylingEditorDialog.js +19 -4
  27. package/esm/List/DateDatasetsListItem.d.ts.map +1 -1
  28. package/esm/List/DateDatasetsListItem.js +1 -2
  29. package/esm/index.d.ts +5 -1
  30. package/esm/index.d.ts.map +1 -1
  31. package/esm/index.js +2 -1
  32. package/esm/sdk-ui-kit.d.ts +204 -36
  33. package/esm/syntaxHighlightingInput/SyntaxHighlightingInput.d.ts +7 -1
  34. package/esm/syntaxHighlightingInput/SyntaxHighlightingInput.d.ts.map +1 -1
  35. package/esm/syntaxHighlightingInput/SyntaxHighlightingInput.js +2 -1
  36. package/esm/syntaxHighlightingInput/hooks/useChangeHandler.d.ts.map +1 -1
  37. package/esm/syntaxHighlightingInput/hooks/useChangeHandler.js +9 -2
  38. package/esm/syntaxHighlightingInput/hooks/useCodemirror.d.ts +4 -1
  39. package/esm/syntaxHighlightingInput/hooks/useCodemirror.d.ts.map +1 -1
  40. package/esm/syntaxHighlightingInput/hooks/useCodemirror.js +2 -2
  41. package/esm/syntaxHighlightingInput/hooks/useCodemirrorChange.d.ts +12 -1
  42. package/esm/syntaxHighlightingInput/hooks/useCodemirrorChange.d.ts.map +1 -1
  43. package/esm/syntaxHighlightingInput/hooks/useCodemirrorChange.js +10 -8
  44. package/esm/syntaxHighlightingInput/hooks/useCodemirrorEditable.d.ts.map +1 -1
  45. package/esm/syntaxHighlightingInput/hooks/useCodemirrorEditable.js +11 -4
  46. package/esm/syntaxHighlightingInput/hooks/useCodemirrorKeymap.d.ts +1 -1
  47. package/esm/syntaxHighlightingInput/hooks/useCodemirrorKeymap.d.ts.map +1 -1
  48. package/esm/syntaxHighlightingInput/hooks/useCodemirrorKeymap.js +26 -22
  49. package/esm/syntaxHighlightingInput/syntaxErrorLinter.d.ts +24 -0
  50. package/esm/syntaxHighlightingInput/syntaxErrorLinter.d.ts.map +1 -0
  51. package/esm/syntaxHighlightingInput/syntaxErrorLinter.js +49 -0
  52. package/package.json +13 -11
  53. package/src/@ui/UiConfigEditor/UiConfigEditor.scss +169 -0
  54. package/src/@ui/UiMenu/UiMenu.scss +6 -0
  55. package/src/@ui/UiSubmenuHeader/UiSubmenuHeader.scss +15 -0
  56. package/src/@ui/index.scss +1 -0
  57. package/styles/css/main.css +132 -0
  58. package/styles/css/main.css.map +1 -1
  59. package/styles/css/stylingEditorDialog.css +5 -0
  60. package/styles/css/stylingEditorDialog.css.map +1 -1
  61. package/styles/scss/stylingEditorDialog.scss +8 -0
  62. package/esm/syntaxHighlightingInput/YamlEditor.d.ts +0 -35
  63. package/esm/syntaxHighlightingInput/YamlEditor.d.ts.map +0 -1
  64. package/esm/syntaxHighlightingInput/YamlEditor.js +0 -32
  65. package/esm/syntaxHighlightingInput/yamlSyntaxLinter.d.ts +0 -12
  66. package/esm/syntaxHighlightingInput/yamlSyntaxLinter.d.ts.map +0 -1
  67. package/esm/syntaxHighlightingInput/yamlSyntaxLinter.js +0 -32
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ConfigEditorLanguage } from "./configEditorLanguage.js";
3
+ import { type IUiConfigEditorContextMenuProps, type IUiConfigEditorLabels } from "./types.js";
4
+ interface IConfigEditorToolbarProps {
5
+ languages: ConfigEditorLanguage[];
6
+ displayLanguage: ConfigEditorLanguage;
7
+ /** Called when the user picks a language on this toolbar (as opposed to a remote prop change). */
8
+ onSelectLanguage: (next: ConfigEditorLanguage) => void;
9
+ labels: IUiConfigEditorLabels | undefined;
10
+ /** `undefined` when there is nothing to re-format, which disables the action. */
11
+ canAutoFormat: boolean;
12
+ onAutoFormat: () => void;
13
+ renderContextMenu?: (props: IUiConfigEditorContextMenuProps) => ReactNode;
14
+ readOnly: boolean;
15
+ disabled: boolean;
16
+ /** Scoped to the editor instance so several editors' radio groups do not merge. */
17
+ languageGroupName: string;
18
+ contextMenuButtonId: string;
19
+ }
20
+ /**
21
+ * The editor's fixed-height toolbar: the language radio group (only when more than one language is
22
+ * offered), the Auto-format action, and the optional context-menu button with its popover.
23
+ */
24
+ export declare function ConfigEditorToolbar({ languages, displayLanguage, onSelectLanguage, labels, canAutoFormat, onAutoFormat, renderContextMenu, readOnly, disabled, languageGroupName, contextMenuButtonId }: IConfigEditorToolbarProps): import("react/jsx-runtime").JSX.Element;
25
+ export {};
26
+ //# sourceMappingURL=ConfigEditorToolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigEditorToolbar.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiConfigEditor/ConfigEditorToolbar.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAQjD,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,KAAK,+BAA+B,EAAE,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAI9F,UAAU,yBAAyB;IAC/B,SAAS,EAAE,oBAAoB,EAAE,CAAC;IAClC,eAAe,EAAE,oBAAoB,CAAC;IACtC,kGAAkG;IAClG,gBAAgB,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACvD,MAAM,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAC1C,iFAAiF;IACjF,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,SAAS,CAAC;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,iBAAiB,EACjB,mBAAmB,EACtB,EAAE,yBAAyB,2CAmF3B"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { bem } from "../@utils/bem.js";
3
+ import { UiButton } from "../UiButton/UiButton.js";
4
+ import { UiDropdown } from "../UiDropdown/UiDropdown.js";
5
+ import { UiIconButton } from "../UiIconButton/UiIconButton.js";
6
+ import { UiRadio } from "../UiRadio/UiRadio.js";
7
+ const { e } = bem("gd-ui-kit-config-editor");
8
+ /**
9
+ * The editor's fixed-height toolbar: the language radio group (only when more than one language is
10
+ * offered), the Auto-format action, and the optional context-menu button with its popover.
11
+ */
12
+ export function ConfigEditorToolbar({ languages, displayLanguage, onSelectLanguage, labels, canAutoFormat, onAutoFormat, renderContextMenu, readOnly, disabled, languageGroupName, contextMenuButtonId, }) {
13
+ const languageLabel = (language) => labels?.languages?.[language] ?? language.toUpperCase();
14
+ return (_jsxs("div", { className: e("toolbar"), children: [languages.length > 1 ? (_jsx("div", { className: e("languages"), role: "radiogroup", "aria-label": labels?.languageSwitcher ?? "Editor language", children: languages.map((radioLanguage) => (_jsx(UiRadio, {
15
+ // One `name` across all of them, so they behave as one native radio
16
+ // group: arrow keys move between them and only one can be chosen.
17
+ name: languageGroupName, value: radioLanguage, label: languageLabel(radioLanguage), checked: displayLanguage === radioLanguage, disabled: disabled, onChange: () => onSelectLanguage(radioLanguage), dataTestId: `s-config-editor-language-${radioLanguage}` }, radioLanguage))) })) : null, _jsxs("div", { className: e("actions", { withMenu: !!renderContextMenu }), children: [
18
+ _jsx(UiButton, { size: "medium", variant: "linkDimmed", label: labels?.autoFormat ?? "Auto-format",
19
+ // Nothing to re-format when the draft is blank or cannot be parsed, so the
20
+ // action is disabled rather than silently doing nothing.
21
+ isDisabled: disabled || readOnly || !canAutoFormat, onClick: onAutoFormat, dataTestId: "s-config-editor-auto-format" }), renderContextMenu ? (_jsx(UiDropdown, { placement: "bottom-end",
22
+ // The dropdown positions against the button, but the design hangs the
23
+ // menu from the toolbar's bottom edge: the 32px button centered in the
24
+ // 36px toolbar leaves 2px below it, plus the divider's 1px.
25
+ offset: 3, closeOnEscape: true, closeOnOutsideClick: true, autofocusOnOpen: true,
26
+ // The popup role is deliberately left at its default. Setting it to
27
+ // "menu" would put `role="menu"` on the positioning wrapper as well, and
28
+ // a menu nested directly inside another menu is invalid — the content is
29
+ // expected to be a `UiMenu`, which brings that role itself.
30
+ renderButton: ({ ref, toggleDropdown, isOpen, ariaAttributes }) => (_jsx(UiIconButton, { ref: ref, id: contextMenuButtonId, icon: "ellipsis", size: "medium", variant: "tertiary", isActive: isOpen, isDisabled: disabled, onClick: toggleDropdown, label: labels?.contextMenu ?? "More actions", accessibilityConfig: { ariaLabel: labels?.contextMenu ?? "More actions" }, ariaAttributes: ariaAttributes, dataTestId: "s-config-editor-context-menu-button" })), renderBody: ({ closeDropdown, ariaAttributes }) => renderContextMenu({
31
+ onClose: closeDropdown,
32
+ ariaAttributes: {
33
+ ...ariaAttributes,
34
+ "aria-labelledby": contextMenuButtonId,
35
+ },
36
+ }) })) : null] })
37
+ ] }));
38
+ }
@@ -0,0 +1,20 @@
1
+ import { type IUiConfigEditorProps } from "./types.js";
2
+ /**
3
+ * A config editor: a CodeMirror source editor with syntax highlighting, line numbers and a lint
4
+ * gutter, plus a toolbar to re-format the text and — when more than one language is offered — to
5
+ * read and edit the same value as either JSON or YAML.
6
+ *
7
+ * The primary language is always the value in and the value out. Any other offered language is a
8
+ * convenience projection converted on the fly, so a caller never has to know which language the user
9
+ * happens to be looking at. A single-language editor never converts at all: what is typed is what is
10
+ * emitted, byte for byte, so YAML comments and formatting the caller cares about survive.
11
+ *
12
+ * Switching language re-emits the value in canonical form, which is what makes switching to YAML and
13
+ * back re-format the JSON. The canonical form matches the `JSON.stringify(value, null, 4)` that
14
+ * callers conventionally seed the field with, so a switch on an untouched value is a no-op rather
15
+ * than something that marks a form dirty.
16
+ *
17
+ * @internal
18
+ */
19
+ export declare function UiConfigEditor({ value, onChange, primaryLanguage, languages, language, onLanguageChange, completionSource, labels, renderContextMenu, label, placeholder, readOnly, disabled, rows, dataTestId, editorRef }: IUiConfigEditorProps): import("react/jsx-runtime").JSX.Element;
20
+ //# sourceMappingURL=UiConfigEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UiConfigEditor.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiConfigEditor/UiConfigEditor.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAIvD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,eAAwB,EACxB,SAA4B,EAC5B,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,MAAM,EACN,iBAAiB,EACjB,KAAK,EACL,WAAW,EACX,QAAgB,EAChB,QAAgB,EAChB,IAAS,EACT,UAAU,EACV,SAAS,EACZ,EAAE,oBAAoB,2CAwQtB"}
@@ -0,0 +1,229 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
+ import { forceLinting, lintGutter } from "@codemirror/lint";
5
+ import { Compartment } from "@codemirror/state";
6
+ import { lineNumbers } from "@codemirror/view";
7
+ import { createSyntaxErrorLinter } from "../../syntaxHighlightingInput/syntaxErrorLinter.js";
8
+ import { SyntaxHighlightingInput } from "../../syntaxHighlightingInput/SyntaxHighlightingInput.js";
9
+ import { yamlPositionAt } from "../../syntaxHighlightingInput/yamlPosition.js";
10
+ import { bem } from "../@utils/bem.js";
11
+ import { useLanguageGrammar } from "./configEditorGrammars.js";
12
+ import { themedHighlighting } from "./configEditorHighlighting.js";
13
+ import { beautify, convertText, isProjectable } from "./configEditorLanguage.js";
14
+ import { ConfigEditorToolbar } from "./ConfigEditorToolbar.js";
15
+ const { b, e } = bem("gd-ui-kit-config-editor");
16
+ /**
17
+ * A config editor: a CodeMirror source editor with syntax highlighting, line numbers and a lint
18
+ * gutter, plus a toolbar to re-format the text and — when more than one language is offered — to
19
+ * read and edit the same value as either JSON or YAML.
20
+ *
21
+ * The primary language is always the value in and the value out. Any other offered language is a
22
+ * convenience projection converted on the fly, so a caller never has to know which language the user
23
+ * happens to be looking at. A single-language editor never converts at all: what is typed is what is
24
+ * emitted, byte for byte, so YAML comments and formatting the caller cares about survive.
25
+ *
26
+ * Switching language re-emits the value in canonical form, which is what makes switching to YAML and
27
+ * back re-format the JSON. The canonical form matches the `JSON.stringify(value, null, 4)` that
28
+ * callers conventionally seed the field with, so a switch on an untouched value is a no-op rather
29
+ * than something that marks a form dirty.
30
+ *
31
+ * @internal
32
+ */
33
+ export function UiConfigEditor({ value, onChange, primaryLanguage = "json", languages = ["json", "yaml"], language, onLanguageChange, completionSource, labels, renderContextMenu, label, placeholder, readOnly = false, disabled = false, rows = 12, dataTestId, editorRef, }) {
34
+ // Scoped to this instance: several editors can sit on one page, and a shared radio `name` would
35
+ // make their language switchers fight over the same group.
36
+ const instanceId = useId();
37
+ const languageGroupName = `config-editor-language-${instanceId}`;
38
+ const contextMenuButtonId = `config-editor-menu-button-${instanceId}`;
39
+ // Only a JSON primary may display another language (see `languages` in types.ts), and only
40
+ // one the caller actually offers — a shared, persisted `language` must not trap a
41
+ // single-language editor in a projection it never presents a way out of. Anything else falls
42
+ // back to the primary rather than letting an unsound display in through the back door.
43
+ const displayLanguage = primaryLanguage === "json" &&
44
+ language !== undefined &&
45
+ languages.length > 1 &&
46
+ languages.includes(language)
47
+ ? language
48
+ : primaryLanguage;
49
+ // The switcher needs the full controlled pair: without `language` a selection could never take
50
+ // effect, and the radios would appear to accept it and immediately snap back.
51
+ const isSwitchable = primaryLanguage === "json" && language !== undefined && !!onLanguageChange;
52
+ // The text actually on screen, in the displayed language. `value` stays primary regardless.
53
+ const [draft, setDraft] = useState(() => convertText(value, primaryLanguage, displayLanguage));
54
+ // Bumped to remount the editor when a language switch rewrites the document; a switch that
55
+ // leaves the text byte-identical (an unprojectable draft) keeps the editor instance, so the
56
+ // caret and undo history survive. The compartment swaps the grammar into the live view then.
57
+ const [editorEpoch, setEditorEpoch] = useState(0);
58
+ const draftRef = useRef(draft);
59
+ draftRef.current = draft;
60
+ // The primary-language text last handed to `onChange`, so a controlled parent echoing it back is
61
+ // not mistaken for an outside edit that should overwrite what the user is typing.
62
+ const lastEmittedRef = useRef(value);
63
+ const lastLanguageRef = useRef(displayLanguage);
64
+ // Set when the language change came from this editor's own switcher. The caller may share the
65
+ // language across several mounted editors, and only the one the user actually interacted with
66
+ // may rewrite its value in the new canonical form — the others only re-render their view.
67
+ const languageChangeIsLocalRef = useRef(false);
68
+ const viewRef = useRef(null);
69
+ const onChangeRef = useRef(onChange);
70
+ onChangeRef.current = onChange;
71
+ // Re-derive the draft when the language changes, or when `value` changes from the outside (an
72
+ // example applied, a dialog reset). useLayoutEffect so the new text is in place before paint.
73
+ useLayoutEffect(() => {
74
+ const languageChanged = lastLanguageRef.current !== displayLanguage;
75
+ if (!languageChanged && value === lastEmittedRef.current) {
76
+ return;
77
+ }
78
+ lastLanguageRef.current = displayLanguage;
79
+ const isLocalLanguageChange = languageChanged && languageChangeIsLocalRef.current;
80
+ languageChangeIsLocalRef.current = false;
81
+ const nextDraft = convertText(value, primaryLanguage, displayLanguage);
82
+ lastEmittedRef.current = value;
83
+ if (languageChanged && (nextDraft !== draftRef.current || completionSource !== undefined)) {
84
+ // A rewritten document remounts the editor: undo has nothing coherent to return to.
85
+ // An identical document keeps the running editor (see `editorEpoch`) — unless a
86
+ // completion source is in play, whose wiring is mount-only and per-language, so
87
+ // keeping the instance would leave the old language's completion behavior active.
88
+ setEditorEpoch((epoch) => epoch + 1);
89
+ }
90
+ setDraft(nextDraft);
91
+ // Report the value in the new language's canonical form — projecting through YAML and back
92
+ // is what re-formats sloppy JSON. Done explicitly: the editor's own value sync is annotated
93
+ // `Transaction.remote`, which the change handler skips.
94
+ //
95
+ // Only for a switch made on this editor, though: a caller may share the language across
96
+ // several mounted editors, and the ones that did not participate in the interaction must
97
+ // not rewrite their values (and dirty their forms) as a side effect.
98
+ //
99
+ // Only when the value really parses: text the user is midway through breaking has no
100
+ // faithful projection, and re-reading it in the other language would change its meaning.
101
+ //
102
+ // And never while the value is not the user's to change: in those modes the toggle is only a
103
+ // way to read the value in another language, so it must not rewrite it.
104
+ if (!isLocalLanguageChange || !isProjectable(value, primaryLanguage) || readOnly || disabled) {
105
+ return;
106
+ }
107
+ const nextValue = convertText(nextDraft, displayLanguage, primaryLanguage);
108
+ if (nextValue !== value) {
109
+ lastEmittedRef.current = nextValue;
110
+ onChangeRef.current(nextValue);
111
+ }
112
+ }, [value, primaryLanguage, displayLanguage, readOnly, disabled, completionSource]);
113
+ const emit = useCallback((nextDraft) => {
114
+ const nextValue = convertText(nextDraft, displayLanguage, primaryLanguage);
115
+ lastEmittedRef.current = nextValue;
116
+ onChange(nextValue);
117
+ }, [displayLanguage, primaryLanguage, onChange]);
118
+ // Only real edits arrive here: the editor's change handler skips the remote-annotated value
119
+ // sync, so this component's own draft pushes never echo back as changes.
120
+ const handleDraftChange = useCallback((nextDraft) => {
121
+ setDraft(nextDraft);
122
+ emit(nextDraft);
123
+ }, [emit]);
124
+ const beautified = useMemo(() => beautify(draft, displayLanguage), [draft, displayLanguage]);
125
+ // Already-formatted text disables the action too: emitting an identical value would only make
126
+ // the caller believe something changed — an As-Code dialog would mark its form dirty.
127
+ const canAutoFormat = beautified !== undefined && beautified !== draft;
128
+ const handleBeautify = useCallback(() => {
129
+ if (beautified === undefined || beautified === draft) {
130
+ return;
131
+ }
132
+ setDraft(beautified);
133
+ emit(beautified);
134
+ }, [beautified, draft, emit]);
135
+ const handleSelectLanguage = useCallback((next) => {
136
+ // Marks the coming language-prop change as this editor's own interaction, so the
137
+ // canonical re-emit runs here and not in other editors sharing the language.
138
+ languageChangeIsLocalRef.current = true;
139
+ onLanguageChange?.(next);
140
+ }, [onLanguageChange]);
141
+ // The handle is rebuilt when editability changes rather than reading a ref written during
142
+ // render: an abandoned render must not be able to leave a speculative value behind. Callers
143
+ // read `ref.current` at call time, so a new handle identity costs them nothing.
144
+ useImperativeHandle(editorRef, () => ({
145
+ insertAtCursor: (text) => {
146
+ const view = viewRef.current;
147
+ // CodeMirror's non-editable state only blocks user input, not programmatic
148
+ // dispatches, so a read-only or in-flight editor has to be refused here.
149
+ if (!view || readOnly || disabled) {
150
+ return;
151
+ }
152
+ const { from, to } = view.state.selection.main;
153
+ // CodeMirror places the caret and keeps the insertion in view itself; the resulting
154
+ // document change flows back out through the editor's own change handler.
155
+ view.dispatch({
156
+ changes: { from, to, insert: text },
157
+ selection: { anchor: from + text.length },
158
+ scrollIntoView: true,
159
+ });
160
+ view.focus();
161
+ },
162
+ focus: () => viewRef.current?.focus(),
163
+ }), [readOnly, disabled]);
164
+ // Completion availability is fixed when the editor mounts: the wrapper is installed only if
165
+ // a source is present then, and it dereferences the live ref, so removing the source later
166
+ // would make an invoked completion throw while adding one would do nothing.
167
+ const hasCompletionRef = useRef(completionSource !== undefined);
168
+ useEffect(() => {
169
+ const hasCompletion = completionSource !== undefined;
170
+ if (hasCompletionRef.current !== hasCompletion) {
171
+ hasCompletionRef.current = hasCompletion;
172
+ setEditorEpoch((epoch) => epoch + 1);
173
+ }
174
+ }, [completionSource]);
175
+ const grammar = useLanguageGrammar(displayLanguage);
176
+ // The grammar lives in a compartment so a first-time load can be injected into the running
177
+ // view: remounting instead would throw away whatever the user did while the chunk was on the
178
+ // wire — selection, undo history, focus — and their next keystroke would land at offset 0.
179
+ const grammarCompartmentRef = useRef(new Compartment());
180
+ useEffect(() => {
181
+ const view = viewRef.current;
182
+ if (view) {
183
+ // `grammar ?? []` and not a skip: switching to a language whose grammar is still
184
+ // loading must clear the previous one, or the document keeps being parsed, highlighted
185
+ // and linted as the old language until the chunk arrives — indefinitely, if it fails.
186
+ view.dispatch({
187
+ effects: grammarCompartmentRef.current.reconfigure(grammar ?? []),
188
+ });
189
+ // The reconfigure changes neither the document nor the lint configuration, so the
190
+ // linter would not run again on its own — stale diagnostics from the previous grammar
191
+ // (or from the grammar-less tree) would stay in the gutter.
192
+ forceLinting(view);
193
+ }
194
+ }, [grammar]);
195
+ // Extensions are read once when CodeMirror initialises, so the editor is remounted (keyed on
196
+ // language) to swap grammars. Losing the undo history is reasonable there: the document was
197
+ // just rewritten in a different language, so there is nothing coherent left to undo into.
198
+ const syntaxErrorMessage = labels?.syntaxError;
199
+ const extensions = useMemo(() => [
200
+ grammarCompartmentRef.current.of(grammar ?? []),
201
+ themedHighlighting,
202
+ lineNumbers(),
203
+ lintGutter(),
204
+ ...(syntaxErrorMessage ? [createSyntaxErrorLinter(syntaxErrorMessage)] : []),
205
+ ],
206
+ // Keyed on `grammar` (which changes with the language) rather than the language itself: a
207
+ // running view gets a late-arriving grammar through the effect above, but any later remount
208
+ // (a completion-source toggle, say) is built from this memo — stale, it would mount an
209
+ // editor with no grammar at all.
210
+ [grammar, syntaxErrorMessage]);
211
+ // The completion source speaks in terms of the document's shape — the mapping keys enclosing the
212
+ // position — so the raw CodeMirror context is resolved into that here. YAML only: the position
213
+ // reader walks a YAML parse tree, and no JSON consumer has a completion schema.
214
+ const onCompletion = useMemo(() => completionSource && displayLanguage === "yaml"
215
+ ? (context) => completionSource(context, yamlPositionAt(context.state, context.pos))
216
+ : undefined, [completionSource, displayLanguage]);
217
+ return (_jsxs("div", { className: b({ disabled, readOnly }), style: { "--config-editor-rows": rows }, "data-testid": dataTestId, children: [
218
+ _jsx(ConfigEditorToolbar, { languages: isSwitchable ? languages : [], displayLanguage: displayLanguage, onSelectLanguage: handleSelectLanguage, labels: labels, canAutoFormat: canAutoFormat, onAutoFormat: handleBeautify, renderContextMenu: renderContextMenu, readOnly: readOnly, disabled: disabled, languageGroupName: languageGroupName, contextMenuButtonId: contextMenuButtonId }), _jsx("div", { className: e("editor"), children: _jsx(SyntaxHighlightingInput, { value: draft, onChange: handleDraftChange,
219
+ // An external replace means a new document here (an applied example, a dialog
220
+ // reset), so the cursor goes to its end — a stale clamped offset would drop the
221
+ // next inserted variable at the very start of the fresh text.
222
+ externalChangeSelection: "end", onCompletion: onCompletion, label: label ?? "Code editor", placeholder: placeholder,
223
+ // CodeMirror's non-editable state keeps the content focusable and selectable,
224
+ // which is what both the read-only view and an in-flight save want.
225
+ disabled: readOnly || disabled, extensions: extensions, onApi: (view) => {
226
+ viewRef.current = view;
227
+ } }, editorEpoch) })
228
+ ] }));
229
+ }
@@ -0,0 +1,15 @@
1
+ import { type Extension } from "@codemirror/state";
2
+ import { type ConfigEditorLanguage } from "./configEditorLanguage.js";
3
+ /**
4
+ * Loads the grammar(s) `UiConfigEditor` needs for the given languages ahead of rendering one.
5
+ *
6
+ * Purely an optimization: the editor loads its grammar itself when it first renders, mounting
7
+ * without highlighting for the moment that takes. Callers that know an editor is coming (and
8
+ * tests that assert on a fully initialized editor synchronously) can await this instead.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function preloadUiConfigEditorGrammars(languages?: ConfigEditorLanguage[]): Promise<void>;
13
+ /** The grammar for `language`, loading it on first use; `undefined` until it has arrived. */
14
+ export declare function useLanguageGrammar(language: ConfigEditorLanguage): Extension | undefined;
15
+ //# sourceMappingURL=configEditorGrammars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configEditorGrammars.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiConfigEditor/configEditorGrammars.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AActE;;;;;;;;GAQG;AACH,wBAAsB,6BAA6B,CAC/C,SAAS,GAAE,oBAAoB,EAAqB,GACrD,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,SAAS,GAAG,SAAS,CAwBxF"}
@@ -0,0 +1,50 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { useEffect, useState } from "react";
3
+ /**
4
+ * The grammars are loaded on demand and cached for the page's lifetime: importing them statically
5
+ * would make every consumer carry both, and a single-language editor — the catalog's YAML-only
6
+ * as-code dialogs, say — would ship a JSON grammar it can never display.
7
+ */
8
+ const grammarLoaders = {
9
+ json: async () => (await import("@codemirror/lang-json")).json(),
10
+ yaml: async () => (await import("@codemirror/lang-yaml")).yaml(),
11
+ };
12
+ const grammarCache = new Map();
13
+ /**
14
+ * Loads the grammar(s) `UiConfigEditor` needs for the given languages ahead of rendering one.
15
+ *
16
+ * Purely an optimization: the editor loads its grammar itself when it first renders, mounting
17
+ * without highlighting for the moment that takes. Callers that know an editor is coming (and
18
+ * tests that assert on a fully initialized editor synchronously) can await this instead.
19
+ *
20
+ * @internal
21
+ */
22
+ export async function preloadUiConfigEditorGrammars(languages = ["json", "yaml"]) {
23
+ await Promise.all(languages.map(async (language) => {
24
+ grammarCache.set(language, await grammarLoaders[language]());
25
+ }));
26
+ }
27
+ /** The grammar for `language`, loading it on first use; `undefined` until it has arrived. */
28
+ export function useLanguageGrammar(language) {
29
+ const [, setLoadedCount] = useState(0);
30
+ useEffect(() => {
31
+ if (grammarCache.has(language)) {
32
+ return undefined;
33
+ }
34
+ let disposed = false;
35
+ void grammarLoaders[language]()
36
+ .then((grammar) => {
37
+ grammarCache.set(language, grammar);
38
+ if (!disposed) {
39
+ setLoadedCount((count) => count + 1);
40
+ }
41
+ })
42
+ // A failed chunk load leaves the editor plain but working; nothing is cached, so the
43
+ // next request for this language retries the import.
44
+ .catch(() => { });
45
+ return () => {
46
+ disposed = true;
47
+ };
48
+ }, [language]);
49
+ return grammarCache.get(language);
50
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `Prec.highest` so this wins over the shared highlight style that `useCodemirror` installs: with
3
+ * equal precedence the earlier extension takes effect, and that one is added first.
4
+ */
5
+ export declare const themedHighlighting: import("@codemirror/state").Extension;
6
+ //# sourceMappingURL=configEditorHighlighting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configEditorHighlighting.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiConfigEditor/configEditorHighlighting.ts"],"names":[],"mappings":"AA6BA;;;GAGG;AACH,eAAO,MAAM,kBAAkB,uCAAyD,CAAC"}
@@ -0,0 +1,31 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
3
+ import { Prec } from "@codemirror/state";
4
+ import { tags as t } from "@lezer/highlight";
5
+ /**
6
+ * Token colors expressed as theme variables, so the editor stays legible on a dark theme.
7
+ *
8
+ * The shared highlight style in `useCodemirror` hardcodes light-theme hex colors, which leaves dark
9
+ * strings on a dark background once a theme flips the surface (the editor's background is itself
10
+ * theme-driven). Only the complementary scale and the semantic palette are safe to lean on here:
11
+ * both invert with the theme, so foreground stays foreground.
12
+ */
13
+ const themedHighlightStyle = HighlightStyle.define([
14
+ // Keys read as the primary content of a config file, so they take the foreground colour.
15
+ {
16
+ tag: [t.propertyName, t.definition(t.propertyName), t.variableName],
17
+ color: "var(--gd-palette-complementary-8)",
18
+ },
19
+ { tag: [t.string, t.special(t.string)], color: "var(--gd-palette-primary-base)" },
20
+ {
21
+ tag: [t.number, t.bool, t.null, t.atom, t.keyword],
22
+ color: "var(--gd-palette-warning-base)",
23
+ },
24
+ { tag: [t.punctuation, t.separator, t.bracket, t.meta], color: "var(--gd-palette-complementary-6)" },
25
+ { tag: t.comment, color: "var(--gd-palette-complementary-6)", fontStyle: "italic" },
26
+ ]);
27
+ /**
28
+ * `Prec.highest` so this wins over the shared highlight style that `useCodemirror` installs: with
29
+ * equal precedence the earlier extension takes effect, and that one is added first.
30
+ */
31
+ export const themedHighlighting = Prec.highest(syntaxHighlighting(themedHighlightStyle));
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A language the config editor can display and emit. Every editor instance has a primary language —
3
+ * the one its value is written in — and may offer the other as an on-the-fly projection of it.
4
+ *
5
+ * @internal
6
+ */
7
+ export type ConfigEditorLanguage = "json" | "yaml";
8
+ /**
9
+ * Whether `text` is a value in `language` that can be projected into another language and back.
10
+ *
11
+ * Text the user is midway through breaking is not: re-reading it in another language could give it a
12
+ * different meaning (`foo` is broken JSON but a valid YAML string), so switching language must leave
13
+ * it alone rather than quietly turning it into something that parses. Neither is a comment-only YAML
14
+ * document, whose projection would be empty — losing the comments.
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function isProjectable(text: string, language: ConfigEditorLanguage): boolean;
19
+ /**
20
+ * Converts text from one of the editor's languages into another.
21
+ *
22
+ * Unconvertible text is passed through verbatim rather than swallowed — unparseable text, and a
23
+ * comment-only YAML document, whose projection would be empty. In the display direction that keeps
24
+ * a language switch from silently discarding what is on screen; in the primary direction it hands
25
+ * the caller the raw text, whose own validation is what reports the problem while the user is
26
+ * mid-edit.
27
+ *
28
+ * @internal
29
+ */
30
+ export declare function convertText(text: string, fromLanguage: ConfigEditorLanguage, toLanguage: ConfigEditorLanguage): string;
31
+ /**
32
+ * Re-formats the displayed text in its own language.
33
+ *
34
+ * YAML is re-formatted structurally — indentation and spacing are normalized while comments, blank
35
+ * lines and scalar styles are kept — so a hand-annotated document is tidied, never stripped.
36
+ *
37
+ * Returns `undefined` when there is nothing to format — blank or unparseable input — so the caller
38
+ * can disable the action instead of offering a no-op.
39
+ *
40
+ * @internal
41
+ */
42
+ export declare function beautify(draft: string, language: ConfigEditorLanguage): string | undefined;
43
+ //# sourceMappingURL=configEditorLanguage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configEditorLanguage.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiConfigEditor/configEditorLanguage.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAmHnD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CASnF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,oBAAoB,EAClC,UAAU,EAAE,oBAAoB,GACjC,MAAM,CAWR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAgB1F"}