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

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 (35) hide show
  1. package/README.md +42 -2
  2. package/package.json +13 -10
  3. package/src/extensions/extension-enablement-controller.ts +40 -9
  4. package/src/extensions/theme-selection-protection.ts +80 -55
  5. package/src/field-remap/chrome-labels.ts +18 -0
  6. package/src/field-remap/convert-note-editor.tsx +30 -24
  7. package/src/field-remap/convert-palette.tsx +6 -0
  8. package/src/field-remap/demo.tsx +8 -2
  9. package/src/field-remap/detail-panel.tsx +225 -192
  10. package/src/field-remap/drag-payload.ts +48 -0
  11. package/src/field-remap/flow-adapter.ts +216 -88
  12. package/src/field-remap/flow-ops.ts +150 -0
  13. package/src/field-remap/flow.tsx +1055 -162
  14. package/src/field-remap/index.ts +2 -0
  15. package/src/field-remap/io-class-browse.tsx +34 -22
  16. package/src/field-remap/keyboard.ts +16 -0
  17. package/src/field-remap/modal-detail.tsx +34 -0
  18. package/src/field-remap/panel.tsx +50 -14
  19. package/src/field-remap/transform-options-editor.tsx +5 -1
  20. package/src/field-remap/view.css +61 -42
  21. package/src/index.ts +7 -0
  22. package/src/management/keybinding-overrides-storage.ts +48 -23
  23. package/src/management/keybinding-settings.tsx +10 -1
  24. package/src/management/use-keybinding-management.ts +69 -19
  25. package/src/shell/appearance-catalog.ts +453 -0
  26. package/src/shell/appearance-controller.ts +331 -0
  27. package/src/shell/appearance-presentation.ts +269 -0
  28. package/src/shell/provider.tsx +147 -18
  29. package/src/shell/settings.tsx +282 -153
  30. package/src/shell/shell.tsx +88 -18
  31. package/src/workbench/appearance-storage.ts +2 -2
  32. package/src/workbench/command-host-controller.tsx +223 -0
  33. package/src/workbench/command-host.tsx +100 -150
  34. package/src/workbench/keybinding-bridge.ts +84 -38
  35. package/src/workbench/shell-command-registration.ts +27 -2
@@ -1,4 +1,13 @@
1
- import { useCallback, useEffect, useMemo, useReducer, useState, type ReactNode } from 'react';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useReducer,
6
+ useRef,
7
+ useState,
8
+ type CSSProperties,
9
+ type ReactNode,
10
+ } from 'react';
2
11
  import { Modal } from '@workbench-kit/react/modal';
3
12
  import { Badge, Button, IconButton } from '@workbench-kit/react/primitives';
4
13
  import {
@@ -18,7 +27,6 @@ import type {
18
27
  import { WORKBENCH_PERMISSION_CONTEXT_KEY_CAN_OPEN_SETTINGS } from '@workbench-kit/platform';
19
28
  import { isPreferenceScope, type PreferenceScope } from '@workbench-kit/workbench-config';
20
29
  import {
21
- resolveActiveThemePreset,
22
30
  DEFAULT_SHELL_PRESET,
23
31
  useResolvedWorkbenchTheme,
24
32
  type WorkbenchCommandDescriptor,
@@ -62,7 +70,17 @@ import {
62
70
  getWorkbenchSecondaryActivityRoute,
63
71
  } from './secondary-actions.js';
64
72
  import { SETTINGS_EXTENSION_ID, WORKBENCH_PREFERENCE_SCOPES } from './settings-constants.js';
65
- import { createSettingsCategories, type WorkbenchThemeOption } from './settings.js';
73
+ import {
74
+ createSettingsCategories,
75
+ createWorkbenchThemeOptionSnapshot,
76
+ type WorkbenchThemeOption,
77
+ } from './settings.js';
78
+ import { createWorkbenchAppearanceCatalogSnapshot } from './appearance-catalog.js';
79
+ import {
80
+ createWorkbenchDocumentAppearanceDiagnosticController,
81
+ createWorkbenchDocumentAppearanceOverrideController,
82
+ } from './appearance-controller.js';
83
+ import { resolveWorkbenchAppearancePresentation } from './appearance-presentation.js';
66
84
  import {
67
85
  createContributedWorkbenchStatusSections,
68
86
  createDefaultWorkbenchStatusSections,
@@ -200,10 +218,6 @@ export function WorkbenchShell({
200
218
  }: WorkbenchShellProps) {
201
219
  const resolvedEditorArea = editorArea ?? null;
202
220
  const resolvedWorkbenchTheme = useResolvedWorkbenchTheme(theme ?? 'system');
203
- const activeThemePreset =
204
- lightPreset !== undefined && darkPreset !== undefined
205
- ? resolveActiveThemePreset(resolvedWorkbenchTheme, { darkPreset, lightPreset })
206
- : undefined;
207
221
  const {
208
222
  activities,
209
223
  commands,
@@ -222,6 +236,26 @@ export function WorkbenchShell({
222
236
  viewHostFactories,
223
237
  views,
224
238
  } = useWorkbench();
239
+ const appearanceThemeOptions = createWorkbenchThemeOptionSnapshot(themeOptions);
240
+ const appearanceCatalog = createWorkbenchAppearanceCatalogSnapshot({
241
+ hostOptions: appearanceThemeOptions,
242
+ themes,
243
+ });
244
+ const appearancePresentation = resolveWorkbenchAppearancePresentation({
245
+ catalog: appearanceCatalog,
246
+ darkPreset,
247
+ lightPreset,
248
+ resolvedSystemTheme: resolvedWorkbenchTheme,
249
+ theme,
250
+ });
251
+ const rootAppearanceStyle = appearancePresentation.legacyTokenOverrides as
252
+ CSSProperties | undefined;
253
+ const documentAppearanceControllerRef = useRef<
254
+ ReturnType<typeof createWorkbenchDocumentAppearanceOverrideController> | undefined
255
+ >(undefined);
256
+ const documentAppearanceDiagnosticControllerRef = useRef<
257
+ ReturnType<typeof createWorkbenchDocumentAppearanceDiagnosticController> | undefined
258
+ >(undefined);
225
259
  const extensionEnablement = useExtensionEnablementController();
226
260
  const contextKeyRevision = useContextKeyRevision(contextKeyService);
227
261
  const contextKeySnapshot = useMemo(
@@ -233,13 +267,14 @@ export function WorkbenchShell({
233
267
  const themeSelectionProtection = useMemo(
234
268
  () =>
235
269
  createThemeSelectionProtectionSnapshot({
270
+ catalog: appearanceCatalog,
236
271
  darkPreset,
237
272
  lightPreset,
238
273
  theme,
239
274
  themeOptions,
240
275
  themes,
241
276
  }),
242
- [darkPreset, lightPreset, theme, themeOptions, themeRevision, themes],
277
+ [appearanceCatalog, darkPreset, lightPreset, theme, themeOptions, themeRevision, themes],
243
278
  );
244
279
  const [preferenceRevision, bumpPreferenceRevision] = useReducer((count: number) => count + 1, 0);
245
280
  const [isHelpOpen, setHelpOpen] = useState(false);
@@ -254,6 +289,9 @@ export function WorkbenchShell({
254
289
  setSettingsCategoryId(categoryId);
255
290
  setSettingsOpen(true);
256
291
  }, []);
292
+ const closeSettingsModal = useCallback(() => {
293
+ setSettingsOpen(false);
294
+ }, []);
257
295
  const settingsCapability = useMemo<WorkbenchSettingsCapability>(
258
296
  () => ({
259
297
  openSettings: showSettingsModal,
@@ -313,11 +351,40 @@ export function WorkbenchShell({
313
351
  });
314
352
 
315
353
  useEffect(() => {
316
- extensionEnablement.setThemeSelectionProtection(themeSelectionProtection);
354
+ return extensionEnablement.setThemeSelectionProtection(themeSelectionProtection);
355
+ }, [extensionEnablement, themeSelectionProtection]);
356
+
357
+ useEffect(() => {
358
+ if (typeof document === 'undefined') {
359
+ return undefined;
360
+ }
361
+ const controller = createWorkbenchDocumentAppearanceOverrideController(
362
+ document.documentElement,
363
+ );
364
+ const diagnosticController = createWorkbenchDocumentAppearanceDiagnosticController(
365
+ document.documentElement,
366
+ );
367
+ documentAppearanceControllerRef.current = controller;
368
+ documentAppearanceDiagnosticControllerRef.current = diagnosticController;
317
369
  return () => {
318
- extensionEnablement.setThemeSelectionProtection(undefined);
370
+ documentAppearanceControllerRef.current = undefined;
371
+ documentAppearanceDiagnosticControllerRef.current = undefined;
372
+ diagnosticController.dispose();
373
+ controller.dispose();
319
374
  };
320
- }, [extensionEnablement, themeSelectionProtection]);
375
+ }, []);
376
+
377
+ useEffect(() => {
378
+ documentAppearanceControllerRef.current?.update(appearancePresentation.legacyTokenOverrides);
379
+ documentAppearanceDiagnosticControllerRef.current?.update({
380
+ unresolvedTheme: appearancePresentation.unresolvedTheme,
381
+ unresolvedThemePreset: appearancePresentation.unresolvedThemePreset,
382
+ });
383
+ }, [
384
+ appearancePresentation.legacyTokenOverrides,
385
+ appearancePresentation.unresolvedTheme,
386
+ appearancePresentation.unresolvedThemePreset,
387
+ ]);
321
388
 
322
389
  useEffect(() => {
323
390
  if (visibleActivityItems.length === 0) {
@@ -374,7 +441,7 @@ export function WorkbenchShell({
374
441
  ...managementCategories,
375
442
  ...(additionalSettingsCategories ?? []),
376
443
  ...createSettingsCategories(
377
- { configurations, extensionCatalog, localizations, themes },
444
+ { appearanceCatalog, configurations, extensionCatalog, localizations },
378
445
  {
379
446
  activeScope: settingsScopeId,
380
447
  darkPreset,
@@ -388,13 +455,14 @@ export function WorkbenchShell({
388
455
  preferenceService,
389
456
  shellPreset,
390
457
  theme,
391
- themeOptions,
458
+ themeOptions: appearanceThemeOptions,
392
459
  },
393
460
  ),
394
461
  ];
395
462
  }, [
396
463
  accountManagement,
397
464
  additionalSettingsCategories,
465
+ appearanceCatalog,
398
466
  commandHost,
399
467
  darkPreset,
400
468
  configurations,
@@ -414,7 +482,7 @@ export function WorkbenchShell({
414
482
  theme,
415
483
  themeRevision,
416
484
  themes,
417
- themeOptions,
485
+ appearanceThemeOptions,
418
486
  ]);
419
487
  const defaultSettingsCategoryId =
420
488
  settingsCategories.find((category) => category.id === SETTINGS_EXTENSION_ID)?.id ??
@@ -699,11 +767,13 @@ export function WorkbenchShell({
699
767
  sizePercent: layout.panel.sizePercent,
700
768
  }}
701
769
  rootClassName={rootClassName}
770
+ rootStyle={rootAppearanceStyle}
702
771
  secondaryArea={resolvedEditorArea}
703
772
  statusSections={resolvedStatusSections}
704
773
  titleBar={resolvedTitleBar}
705
- theme={resolvedWorkbenchTheme}
706
- themePreset={activeThemePreset}
774
+ theme={appearancePresentation.theme}
775
+ themePreference={appearancePresentation.themePreference}
776
+ themePreset={appearancePresentation.themePreset}
707
777
  shellPreset={shellPreset}
708
778
  overlays={
709
779
  <>
@@ -721,7 +791,7 @@ export function WorkbenchShell({
721
791
  categories={settingsCategories}
722
792
  defaultActiveCategoryId={defaultSettingsCategoryId}
723
793
  defaultActiveScopeId="workspace"
724
- footer={<Button onClick={() => setSettingsOpen(false)}>Close</Button>}
794
+ footer={<Button onClick={closeSettingsModal}>Close</Button>}
725
795
  scopes={[...WORKBENCH_PREFERENCE_SCOPES]}
726
796
  searchValue={settingsSearchValue}
727
797
  title={chromeLabels.settingsLabel}
@@ -733,7 +803,7 @@ export function WorkbenchShell({
733
803
  </Badge>
734
804
  }
735
805
  onActiveCategoryIdChange={setSettingsCategoryId}
736
- onClose={() => setSettingsOpen(false)}
806
+ onClose={closeSettingsModal}
737
807
  onScopeChange={(scopeId) => {
738
808
  if (isPreferenceScope(scopeId)) {
739
809
  setSettingsScopeId(scopeId);
@@ -88,8 +88,8 @@ function normalizeWorkbenchAppearance(value: unknown): WorkbenchAppearanceSettin
88
88
  const record = value as Record<string, unknown>;
89
89
  const themePreference = normalizeThemePreference(record.themePreference);
90
90
  // Preset ids may belong to a contributed theme, which isn't known until extensions
91
- // register, so we can only validate shape here; the appearance UI falls back to a
92
- // default option if the persisted id no longer resolves to anything.
91
+ // register, so we can only validate shape here. The appearance UI keeps an unresolved
92
+ // string visible until the user explicitly chooses a current option.
93
93
  const lightPreset = normalizePresetId(
94
94
  record.lightPreset,
95
95
  DEFAULT_WORKBENCH_APPEARANCE.lightPreset,
@@ -0,0 +1,223 @@
1
+ import {
2
+ WorkbenchCommandPalette,
3
+ WorkbenchQuickOpen,
4
+ WorkbenchShortcutCommandBridge,
5
+ matchesWorkbenchCommandPaletteShortcut,
6
+ matchesWorkbenchQuickAccessShortcut,
7
+ resolveQuickOpenItemPath,
8
+ type QuickOpenItem,
9
+ type QuickOpenProvider,
10
+ type QuickOpenSelectContext,
11
+ type WorkbenchCommandDescriptor,
12
+ type WorkbenchCommandRunContext,
13
+ type WorkbenchShortcutCommandBridgeProps,
14
+ } from '@workbench-kit/react/workbench/command-ui';
15
+ import { useCallback, useEffect, useState, type JSX } from 'react';
16
+
17
+ const WORKSPACE_OPEN_COMMAND_ID = 'workspace.open' as const;
18
+ const EMPTY_QUICK_OPEN_PROVIDERS: readonly QuickOpenProvider[] = Object.freeze([]);
19
+
20
+ export type WorkbenchCommandHostExecutor = (
21
+ commandId: string,
22
+ ...args: unknown[]
23
+ ) => unknown | Promise<unknown>;
24
+
25
+ export interface WorkbenchCommandHostControllerProps<TContext = unknown> {
26
+ commands: readonly WorkbenchCommandDescriptor[];
27
+ executeCommand: WorkbenchCommandHostExecutor;
28
+
29
+ commandPaletteCloseLabel?: string;
30
+ commandPaletteEmptyLabel?: string;
31
+ commandPalettePlaceholder?: string;
32
+ commandPaletteTitle?: string;
33
+ enableCommandPalette?: boolean;
34
+ enableQuickOpen?: boolean;
35
+ quickOpenCloseLabel?: string;
36
+ quickOpenEmptyLabel?: string;
37
+ quickOpenPlaceholder?: string;
38
+ quickOpenProviders?: readonly QuickOpenProvider[];
39
+ quickOpenTitle?: string;
40
+
41
+ onOpenQuickOpenItem?: (item: QuickOpenItem, context: QuickOpenSelectContext) => boolean | void;
42
+ onRunCommand?: (
43
+ command: WorkbenchCommandDescriptor,
44
+ context: WorkbenchCommandRunContext,
45
+ ) => boolean | void;
46
+
47
+ shortcutBridge?: false | WorkbenchShortcutCommandBridgeProps<TContext>;
48
+ }
49
+
50
+ export function WorkbenchCommandHostController<TContext = unknown>({
51
+ commands,
52
+ executeCommand,
53
+ commandPaletteCloseLabel = 'Close command palette',
54
+ commandPaletteEmptyLabel = 'No commands match your search',
55
+ commandPalettePlaceholder = 'Search commands',
56
+ commandPaletteTitle = 'Command Palette',
57
+ enableCommandPalette = true,
58
+ enableQuickOpen = true,
59
+ quickOpenCloseLabel = 'Close Quick Open',
60
+ quickOpenEmptyLabel = 'No matching files',
61
+ quickOpenPlaceholder = 'Search files by name',
62
+ quickOpenProviders = EMPTY_QUICK_OPEN_PROVIDERS,
63
+ quickOpenTitle = 'Quick Open',
64
+ onOpenQuickOpenItem,
65
+ onRunCommand,
66
+ shortcutBridge,
67
+ }: WorkbenchCommandHostControllerProps<TContext>): JSX.Element {
68
+ const [paletteOpen, setPaletteOpen] = useState(false);
69
+ const [paletteQuery, setPaletteQuery] = useState('');
70
+ const [quickOpenOpen, setQuickOpenOpen] = useState(false);
71
+ const [quickOpenQuery, setQuickOpenQuery] = useState('');
72
+
73
+ const closePalette = useCallback(() => {
74
+ setPaletteOpen(false);
75
+ }, []);
76
+
77
+ const openPalette = useCallback((query = '') => {
78
+ setQuickOpenOpen(false);
79
+ setPaletteQuery(query);
80
+ setPaletteOpen(true);
81
+ }, []);
82
+
83
+ const closeQuickOpen = useCallback(() => {
84
+ setQuickOpenOpen(false);
85
+ }, []);
86
+
87
+ const openQuickOpen = useCallback((query = '') => {
88
+ setPaletteOpen(false);
89
+ setQuickOpenQuery(query);
90
+ setQuickOpenOpen(true);
91
+ }, []);
92
+
93
+ const runPaletteCommand = useCallback(
94
+ (command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => {
95
+ if (onRunCommand?.(command, context)) {
96
+ closePalette();
97
+ return;
98
+ }
99
+
100
+ runCommandHostExecution(() => executeCommand(command.id), closePalette);
101
+ },
102
+ [closePalette, executeCommand, onRunCommand],
103
+ );
104
+
105
+ const runQuickOpenItem = useCallback(
106
+ (item: QuickOpenItem, context: QuickOpenSelectContext) => {
107
+ if (onOpenQuickOpenItem?.(item, context)) {
108
+ closeQuickOpen();
109
+ return;
110
+ }
111
+
112
+ const path = resolveQuickOpenItemPath(item);
113
+ if (!path) {
114
+ closeQuickOpen();
115
+ return;
116
+ }
117
+
118
+ runCommandHostExecution(
119
+ () => executeCommand(WORKSPACE_OPEN_COMMAND_ID, { path }),
120
+ closeQuickOpen,
121
+ );
122
+ },
123
+ [closeQuickOpen, executeCommand, onOpenQuickOpenItem],
124
+ );
125
+
126
+ useEffect(() => {
127
+ if (!enableCommandPalette && !enableQuickOpen) {
128
+ return undefined;
129
+ }
130
+
131
+ const onKeyDown = (event: KeyboardEvent) => {
132
+ if (enableCommandPalette && matchesWorkbenchCommandPaletteShortcut(event)) {
133
+ event.preventDefault();
134
+ openPalette('>');
135
+ return;
136
+ }
137
+
138
+ if (matchesWorkbenchQuickAccessShortcut(event)) {
139
+ event.preventDefault();
140
+ if (enableQuickOpen) {
141
+ openQuickOpen();
142
+ return;
143
+ }
144
+
145
+ if (enableCommandPalette) {
146
+ openPalette();
147
+ }
148
+ }
149
+ };
150
+
151
+ window.addEventListener('keydown', onKeyDown);
152
+ return () => {
153
+ window.removeEventListener('keydown', onKeyDown);
154
+ };
155
+ }, [enableCommandPalette, enableQuickOpen, openPalette, openQuickOpen]);
156
+
157
+ return (
158
+ <>
159
+ {shortcutBridge ? <WorkbenchShortcutCommandBridge {...shortcutBridge} /> : null}
160
+ {enableCommandPalette ? (
161
+ <WorkbenchCommandPalette
162
+ closeLabel={commandPaletteCloseLabel}
163
+ commands={commands}
164
+ emptyLabel={commandPaletteEmptyLabel}
165
+ open={paletteOpen}
166
+ placeholder={commandPalettePlaceholder}
167
+ query={paletteQuery}
168
+ title={commandPaletteTitle}
169
+ onClose={closePalette}
170
+ onQueryChange={setPaletteQuery}
171
+ onRunCommand={runPaletteCommand}
172
+ />
173
+ ) : null}
174
+ {enableQuickOpen ? (
175
+ <WorkbenchQuickOpen
176
+ closeLabel={quickOpenCloseLabel}
177
+ emptyLabel={quickOpenEmptyLabel}
178
+ open={quickOpenOpen}
179
+ placeholder={quickOpenPlaceholder}
180
+ providers={quickOpenProviders}
181
+ query={quickOpenQuery}
182
+ title={quickOpenTitle}
183
+ onClose={closeQuickOpen}
184
+ onQueryChange={setQuickOpenQuery}
185
+ onSelectItem={runQuickOpenItem}
186
+ />
187
+ ) : null}
188
+ </>
189
+ );
190
+ }
191
+
192
+ function runCommandHostExecution(execute: () => unknown, close: () => void): void {
193
+ let result: unknown;
194
+ try {
195
+ result = execute();
196
+ } catch (error) {
197
+ close();
198
+ throw error;
199
+ }
200
+
201
+ let pending: boolean;
202
+ try {
203
+ pending = isPromiseLike(result);
204
+ } catch (error) {
205
+ close();
206
+ throw error;
207
+ }
208
+
209
+ if (!pending) {
210
+ close();
211
+ return;
212
+ }
213
+
214
+ void Promise.resolve(result).finally(close);
215
+ }
216
+
217
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
218
+ return (
219
+ ((typeof value === 'object' && value !== null) || typeof value === 'function') &&
220
+ 'then' in value &&
221
+ typeof value.then === 'function'
222
+ );
223
+ }