@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.
Files changed (95) hide show
  1. package/README.md +41 -0
  2. package/package.json +9 -9
  3. package/src/{chat-ai-mock-proposals.ts → chat/ai-mock-proposals.ts} +39 -39
  4. package/src/{chat-command-input.ts → chat/command-input.ts} +52 -52
  5. package/src/{chat-command-policy.ts → chat/command-policy.ts} +54 -54
  6. package/src/{chat-command-surface.tsx → chat/command-surface.tsx} +155 -155
  7. package/src/{use-workbench-chat-command-proposals.ts → chat/use-chat-command-proposals.ts} +194 -194
  8. package/src/{chat-view-data.ts → chat/view-data.ts} +20 -20
  9. package/src/{chat-view.tsx → chat/view.tsx} +220 -220
  10. package/src/{command-inspector-surface.tsx → commands/inspector-surface.tsx} +46 -46
  11. package/src/{use-workbench-command-descriptors.ts → commands/use-command-descriptors.ts} +41 -41
  12. package/src/{use-context-key-revision.ts → commands/use-context-key-revision.ts} +18 -18
  13. package/src/{commands-view-data.ts → commands/view-data.ts} +17 -17
  14. package/src/{commands-view.tsx → commands/view.tsx} +43 -43
  15. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -124
  16. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -17
  17. package/src/devtools/index.ts +11 -11
  18. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -84
  19. package/src/devtools/workbench-devtools-snapshot.ts +140 -140
  20. package/src/devtools/workbench-devtools.css +82 -82
  21. package/src/{editor-area-dnd.ts → editor/area-dnd.ts} +69 -69
  22. package/src/{editor-area-model.ts → editor/area-model.ts} +71 -71
  23. package/src/{editor-area.css → editor/area.css} +454 -454
  24. package/src/{editor-area.tsx → editor/area.tsx} +676 -676
  25. package/src/{editor-host-surface.tsx → editor/host-surface.tsx} +631 -634
  26. package/src/{editor-pane-visibility.ts → editor/pane-visibility.ts} +130 -130
  27. package/src/{editor-resource.ts → editor/resource.ts} +36 -36
  28. package/src/{editor-state-storage.ts → editor/state-storage.ts} +182 -193
  29. package/src/{editor-tab-context-menu.ts → editor/tab-context-menu.ts} +118 -118
  30. package/src/{use-editor.ts → editor/use-editor.ts} +114 -107
  31. package/src/{editor-view-providers.tsx → editor/view-providers.tsx} +552 -552
  32. package/src/{editor-workspace-file-availability.ts → editor/workspace-file-availability.ts} +15 -15
  33. package/src/{editor-workspace-reconcile.tsx → editor/workspace-reconcile.tsx} +31 -28
  34. package/src/{explorer-context-menu.ts → explorer/context-menu.ts} +114 -114
  35. package/src/{createCommandWorkspaceExplorerPort.ts → explorer/create-command-workspace-explorer-port.ts} +86 -86
  36. package/src/{explorer-reveal.ts → explorer/reveal.ts} +112 -112
  37. package/src/{search-view-data.ts → explorer/search-view-data.ts} +15 -15
  38. package/src/{search-view.tsx → explorer/search-view.tsx} +65 -62
  39. package/src/{use-active-workspace-path.ts → explorer/use-active-workspace-path.ts} +15 -15
  40. package/src/{explorer-view-data.ts → explorer/view-data.ts} +58 -58
  41. package/src/{explorer-view.tsx → explorer/view.tsx} +227 -224
  42. package/src/{extension-context-menu.ts → extensions/context-menu.ts} +52 -52
  43. package/src/{extension-management-model.ts → extensions/management-model.ts} +313 -313
  44. package/src/{use-extension-management.ts → extensions/use-extension-management.ts} +241 -193
  45. package/src/{extensions-view-data.ts → extensions/view-data.ts} +19 -19
  46. package/src/{extensions-view.tsx → extensions/view.tsx} +63 -38
  47. package/src/{field-remap-demo.tsx → field-remap/demo.tsx} +18 -18
  48. package/src/{field-remap-editor-surface.tsx → field-remap/editor-surface.tsx} +36 -36
  49. package/src/{field-remap-flow-adapter.ts → field-remap/flow-adapter.ts} +240 -240
  50. package/src/{field-remap-flow.tsx → field-remap/flow.tsx} +369 -369
  51. package/src/{field-remap-graph.tsx → field-remap/graph.tsx} +559 -559
  52. package/src/field-remap/jsonata-transform.ts +166 -0
  53. package/src/{field-remap-panel.tsx → field-remap/panel.tsx} +193 -175
  54. package/src/{field-remap-samples.ts → field-remap/samples.ts} +352 -352
  55. package/src/{table-mapping-adapter.ts → field-remap/table-mapping-adapter.ts} +59 -59
  56. package/src/{field-remap-tree.tsx → field-remap/tree.tsx} +404 -404
  57. package/src/{field-remap-view-data.ts → field-remap/view-data.ts} +16 -16
  58. package/src/{field-remap-view.css → field-remap/view.css} +676 -676
  59. package/src/{field-remap-view.tsx → field-remap/view.tsx} +86 -86
  60. package/src/index.ts +212 -199
  61. package/src/{json-form-source-patch.ts → jdw/json-form-source-patch.ts} +323 -323
  62. package/src/{jdw-lab-view-data.ts → jdw/lab-view-data.ts} +25 -25
  63. package/src/{jdw-lab-view.css → jdw/lab-view.css} +21 -21
  64. package/src/{jdw-lab-view.tsx → jdw/lab-view.tsx} +79 -79
  65. package/src/{jdw-widget-form-view.tsx → jdw/widget-form-view.tsx} +68 -65
  66. package/src/{jdw-widget-preview-view.tsx → jdw/widget-preview-view.tsx} +54 -51
  67. package/src/{keybinding-overrides-storage.ts → management/keybinding-overrides-storage.ts} +49 -57
  68. package/src/{keybinding-management-settings.tsx → management/keybinding-settings.tsx} +16 -16
  69. package/src/{management-palette-commands.ts → management/palette-commands.ts} +38 -38
  70. package/src/{preference-settings-storage.ts → management/preference-settings-storage.ts} +50 -58
  71. package/src/{management-settings-ids.ts → management/settings-ids.ts} +12 -12
  72. package/src/{management-settings.tsx → management/settings.tsx} +96 -75
  73. package/src/{use-command-management.ts → management/use-command-management.ts} +191 -191
  74. package/src/{use-keybinding-management.ts → management/use-keybinding-management.ts} +130 -130
  75. package/src/{shell-model.tsx → shell/model.tsx} +149 -149
  76. package/src/{provider.tsx → shell/provider.tsx} +603 -573
  77. package/src/{shell-secondary-actions.tsx → shell/secondary-actions.tsx} +59 -59
  78. package/src/{shell-settings-constants.ts → shell/settings-constants.ts} +9 -9
  79. package/src/{shell-settings.tsx → shell/settings.tsx} +599 -599
  80. package/src/{shell.tsx → shell/shell.tsx} +680 -649
  81. package/src/{shell-titlebar-layout-controls.tsx → shell/titlebar-layout-controls.tsx} +4 -4
  82. package/src/{shell-view-host.tsx → shell/view-host.tsx} +212 -199
  83. package/src/{workbench-appearance-storage.ts → workbench/appearance-storage.ts} +107 -115
  84. package/src/{workbench-command-host.tsx → workbench/command-host.tsx} +243 -243
  85. package/src/{workbench-command-palette.ts → workbench/command-palette.ts} +208 -208
  86. package/src/{workbench-keybinding-bridge.ts → workbench/keybinding-bridge.ts} +54 -54
  87. package/src/{workbench-layout-storage.ts → workbench/layout-storage.ts} +134 -145
  88. package/src/{workbench-profile-modal.tsx → workbench/profile-modal.tsx} +118 -118
  89. package/src/{workbench-shell-command-registration.ts → workbench/shell-command-registration.ts} +58 -58
  90. package/src/{workbench-startup-gate.tsx → workbench/startup-gate.tsx} +136 -136
  91. package/src/{workbench-status-sections.ts → workbench/status-sections.ts} +43 -43
  92. package/src/{use-persisted-workbench-appearance.ts → workbench/use-persisted-appearance.ts} +33 -33
  93. package/src/{workbench-user-commands.ts → workbench/user-commands.ts} +46 -46
  94. package/src/{workspace-view-state.ts → workbench/workspace-view-state.ts} +35 -35
  95. 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 './shell-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
- }
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
+ }