@workbench-kit/shell-react 0.0.2-prototype.0.2.6 → 0.0.2-prototype.0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/package.json +9 -9
- package/src/{chat-ai-mock-proposals.ts → chat/ai-mock-proposals.ts} +39 -39
- package/src/{chat-command-input.ts → chat/command-input.ts} +52 -52
- package/src/{chat-command-policy.ts → chat/command-policy.ts} +54 -54
- package/src/{chat-command-surface.tsx → chat/command-surface.tsx} +155 -155
- package/src/{use-workbench-chat-command-proposals.ts → chat/use-chat-command-proposals.ts} +194 -194
- package/src/{chat-view-data.ts → chat/view-data.ts} +20 -20
- package/src/{chat-view.tsx → chat/view.tsx} +220 -220
- package/src/{command-inspector-surface.tsx → commands/inspector-surface.tsx} +46 -46
- package/src/{use-workbench-command-descriptors.ts → commands/use-command-descriptors.ts} +41 -41
- package/src/{use-context-key-revision.ts → commands/use-context-key-revision.ts} +18 -18
- package/src/{commands-view-data.ts → commands/view-data.ts} +17 -17
- package/src/{commands-view.tsx → commands/view.tsx} +43 -43
- package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -124
- package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -17
- package/src/devtools/index.ts +11 -11
- package/src/devtools/use-workbench-devtools-snapshot.ts +84 -84
- package/src/devtools/workbench-devtools-snapshot.ts +140 -140
- package/src/devtools/workbench-devtools.css +82 -82
- package/src/{editor-area-dnd.ts → editor/area-dnd.ts} +69 -69
- package/src/{editor-area-model.ts → editor/area-model.ts} +71 -71
- package/src/{editor-area.css → editor/area.css} +454 -454
- package/src/{editor-area.tsx → editor/area.tsx} +676 -676
- package/src/{editor-host-surface.tsx → editor/host-surface.tsx} +631 -634
- package/src/{editor-pane-visibility.ts → editor/pane-visibility.ts} +130 -130
- package/src/{editor-resource.ts → editor/resource.ts} +36 -36
- package/src/{editor-state-storage.ts → editor/state-storage.ts} +182 -193
- package/src/{editor-tab-context-menu.ts → editor/tab-context-menu.ts} +118 -118
- package/src/{use-editor.ts → editor/use-editor.ts} +114 -107
- package/src/{editor-view-providers.tsx → editor/view-providers.tsx} +552 -552
- package/src/{editor-workspace-file-availability.ts → editor/workspace-file-availability.ts} +15 -15
- package/src/{editor-workspace-reconcile.tsx → editor/workspace-reconcile.tsx} +31 -28
- package/src/{explorer-context-menu.ts → explorer/context-menu.ts} +114 -114
- package/src/{createCommandWorkspaceExplorerPort.ts → explorer/create-command-workspace-explorer-port.ts} +86 -86
- package/src/{explorer-reveal.ts → explorer/reveal.ts} +112 -112
- package/src/{search-view-data.ts → explorer/search-view-data.ts} +15 -15
- package/src/{search-view.tsx → explorer/search-view.tsx} +65 -62
- package/src/{use-active-workspace-path.ts → explorer/use-active-workspace-path.ts} +15 -15
- package/src/{explorer-view-data.ts → explorer/view-data.ts} +58 -58
- package/src/{explorer-view.tsx → explorer/view.tsx} +227 -224
- package/src/{extension-context-menu.ts → extensions/context-menu.ts} +52 -52
- package/src/{extension-management-model.ts → extensions/management-model.ts} +313 -313
- package/src/{use-extension-management.ts → extensions/use-extension-management.ts} +241 -193
- package/src/{extensions-view-data.ts → extensions/view-data.ts} +19 -19
- package/src/{extensions-view.tsx → extensions/view.tsx} +63 -38
- package/src/{field-remap-demo.tsx → field-remap/demo.tsx} +18 -18
- package/src/{field-remap-editor-surface.tsx → field-remap/editor-surface.tsx} +36 -36
- package/src/{field-remap-flow-adapter.ts → field-remap/flow-adapter.ts} +240 -240
- package/src/{field-remap-flow.tsx → field-remap/flow.tsx} +369 -369
- package/src/{field-remap-graph.tsx → field-remap/graph.tsx} +559 -559
- package/src/field-remap/jsonata-transform.ts +166 -0
- package/src/{field-remap-panel.tsx → field-remap/panel.tsx} +193 -175
- package/src/{field-remap-samples.ts → field-remap/samples.ts} +352 -352
- package/src/{table-mapping-adapter.ts → field-remap/table-mapping-adapter.ts} +59 -59
- package/src/{field-remap-tree.tsx → field-remap/tree.tsx} +404 -404
- package/src/{field-remap-view-data.ts → field-remap/view-data.ts} +16 -16
- package/src/{field-remap-view.css → field-remap/view.css} +676 -676
- package/src/{field-remap-view.tsx → field-remap/view.tsx} +86 -86
- package/src/index.ts +212 -199
- package/src/{json-form-source-patch.ts → jdw/json-form-source-patch.ts} +323 -323
- package/src/{jdw-lab-view-data.ts → jdw/lab-view-data.ts} +25 -25
- package/src/{jdw-lab-view.css → jdw/lab-view.css} +21 -21
- package/src/{jdw-lab-view.tsx → jdw/lab-view.tsx} +79 -79
- package/src/{jdw-widget-form-view.tsx → jdw/widget-form-view.tsx} +68 -65
- package/src/{jdw-widget-preview-view.tsx → jdw/widget-preview-view.tsx} +54 -51
- package/src/{keybinding-overrides-storage.ts → management/keybinding-overrides-storage.ts} +49 -57
- package/src/{keybinding-management-settings.tsx → management/keybinding-settings.tsx} +16 -16
- package/src/{management-palette-commands.ts → management/palette-commands.ts} +38 -38
- package/src/{preference-settings-storage.ts → management/preference-settings-storage.ts} +50 -58
- package/src/{management-settings-ids.ts → management/settings-ids.ts} +12 -12
- package/src/{management-settings.tsx → management/settings.tsx} +96 -75
- package/src/{use-command-management.ts → management/use-command-management.ts} +191 -191
- package/src/{use-keybinding-management.ts → management/use-keybinding-management.ts} +130 -130
- package/src/{shell-model.tsx → shell/model.tsx} +149 -149
- package/src/{provider.tsx → shell/provider.tsx} +603 -573
- package/src/{shell-secondary-actions.tsx → shell/secondary-actions.tsx} +59 -59
- package/src/{shell-settings-constants.ts → shell/settings-constants.ts} +9 -9
- package/src/{shell-settings.tsx → shell/settings.tsx} +599 -599
- package/src/{shell.tsx → shell/shell.tsx} +680 -649
- package/src/{shell-titlebar-layout-controls.tsx → shell/titlebar-layout-controls.tsx} +4 -4
- package/src/{shell-view-host.tsx → shell/view-host.tsx} +212 -199
- package/src/{workbench-appearance-storage.ts → workbench/appearance-storage.ts} +107 -115
- package/src/{workbench-command-host.tsx → workbench/command-host.tsx} +243 -243
- package/src/{workbench-command-palette.ts → workbench/command-palette.ts} +208 -208
- package/src/{workbench-keybinding-bridge.ts → workbench/keybinding-bridge.ts} +54 -54
- package/src/{workbench-layout-storage.ts → workbench/layout-storage.ts} +134 -145
- package/src/{workbench-profile-modal.tsx → workbench/profile-modal.tsx} +118 -118
- package/src/{workbench-shell-command-registration.ts → workbench/shell-command-registration.ts} +58 -58
- package/src/{workbench-startup-gate.tsx → workbench/startup-gate.tsx} +136 -136
- package/src/{workbench-status-sections.ts → workbench/status-sections.ts} +43 -43
- package/src/{use-persisted-workbench-appearance.ts → workbench/use-persisted-appearance.ts} +33 -33
- package/src/{workbench-user-commands.ts → workbench/user-commands.ts} +46 -46
- package/src/{workspace-view-state.ts → workbench/workspace-view-state.ts} +35 -35
- package/src/jsonata-transform.ts +0 -36
|
@@ -1,599 +1,599 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
2
|
-
import { Badge, Checkbox, Field, Select } from '@workbench-kit/react/primitives';
|
|
3
|
-
import {
|
|
4
|
-
applyWorkbenchAppearance,
|
|
5
|
-
DARK_THEME_PRESET_OPTIONS,
|
|
6
|
-
DEFAULT_SHELL_PRESET,
|
|
7
|
-
LIGHT_THEME_PRESET_OPTIONS,
|
|
8
|
-
SHELL_PRESET_OPTIONS,
|
|
9
|
-
WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS,
|
|
10
|
-
WORKBENCH_APPEARANCE_FIELD_LABELS,
|
|
11
|
-
WORKBENCH_COLOR_SCHEME_OPTIONS,
|
|
12
|
-
type WorkbenchColorSchemePreference,
|
|
13
|
-
type WorkbenchThemePresetOption,
|
|
14
|
-
} from '@workbench-kit/react/workbench';
|
|
15
|
-
import {
|
|
16
|
-
WorkbenchSettingsSection,
|
|
17
|
-
type WorkbenchSettingsCategory,
|
|
18
|
-
} from '@workbench-kit/react/workbench/settings';
|
|
19
|
-
import {
|
|
20
|
-
applyThemeTokenOverrides,
|
|
21
|
-
type ExtensionRegistry,
|
|
22
|
-
type PreferenceService,
|
|
23
|
-
} from '@workbench-kit/workbench-core';
|
|
24
|
-
import type { PreferenceScope } from '@workbench-kit/workbench-config';
|
|
25
|
-
|
|
26
|
-
import { useWorkbench } from './provider.js';
|
|
27
|
-
import { WORKBENCH_PREFERENCE_SCOPES } from './
|
|
28
|
-
|
|
29
|
-
const APPEARANCE_SETTINGS_CATEGORY_ID = 'workbench.appearance';
|
|
30
|
-
|
|
31
|
-
export interface WorkbenchThemeOption {
|
|
32
|
-
description?: ReactNode;
|
|
33
|
-
id: string;
|
|
34
|
-
label: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface WorkbenchLocaleOption {
|
|
38
|
-
id: string;
|
|
39
|
-
label: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface WorkbenchAppearanceSettingsInput {
|
|
43
|
-
darkPreset?: string | undefined;
|
|
44
|
-
lightPreset?: string | undefined;
|
|
45
|
-
locale?: string | undefined;
|
|
46
|
-
localeOptions?: readonly WorkbenchLocaleOption[] | undefined;
|
|
47
|
-
onDarkPresetChange?: ((preset: string) => void) | undefined;
|
|
48
|
-
onLightPresetChange?: ((preset: string) => void) | undefined;
|
|
49
|
-
onLocaleChange?: ((locale: string) => void) | undefined;
|
|
50
|
-
onShellPresetChange?: ((preset: string) => void) | undefined;
|
|
51
|
-
onThemeChange?: ((theme: string) => void) | undefined;
|
|
52
|
-
shellPreset?: string | undefined;
|
|
53
|
-
theme?: string | undefined;
|
|
54
|
-
themeOptions?: readonly WorkbenchThemeOption[] | undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface WorkbenchSettingsCategoryInput extends WorkbenchAppearanceSettingsInput {
|
|
58
|
-
activeScope: PreferenceScope;
|
|
59
|
-
preferenceService: PreferenceService;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function createSettingsCategories(
|
|
63
|
-
extensionRegistry: ExtensionRegistry,
|
|
64
|
-
{
|
|
65
|
-
activeScope,
|
|
66
|
-
darkPreset,
|
|
67
|
-
lightPreset,
|
|
68
|
-
locale,
|
|
69
|
-
onDarkPresetChange,
|
|
70
|
-
onLightPresetChange,
|
|
71
|
-
onLocaleChange,
|
|
72
|
-
onShellPresetChange,
|
|
73
|
-
onThemeChange,
|
|
74
|
-
preferenceService,
|
|
75
|
-
shellPreset,
|
|
76
|
-
theme,
|
|
77
|
-
themeOptions,
|
|
78
|
-
}: WorkbenchSettingsCategoryInput,
|
|
79
|
-
): WorkbenchSettingsCategory[] {
|
|
80
|
-
const configurations = extensionRegistry.configurations.getConfigurations();
|
|
81
|
-
const mergedThemeOptions = mergeThemeOptions(themeOptions, extensionRegistry.themes.getThemes());
|
|
82
|
-
const localeOptions = buildLocaleOptions(extensionRegistry.localizations.getLocalizations());
|
|
83
|
-
const appearanceCategory = createAppearanceSettingsCategory({
|
|
84
|
-
darkPreset,
|
|
85
|
-
lightPreset,
|
|
86
|
-
locale,
|
|
87
|
-
localeOptions,
|
|
88
|
-
onDarkPresetChange,
|
|
89
|
-
onLightPresetChange,
|
|
90
|
-
onLocaleChange,
|
|
91
|
-
onShellPresetChange,
|
|
92
|
-
onThemeChange,
|
|
93
|
-
shellPreset,
|
|
94
|
-
theme,
|
|
95
|
-
themeOptions: mergedThemeOptions,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
if (configurations.length === 0) {
|
|
99
|
-
const fallbackCategory = {
|
|
100
|
-
content: (
|
|
101
|
-
<WorkbenchSettingsSection
|
|
102
|
-
id="workbench-settings-empty"
|
|
103
|
-
title="Workbench"
|
|
104
|
-
description="No extension settings are currently registered."
|
|
105
|
-
>
|
|
106
|
-
<p className="workbench-settings-empty">Enable extensions to contribute settings.</p>
|
|
107
|
-
</WorkbenchSettingsSection>
|
|
108
|
-
),
|
|
109
|
-
id: 'workbench',
|
|
110
|
-
label: 'Workbench',
|
|
111
|
-
} satisfies WorkbenchSettingsCategory;
|
|
112
|
-
|
|
113
|
-
return appearanceCategory ? [appearanceCategory, fallbackCategory] : [fallbackCategory];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const contributedCategories = configurations.map(({ extensionId, configuration }) => {
|
|
117
|
-
const extension = extensionRegistry.getExtension(extensionId);
|
|
118
|
-
const displayName = extension?.manifest.displayName ?? titleFromExtensionId(extensionId);
|
|
119
|
-
const properties = Object.entries(configuration.properties ?? {});
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
content: (
|
|
123
|
-
<WorkbenchSettingsSection
|
|
124
|
-
id={`workbench-settings-${slugId(extensionId)}`}
|
|
125
|
-
title={displayName}
|
|
126
|
-
description={`${properties.length} ${
|
|
127
|
-
properties.length === 1 ? 'setting is' : 'settings are'
|
|
128
|
-
} contributed by ${extensionId}.`}
|
|
129
|
-
>
|
|
130
|
-
{properties.length ? (
|
|
131
|
-
<div className="workbench-settings-contribution-list">
|
|
132
|
-
{properties.map(([key, value]) => (
|
|
133
|
-
<SettingContributionField
|
|
134
|
-
key={key}
|
|
135
|
-
activeScope={activeScope}
|
|
136
|
-
preferenceService={preferenceService}
|
|
137
|
-
propertyKey={key}
|
|
138
|
-
propertyValue={value}
|
|
139
|
-
/>
|
|
140
|
-
))}
|
|
141
|
-
</div>
|
|
142
|
-
) : (
|
|
143
|
-
<p className="workbench-settings-empty">
|
|
144
|
-
This extension registered a configuration section without properties.
|
|
145
|
-
</p>
|
|
146
|
-
)}
|
|
147
|
-
</WorkbenchSettingsSection>
|
|
148
|
-
),
|
|
149
|
-
id: extensionId,
|
|
150
|
-
label: displayName,
|
|
151
|
-
title: extensionId,
|
|
152
|
-
} satisfies WorkbenchSettingsCategory;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
return appearanceCategory
|
|
156
|
-
? [appearanceCategory, ...contributedCategories]
|
|
157
|
-
: contributedCategories;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function formatPreferenceScopeLabel(scope: PreferenceScope): string {
|
|
161
|
-
return WORKBENCH_PREFERENCE_SCOPES.find((candidate) => candidate.id === scope)?.label ?? scope;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function mergeThemeOptions(
|
|
165
|
-
baseOptions: readonly WorkbenchThemeOption[] | undefined,
|
|
166
|
-
contributedThemes: readonly {
|
|
167
|
-
id: string;
|
|
168
|
-
label: string;
|
|
169
|
-
tokenOverrides?: Record<string, string> | undefined;
|
|
170
|
-
}[],
|
|
171
|
-
): readonly WorkbenchThemeOption[] {
|
|
172
|
-
const merged = new Map<string, WorkbenchThemeOption>();
|
|
173
|
-
|
|
174
|
-
for (const option of baseOptions ?? []) {
|
|
175
|
-
merged.set(option.id, option);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (const theme of contributedThemes) {
|
|
179
|
-
merged.set(theme.id, {
|
|
180
|
-
description: theme.tokenOverrides
|
|
181
|
-
? 'Contributed theme with token overrides.'
|
|
182
|
-
: 'Contributed theme.',
|
|
183
|
-
id: theme.id,
|
|
184
|
-
label: theme.label,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return [...merged.values()];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function buildLocaleOptions(
|
|
192
|
-
localizations: readonly { locale: string; label: string }[],
|
|
193
|
-
): readonly WorkbenchLocaleOption[] {
|
|
194
|
-
const options: WorkbenchLocaleOption[] = [{ id: 'en', label: 'English' }];
|
|
195
|
-
|
|
196
|
-
for (const localization of localizations) {
|
|
197
|
-
options.push({ id: localization.locale, label: localization.label });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return options;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function createAppearanceSettingsCategory({
|
|
204
|
-
darkPreset,
|
|
205
|
-
lightPreset,
|
|
206
|
-
locale,
|
|
207
|
-
localeOptions,
|
|
208
|
-
onDarkPresetChange,
|
|
209
|
-
onLightPresetChange,
|
|
210
|
-
onLocaleChange,
|
|
211
|
-
onShellPresetChange,
|
|
212
|
-
onThemeChange,
|
|
213
|
-
shellPreset,
|
|
214
|
-
theme,
|
|
215
|
-
themeOptions,
|
|
216
|
-
}: WorkbenchAppearanceSettingsInput & {
|
|
217
|
-
localeOptions: readonly WorkbenchLocaleOption[];
|
|
218
|
-
themeOptions: readonly WorkbenchThemeOption[];
|
|
219
|
-
}): WorkbenchSettingsCategory | undefined {
|
|
220
|
-
const usesAppearancePresets = lightPreset !== undefined && darkPreset !== undefined;
|
|
221
|
-
|
|
222
|
-
if (!usesAppearancePresets && !themeOptions?.length && !localeOptions?.length) {
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
content: (
|
|
228
|
-
<AppearanceSettingsSection
|
|
229
|
-
darkPreset={darkPreset}
|
|
230
|
-
lightPreset={lightPreset}
|
|
231
|
-
locale={locale}
|
|
232
|
-
localeOptions={localeOptions ?? []}
|
|
233
|
-
shellPreset={shellPreset}
|
|
234
|
-
theme={theme}
|
|
235
|
-
themeOptions={themeOptions ?? []}
|
|
236
|
-
onDarkPresetChange={onDarkPresetChange}
|
|
237
|
-
onLightPresetChange={onLightPresetChange}
|
|
238
|
-
onLocaleChange={onLocaleChange}
|
|
239
|
-
onShellPresetChange={onShellPresetChange}
|
|
240
|
-
onThemeChange={onThemeChange}
|
|
241
|
-
/>
|
|
242
|
-
),
|
|
243
|
-
id: APPEARANCE_SETTINGS_CATEGORY_ID,
|
|
244
|
-
label: 'Appearance',
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function AppearanceSettingsSection({
|
|
249
|
-
darkPreset,
|
|
250
|
-
lightPreset,
|
|
251
|
-
locale,
|
|
252
|
-
localeOptions,
|
|
253
|
-
onDarkPresetChange,
|
|
254
|
-
onLightPresetChange,
|
|
255
|
-
onLocaleChange,
|
|
256
|
-
onShellPresetChange,
|
|
257
|
-
onThemeChange,
|
|
258
|
-
shellPreset = DEFAULT_SHELL_PRESET,
|
|
259
|
-
theme,
|
|
260
|
-
themeOptions,
|
|
261
|
-
}: WorkbenchAppearanceSettingsInput & {
|
|
262
|
-
localeOptions: readonly WorkbenchLocaleOption[];
|
|
263
|
-
themeOptions: readonly WorkbenchThemeOption[];
|
|
264
|
-
}) {
|
|
265
|
-
const { extensionRegistry } = useWorkbench();
|
|
266
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
267
|
-
const previousThemeOverridesRef = useRef<Readonly<Record<string, string>> | undefined>(undefined);
|
|
268
|
-
const usesAppearancePresets = lightPreset !== undefined && darkPreset !== undefined;
|
|
269
|
-
const lightPresetOptions = useMemo<readonly WorkbenchThemePresetOption[]>(
|
|
270
|
-
() => [
|
|
271
|
-
...LIGHT_THEME_PRESET_OPTIONS,
|
|
272
|
-
...extensionRegistry.themes
|
|
273
|
-
.getThemes()
|
|
274
|
-
.filter((contributedTheme) => contributedTheme.mode === 'light')
|
|
275
|
-
.map((contributedTheme) => ({ id: contributedTheme.id, label: contributedTheme.label })),
|
|
276
|
-
],
|
|
277
|
-
[extensionRegistry.themes],
|
|
278
|
-
);
|
|
279
|
-
const darkPresetOptions = useMemo<readonly WorkbenchThemePresetOption[]>(
|
|
280
|
-
() => [
|
|
281
|
-
...DARK_THEME_PRESET_OPTIONS,
|
|
282
|
-
...extensionRegistry.themes
|
|
283
|
-
.getThemes()
|
|
284
|
-
.filter((contributedTheme) => contributedTheme.mode === 'dark')
|
|
285
|
-
.map((contributedTheme) => ({ id: contributedTheme.id, label: contributedTheme.label })),
|
|
286
|
-
],
|
|
287
|
-
[extensionRegistry.themes],
|
|
288
|
-
);
|
|
289
|
-
const selectedTheme = themeOptions.find((option) => option.id === theme) ?? themeOptions[0];
|
|
290
|
-
const selectedThemeId = selectedTheme?.id ?? '';
|
|
291
|
-
const selectedColorScheme =
|
|
292
|
-
WORKBENCH_COLOR_SCHEME_OPTIONS.find((option) => option.id === theme) ??
|
|
293
|
-
WORKBENCH_COLOR_SCHEME_OPTIONS[0];
|
|
294
|
-
const selectedColorSchemeId = (selectedColorScheme?.id ??
|
|
295
|
-
'system') as WorkbenchColorSchemePreference;
|
|
296
|
-
const selectedLightPreset =
|
|
297
|
-
lightPresetOptions.find((option) => option.id === lightPreset) ?? lightPresetOptions[0];
|
|
298
|
-
const selectedDarkPreset =
|
|
299
|
-
darkPresetOptions.find((option) => option.id === darkPreset) ?? darkPresetOptions[0];
|
|
300
|
-
const selectedLocale = localeOptions.find((option) => option.id === locale) ?? localeOptions[0];
|
|
301
|
-
const selectedLocaleId = selectedLocale?.id ?? 'en';
|
|
302
|
-
const selectedShellPreset =
|
|
303
|
-
SHELL_PRESET_OPTIONS.find((option) => option.id === shellPreset) ?? SHELL_PRESET_OPTIONS[0];
|
|
304
|
-
|
|
305
|
-
useEffect(() => {
|
|
306
|
-
if (typeof document === 'undefined') {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
let activeThemeId: string | undefined;
|
|
311
|
-
|
|
312
|
-
if (usesAppearancePresets) {
|
|
313
|
-
const appearanceSettings = {
|
|
314
|
-
darkPreset: selectedDarkPreset.id,
|
|
315
|
-
lightPreset: selectedLightPreset.id,
|
|
316
|
-
shellPreset: selectedShellPreset.id,
|
|
317
|
-
themePreference: selectedColorSchemeId,
|
|
318
|
-
};
|
|
319
|
-
const { resolvedTheme } = applyWorkbenchAppearance(
|
|
320
|
-
document.documentElement,
|
|
321
|
-
appearanceSettings,
|
|
322
|
-
);
|
|
323
|
-
activeThemeId = resolvedTheme === 'light' ? selectedLightPreset.id : selectedDarkPreset.id;
|
|
324
|
-
|
|
325
|
-
const workbenchRoot = containerRef.current?.closest<HTMLElement>(
|
|
326
|
-
'[data-theme-preset], [data-theme], [data-shell-preset]',
|
|
327
|
-
);
|
|
328
|
-
if (workbenchRoot && workbenchRoot !== document.documentElement) {
|
|
329
|
-
applyWorkbenchAppearance(workbenchRoot, appearanceSettings);
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
activeThemeId = selectedThemeId;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const contributedTheme = activeThemeId
|
|
336
|
-
? extensionRegistry.themes.getTheme(activeThemeId)
|
|
337
|
-
: undefined;
|
|
338
|
-
|
|
339
|
-
// The actual workbench root (e.g. `.ide-root`) re-declares `data-theme-preset` locally,
|
|
340
|
-
// which shadows inheritance from `documentElement` for everything rendered inside it.
|
|
341
|
-
// Apply the override there too, or it only ever reaches stray document.body portals.
|
|
342
|
-
const workbenchRoot = containerRef.current?.closest<HTMLElement>(
|
|
343
|
-
'[data-theme-preset], [data-theme]',
|
|
344
|
-
);
|
|
345
|
-
const overrideTargets =
|
|
346
|
-
workbenchRoot && workbenchRoot !== document.documentElement
|
|
347
|
-
? [document.documentElement, workbenchRoot]
|
|
348
|
-
: [document.documentElement];
|
|
349
|
-
|
|
350
|
-
for (const target of overrideTargets) {
|
|
351
|
-
applyThemeTokenOverrides(
|
|
352
|
-
target,
|
|
353
|
-
contributedTheme?.tokenOverrides,
|
|
354
|
-
previousThemeOverridesRef.current,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
previousThemeOverridesRef.current = contributedTheme?.tokenOverrides;
|
|
358
|
-
}, [
|
|
359
|
-
extensionRegistry.themes,
|
|
360
|
-
selectedColorSchemeId,
|
|
361
|
-
selectedDarkPreset.id,
|
|
362
|
-
selectedLightPreset.id,
|
|
363
|
-
selectedShellPreset.id,
|
|
364
|
-
selectedThemeId,
|
|
365
|
-
usesAppearancePresets,
|
|
366
|
-
]);
|
|
367
|
-
|
|
368
|
-
return (
|
|
369
|
-
<WorkbenchSettingsSection
|
|
370
|
-
id="workbench-settings-appearance"
|
|
371
|
-
title="Appearance"
|
|
372
|
-
description="Configure how the workbench is presented."
|
|
373
|
-
>
|
|
374
|
-
<div ref={containerRef} className="workbench-appearance-settings">
|
|
375
|
-
{usesAppearancePresets ? (
|
|
376
|
-
<>
|
|
377
|
-
<Field
|
|
378
|
-
className="workbench-appearance-settings__field"
|
|
379
|
-
label={WORKBENCH_APPEARANCE_FIELD_LABELS.colorScheme}
|
|
380
|
-
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.colorScheme}
|
|
381
|
-
>
|
|
382
|
-
<Select
|
|
383
|
-
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.colorScheme}
|
|
384
|
-
controlWidth="full"
|
|
385
|
-
disabled={!onThemeChange}
|
|
386
|
-
value={selectedColorSchemeId}
|
|
387
|
-
onValueChange={(nextTheme) => onThemeChange?.(nextTheme)}
|
|
388
|
-
>
|
|
389
|
-
{WORKBENCH_COLOR_SCHEME_OPTIONS.map((option) => (
|
|
390
|
-
<option key={option.id} value={option.id}>
|
|
391
|
-
{option.label}
|
|
392
|
-
</option>
|
|
393
|
-
))}
|
|
394
|
-
</Select>
|
|
395
|
-
</Field>
|
|
396
|
-
<Field
|
|
397
|
-
className="workbench-appearance-settings__field"
|
|
398
|
-
label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredLightColorTheme}
|
|
399
|
-
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.preferredLightColorTheme}
|
|
400
|
-
>
|
|
401
|
-
<Select
|
|
402
|
-
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredLightColorTheme}
|
|
403
|
-
controlWidth="full"
|
|
404
|
-
disabled={!onLightPresetChange}
|
|
405
|
-
value={selectedLightPreset.id}
|
|
406
|
-
onValueChange={(nextPreset) => onLightPresetChange?.(nextPreset)}
|
|
407
|
-
>
|
|
408
|
-
{lightPresetOptions.map((option) => (
|
|
409
|
-
<option key={option.id} value={option.id}>
|
|
410
|
-
{option.label}
|
|
411
|
-
</option>
|
|
412
|
-
))}
|
|
413
|
-
</Select>
|
|
414
|
-
</Field>
|
|
415
|
-
<Field
|
|
416
|
-
className="workbench-appearance-settings__field"
|
|
417
|
-
label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredDarkColorTheme}
|
|
418
|
-
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.preferredDarkColorTheme}
|
|
419
|
-
>
|
|
420
|
-
<Select
|
|
421
|
-
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredDarkColorTheme}
|
|
422
|
-
controlWidth="full"
|
|
423
|
-
disabled={!onDarkPresetChange}
|
|
424
|
-
value={selectedDarkPreset.id}
|
|
425
|
-
onValueChange={(nextPreset) => onDarkPresetChange?.(nextPreset)}
|
|
426
|
-
>
|
|
427
|
-
{darkPresetOptions.map((option) => (
|
|
428
|
-
<option key={option.id} value={option.id}>
|
|
429
|
-
{option.label}
|
|
430
|
-
</option>
|
|
431
|
-
))}
|
|
432
|
-
</Select>
|
|
433
|
-
</Field>
|
|
434
|
-
<Field
|
|
435
|
-
className="workbench-appearance-settings__field"
|
|
436
|
-
label={WORKBENCH_APPEARANCE_FIELD_LABELS.workbenchLayout}
|
|
437
|
-
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.workbenchLayout}
|
|
438
|
-
>
|
|
439
|
-
<Select
|
|
440
|
-
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.workbenchLayout}
|
|
441
|
-
controlWidth="full"
|
|
442
|
-
disabled={!onShellPresetChange}
|
|
443
|
-
value={selectedShellPreset.id}
|
|
444
|
-
onValueChange={(nextPreset) => onShellPresetChange?.(nextPreset)}
|
|
445
|
-
>
|
|
446
|
-
{SHELL_PRESET_OPTIONS.map((option) => (
|
|
447
|
-
<option key={option.id} value={option.id}>
|
|
448
|
-
{option.label}
|
|
449
|
-
</option>
|
|
450
|
-
))}
|
|
451
|
-
</Select>
|
|
452
|
-
</Field>
|
|
453
|
-
</>
|
|
454
|
-
) : themeOptions.length ? (
|
|
455
|
-
<Field
|
|
456
|
-
className="workbench-appearance-settings__field"
|
|
457
|
-
label="Color theme"
|
|
458
|
-
description="Select the active workbench color theme."
|
|
459
|
-
>
|
|
460
|
-
<Select
|
|
461
|
-
aria-label="Color theme"
|
|
462
|
-
controlWidth="full"
|
|
463
|
-
disabled={!onThemeChange}
|
|
464
|
-
value={selectedThemeId}
|
|
465
|
-
onValueChange={(nextTheme) => onThemeChange?.(nextTheme)}
|
|
466
|
-
>
|
|
467
|
-
{themeOptions.map((option) => (
|
|
468
|
-
<option key={option.id} value={option.id}>
|
|
469
|
-
{option.label}
|
|
470
|
-
</option>
|
|
471
|
-
))}
|
|
472
|
-
</Select>
|
|
473
|
-
{selectedTheme?.description ? (
|
|
474
|
-
<p className="workbench-appearance-settings__description">
|
|
475
|
-
{selectedTheme.description}
|
|
476
|
-
</p>
|
|
477
|
-
) : null}
|
|
478
|
-
</Field>
|
|
479
|
-
) : null}
|
|
480
|
-
{localeOptions.length > 1 ? (
|
|
481
|
-
<Field
|
|
482
|
-
className="workbench-appearance-settings__field"
|
|
483
|
-
label="Display language"
|
|
484
|
-
description="Select the active workbench display language."
|
|
485
|
-
>
|
|
486
|
-
<Select
|
|
487
|
-
aria-label="Display language"
|
|
488
|
-
controlWidth="full"
|
|
489
|
-
disabled={!onLocaleChange}
|
|
490
|
-
value={selectedLocaleId}
|
|
491
|
-
onValueChange={(nextLocale) => onLocaleChange?.(nextLocale)}
|
|
492
|
-
>
|
|
493
|
-
{localeOptions.map((option) => (
|
|
494
|
-
<option key={option.id} value={option.id}>
|
|
495
|
-
{option.label}
|
|
496
|
-
</option>
|
|
497
|
-
))}
|
|
498
|
-
</Select>
|
|
499
|
-
</Field>
|
|
500
|
-
) : null}
|
|
501
|
-
</div>
|
|
502
|
-
</WorkbenchSettingsSection>
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function SettingContributionField({
|
|
507
|
-
activeScope,
|
|
508
|
-
preferenceService,
|
|
509
|
-
propertyKey,
|
|
510
|
-
propertyValue,
|
|
511
|
-
}: {
|
|
512
|
-
activeScope: PreferenceScope;
|
|
513
|
-
preferenceService: PreferenceService;
|
|
514
|
-
propertyKey: string;
|
|
515
|
-
propertyValue: unknown;
|
|
516
|
-
}) {
|
|
517
|
-
const property = isRecord(propertyValue) ? propertyValue : {};
|
|
518
|
-
const description = typeof property.description === 'string' ? property.description : undefined;
|
|
519
|
-
const scope = typeof property.scope === 'string' ? property.scope : undefined;
|
|
520
|
-
const type = formatSettingType(property.type);
|
|
521
|
-
const hasDefault = Object.prototype.hasOwnProperty.call(property, 'default');
|
|
522
|
-
const inspection = preferenceService.inspect(propertyKey);
|
|
523
|
-
const scopedValue = preferenceService.getScopedValue(propertyKey, activeScope);
|
|
524
|
-
const editableValue =
|
|
525
|
-
scopedValue !== undefined ? scopedValue : (inspection.effectiveValue ?? property.default);
|
|
526
|
-
|
|
527
|
-
return (
|
|
528
|
-
<Field
|
|
529
|
-
className="workbench-settings-contribution-field"
|
|
530
|
-
label={<code>{propertyKey}</code>}
|
|
531
|
-
description={description}
|
|
532
|
-
>
|
|
533
|
-
<div className="workbench-settings-contribution-meta">
|
|
534
|
-
{type ? <Badge variant="muted">{type}</Badge> : null}
|
|
535
|
-
{scope ? <Badge variant="muted">{scope}</Badge> : null}
|
|
536
|
-
<Badge variant="muted">effective: {formatSettingDefault(inspection.effectiveValue)}</Badge>
|
|
537
|
-
</div>
|
|
538
|
-
{property.type === 'boolean' ? (
|
|
539
|
-
<Checkbox
|
|
540
|
-
checked={editableValue === true}
|
|
541
|
-
label={`${formatPreferenceScopeLabel(activeScope)} value`}
|
|
542
|
-
onCheckedChange={(checked) => {
|
|
543
|
-
preferenceService.setScopedValue(propertyKey, activeScope, checked);
|
|
544
|
-
}}
|
|
545
|
-
/>
|
|
546
|
-
) : hasDefault ? (
|
|
547
|
-
<code className="workbench-settings-contribution-default">
|
|
548
|
-
default: {formatSettingDefault(property.default)}
|
|
549
|
-
</code>
|
|
550
|
-
) : null}
|
|
551
|
-
</Field>
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function formatSettingType(value: unknown): string | undefined {
|
|
556
|
-
if (typeof value === 'string') {
|
|
557
|
-
return value;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
|
|
561
|
-
return value.join(' | ');
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return undefined;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function formatSettingDefault(value: unknown): string {
|
|
568
|
-
if (value === undefined) {
|
|
569
|
-
return 'undefined';
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (typeof value === 'string') {
|
|
573
|
-
return JSON.stringify(value);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
try {
|
|
577
|
-
return JSON.stringify(value) ?? String(value);
|
|
578
|
-
} catch {
|
|
579
|
-
return String(value);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function titleFromExtensionId(extensionId: string): string {
|
|
584
|
-
const parts = extensionId.split('.').filter(Boolean);
|
|
585
|
-
const lastPart = parts[parts.length - 1];
|
|
586
|
-
|
|
587
|
-
return (
|
|
588
|
-
lastPart?.replace(/[-_]/g, ' ').replace(/\b\w/g, (letter: string) => letter.toUpperCase()) ??
|
|
589
|
-
extensionId
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function slugId(value: string): string {
|
|
594
|
-
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
598
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
599
|
-
}
|
|
1
|
+
import { useEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
2
|
+
import { Badge, Checkbox, Field, Select } from '@workbench-kit/react/primitives';
|
|
3
|
+
import {
|
|
4
|
+
applyWorkbenchAppearance,
|
|
5
|
+
DARK_THEME_PRESET_OPTIONS,
|
|
6
|
+
DEFAULT_SHELL_PRESET,
|
|
7
|
+
LIGHT_THEME_PRESET_OPTIONS,
|
|
8
|
+
SHELL_PRESET_OPTIONS,
|
|
9
|
+
WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS,
|
|
10
|
+
WORKBENCH_APPEARANCE_FIELD_LABELS,
|
|
11
|
+
WORKBENCH_COLOR_SCHEME_OPTIONS,
|
|
12
|
+
type WorkbenchColorSchemePreference,
|
|
13
|
+
type WorkbenchThemePresetOption,
|
|
14
|
+
} from '@workbench-kit/react/workbench';
|
|
15
|
+
import {
|
|
16
|
+
WorkbenchSettingsSection,
|
|
17
|
+
type WorkbenchSettingsCategory,
|
|
18
|
+
} from '@workbench-kit/react/workbench/settings';
|
|
19
|
+
import {
|
|
20
|
+
applyThemeTokenOverrides,
|
|
21
|
+
type ExtensionRegistry,
|
|
22
|
+
type PreferenceService,
|
|
23
|
+
} from '@workbench-kit/workbench-core';
|
|
24
|
+
import type { PreferenceScope } from '@workbench-kit/workbench-config';
|
|
25
|
+
|
|
26
|
+
import { useWorkbench } from './provider.js';
|
|
27
|
+
import { WORKBENCH_PREFERENCE_SCOPES } from './settings-constants.js';
|
|
28
|
+
|
|
29
|
+
const APPEARANCE_SETTINGS_CATEGORY_ID = 'workbench.appearance';
|
|
30
|
+
|
|
31
|
+
export interface WorkbenchThemeOption {
|
|
32
|
+
description?: ReactNode;
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkbenchLocaleOption {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface WorkbenchAppearanceSettingsInput {
|
|
43
|
+
darkPreset?: string | undefined;
|
|
44
|
+
lightPreset?: string | undefined;
|
|
45
|
+
locale?: string | undefined;
|
|
46
|
+
localeOptions?: readonly WorkbenchLocaleOption[] | undefined;
|
|
47
|
+
onDarkPresetChange?: ((preset: string) => void) | undefined;
|
|
48
|
+
onLightPresetChange?: ((preset: string) => void) | undefined;
|
|
49
|
+
onLocaleChange?: ((locale: string) => void) | undefined;
|
|
50
|
+
onShellPresetChange?: ((preset: string) => void) | undefined;
|
|
51
|
+
onThemeChange?: ((theme: string) => void) | undefined;
|
|
52
|
+
shellPreset?: string | undefined;
|
|
53
|
+
theme?: string | undefined;
|
|
54
|
+
themeOptions?: readonly WorkbenchThemeOption[] | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WorkbenchSettingsCategoryInput extends WorkbenchAppearanceSettingsInput {
|
|
58
|
+
activeScope: PreferenceScope;
|
|
59
|
+
preferenceService: PreferenceService;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createSettingsCategories(
|
|
63
|
+
extensionRegistry: ExtensionRegistry,
|
|
64
|
+
{
|
|
65
|
+
activeScope,
|
|
66
|
+
darkPreset,
|
|
67
|
+
lightPreset,
|
|
68
|
+
locale,
|
|
69
|
+
onDarkPresetChange,
|
|
70
|
+
onLightPresetChange,
|
|
71
|
+
onLocaleChange,
|
|
72
|
+
onShellPresetChange,
|
|
73
|
+
onThemeChange,
|
|
74
|
+
preferenceService,
|
|
75
|
+
shellPreset,
|
|
76
|
+
theme,
|
|
77
|
+
themeOptions,
|
|
78
|
+
}: WorkbenchSettingsCategoryInput,
|
|
79
|
+
): WorkbenchSettingsCategory[] {
|
|
80
|
+
const configurations = extensionRegistry.configurations.getConfigurations();
|
|
81
|
+
const mergedThemeOptions = mergeThemeOptions(themeOptions, extensionRegistry.themes.getThemes());
|
|
82
|
+
const localeOptions = buildLocaleOptions(extensionRegistry.localizations.getLocalizations());
|
|
83
|
+
const appearanceCategory = createAppearanceSettingsCategory({
|
|
84
|
+
darkPreset,
|
|
85
|
+
lightPreset,
|
|
86
|
+
locale,
|
|
87
|
+
localeOptions,
|
|
88
|
+
onDarkPresetChange,
|
|
89
|
+
onLightPresetChange,
|
|
90
|
+
onLocaleChange,
|
|
91
|
+
onShellPresetChange,
|
|
92
|
+
onThemeChange,
|
|
93
|
+
shellPreset,
|
|
94
|
+
theme,
|
|
95
|
+
themeOptions: mergedThemeOptions,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (configurations.length === 0) {
|
|
99
|
+
const fallbackCategory = {
|
|
100
|
+
content: (
|
|
101
|
+
<WorkbenchSettingsSection
|
|
102
|
+
id="workbench-settings-empty"
|
|
103
|
+
title="Workbench"
|
|
104
|
+
description="No extension settings are currently registered."
|
|
105
|
+
>
|
|
106
|
+
<p className="workbench-settings-empty">Enable extensions to contribute settings.</p>
|
|
107
|
+
</WorkbenchSettingsSection>
|
|
108
|
+
),
|
|
109
|
+
id: 'workbench',
|
|
110
|
+
label: 'Workbench',
|
|
111
|
+
} satisfies WorkbenchSettingsCategory;
|
|
112
|
+
|
|
113
|
+
return appearanceCategory ? [appearanceCategory, fallbackCategory] : [fallbackCategory];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const contributedCategories = configurations.map(({ extensionId, configuration }) => {
|
|
117
|
+
const extension = extensionRegistry.getExtension(extensionId);
|
|
118
|
+
const displayName = extension?.manifest.displayName ?? titleFromExtensionId(extensionId);
|
|
119
|
+
const properties = Object.entries(configuration.properties ?? {});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
content: (
|
|
123
|
+
<WorkbenchSettingsSection
|
|
124
|
+
id={`workbench-settings-${slugId(extensionId)}`}
|
|
125
|
+
title={displayName}
|
|
126
|
+
description={`${properties.length} ${
|
|
127
|
+
properties.length === 1 ? 'setting is' : 'settings are'
|
|
128
|
+
} contributed by ${extensionId}.`}
|
|
129
|
+
>
|
|
130
|
+
{properties.length ? (
|
|
131
|
+
<div className="workbench-settings-contribution-list">
|
|
132
|
+
{properties.map(([key, value]) => (
|
|
133
|
+
<SettingContributionField
|
|
134
|
+
key={key}
|
|
135
|
+
activeScope={activeScope}
|
|
136
|
+
preferenceService={preferenceService}
|
|
137
|
+
propertyKey={key}
|
|
138
|
+
propertyValue={value}
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
) : (
|
|
143
|
+
<p className="workbench-settings-empty">
|
|
144
|
+
This extension registered a configuration section without properties.
|
|
145
|
+
</p>
|
|
146
|
+
)}
|
|
147
|
+
</WorkbenchSettingsSection>
|
|
148
|
+
),
|
|
149
|
+
id: extensionId,
|
|
150
|
+
label: displayName,
|
|
151
|
+
title: extensionId,
|
|
152
|
+
} satisfies WorkbenchSettingsCategory;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return appearanceCategory
|
|
156
|
+
? [appearanceCategory, ...contributedCategories]
|
|
157
|
+
: contributedCategories;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatPreferenceScopeLabel(scope: PreferenceScope): string {
|
|
161
|
+
return WORKBENCH_PREFERENCE_SCOPES.find((candidate) => candidate.id === scope)?.label ?? scope;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mergeThemeOptions(
|
|
165
|
+
baseOptions: readonly WorkbenchThemeOption[] | undefined,
|
|
166
|
+
contributedThemes: readonly {
|
|
167
|
+
id: string;
|
|
168
|
+
label: string;
|
|
169
|
+
tokenOverrides?: Record<string, string> | undefined;
|
|
170
|
+
}[],
|
|
171
|
+
): readonly WorkbenchThemeOption[] {
|
|
172
|
+
const merged = new Map<string, WorkbenchThemeOption>();
|
|
173
|
+
|
|
174
|
+
for (const option of baseOptions ?? []) {
|
|
175
|
+
merged.set(option.id, option);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const theme of contributedThemes) {
|
|
179
|
+
merged.set(theme.id, {
|
|
180
|
+
description: theme.tokenOverrides
|
|
181
|
+
? 'Contributed theme with token overrides.'
|
|
182
|
+
: 'Contributed theme.',
|
|
183
|
+
id: theme.id,
|
|
184
|
+
label: theme.label,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return [...merged.values()];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildLocaleOptions(
|
|
192
|
+
localizations: readonly { locale: string; label: string }[],
|
|
193
|
+
): readonly WorkbenchLocaleOption[] {
|
|
194
|
+
const options: WorkbenchLocaleOption[] = [{ id: 'en', label: 'English' }];
|
|
195
|
+
|
|
196
|
+
for (const localization of localizations) {
|
|
197
|
+
options.push({ id: localization.locale, label: localization.label });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return options;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createAppearanceSettingsCategory({
|
|
204
|
+
darkPreset,
|
|
205
|
+
lightPreset,
|
|
206
|
+
locale,
|
|
207
|
+
localeOptions,
|
|
208
|
+
onDarkPresetChange,
|
|
209
|
+
onLightPresetChange,
|
|
210
|
+
onLocaleChange,
|
|
211
|
+
onShellPresetChange,
|
|
212
|
+
onThemeChange,
|
|
213
|
+
shellPreset,
|
|
214
|
+
theme,
|
|
215
|
+
themeOptions,
|
|
216
|
+
}: WorkbenchAppearanceSettingsInput & {
|
|
217
|
+
localeOptions: readonly WorkbenchLocaleOption[];
|
|
218
|
+
themeOptions: readonly WorkbenchThemeOption[];
|
|
219
|
+
}): WorkbenchSettingsCategory | undefined {
|
|
220
|
+
const usesAppearancePresets = lightPreset !== undefined && darkPreset !== undefined;
|
|
221
|
+
|
|
222
|
+
if (!usesAppearancePresets && !themeOptions?.length && !localeOptions?.length) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
content: (
|
|
228
|
+
<AppearanceSettingsSection
|
|
229
|
+
darkPreset={darkPreset}
|
|
230
|
+
lightPreset={lightPreset}
|
|
231
|
+
locale={locale}
|
|
232
|
+
localeOptions={localeOptions ?? []}
|
|
233
|
+
shellPreset={shellPreset}
|
|
234
|
+
theme={theme}
|
|
235
|
+
themeOptions={themeOptions ?? []}
|
|
236
|
+
onDarkPresetChange={onDarkPresetChange}
|
|
237
|
+
onLightPresetChange={onLightPresetChange}
|
|
238
|
+
onLocaleChange={onLocaleChange}
|
|
239
|
+
onShellPresetChange={onShellPresetChange}
|
|
240
|
+
onThemeChange={onThemeChange}
|
|
241
|
+
/>
|
|
242
|
+
),
|
|
243
|
+
id: APPEARANCE_SETTINGS_CATEGORY_ID,
|
|
244
|
+
label: 'Appearance',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function AppearanceSettingsSection({
|
|
249
|
+
darkPreset,
|
|
250
|
+
lightPreset,
|
|
251
|
+
locale,
|
|
252
|
+
localeOptions,
|
|
253
|
+
onDarkPresetChange,
|
|
254
|
+
onLightPresetChange,
|
|
255
|
+
onLocaleChange,
|
|
256
|
+
onShellPresetChange,
|
|
257
|
+
onThemeChange,
|
|
258
|
+
shellPreset = DEFAULT_SHELL_PRESET,
|
|
259
|
+
theme,
|
|
260
|
+
themeOptions,
|
|
261
|
+
}: WorkbenchAppearanceSettingsInput & {
|
|
262
|
+
localeOptions: readonly WorkbenchLocaleOption[];
|
|
263
|
+
themeOptions: readonly WorkbenchThemeOption[];
|
|
264
|
+
}) {
|
|
265
|
+
const { extensionRegistry } = useWorkbench();
|
|
266
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
267
|
+
const previousThemeOverridesRef = useRef<Readonly<Record<string, string>> | undefined>(undefined);
|
|
268
|
+
const usesAppearancePresets = lightPreset !== undefined && darkPreset !== undefined;
|
|
269
|
+
const lightPresetOptions = useMemo<readonly WorkbenchThemePresetOption[]>(
|
|
270
|
+
() => [
|
|
271
|
+
...LIGHT_THEME_PRESET_OPTIONS,
|
|
272
|
+
...extensionRegistry.themes
|
|
273
|
+
.getThemes()
|
|
274
|
+
.filter((contributedTheme) => contributedTheme.mode === 'light')
|
|
275
|
+
.map((contributedTheme) => ({ id: contributedTheme.id, label: contributedTheme.label })),
|
|
276
|
+
],
|
|
277
|
+
[extensionRegistry.themes],
|
|
278
|
+
);
|
|
279
|
+
const darkPresetOptions = useMemo<readonly WorkbenchThemePresetOption[]>(
|
|
280
|
+
() => [
|
|
281
|
+
...DARK_THEME_PRESET_OPTIONS,
|
|
282
|
+
...extensionRegistry.themes
|
|
283
|
+
.getThemes()
|
|
284
|
+
.filter((contributedTheme) => contributedTheme.mode === 'dark')
|
|
285
|
+
.map((contributedTheme) => ({ id: contributedTheme.id, label: contributedTheme.label })),
|
|
286
|
+
],
|
|
287
|
+
[extensionRegistry.themes],
|
|
288
|
+
);
|
|
289
|
+
const selectedTheme = themeOptions.find((option) => option.id === theme) ?? themeOptions[0];
|
|
290
|
+
const selectedThemeId = selectedTheme?.id ?? '';
|
|
291
|
+
const selectedColorScheme =
|
|
292
|
+
WORKBENCH_COLOR_SCHEME_OPTIONS.find((option) => option.id === theme) ??
|
|
293
|
+
WORKBENCH_COLOR_SCHEME_OPTIONS[0];
|
|
294
|
+
const selectedColorSchemeId = (selectedColorScheme?.id ??
|
|
295
|
+
'system') as WorkbenchColorSchemePreference;
|
|
296
|
+
const selectedLightPreset =
|
|
297
|
+
lightPresetOptions.find((option) => option.id === lightPreset) ?? lightPresetOptions[0];
|
|
298
|
+
const selectedDarkPreset =
|
|
299
|
+
darkPresetOptions.find((option) => option.id === darkPreset) ?? darkPresetOptions[0];
|
|
300
|
+
const selectedLocale = localeOptions.find((option) => option.id === locale) ?? localeOptions[0];
|
|
301
|
+
const selectedLocaleId = selectedLocale?.id ?? 'en';
|
|
302
|
+
const selectedShellPreset =
|
|
303
|
+
SHELL_PRESET_OPTIONS.find((option) => option.id === shellPreset) ?? SHELL_PRESET_OPTIONS[0];
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (typeof document === 'undefined') {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let activeThemeId: string | undefined;
|
|
311
|
+
|
|
312
|
+
if (usesAppearancePresets) {
|
|
313
|
+
const appearanceSettings = {
|
|
314
|
+
darkPreset: selectedDarkPreset.id,
|
|
315
|
+
lightPreset: selectedLightPreset.id,
|
|
316
|
+
shellPreset: selectedShellPreset.id,
|
|
317
|
+
themePreference: selectedColorSchemeId,
|
|
318
|
+
};
|
|
319
|
+
const { resolvedTheme } = applyWorkbenchAppearance(
|
|
320
|
+
document.documentElement,
|
|
321
|
+
appearanceSettings,
|
|
322
|
+
);
|
|
323
|
+
activeThemeId = resolvedTheme === 'light' ? selectedLightPreset.id : selectedDarkPreset.id;
|
|
324
|
+
|
|
325
|
+
const workbenchRoot = containerRef.current?.closest<HTMLElement>(
|
|
326
|
+
'[data-theme-preset], [data-theme], [data-shell-preset]',
|
|
327
|
+
);
|
|
328
|
+
if (workbenchRoot && workbenchRoot !== document.documentElement) {
|
|
329
|
+
applyWorkbenchAppearance(workbenchRoot, appearanceSettings);
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
activeThemeId = selectedThemeId;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const contributedTheme = activeThemeId
|
|
336
|
+
? extensionRegistry.themes.getTheme(activeThemeId)
|
|
337
|
+
: undefined;
|
|
338
|
+
|
|
339
|
+
// The actual workbench root (e.g. `.ide-root`) re-declares `data-theme-preset` locally,
|
|
340
|
+
// which shadows inheritance from `documentElement` for everything rendered inside it.
|
|
341
|
+
// Apply the override there too, or it only ever reaches stray document.body portals.
|
|
342
|
+
const workbenchRoot = containerRef.current?.closest<HTMLElement>(
|
|
343
|
+
'[data-theme-preset], [data-theme]',
|
|
344
|
+
);
|
|
345
|
+
const overrideTargets =
|
|
346
|
+
workbenchRoot && workbenchRoot !== document.documentElement
|
|
347
|
+
? [document.documentElement, workbenchRoot]
|
|
348
|
+
: [document.documentElement];
|
|
349
|
+
|
|
350
|
+
for (const target of overrideTargets) {
|
|
351
|
+
applyThemeTokenOverrides(
|
|
352
|
+
target,
|
|
353
|
+
contributedTheme?.tokenOverrides,
|
|
354
|
+
previousThemeOverridesRef.current,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
previousThemeOverridesRef.current = contributedTheme?.tokenOverrides;
|
|
358
|
+
}, [
|
|
359
|
+
extensionRegistry.themes,
|
|
360
|
+
selectedColorSchemeId,
|
|
361
|
+
selectedDarkPreset.id,
|
|
362
|
+
selectedLightPreset.id,
|
|
363
|
+
selectedShellPreset.id,
|
|
364
|
+
selectedThemeId,
|
|
365
|
+
usesAppearancePresets,
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<WorkbenchSettingsSection
|
|
370
|
+
id="workbench-settings-appearance"
|
|
371
|
+
title="Appearance"
|
|
372
|
+
description="Configure how the workbench is presented."
|
|
373
|
+
>
|
|
374
|
+
<div ref={containerRef} className="workbench-appearance-settings">
|
|
375
|
+
{usesAppearancePresets ? (
|
|
376
|
+
<>
|
|
377
|
+
<Field
|
|
378
|
+
className="workbench-appearance-settings__field"
|
|
379
|
+
label={WORKBENCH_APPEARANCE_FIELD_LABELS.colorScheme}
|
|
380
|
+
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.colorScheme}
|
|
381
|
+
>
|
|
382
|
+
<Select
|
|
383
|
+
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.colorScheme}
|
|
384
|
+
controlWidth="full"
|
|
385
|
+
disabled={!onThemeChange}
|
|
386
|
+
value={selectedColorSchemeId}
|
|
387
|
+
onValueChange={(nextTheme) => onThemeChange?.(nextTheme)}
|
|
388
|
+
>
|
|
389
|
+
{WORKBENCH_COLOR_SCHEME_OPTIONS.map((option) => (
|
|
390
|
+
<option key={option.id} value={option.id}>
|
|
391
|
+
{option.label}
|
|
392
|
+
</option>
|
|
393
|
+
))}
|
|
394
|
+
</Select>
|
|
395
|
+
</Field>
|
|
396
|
+
<Field
|
|
397
|
+
className="workbench-appearance-settings__field"
|
|
398
|
+
label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredLightColorTheme}
|
|
399
|
+
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.preferredLightColorTheme}
|
|
400
|
+
>
|
|
401
|
+
<Select
|
|
402
|
+
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredLightColorTheme}
|
|
403
|
+
controlWidth="full"
|
|
404
|
+
disabled={!onLightPresetChange}
|
|
405
|
+
value={selectedLightPreset.id}
|
|
406
|
+
onValueChange={(nextPreset) => onLightPresetChange?.(nextPreset)}
|
|
407
|
+
>
|
|
408
|
+
{lightPresetOptions.map((option) => (
|
|
409
|
+
<option key={option.id} value={option.id}>
|
|
410
|
+
{option.label}
|
|
411
|
+
</option>
|
|
412
|
+
))}
|
|
413
|
+
</Select>
|
|
414
|
+
</Field>
|
|
415
|
+
<Field
|
|
416
|
+
className="workbench-appearance-settings__field"
|
|
417
|
+
label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredDarkColorTheme}
|
|
418
|
+
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.preferredDarkColorTheme}
|
|
419
|
+
>
|
|
420
|
+
<Select
|
|
421
|
+
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredDarkColorTheme}
|
|
422
|
+
controlWidth="full"
|
|
423
|
+
disabled={!onDarkPresetChange}
|
|
424
|
+
value={selectedDarkPreset.id}
|
|
425
|
+
onValueChange={(nextPreset) => onDarkPresetChange?.(nextPreset)}
|
|
426
|
+
>
|
|
427
|
+
{darkPresetOptions.map((option) => (
|
|
428
|
+
<option key={option.id} value={option.id}>
|
|
429
|
+
{option.label}
|
|
430
|
+
</option>
|
|
431
|
+
))}
|
|
432
|
+
</Select>
|
|
433
|
+
</Field>
|
|
434
|
+
<Field
|
|
435
|
+
className="workbench-appearance-settings__field"
|
|
436
|
+
label={WORKBENCH_APPEARANCE_FIELD_LABELS.workbenchLayout}
|
|
437
|
+
description={WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS.workbenchLayout}
|
|
438
|
+
>
|
|
439
|
+
<Select
|
|
440
|
+
aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.workbenchLayout}
|
|
441
|
+
controlWidth="full"
|
|
442
|
+
disabled={!onShellPresetChange}
|
|
443
|
+
value={selectedShellPreset.id}
|
|
444
|
+
onValueChange={(nextPreset) => onShellPresetChange?.(nextPreset)}
|
|
445
|
+
>
|
|
446
|
+
{SHELL_PRESET_OPTIONS.map((option) => (
|
|
447
|
+
<option key={option.id} value={option.id}>
|
|
448
|
+
{option.label}
|
|
449
|
+
</option>
|
|
450
|
+
))}
|
|
451
|
+
</Select>
|
|
452
|
+
</Field>
|
|
453
|
+
</>
|
|
454
|
+
) : themeOptions.length ? (
|
|
455
|
+
<Field
|
|
456
|
+
className="workbench-appearance-settings__field"
|
|
457
|
+
label="Color theme"
|
|
458
|
+
description="Select the active workbench color theme."
|
|
459
|
+
>
|
|
460
|
+
<Select
|
|
461
|
+
aria-label="Color theme"
|
|
462
|
+
controlWidth="full"
|
|
463
|
+
disabled={!onThemeChange}
|
|
464
|
+
value={selectedThemeId}
|
|
465
|
+
onValueChange={(nextTheme) => onThemeChange?.(nextTheme)}
|
|
466
|
+
>
|
|
467
|
+
{themeOptions.map((option) => (
|
|
468
|
+
<option key={option.id} value={option.id}>
|
|
469
|
+
{option.label}
|
|
470
|
+
</option>
|
|
471
|
+
))}
|
|
472
|
+
</Select>
|
|
473
|
+
{selectedTheme?.description ? (
|
|
474
|
+
<p className="workbench-appearance-settings__description">
|
|
475
|
+
{selectedTheme.description}
|
|
476
|
+
</p>
|
|
477
|
+
) : null}
|
|
478
|
+
</Field>
|
|
479
|
+
) : null}
|
|
480
|
+
{localeOptions.length > 1 ? (
|
|
481
|
+
<Field
|
|
482
|
+
className="workbench-appearance-settings__field"
|
|
483
|
+
label="Display language"
|
|
484
|
+
description="Select the active workbench display language."
|
|
485
|
+
>
|
|
486
|
+
<Select
|
|
487
|
+
aria-label="Display language"
|
|
488
|
+
controlWidth="full"
|
|
489
|
+
disabled={!onLocaleChange}
|
|
490
|
+
value={selectedLocaleId}
|
|
491
|
+
onValueChange={(nextLocale) => onLocaleChange?.(nextLocale)}
|
|
492
|
+
>
|
|
493
|
+
{localeOptions.map((option) => (
|
|
494
|
+
<option key={option.id} value={option.id}>
|
|
495
|
+
{option.label}
|
|
496
|
+
</option>
|
|
497
|
+
))}
|
|
498
|
+
</Select>
|
|
499
|
+
</Field>
|
|
500
|
+
) : null}
|
|
501
|
+
</div>
|
|
502
|
+
</WorkbenchSettingsSection>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function SettingContributionField({
|
|
507
|
+
activeScope,
|
|
508
|
+
preferenceService,
|
|
509
|
+
propertyKey,
|
|
510
|
+
propertyValue,
|
|
511
|
+
}: {
|
|
512
|
+
activeScope: PreferenceScope;
|
|
513
|
+
preferenceService: PreferenceService;
|
|
514
|
+
propertyKey: string;
|
|
515
|
+
propertyValue: unknown;
|
|
516
|
+
}) {
|
|
517
|
+
const property = isRecord(propertyValue) ? propertyValue : {};
|
|
518
|
+
const description = typeof property.description === 'string' ? property.description : undefined;
|
|
519
|
+
const scope = typeof property.scope === 'string' ? property.scope : undefined;
|
|
520
|
+
const type = formatSettingType(property.type);
|
|
521
|
+
const hasDefault = Object.prototype.hasOwnProperty.call(property, 'default');
|
|
522
|
+
const inspection = preferenceService.inspect(propertyKey);
|
|
523
|
+
const scopedValue = preferenceService.getScopedValue(propertyKey, activeScope);
|
|
524
|
+
const editableValue =
|
|
525
|
+
scopedValue !== undefined ? scopedValue : (inspection.effectiveValue ?? property.default);
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<Field
|
|
529
|
+
className="workbench-settings-contribution-field"
|
|
530
|
+
label={<code>{propertyKey}</code>}
|
|
531
|
+
description={description}
|
|
532
|
+
>
|
|
533
|
+
<div className="workbench-settings-contribution-meta">
|
|
534
|
+
{type ? <Badge variant="muted">{type}</Badge> : null}
|
|
535
|
+
{scope ? <Badge variant="muted">{scope}</Badge> : null}
|
|
536
|
+
<Badge variant="muted">effective: {formatSettingDefault(inspection.effectiveValue)}</Badge>
|
|
537
|
+
</div>
|
|
538
|
+
{property.type === 'boolean' ? (
|
|
539
|
+
<Checkbox
|
|
540
|
+
checked={editableValue === true}
|
|
541
|
+
label={`${formatPreferenceScopeLabel(activeScope)} value`}
|
|
542
|
+
onCheckedChange={(checked) => {
|
|
543
|
+
preferenceService.setScopedValue(propertyKey, activeScope, checked);
|
|
544
|
+
}}
|
|
545
|
+
/>
|
|
546
|
+
) : hasDefault ? (
|
|
547
|
+
<code className="workbench-settings-contribution-default">
|
|
548
|
+
default: {formatSettingDefault(property.default)}
|
|
549
|
+
</code>
|
|
550
|
+
) : null}
|
|
551
|
+
</Field>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function formatSettingType(value: unknown): string | undefined {
|
|
556
|
+
if (typeof value === 'string') {
|
|
557
|
+
return value;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
|
|
561
|
+
return value.join(' | ');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function formatSettingDefault(value: unknown): string {
|
|
568
|
+
if (value === undefined) {
|
|
569
|
+
return 'undefined';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (typeof value === 'string') {
|
|
573
|
+
return JSON.stringify(value);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
return JSON.stringify(value) ?? String(value);
|
|
578
|
+
} catch {
|
|
579
|
+
return String(value);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function titleFromExtensionId(extensionId: string): string {
|
|
584
|
+
const parts = extensionId.split('.').filter(Boolean);
|
|
585
|
+
const lastPart = parts[parts.length - 1];
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
lastPart?.replace(/[-_]/g, ' ').replace(/\b\w/g, (letter: string) => letter.toUpperCase()) ??
|
|
589
|
+
extensionId
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function slugId(value: string): string {
|
|
594
|
+
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
598
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
599
|
+
}
|