@workbench-kit/shell-react 0.0.2-prototype.0.2.41 → 0.0.2-prototype.0.2.44

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 (39) hide show
  1. package/README.md +81 -2
  2. package/package.json +14 -10
  3. package/src/editor/workspace-reconcile.tsx +12 -7
  4. package/src/extensions/extension-enablement-controller.ts +40 -9
  5. package/src/extensions/theme-selection-protection.ts +80 -55
  6. package/src/field-remap/chrome-labels.ts +117 -0
  7. package/src/field-remap/convert-note-editor.tsx +119 -104
  8. package/src/field-remap/convert-palette.tsx +6 -0
  9. package/src/field-remap/demo.tsx +8 -2
  10. package/src/field-remap/detail-panel.tsx +535 -434
  11. package/src/field-remap/document-io.tsx +299 -0
  12. package/src/field-remap/drag-payload.ts +48 -0
  13. package/src/field-remap/flow-adapter.ts +216 -88
  14. package/src/field-remap/flow-ops.ts +150 -0
  15. package/src/field-remap/flow.tsx +1227 -272
  16. package/src/field-remap/index.ts +2 -0
  17. package/src/field-remap/io-class-browse.tsx +34 -22
  18. package/src/field-remap/keyboard.ts +16 -0
  19. package/src/field-remap/modal-detail.tsx +34 -0
  20. package/src/field-remap/panel.tsx +110 -14
  21. package/src/field-remap/transform-options-editor.tsx +68 -48
  22. package/src/field-remap/view.css +107 -189
  23. package/src/index.ts +7 -0
  24. package/src/keybinding-management-settings.ts +4 -0
  25. package/src/management/keybinding-overrides-storage.ts +48 -23
  26. package/src/management/keybinding-settings-view.tsx +31 -0
  27. package/src/management/keybinding-settings.tsx +4 -12
  28. package/src/management/use-keybinding-management.ts +72 -22
  29. package/src/shell/appearance-catalog.ts +453 -0
  30. package/src/shell/appearance-controller.ts +331 -0
  31. package/src/shell/appearance-presentation.ts +269 -0
  32. package/src/shell/provider.tsx +157 -19
  33. package/src/shell/settings.tsx +282 -153
  34. package/src/shell/shell.tsx +88 -18
  35. package/src/workbench/appearance-storage.ts +2 -2
  36. package/src/workbench/command-host-controller.tsx +223 -0
  37. package/src/workbench/command-host.tsx +100 -150
  38. package/src/workbench/keybinding-bridge.ts +84 -38
  39. package/src/workbench/shell-command-registration.ts +27 -2
@@ -1,32 +1,34 @@
1
- import { useEffect, useMemo, useRef, type ReactNode } from 'react';
1
+ import type { ReactNode } from 'react';
2
2
  import { Badge, Checkbox, Field, Select } from '@workbench-kit/react/primitives';
3
3
  import {
4
- applyWorkbenchAppearance,
5
- DARK_THEME_PRESET_OPTIONS,
6
4
  DEFAULT_SHELL_PRESET,
7
- LIGHT_THEME_PRESET_OPTIONS,
8
5
  SHELL_PRESET_OPTIONS,
9
6
  WORKBENCH_APPEARANCE_FIELD_DESCRIPTIONS,
10
7
  WORKBENCH_APPEARANCE_FIELD_LABELS,
11
8
  WORKBENCH_COLOR_SCHEME_OPTIONS,
12
- type WorkbenchColorSchemePreference,
13
- type WorkbenchThemePresetOption,
14
9
  } from '@workbench-kit/react/workbench';
15
10
  import {
16
11
  WorkbenchSettingsSection,
17
12
  type WorkbenchSettingsCategory,
18
13
  } from '@workbench-kit/react/workbench/settings';
19
14
  import {
20
- applyThemeTokenOverrides,
21
15
  type ConfigurationRegistry,
22
16
  type LocalizationRegistry,
23
17
  type PreferenceService,
24
- type ThemeRegistry,
25
18
  } from '@workbench-kit/workbench-core';
26
19
  import type { PreferenceScope } from '@workbench-kit/workbench-config';
27
20
 
28
21
  import { isRecord } from '../is-record.js';
29
- import { useWorkbench, type WorkbenchExtensionCatalogReader } from './provider.js';
22
+ import type { WorkbenchExtensionCatalogReader } from './provider.js';
23
+ import {
24
+ getWorkbenchAppearanceCatalogEntries,
25
+ resolveWorkbenchAppearanceSelection,
26
+ type WorkbenchAppearanceCatalogEntry,
27
+ type WorkbenchAppearanceCatalogSnapshot,
28
+ type WorkbenchAppearanceSelectionResolution,
29
+ type WorkbenchAppearanceSelectionTarget,
30
+ } from './appearance-catalog.js';
31
+ import { classifyWorkbenchAppearanceThemeSelection } from './appearance-presentation.js';
30
32
  import { WORKBENCH_PREFERENCE_SCOPES } from './settings-constants.js';
31
33
 
32
34
  const APPEARANCE_SETTINGS_CATEGORY_ID = 'workbench.appearance';
@@ -37,6 +39,43 @@ export interface WorkbenchThemeOption {
37
39
  label: string;
38
40
  }
39
41
 
42
+ /** Captures one renderer-private own-data view shared by catalog and Settings projection. */
43
+ export function createWorkbenchThemeOptionSnapshot(
44
+ options: readonly WorkbenchThemeOption[] | undefined,
45
+ ): readonly WorkbenchThemeOption[] | undefined {
46
+ if (options === undefined) {
47
+ return undefined;
48
+ }
49
+
50
+ const snapshot = new Array<WorkbenchThemeOption>(options.length);
51
+ options.forEach((option, sourceOrdinal) => {
52
+ const idDescriptor = Object.getOwnPropertyDescriptor(option, 'id');
53
+ const labelDescriptor = Object.getOwnPropertyDescriptor(option, 'label');
54
+ if (
55
+ !idDescriptor ||
56
+ !('value' in idDescriptor) ||
57
+ typeof idDescriptor.value !== 'string' ||
58
+ !labelDescriptor ||
59
+ !('value' in labelDescriptor) ||
60
+ typeof labelDescriptor.value !== 'string'
61
+ ) {
62
+ return;
63
+ }
64
+
65
+ const descriptionDescriptor = Object.getOwnPropertyDescriptor(option, 'description');
66
+ const captured = {
67
+ id: idDescriptor.value,
68
+ label: labelDescriptor.value,
69
+ ...(descriptionDescriptor && 'value' in descriptionDescriptor
70
+ ? { description: descriptionDescriptor.value as ReactNode }
71
+ : {}),
72
+ };
73
+ snapshot[sourceOrdinal] = Object.freeze(captured);
74
+ });
75
+
76
+ return Object.freeze(snapshot);
77
+ }
78
+
40
79
  export interface WorkbenchLocaleOption {
41
80
  id: string;
42
81
  label: string;
@@ -63,10 +102,10 @@ export interface WorkbenchSettingsCategoryInput extends WorkbenchAppearanceSetti
63
102
  }
64
103
 
65
104
  export interface WorkbenchSettingsContributionAccess {
105
+ readonly appearanceCatalog: WorkbenchAppearanceCatalogSnapshot;
66
106
  readonly configurations: ConfigurationRegistry;
67
107
  readonly extensionCatalog: WorkbenchExtensionCatalogReader;
68
108
  readonly localizations: LocalizationRegistry;
69
- readonly themes: ThemeRegistry;
70
109
  }
71
110
 
72
111
  export function createSettingsCategories(
@@ -88,9 +127,9 @@ export function createSettingsCategories(
88
127
  }: WorkbenchSettingsCategoryInput,
89
128
  ): WorkbenchSettingsCategory[] {
90
129
  const configurations = contributions.configurations.getConfigurations();
91
- const mergedThemeOptions = mergeThemeOptions(themeOptions, contributions.themes.getThemes());
92
130
  const localeOptions = buildLocaleOptions(contributions.localizations.getLocalizations());
93
131
  const appearanceCategory = createAppearanceSettingsCategory({
132
+ appearanceCatalog: contributions.appearanceCatalog,
94
133
  darkPreset,
95
134
  lightPreset,
96
135
  locale,
@@ -102,7 +141,7 @@ export function createSettingsCategories(
102
141
  onThemeChange,
103
142
  shellPreset,
104
143
  theme,
105
- themeOptions: mergedThemeOptions,
144
+ themeOptions,
106
145
  });
107
146
 
108
147
  if (configurations.length === 0) {
@@ -171,33 +210,6 @@ function formatPreferenceScopeLabel(scope: PreferenceScope): string {
171
210
  return WORKBENCH_PREFERENCE_SCOPES.find((candidate) => candidate.id === scope)?.label ?? scope;
172
211
  }
173
212
 
174
- function mergeThemeOptions(
175
- baseOptions: readonly WorkbenchThemeOption[] | undefined,
176
- contributedThemes: readonly {
177
- id: string;
178
- label: string;
179
- tokenOverrides?: Record<string, string> | undefined;
180
- }[],
181
- ): readonly WorkbenchThemeOption[] {
182
- const merged = new Map<string, WorkbenchThemeOption>();
183
-
184
- for (const option of baseOptions ?? []) {
185
- merged.set(option.id, option);
186
- }
187
-
188
- for (const theme of contributedThemes) {
189
- merged.set(theme.id, {
190
- description: theme.tokenOverrides
191
- ? 'Contributed theme with token overrides.'
192
- : 'Contributed theme.',
193
- id: theme.id,
194
- label: theme.label,
195
- });
196
- }
197
-
198
- return [...merged.values()];
199
- }
200
-
201
213
  function buildLocaleOptions(
202
214
  localizations: readonly { locale: string; label: string }[],
203
215
  ): readonly WorkbenchLocaleOption[] {
@@ -210,7 +222,121 @@ function buildLocaleOptions(
210
222
  return options;
211
223
  }
212
224
 
225
+ interface WorkbenchAppearancePresentedOption extends WorkbenchThemeOption {
226
+ readonly entry: WorkbenchAppearanceCatalogEntry;
227
+ }
228
+
229
+ function projectAppearanceOptions(
230
+ catalog: WorkbenchAppearanceCatalogSnapshot,
231
+ target: WorkbenchAppearanceSelectionTarget,
232
+ hostOptions: readonly WorkbenchThemeOption[] | undefined,
233
+ ): readonly WorkbenchAppearancePresentedOption[] {
234
+ const projected: WorkbenchAppearancePresentedOption[] = [];
235
+ const visitedIds = new Set<string>();
236
+
237
+ for (const entry of getWorkbenchAppearanceCatalogEntries(catalog, target)) {
238
+ if (visitedIds.has(entry.id)) {
239
+ continue;
240
+ }
241
+ visitedIds.add(entry.id);
242
+
243
+ const resolution = resolveWorkbenchAppearanceSelection(catalog, target, entry.id);
244
+ if (resolution.status !== 'resolved') {
245
+ continue;
246
+ }
247
+
248
+ let description: ReactNode;
249
+ if (entry.source === 'host-option') {
250
+ const original = hostOptions?.[entry.sourceOrdinal];
251
+ if (!original || original.id !== entry.id || original.label !== entry.label) {
252
+ continue;
253
+ }
254
+ description = original.description;
255
+ } else if (entry.source === 'legacy-extension-theme' || entry.source === 'legacy-host-theme') {
256
+ description = entry.hasLegacyCssOverrides
257
+ ? 'Contributed theme that declares token overrides.'
258
+ : 'Contributed theme.';
259
+ }
260
+
261
+ projected.push({ description, entry, id: entry.id, label: entry.label });
262
+ }
263
+
264
+ return projected;
265
+ }
266
+
267
+ function findProjectedOption(
268
+ options: readonly WorkbenchAppearancePresentedOption[],
269
+ id: string | undefined,
270
+ ): WorkbenchAppearancePresentedOption | undefined {
271
+ return id === undefined ? undefined : options.find((option) => option.id === id);
272
+ }
273
+
274
+ function resolveRawAppearanceSelection(
275
+ catalog: WorkbenchAppearanceCatalogSnapshot,
276
+ target: WorkbenchAppearanceSelectionTarget,
277
+ rawValue: string | undefined,
278
+ ): WorkbenchAppearanceSelectionResolution | undefined {
279
+ return rawValue === undefined
280
+ ? undefined
281
+ : resolveWorkbenchAppearanceSelection(catalog, target, rawValue);
282
+ }
283
+
284
+ function describeAppearanceResolution(
285
+ rawValue: string,
286
+ resolution: Exclude<WorkbenchAppearanceSelectionResolution, { status: 'resolved' }>,
287
+ ): string {
288
+ switch (resolution.status) {
289
+ case 'conflicted':
290
+ return `The appearance “${rawValue}” has conflicting sources. Choose another listed option to recover.`;
291
+ case 'wrong-scheme':
292
+ return `The appearance “${rawValue}” does not belong to the ${resolution.expected} scheme. Choose a listed option to recover.`;
293
+ case 'unresolved':
294
+ return `The appearance “${rawValue}” is unavailable. Choose a listed option to recover.`;
295
+ }
296
+ }
297
+
298
+ function renderInvalidAppearanceOption(
299
+ rawValue: string | undefined,
300
+ resolution: WorkbenchAppearanceSelectionResolution | undefined,
301
+ ) {
302
+ if (rawValue === undefined) {
303
+ return (
304
+ <option disabled value="">
305
+ No appearance selected
306
+ </option>
307
+ );
308
+ }
309
+ if (!resolution || resolution.status === 'resolved') {
310
+ return null;
311
+ }
312
+ return (
313
+ <option disabled value={rawValue}>
314
+ Unavailable: {rawValue}
315
+ </option>
316
+ );
317
+ }
318
+
319
+ function AppearanceSelectionDiagnostic({
320
+ id,
321
+ rawValue,
322
+ resolution,
323
+ }: {
324
+ id: string;
325
+ rawValue: string | undefined;
326
+ resolution: WorkbenchAppearanceSelectionResolution | undefined;
327
+ }) {
328
+ if (rawValue === undefined || !resolution || resolution.status === 'resolved') {
329
+ return null;
330
+ }
331
+ return (
332
+ <p id={id} className="workbench-appearance-settings__description" role="status">
333
+ {describeAppearanceResolution(rawValue, resolution)}
334
+ </p>
335
+ );
336
+ }
337
+
213
338
  function createAppearanceSettingsCategory({
339
+ appearanceCatalog,
214
340
  darkPreset,
215
341
  lightPreset,
216
342
  locale,
@@ -224,25 +350,20 @@ function createAppearanceSettingsCategory({
224
350
  theme,
225
351
  themeOptions,
226
352
  }: WorkbenchAppearanceSettingsInput & {
353
+ appearanceCatalog: WorkbenchAppearanceCatalogSnapshot;
227
354
  localeOptions: readonly WorkbenchLocaleOption[];
228
- themeOptions: readonly WorkbenchThemeOption[];
229
355
  }): WorkbenchSettingsCategory | undefined {
230
- const usesAppearancePresets = lightPreset !== undefined && darkPreset !== undefined;
231
-
232
- if (!usesAppearancePresets && !themeOptions?.length && !localeOptions?.length) {
233
- return undefined;
234
- }
235
-
236
356
  return {
237
357
  content: (
238
358
  <AppearanceSettingsSection
359
+ appearanceCatalog={appearanceCatalog}
239
360
  darkPreset={darkPreset}
240
361
  lightPreset={lightPreset}
241
362
  locale={locale}
242
363
  localeOptions={localeOptions ?? []}
243
364
  shellPreset={shellPreset}
244
365
  theme={theme}
245
- themeOptions={themeOptions ?? []}
366
+ themeOptions={themeOptions}
246
367
  onDarkPresetChange={onDarkPresetChange}
247
368
  onLightPresetChange={onLightPresetChange}
248
369
  onLocaleChange={onLocaleChange}
@@ -256,6 +377,7 @@ function createAppearanceSettingsCategory({
256
377
  }
257
378
 
258
379
  function AppearanceSettingsSection({
380
+ appearanceCatalog,
259
381
  darkPreset,
260
382
  lightPreset,
261
383
  locale,
@@ -269,119 +391,63 @@ function AppearanceSettingsSection({
269
391
  theme,
270
392
  themeOptions,
271
393
  }: WorkbenchAppearanceSettingsInput & {
394
+ appearanceCatalog: WorkbenchAppearanceCatalogSnapshot;
272
395
  localeOptions: readonly WorkbenchLocaleOption[];
273
- themeOptions: readonly WorkbenchThemeOption[];
274
396
  }) {
275
- const { themes } = useWorkbench();
276
- const themeRevision = themes.getRevision();
277
- const containerRef = useRef<HTMLDivElement>(null);
278
- const previousThemeOverridesRef = useRef<Readonly<Record<string, string>> | undefined>(undefined);
279
397
  const usesAppearancePresets = lightPreset !== undefined && darkPreset !== undefined;
280
- const lightPresetOptions = useMemo<readonly WorkbenchThemePresetOption[]>(
281
- () => [
282
- ...LIGHT_THEME_PRESET_OPTIONS,
283
- ...themes
284
- .getThemes()
285
- .filter((contributedTheme) => contributedTheme.mode === 'light')
286
- .map((contributedTheme) => ({ id: contributedTheme.id, label: contributedTheme.label })),
287
- ],
288
- [themeRevision, themes],
398
+ const themeSelection = classifyWorkbenchAppearanceThemeSelection(theme);
399
+ const flatThemeProjection = projectAppearanceOptions(
400
+ appearanceCatalog,
401
+ 'flat-theme',
402
+ themeOptions,
403
+ );
404
+ const lightPresetProjection = projectAppearanceOptions(
405
+ appearanceCatalog,
406
+ 'light-preset',
407
+ themeOptions,
408
+ );
409
+ const darkPresetProjection = projectAppearanceOptions(
410
+ appearanceCatalog,
411
+ 'dark-preset',
412
+ themeOptions,
289
413
  );
290
- const darkPresetOptions = useMemo<readonly WorkbenchThemePresetOption[]>(
291
- () => [
292
- ...DARK_THEME_PRESET_OPTIONS,
293
- ...themes
294
- .getThemes()
295
- .filter((contributedTheme) => contributedTheme.mode === 'dark')
296
- .map((contributedTheme) => ({ id: contributedTheme.id, label: contributedTheme.label })),
297
- ],
298
- [themeRevision, themes],
414
+ const flatThemeResolution = resolveRawAppearanceSelection(
415
+ appearanceCatalog,
416
+ 'flat-theme',
417
+ themeSelection.kind === 'flat-theme' ? themeSelection.rawTheme : undefined,
299
418
  );
300
- const selectedTheme = themeOptions.find((option) => option.id === theme) ?? themeOptions[0];
301
- const selectedThemeId = selectedTheme?.id ?? '';
302
- const selectedColorScheme =
303
- WORKBENCH_COLOR_SCHEME_OPTIONS.find((option) => option.id === theme) ??
304
- WORKBENCH_COLOR_SCHEME_OPTIONS[0];
305
- const selectedColorSchemeId = (selectedColorScheme?.id ??
306
- 'system') as WorkbenchColorSchemePreference;
307
- const selectedLightPreset =
308
- lightPresetOptions.find((option) => option.id === lightPreset) ?? lightPresetOptions[0];
309
- const selectedDarkPreset =
310
- darkPresetOptions.find((option) => option.id === darkPreset) ?? darkPresetOptions[0];
419
+ const lightPresetResolution = resolveRawAppearanceSelection(
420
+ appearanceCatalog,
421
+ 'light-preset',
422
+ lightPreset,
423
+ );
424
+ const darkPresetResolution = resolveRawAppearanceSelection(
425
+ appearanceCatalog,
426
+ 'dark-preset',
427
+ darkPreset,
428
+ );
429
+ const selectedTheme = findProjectedOption(
430
+ flatThemeProjection,
431
+ themeSelection.kind === 'flat-theme' ? themeSelection.rawTheme : undefined,
432
+ );
433
+ const selectedThemeValue =
434
+ themeSelection.kind === 'base-preference' ? themeSelection.preference : themeSelection.rawTheme;
435
+ const colorSchemeDiagnostic =
436
+ usesAppearancePresets && themeSelection.kind === 'flat-theme'
437
+ ? `The color scheme “${themeSelection.rawTheme}” is unavailable. Choose a listed scheme to recover.`
438
+ : undefined;
311
439
  const selectedLocale = localeOptions.find((option) => option.id === locale) ?? localeOptions[0];
312
440
  const selectedLocaleId = selectedLocale?.id ?? 'en';
313
441
  const selectedShellPreset =
314
442
  SHELL_PRESET_OPTIONS.find((option) => option.id === shellPreset) ?? SHELL_PRESET_OPTIONS[0];
315
443
 
316
- useEffect(() => {
317
- if (typeof document === 'undefined') {
318
- return;
319
- }
320
-
321
- let activeThemeId: string | undefined;
322
-
323
- if (usesAppearancePresets) {
324
- const appearanceSettings = {
325
- darkPreset: selectedDarkPreset.id,
326
- lightPreset: selectedLightPreset.id,
327
- shellPreset: selectedShellPreset.id,
328
- themePreference: selectedColorSchemeId,
329
- };
330
- const { resolvedTheme } = applyWorkbenchAppearance(
331
- document.documentElement,
332
- appearanceSettings,
333
- );
334
- activeThemeId = resolvedTheme === 'light' ? selectedLightPreset.id : selectedDarkPreset.id;
335
-
336
- const workbenchRoot = containerRef.current?.closest<HTMLElement>(
337
- '[data-theme-preset], [data-theme], [data-shell-preset]',
338
- );
339
- if (workbenchRoot && workbenchRoot !== document.documentElement) {
340
- applyWorkbenchAppearance(workbenchRoot, appearanceSettings);
341
- }
342
- } else {
343
- activeThemeId = selectedThemeId;
344
- }
345
-
346
- const contributedTheme = activeThemeId ? themes.getTheme(activeThemeId) : undefined;
347
-
348
- // The actual workbench root (e.g. `.ide-root`) re-declares `data-theme-preset` locally,
349
- // which shadows inheritance from `documentElement` for everything rendered inside it.
350
- // Apply the override there too, or it only ever reaches stray document.body portals.
351
- const workbenchRoot = containerRef.current?.closest<HTMLElement>(
352
- '[data-theme-preset], [data-theme]',
353
- );
354
- const overrideTargets =
355
- workbenchRoot && workbenchRoot !== document.documentElement
356
- ? [document.documentElement, workbenchRoot]
357
- : [document.documentElement];
358
-
359
- for (const target of overrideTargets) {
360
- applyThemeTokenOverrides(
361
- target,
362
- contributedTheme?.tokenOverrides,
363
- previousThemeOverridesRef.current,
364
- );
365
- }
366
- previousThemeOverridesRef.current = contributedTheme?.tokenOverrides;
367
- }, [
368
- themeRevision,
369
- themes,
370
- selectedColorSchemeId,
371
- selectedDarkPreset.id,
372
- selectedLightPreset.id,
373
- selectedShellPreset.id,
374
- selectedThemeId,
375
- usesAppearancePresets,
376
- ]);
377
-
378
444
  return (
379
445
  <WorkbenchSettingsSection
380
446
  id="workbench-settings-appearance"
381
447
  title="Appearance"
382
448
  description="Configure how the workbench is presented."
383
449
  >
384
- <div ref={containerRef} className="workbench-appearance-settings">
450
+ <div className="workbench-appearance-settings">
385
451
  {usesAppearancePresets ? (
386
452
  <>
387
453
  <Field
@@ -391,17 +457,34 @@ function AppearanceSettingsSection({
391
457
  >
392
458
  <Select
393
459
  aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.colorScheme}
460
+ aria-describedby={
461
+ colorSchemeDiagnostic ? 'workbench-color-scheme-diagnostic' : undefined
462
+ }
394
463
  controlWidth="full"
395
464
  disabled={!onThemeChange}
396
- value={selectedColorSchemeId}
465
+ value={selectedThemeValue}
397
466
  onValueChange={(nextTheme) => onThemeChange?.(nextTheme)}
398
467
  >
468
+ {colorSchemeDiagnostic ? (
469
+ <option disabled value={theme}>
470
+ Unavailable: {theme}
471
+ </option>
472
+ ) : null}
399
473
  {WORKBENCH_COLOR_SCHEME_OPTIONS.map((option) => (
400
474
  <option key={option.id} value={option.id}>
401
475
  {option.label}
402
476
  </option>
403
477
  ))}
404
478
  </Select>
479
+ {colorSchemeDiagnostic ? (
480
+ <p
481
+ id="workbench-color-scheme-diagnostic"
482
+ className="workbench-appearance-settings__description"
483
+ role="status"
484
+ >
485
+ {colorSchemeDiagnostic}
486
+ </p>
487
+ ) : null}
405
488
  </Field>
406
489
  <Field
407
490
  className="workbench-appearance-settings__field"
@@ -410,17 +493,28 @@ function AppearanceSettingsSection({
410
493
  >
411
494
  <Select
412
495
  aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredLightColorTheme}
496
+ aria-describedby={
497
+ lightPresetResolution && lightPresetResolution.status !== 'resolved'
498
+ ? 'workbench-light-preset-diagnostic'
499
+ : undefined
500
+ }
413
501
  controlWidth="full"
414
502
  disabled={!onLightPresetChange}
415
- value={selectedLightPreset.id}
503
+ value={lightPreset ?? ''}
416
504
  onValueChange={(nextPreset) => onLightPresetChange?.(nextPreset)}
417
505
  >
418
- {lightPresetOptions.map((option) => (
506
+ {renderInvalidAppearanceOption(lightPreset, lightPresetResolution)}
507
+ {lightPresetProjection.map((option) => (
419
508
  <option key={option.id} value={option.id}>
420
509
  {option.label}
421
510
  </option>
422
511
  ))}
423
512
  </Select>
513
+ <AppearanceSelectionDiagnostic
514
+ id="workbench-light-preset-diagnostic"
515
+ rawValue={lightPreset}
516
+ resolution={lightPresetResolution}
517
+ />
424
518
  </Field>
425
519
  <Field
426
520
  className="workbench-appearance-settings__field"
@@ -429,17 +523,28 @@ function AppearanceSettingsSection({
429
523
  >
430
524
  <Select
431
525
  aria-label={WORKBENCH_APPEARANCE_FIELD_LABELS.preferredDarkColorTheme}
526
+ aria-describedby={
527
+ darkPresetResolution && darkPresetResolution.status !== 'resolved'
528
+ ? 'workbench-dark-preset-diagnostic'
529
+ : undefined
530
+ }
432
531
  controlWidth="full"
433
532
  disabled={!onDarkPresetChange}
434
- value={selectedDarkPreset.id}
533
+ value={darkPreset ?? ''}
435
534
  onValueChange={(nextPreset) => onDarkPresetChange?.(nextPreset)}
436
535
  >
437
- {darkPresetOptions.map((option) => (
536
+ {renderInvalidAppearanceOption(darkPreset, darkPresetResolution)}
537
+ {darkPresetProjection.map((option) => (
438
538
  <option key={option.id} value={option.id}>
439
539
  {option.label}
440
540
  </option>
441
541
  ))}
442
542
  </Select>
543
+ <AppearanceSelectionDiagnostic
544
+ id="workbench-dark-preset-diagnostic"
545
+ rawValue={darkPreset}
546
+ resolution={darkPresetResolution}
547
+ />
443
548
  </Field>
444
549
  <Field
445
550
  className="workbench-appearance-settings__field"
@@ -461,7 +566,7 @@ function AppearanceSettingsSection({
461
566
  </Select>
462
567
  </Field>
463
568
  </>
464
- ) : themeOptions.length ? (
569
+ ) : (
465
570
  <Field
466
571
  className="workbench-appearance-settings__field"
467
572
  label="Color theme"
@@ -469,24 +574,48 @@ function AppearanceSettingsSection({
469
574
  >
470
575
  <Select
471
576
  aria-label="Color theme"
577
+ aria-describedby={
578
+ flatThemeResolution && flatThemeResolution.status !== 'resolved'
579
+ ? 'workbench-flat-theme-diagnostic'
580
+ : undefined
581
+ }
472
582
  controlWidth="full"
473
583
  disabled={!onThemeChange}
474
- value={selectedThemeId}
584
+ value={selectedThemeValue}
475
585
  onValueChange={(nextTheme) => onThemeChange?.(nextTheme)}
476
586
  >
477
- {themeOptions.map((option) => (
587
+ {renderInvalidAppearanceOption(
588
+ themeSelection.kind === 'flat-theme' ? themeSelection.rawTheme : undefined,
589
+ flatThemeResolution,
590
+ )}
591
+ {WORKBENCH_COLOR_SCHEME_OPTIONS.map((option) => (
478
592
  <option key={option.id} value={option.id}>
479
593
  {option.label}
480
594
  </option>
481
595
  ))}
596
+ {flatThemeProjection
597
+ .filter(
598
+ (option) =>
599
+ classifyWorkbenchAppearanceThemeSelection(option.id).kind === 'flat-theme',
600
+ )
601
+ .map((option) => (
602
+ <option key={option.id} value={option.id}>
603
+ {option.label}
604
+ </option>
605
+ ))}
482
606
  </Select>
483
607
  {selectedTheme?.description ? (
484
608
  <p className="workbench-appearance-settings__description">
485
609
  {selectedTheme.description}
486
610
  </p>
487
611
  ) : null}
612
+ <AppearanceSelectionDiagnostic
613
+ id="workbench-flat-theme-diagnostic"
614
+ rawValue={themeSelection.kind === 'flat-theme' ? themeSelection.rawTheme : undefined}
615
+ resolution={flatThemeResolution}
616
+ />
488
617
  </Field>
489
- ) : null}
618
+ )}
490
619
  {localeOptions.length > 1 ? (
491
620
  <Field
492
621
  className="workbench-appearance-settings__field"