@workbench-kit/shell-react 0.0.2-prototype.0.2.13 → 0.0.2-prototype.0.2.15

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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Host-overridable chrome strings for WorkbenchShell surfaces
3
+ * (ActivityBar / StatusBar aria, secondary Profile/Settings, command palette,
4
+ * Quick Open).
5
+ *
6
+ * Resolution order per string: `labels[key]` → `t(capabilityId, default)` → English default.
7
+ * Kit does not ship locale packs — hosts inject `t` or partial `labels`.
8
+ */
9
+
10
+ export type WorkbenchTranslate = (
11
+ key: string,
12
+ fallback: string,
13
+ params?: Readonly<Record<string, string | number>>,
14
+ ) => string;
15
+
16
+ /** Optional host i18n bag (same `t` signature as Field Remap chrome). */
17
+ export interface WorkbenchI18n {
18
+ readonly t: WorkbenchTranslate;
19
+ }
20
+
21
+ export interface WorkbenchShellChromeLabels {
22
+ readonly activityBarAriaLabel: string;
23
+ readonly statusBarAriaLabel: string;
24
+ readonly profileLabel: string;
25
+ readonly profileTitle: string;
26
+ readonly settingsLabel: string;
27
+ readonly commandPaletteTitle: string;
28
+ readonly commandPalettePlaceholder: string;
29
+ readonly commandPaletteCloseLabel: string;
30
+ readonly commandPaletteEmptyLabel: string;
31
+ readonly quickOpenTitle: string;
32
+ readonly quickOpenPlaceholder: string;
33
+ readonly quickOpenCloseLabel: string;
34
+ readonly quickOpenEmptyLabel: string;
35
+ }
36
+
37
+ export const defaultWorkbenchShellChromeLabels: WorkbenchShellChromeLabels = {
38
+ activityBarAriaLabel: 'Activity bar',
39
+ statusBarAriaLabel: 'Status bar',
40
+ profileLabel: 'Profile',
41
+ profileTitle: 'Open profile',
42
+ settingsLabel: 'Settings',
43
+ commandPaletteTitle: 'Command Palette',
44
+ commandPalettePlaceholder: 'Search commands',
45
+ commandPaletteCloseLabel: 'Close command palette',
46
+ commandPaletteEmptyLabel: 'No commands match your search',
47
+ quickOpenTitle: 'Quick Open',
48
+ quickOpenPlaceholder: 'Search files by name',
49
+ quickOpenCloseLabel: 'Close Quick Open',
50
+ quickOpenEmptyLabel: 'No matching files',
51
+ };
52
+
53
+ /** Stable capability ids for optional `t()` injection (not free prose). */
54
+ export const workbenchShellChromeLabelKeys = {
55
+ activityBarAriaLabel: 'shell.activityBar',
56
+ statusBarAriaLabel: 'shell.statusBar',
57
+ profileLabel: 'shell.profile',
58
+ profileTitle: 'shell.profileTitle',
59
+ settingsLabel: 'shell.settings',
60
+ commandPaletteTitle: 'commandPalette.title',
61
+ commandPalettePlaceholder: 'commandPalette.placeholder',
62
+ commandPaletteCloseLabel: 'commandPalette.close',
63
+ commandPaletteEmptyLabel: 'commandPalette.empty',
64
+ quickOpenTitle: 'quickOpen.title',
65
+ quickOpenPlaceholder: 'quickOpen.placeholder',
66
+ quickOpenCloseLabel: 'quickOpen.close',
67
+ quickOpenEmptyLabel: 'quickOpen.empty',
68
+ } as const satisfies Record<keyof WorkbenchShellChromeLabels, string>;
69
+
70
+ export function resolveWorkbenchShellChromeLabels(
71
+ labels?: Partial<WorkbenchShellChromeLabels> | undefined,
72
+ t?: WorkbenchTranslate | undefined,
73
+ ): WorkbenchShellChromeLabels {
74
+ const resolve = <K extends keyof WorkbenchShellChromeLabels>(key: K): string => {
75
+ const override = labels?.[key];
76
+ if (override !== undefined) {
77
+ return override;
78
+ }
79
+ const fallback = defaultWorkbenchShellChromeLabels[key];
80
+ return t?.(workbenchShellChromeLabelKeys[key], fallback) ?? fallback;
81
+ };
82
+
83
+ return {
84
+ activityBarAriaLabel: resolve('activityBarAriaLabel'),
85
+ statusBarAriaLabel: resolve('statusBarAriaLabel'),
86
+ profileLabel: resolve('profileLabel'),
87
+ profileTitle: resolve('profileTitle'),
88
+ settingsLabel: resolve('settingsLabel'),
89
+ commandPaletteTitle: resolve('commandPaletteTitle'),
90
+ commandPalettePlaceholder: resolve('commandPalettePlaceholder'),
91
+ commandPaletteCloseLabel: resolve('commandPaletteCloseLabel'),
92
+ commandPaletteEmptyLabel: resolve('commandPaletteEmptyLabel'),
93
+ quickOpenTitle: resolve('quickOpenTitle'),
94
+ quickOpenPlaceholder: resolve('quickOpenPlaceholder'),
95
+ quickOpenCloseLabel: resolve('quickOpenCloseLabel'),
96
+ quickOpenEmptyLabel: resolve('quickOpenEmptyLabel'),
97
+ };
98
+ }
@@ -1,8 +1,13 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { ActivityBarItem, StatusBarSectionModel } from '@workbench-kit/react/workbench/shell';
2
+ import type {
3
+ ActivityBarItem,
4
+ StatusBarItemModel,
5
+ StatusBarSectionModel,
6
+ } from '@workbench-kit/react/workbench/shell';
3
7
  import type {
4
8
  ExtensionDependencyDiagnosticSeverity,
5
9
  WorkbenchActivityContribution,
10
+ WorkbenchStatusBarContribution,
6
11
  WorkbenchViewContainerContribution,
7
12
  WorkbenchViewContribution,
8
13
  } from '@workbench-kit/workbench-core';
@@ -65,6 +70,63 @@ export function createWorkbenchShellActivityItems({
65
70
  });
66
71
  }
67
72
 
73
+ function compareStatusBarPriority(
74
+ left: WorkbenchStatusBarContribution,
75
+ right: WorkbenchStatusBarContribution,
76
+ ): number {
77
+ const leftPriority = left.priority ?? 0;
78
+ const rightPriority = right.priority ?? 0;
79
+ if (leftPriority !== rightPriority) {
80
+ return rightPriority - leftPriority;
81
+ }
82
+
83
+ return left.id.localeCompare(right.id);
84
+ }
85
+
86
+ function toStatusBarItemModel(item: WorkbenchStatusBarContribution): StatusBarItemModel {
87
+ return {
88
+ id: item.id,
89
+ label: item.text,
90
+ order: item.priority,
91
+ title: item.text,
92
+ };
93
+ }
94
+
95
+ /** Map extension `contributes.statusBar` items into shell status sections. */
96
+ export function createContributedWorkbenchStatusSections(
97
+ items: readonly WorkbenchStatusBarContribution[],
98
+ ): StatusBarSectionModel[] {
99
+ const leftItems = items
100
+ .filter((item) => item.alignment === 'left')
101
+ .sort(compareStatusBarPriority)
102
+ .map(toStatusBarItemModel);
103
+ const rightItems = items
104
+ .filter((item) => item.alignment === 'right')
105
+ .sort(compareStatusBarPriority)
106
+ .map(toStatusBarItemModel);
107
+
108
+ return [
109
+ ...(leftItems.length > 0
110
+ ? [
111
+ {
112
+ align: 'start' as const,
113
+ id: 'extension-status-left',
114
+ items: leftItems,
115
+ },
116
+ ]
117
+ : []),
118
+ ...(rightItems.length > 0
119
+ ? [
120
+ {
121
+ align: 'end' as const,
122
+ id: 'extension-status-right',
123
+ items: rightItems,
124
+ },
125
+ ]
126
+ : []),
127
+ ];
128
+ }
129
+
68
130
  export function createDefaultWorkbenchStatusSections({
69
131
  dependencyDiagnostics,
70
132
  extensionCount,
@@ -11,6 +11,10 @@ export interface WorkbenchSecondaryActivityItemsInput {
11
11
  isProfileOpen: boolean;
12
12
  isSettingsOpen: boolean;
13
13
  showSettings?: boolean | undefined;
14
+ /** Resolved chrome labels (defaults match English kit copy). */
15
+ profileLabel?: string | undefined;
16
+ profileTitle?: string | undefined;
17
+ settingsLabel?: string | undefined;
14
18
  }
15
19
 
16
20
  export function createWorkbenchSecondaryActivityItems({
@@ -18,6 +22,9 @@ export function createWorkbenchSecondaryActivityItems({
18
22
  isProfileOpen,
19
23
  isSettingsOpen,
20
24
  showSettings = true,
25
+ profileLabel = 'Profile',
26
+ profileTitle = 'Open profile',
27
+ settingsLabel = 'Settings',
21
28
  }: WorkbenchSecondaryActivityItemsInput): ActivityBarItem[] {
22
29
  return [
23
30
  ...(hasProfile
@@ -26,8 +33,8 @@ export function createWorkbenchSecondaryActivityItems({
26
33
  active: isProfileOpen,
27
34
  icon: <i aria-hidden="true" className="codicon codicon-account" />,
28
35
  id: WORKBENCH_PROFILE_ACTIVITY_ITEM_ID,
29
- label: 'Profile',
30
- title: 'Open profile',
36
+ label: profileLabel,
37
+ title: profileTitle,
31
38
  },
32
39
  ]
33
40
  : []),
@@ -37,7 +44,7 @@ export function createWorkbenchSecondaryActivityItems({
37
44
  active: isSettingsOpen,
38
45
  icon: <i aria-hidden="true" className="codicon codicon-settings-gear" />,
39
46
  id: WORKBENCH_SETTINGS_ACTIVITY_ITEM_ID,
40
- label: 'Settings',
47
+ label: settingsLabel,
41
48
  },
42
49
  ]
43
50
  : []),
@@ -35,6 +35,11 @@ import {
35
35
  sortActivityBarItems,
36
36
  } from '@workbench-kit/react/workbench/activityBarOrder';
37
37
  import { useWorkbench } from './provider.js';
38
+ import {
39
+ resolveWorkbenchShellChromeLabels,
40
+ type WorkbenchShellChromeLabels,
41
+ type WorkbenchTranslate,
42
+ } from './chrome-labels.js';
38
43
  import { WorkbenchCommandHost, type WorkbenchCommandHostProps } from '../workbench/command-host.js';
39
44
  import {
40
45
  MANAGE_ACCOUNTS_COMMAND_ID,
@@ -60,10 +65,12 @@ import {
60
65
  import { SETTINGS_EXTENSION_ID, WORKBENCH_PREFERENCE_SCOPES } from './settings-constants.js';
61
66
  import { createSettingsCategories, type WorkbenchThemeOption } from './settings.js';
62
67
  import {
68
+ createContributedWorkbenchStatusSections,
63
69
  createDefaultWorkbenchStatusSections,
64
70
  createWorkbenchShellActivityItems,
65
71
  } from './model.js';
66
- import { renderDefaultPrimarySidebar } from './view-host.js';
72
+ import { mergeWorkbenchStatusSections } from '../workbench/status-sections.js';
73
+ import { renderDefaultBottomPanel, renderDefaultPrimarySidebar } from './view-host.js';
67
74
  import { WorkbenchShellTitleBarLayoutControls } from './titlebar-layout-controls.js';
68
75
  import { WorkbenchProfileModal, type WorkbenchProfileInput } from '../workbench/profile-modal.js';
69
76
  import { useContextKeyRevision } from '../commands/use-context-key-revision.js';
@@ -102,6 +109,11 @@ export interface WorkbenchShellProps {
102
109
  editorArea?: ReactNode;
103
110
  helpContent?: ReactNode;
104
111
  helpTitle?: ReactNode;
112
+ /**
113
+ * Partial chrome label overrides for ActivityBar / StatusBar / secondary items /
114
+ * command palette. Wins over `t` when both are set for the same key.
115
+ */
116
+ labels?: Partial<WorkbenchShellChromeLabels> | undefined;
105
117
  lightPreset?: string | undefined;
106
118
  onDarkPresetChange?: ((preset: string) => void) | undefined;
107
119
  onLightPresetChange?: ((preset: string) => void) | undefined;
@@ -116,6 +128,11 @@ export interface WorkbenchShellProps {
116
128
  rootClassName?: string;
117
129
  shellPreset?: string | undefined;
118
130
  statusSections?: StatusBarSectionModel[];
131
+ /**
132
+ * Optional `t(key, fallback)` injection for shell chrome strings.
133
+ * Missing `t` keeps English defaults from `resolveWorkbenchShellChromeLabels`.
134
+ */
135
+ t?: WorkbenchTranslate | undefined;
119
136
  theme?: string;
120
137
  themeOptions?: readonly WorkbenchThemeOption[] | undefined;
121
138
  title?: ReactNode;
@@ -148,6 +165,7 @@ export function WorkbenchShell({
148
165
  editorArea,
149
166
  helpContent,
150
167
  helpTitle = 'Workbench Help',
168
+ labels: labelOverrides,
151
169
  lightPreset,
152
170
  locale = 'en',
153
171
  onDarkPresetChange,
@@ -162,6 +180,7 @@ export function WorkbenchShell({
162
180
  rootClassName,
163
181
  shellPreset = DEFAULT_SHELL_PRESET,
164
182
  statusSections,
183
+ t,
165
184
  theme,
166
185
  themeOptions,
167
186
  title = 'Workbench',
@@ -210,15 +229,20 @@ export function WorkbenchShell({
210
229
  const resolvedStatusSections = useMemo(
211
230
  () =>
212
231
  statusSections ??
213
- createDefaultWorkbenchStatusSections({
214
- dependencyDiagnostics: extensionRegistry.getDependencyDiagnostics(),
215
- extensionCount: extensionRegistry.getExtensions().length,
216
- missingExtensionIds,
217
- profile,
218
- }),
232
+ mergeWorkbenchStatusSections(
233
+ createDefaultWorkbenchStatusSections({
234
+ dependencyDiagnostics: extensionRegistry.getDependencyDiagnostics(),
235
+ extensionCount: extensionRegistry.getExtensions().length,
236
+ missingExtensionIds,
237
+ profile,
238
+ }),
239
+ createContributedWorkbenchStatusSections(extensionRegistry.statusBar.getStatusBarItems()),
240
+ ),
219
241
  [extensionRegistry, missingExtensionIds, profile, statusSections],
220
242
  );
221
243
  const activeViewContainerId = layout.sideBar.activeViewContainer;
244
+ const activePanelViewContainerId = layout.panel.activeViewContainer;
245
+ const panelViewContainers = extensionRegistry.views.getViewContainers('panel');
222
246
  const visibleActivities = useMemo(
223
247
  () =>
224
248
  filterActivitiesByWhenClause(
@@ -231,7 +255,7 @@ export function WorkbenchShell({
231
255
  createWorkbenchShellActivityItems({
232
256
  activeViewContainerId,
233
257
  activities: visibleActivities,
234
- viewContainers: extensionRegistry.views.getViewContainers(),
258
+ viewContainers: extensionRegistry.views.getViewContainers('activitybar'),
235
259
  views: extensionRegistry.views.getViews(),
236
260
  }),
237
261
  layout.activityBar.itemOrder,
@@ -243,11 +267,18 @@ export function WorkbenchShell({
243
267
  const canOpenSettingsValue = contextKeyService.get(
244
268
  WORKBENCH_PERMISSION_CONTEXT_KEY_CAN_OPEN_SETTINGS,
245
269
  );
270
+ const chromeLabels = useMemo(
271
+ () => resolveWorkbenchShellChromeLabels(labelOverrides, t),
272
+ [labelOverrides, t],
273
+ );
246
274
  const secondaryActivityItems = createWorkbenchSecondaryActivityItems({
247
275
  hasProfile: profile !== undefined,
248
276
  isProfileOpen,
249
277
  isSettingsOpen,
250
278
  showSettings: canOpenSettingsValue !== false,
279
+ profileLabel: chromeLabels.profileLabel,
280
+ profileTitle: chromeLabels.profileTitle,
281
+ settingsLabel: chromeLabels.settingsLabel,
251
282
  });
252
283
 
253
284
  useEffect(() => {
@@ -260,6 +291,20 @@ export function WorkbenchShell({
260
291
  layoutService.setActiveViewContainer(visibleActivityItems[0]?.id);
261
292
  }
262
293
  }, [activeViewContainerId, layoutService, visibleActivityItems]);
294
+
295
+ useEffect(() => {
296
+ if (panelViewContainers.length === 0) {
297
+ return;
298
+ }
299
+
300
+ const panelContainerIds = new Set(panelViewContainers.map((container) => container.id));
301
+ if (
302
+ activePanelViewContainerId === undefined ||
303
+ !panelContainerIds.has(activePanelViewContainerId)
304
+ ) {
305
+ layoutService.setActivePanelViewContainer(panelViewContainers[0]?.id);
306
+ }
307
+ }, [activePanelViewContainerId, layoutService, panelViewContainers]);
263
308
  const settingsCategories = useMemo(() => {
264
309
  const managementCategories: WorkbenchSettingsCategory[] = [];
265
310
 
@@ -389,6 +434,16 @@ export function WorkbenchShell({
389
434
  }
390
435
  }, [activeViewContainerId, extensionRegistry, forceRender]);
391
436
 
437
+ useEffect(() => {
438
+ if (!activePanelViewContainerId) {
439
+ return;
440
+ }
441
+
442
+ for (const view of extensionRegistry.views.getViews(activePanelViewContainerId)) {
443
+ void extensionRegistry.activateView(view.id).then(forceRender);
444
+ }
445
+ }, [activePanelViewContainerId, extensionRegistry, forceRender]);
446
+
392
447
  useEffect(() => {
393
448
  if (extensionRegistry.capabilityRegistry.has(WORKBENCH_SETTINGS_CAPABILITY_ID)) {
394
449
  return undefined;
@@ -429,6 +484,17 @@ export function WorkbenchShell({
429
484
  return {
430
485
  ...hostProps,
431
486
  additionalCommands,
487
+ commandPaletteCloseLabel:
488
+ hostProps.commandPaletteCloseLabel ?? chromeLabels.commandPaletteCloseLabel,
489
+ commandPaletteEmptyLabel:
490
+ hostProps.commandPaletteEmptyLabel ?? chromeLabels.commandPaletteEmptyLabel,
491
+ commandPalettePlaceholder:
492
+ hostProps.commandPalettePlaceholder ?? chromeLabels.commandPalettePlaceholder,
493
+ commandPaletteTitle: hostProps.commandPaletteTitle ?? chromeLabels.commandPaletteTitle,
494
+ quickOpenCloseLabel: hostProps.quickOpenCloseLabel ?? chromeLabels.quickOpenCloseLabel,
495
+ quickOpenEmptyLabel: hostProps.quickOpenEmptyLabel ?? chromeLabels.quickOpenEmptyLabel,
496
+ quickOpenPlaceholder: hostProps.quickOpenPlaceholder ?? chromeLabels.quickOpenPlaceholder,
497
+ quickOpenTitle: hostProps.quickOpenTitle ?? chromeLabels.quickOpenTitle,
432
498
  onRunCommand: (command, context) => {
433
499
  if (command.id === MANAGE_COMMANDS_COMMAND_ID) {
434
500
  layoutService.setActiveViewContainer(BUILTIN_COMMANDS_VIEW_CONTAINER_ID);
@@ -455,7 +521,7 @@ export function WorkbenchShell({
455
521
  return hostProps.onRunCommand?.(command, context) ?? false;
456
522
  },
457
523
  };
458
- }, [commandHost, layoutService]);
524
+ }, [chromeLabels, commandHost, layoutService]);
459
525
 
460
526
  const handleStatusItemActivate = useCallback(
461
527
  (item: StatusBarItemModel) => {
@@ -473,14 +539,28 @@ export function WorkbenchShell({
473
539
  return;
474
540
  }
475
541
 
542
+ const contributed = extensionRegistry.statusBar.getStatusBarItem(item.id);
543
+ if (contributed?.command) {
544
+ void executeCommand(contributed.command).catch(() => undefined);
545
+ return;
546
+ }
547
+
476
548
  onStatusItemActivate?.(item);
477
549
  },
478
- [accountManagement, onStatusItemActivate, profile, showProfileModal],
550
+ [
551
+ accountManagement,
552
+ executeCommand,
553
+ extensionRegistry,
554
+ onStatusItemActivate,
555
+ profile,
556
+ showProfileModal,
557
+ ],
479
558
  );
480
559
 
481
560
  return (
482
561
  <ReactWorkbenchShell
483
562
  activityBar={{
563
+ 'aria-label': chromeLabels.activityBarAriaLabel,
484
564
  visible: layout.activityBar.visible,
485
565
  items: visibleActivityItems,
486
566
  reorderable: true,
@@ -506,6 +586,7 @@ export function WorkbenchShell({
506
586
  },
507
587
  }}
508
588
  compactStatus={compactStatus}
589
+ statusBarAriaLabel={chromeLabels.statusBarAriaLabel}
509
590
  onStatusItemActivate={handleStatusItemActivate}
510
591
  primarySidebar={{
511
592
  isVisible: layout.sideBar.visible,
@@ -536,7 +617,20 @@ export function WorkbenchShell({
536
617
  }}
537
618
  bottomPanel={{
538
619
  isVisible: layout.panel.visible,
539
- node: <section aria-label="Panel" className="workbench-bottom-panel" />,
620
+ node: renderDefaultBottomPanel(extensionRegistry, activePanelViewContainerId, {
621
+ catalogTrustPolicy,
622
+ catalogUrl,
623
+ onActiveViewContainerChange: (viewContainerId) => {
624
+ layoutService.setActivePanelViewContainer(viewContainerId);
625
+ if (!layout.panel.visible) {
626
+ layoutService.setPanelVisible(true);
627
+ }
628
+ },
629
+ }),
630
+ onSizePercentChange: (sizePercent) => {
631
+ layoutService.setPanelSizePercent(sizePercent);
632
+ },
633
+ sizePercent: layout.panel.sizePercent,
540
634
  }}
541
635
  rootClassName={rootClassName}
542
636
  secondaryArea={resolvedEditorArea}
@@ -563,7 +657,7 @@ export function WorkbenchShell({
563
657
  footer={<Button onClick={() => setSettingsOpen(false)}>Close</Button>}
564
658
  scopes={[...WORKBENCH_PREFERENCE_SCOPES]}
565
659
  searchValue={settingsSearchValue}
566
- title="Settings"
660
+ title={chromeLabels.settingsLabel}
567
661
  titleSuffix={
568
662
  <Badge variant="muted">
569
663
  {settingsContributionCount === 1
@@ -56,6 +56,79 @@ export function renderDefaultPrimarySidebar(
56
56
  );
57
57
  }
58
58
 
59
+ export function renderDefaultBottomPanel(
60
+ extensionRegistry: Pick<ExtensionRegistry, 'viewHostFactories' | 'views'>,
61
+ activeViewContainerId: string | undefined,
62
+ options: {
63
+ catalogTrustPolicy?: ExtensionCatalogTrustPolicy | undefined;
64
+ catalogUrl?: string | undefined;
65
+ onActiveViewContainerChange?: ((viewContainerId: string) => void) | undefined;
66
+ } = {},
67
+ ) {
68
+ const containers = extensionRegistry.views.getViewContainers('panel');
69
+ if (containers.length === 0) {
70
+ return (
71
+ <section aria-label="Panel" className="workbench-bottom-panel">
72
+ <div className="workbench-bottom-panel__empty">No panel views contributed.</div>
73
+ </section>
74
+ );
75
+ }
76
+
77
+ const resolvedActiveViewContainerId =
78
+ activeViewContainerId !== undefined &&
79
+ containers.some((container) => container.id === activeViewContainerId)
80
+ ? activeViewContainerId
81
+ : containers[0]?.id;
82
+ const views = resolvedActiveViewContainerId
83
+ ? extensionRegistry.views.getViews(resolvedActiveViewContainerId)
84
+ : [];
85
+
86
+ return (
87
+ <section aria-label="Panel" className="workbench-bottom-panel">
88
+ <div className="workbench-bottom-panel__header" role="tablist" aria-label="Panel views">
89
+ {containers.map((container) => {
90
+ const isActive = container.id === resolvedActiveViewContainerId;
91
+ return (
92
+ <button
93
+ key={container.id}
94
+ type="button"
95
+ role="tab"
96
+ aria-selected={isActive}
97
+ className={
98
+ isActive
99
+ ? 'workbench-bottom-panel__tab workbench-bottom-panel__tab--active'
100
+ : 'workbench-bottom-panel__tab'
101
+ }
102
+ data-panel-view-container-id={container.id}
103
+ onClick={() => options.onActiveViewContainerChange?.(container.id)}
104
+ >
105
+ {container.title}
106
+ </button>
107
+ );
108
+ })}
109
+ </div>
110
+ <div className="workbench-bottom-panel__body">
111
+ {views.length === 0 ? (
112
+ <div className="workbench-bottom-panel__empty">No views in this panel container.</div>
113
+ ) : (
114
+ views.map((view) => (
115
+ <section key={view.id} data-view-id={view.id} className="workbench-bottom-panel__view">
116
+ <WorkbenchViewHost
117
+ catalogTrustPolicy={options.catalogTrustPolicy}
118
+ catalogUrl={options.catalogUrl}
119
+ fallback={view.name}
120
+ provider={extensionRegistry.views.getViewProvider(view.id)}
121
+ viewHostFactories={extensionRegistry.viewHostFactories}
122
+ viewId={view.id}
123
+ />
124
+ </section>
125
+ ))
126
+ )}
127
+ </div>
128
+ </section>
129
+ );
130
+ }
131
+
59
132
  export function WorkbenchViewHost({
60
133
  catalogTrustPolicy,
61
134
  catalogUrl,
@@ -0,0 +1,66 @@
1
+ import {
2
+ createBrowserWorkbenchStorage,
3
+ type WorkbenchStorageAdapter,
4
+ type WorkbenchStorageReader,
5
+ type WorkbenchStorageWriter,
6
+ } from '@workbench-kit/workbench-core';
7
+
8
+ export function resolveLocalWorkbenchStorage<
9
+ TStorage extends WorkbenchStorageReader | WorkbenchStorageWriter = WorkbenchStorageAdapter,
10
+ >(storage?: TStorage): TStorage | WorkbenchStorageAdapter | undefined {
11
+ return storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
12
+ }
13
+
14
+ export function readLocalJsonStorage<T>(
15
+ storageKey: string,
16
+ parse: (value: unknown) => T,
17
+ fallback: () => T,
18
+ storage?: WorkbenchStorageReader,
19
+ ): T {
20
+ const resolvedStorage = resolveLocalWorkbenchStorage(storage);
21
+ if (!resolvedStorage) {
22
+ return fallback();
23
+ }
24
+
25
+ try {
26
+ const raw = resolvedStorage.getItem(storageKey);
27
+ if (!raw) {
28
+ return fallback();
29
+ }
30
+
31
+ return parse(JSON.parse(raw) as unknown);
32
+ } catch {
33
+ return fallback();
34
+ }
35
+ }
36
+
37
+ export function writeLocalJsonStorage<T>(
38
+ storageKey: string,
39
+ value: T,
40
+ storage?: WorkbenchStorageWriter,
41
+ options: {
42
+ readonly errorMode?: 'ignore' | 'throw';
43
+ readonly toStorageValue?: (value: T) => unknown;
44
+ } = {},
45
+ ): void {
46
+ const resolvedStorage = resolveLocalWorkbenchStorage(storage);
47
+ if (!resolvedStorage) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ resolvedStorage.setItem(
53
+ storageKey,
54
+ JSON.stringify((options.toStorageValue ?? identity)(value), null, 2),
55
+ );
56
+ } catch (error) {
57
+ if (options.errorMode === 'throw') {
58
+ throw error;
59
+ }
60
+ // Local storage is best-effort; quota, security, and serialization errors stay non-fatal.
61
+ }
62
+ }
63
+
64
+ function identity<T>(value: T): T {
65
+ return value;
66
+ }
@@ -7,11 +7,16 @@ import {
7
7
  type WorkbenchColorSchemePreference,
8
8
  } from '@workbench-kit/react/workbench';
9
9
  import {
10
- createBrowserWorkbenchStorage,
11
10
  type WorkbenchStorageReader,
12
11
  type WorkbenchStorageWriter,
13
12
  } from '@workbench-kit/workbench-core';
14
13
 
14
+ import {
15
+ readLocalJsonStorage,
16
+ resolveLocalWorkbenchStorage,
17
+ writeLocalJsonStorage,
18
+ } from '../storage/local-json-storage.js';
19
+
15
20
  export type { WorkbenchAppearanceSettings } from '@workbench-kit/react/workbench/themePresets';
16
21
 
17
22
  export const DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY = 'workbench-kit/.workbench/appearance';
@@ -24,28 +29,19 @@ export const DEFAULT_WORKBENCH_APPEARANCE: WorkbenchAppearanceSettings = {
24
29
  };
25
30
 
26
31
  export function isWorkbenchAppearancePersistenceAvailable(): boolean {
27
- return createBrowserWorkbenchStorage({ kind: 'local' }) !== undefined;
32
+ return resolveLocalWorkbenchStorage() !== undefined;
28
33
  }
29
34
 
30
35
  export function readPersistedWorkbenchAppearance(
31
36
  storageKey = DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY,
32
37
  storage?: WorkbenchStorageReader,
33
38
  ): WorkbenchAppearanceSettings {
34
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
35
- if (!resolvedStorage) {
36
- return DEFAULT_WORKBENCH_APPEARANCE;
37
- }
38
-
39
- try {
40
- const raw = resolvedStorage.getItem(storageKey);
41
- if (!raw) {
42
- return DEFAULT_WORKBENCH_APPEARANCE;
43
- }
44
-
45
- return normalizeWorkbenchAppearance(JSON.parse(raw) as unknown);
46
- } catch {
47
- return DEFAULT_WORKBENCH_APPEARANCE;
48
- }
39
+ return readLocalJsonStorage(
40
+ storageKey,
41
+ normalizeWorkbenchAppearance,
42
+ () => DEFAULT_WORKBENCH_APPEARANCE,
43
+ storage,
44
+ );
49
45
  }
50
46
 
51
47
  export function writePersistedWorkbenchAppearance(
@@ -53,16 +49,7 @@ export function writePersistedWorkbenchAppearance(
53
49
  storageKey = DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY,
54
50
  storage?: WorkbenchStorageWriter,
55
51
  ): void {
56
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
57
- if (!resolvedStorage) {
58
- return;
59
- }
60
-
61
- try {
62
- resolvedStorage.setItem(storageKey, JSON.stringify(settings, null, 2));
63
- } catch {
64
- // Ignore quota and security errors so the shell keeps working offline.
65
- }
52
+ writeLocalJsonStorage(storageKey, settings, storage);
66
53
  }
67
54
 
68
55
  function normalizeWorkbenchAppearance(value: unknown): WorkbenchAppearanceSettings {