@workbench-kit/react 0.0.1-prototype.0 → 0.0.2-prototype.0.1.2

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 (155) hide show
  1. package/package.json +33 -9
  2. package/src/index.ts +545 -43
  3. package/src/jdw/JdwPreview.tsx +38 -0
  4. package/src/jdw/JsonCodeEditorPane.tsx +274 -0
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +29 -0
  6. package/src/jdw/builtins/renderBuiltinWidgetNode.tsx +126 -0
  7. package/src/jdw/createBuiltinJdwRegistry.ts +111 -0
  8. package/src/jdw/cssRenderBackend.tsx +134 -0
  9. package/src/jdw/document.ts +38 -0
  10. package/src/jdw/fixtures/jdw-fixtures.ts +52 -0
  11. package/src/jdw/fixtures/jdw-sample-screens.ts +522 -0
  12. package/src/jdw/index.ts +45 -0
  13. package/src/jdw/parse.ts +1 -0
  14. package/src/jdw/renderJdw.tsx +32 -0
  15. package/src/json-config/JsonConfigWorkbench.tsx +316 -0
  16. package/src/json-config/index.ts +6 -0
  17. package/src/json-config/json-config-editor-state.ts +42 -0
  18. package/src/layout/Panel.tsx +146 -1
  19. package/src/layout/SideBarViewFrame.tsx +367 -328
  20. package/src/layout/WorkbenchCanvas.tsx +849 -0
  21. package/src/layout/WorkbenchFullscreen.tsx +368 -0
  22. package/src/layout/WorkbenchLayout.tsx +5 -0
  23. package/src/layout/WorkbenchLayoutBase.tsx +294 -0
  24. package/src/layout/WorkbenchPropertyPanel.tsx +497 -0
  25. package/src/layout/WorkbenchSidebarActions.tsx +274 -0
  26. package/src/layout/WorkbenchTree.tsx +351 -0
  27. package/src/layout/layoutHelpers.ts +25 -0
  28. package/src/modal/ConfirmDialog.tsx +61 -53
  29. package/src/modal/Modal.tsx +295 -35
  30. package/src/overlay/ContextMenu.tsx +146 -145
  31. package/src/primitives/AbsoluteBox.tsx +46 -0
  32. package/src/primitives/Badge.tsx +12 -12
  33. package/src/primitives/Button.tsx +44 -14
  34. package/src/primitives/Checkbox.tsx +11 -5
  35. package/src/primitives/EmptyState.tsx +27 -26
  36. package/src/primitives/Field.tsx +38 -38
  37. package/src/primitives/IconButton.tsx +5 -2
  38. package/src/primitives/List.tsx +111 -0
  39. package/src/primitives/NumberInput.tsx +38 -0
  40. package/src/primitives/ScrollArea.tsx +42 -0
  41. package/src/primitives/Select.tsx +1 -12
  42. package/src/primitives/StatusBar.tsx +46 -0
  43. package/src/primitives/TextArea.tsx +36 -0
  44. package/src/primitives/TextInput.tsx +11 -2
  45. package/src/primitives/WorkbenchChrome.tsx +424 -0
  46. package/src/primitives/WorkbenchEditor.tsx +394 -0
  47. package/src/primitives/index.ts +105 -0
  48. package/src/primitives/select/Select.tsx +266 -0
  49. package/src/primitives/select/index.ts +2 -0
  50. package/src/primitives/select/options.ts +38 -0
  51. package/src/primitives/select/overlay.ts +63 -0
  52. package/src/primitives/select/select.app.css +9 -0
  53. package/src/primitives/select/select.css +140 -0
  54. package/src/primitives/select/types.ts +18 -0
  55. package/src/primitives/styles.css +955 -0
  56. package/src/scrollbars.css +69 -0
  57. package/src/styles.css +7738 -1762
  58. package/src/utils/codicon.ts +17 -0
  59. package/src/widget-asset/WidgetAssetEditor.tsx +140 -0
  60. package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -0
  61. package/src/widget-asset/WidgetAssetModeControls.tsx +42 -0
  62. package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -0
  63. package/src/widget-asset/WidgetAssetWorkbench.tsx +112 -0
  64. package/src/widget-asset/index.ts +17 -0
  65. package/src/widget-asset/widget-asset-document.ts +37 -0
  66. package/src/widget-asset/widget-asset-mode.ts +3 -0
  67. package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -0
  68. package/src/widget-studio/create-widget-studio-workspace-editor.tsx +79 -0
  69. package/src/widget-studio/index.ts +7 -0
  70. package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +26 -0
  71. package/src/widget-tree/WidgetAssetPalette.tsx +77 -0
  72. package/src/widget-tree/WidgetInspectorPanel.tsx +266 -0
  73. package/src/widget-tree/WidgetSourceEditor.tsx +56 -0
  74. package/src/widget-tree/WidgetTreeLab.tsx +272 -0
  75. package/src/widget-tree/WidgetTreeModeControls.tsx +44 -0
  76. package/src/widget-tree/WidgetTreeSidePanel.tsx +49 -0
  77. package/src/widget-tree/WidgetTreeView.tsx +97 -0
  78. package/src/widget-tree/WidgetTreeWorkbench.tsx +111 -0
  79. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +154 -0
  80. package/src/widget-tree/demo-registry.ts +32 -0
  81. package/src/widget-tree/demo-widget-assets.ts +4 -0
  82. package/src/widget-tree/index.ts +27 -0
  83. package/src/widget-tree/widget-tree-document.ts +20 -0
  84. package/src/widget-tree/widget-tree-layout.ts +31 -0
  85. package/src/widget-tree/widget-tree-mode.ts +7 -0
  86. package/src/widget-tree/widget-tree-play-helpers.ts +74 -0
  87. package/src/workbench/ActivityBar.tsx +52 -52
  88. package/src/workbench/ArtifactShell.tsx +397 -0
  89. package/src/workbench/CommandPalette.tsx +819 -0
  90. package/src/workbench/ConfirmationFlow.tsx +245 -0
  91. package/src/workbench/MarkdownPreview.tsx +151 -0
  92. package/src/workbench/ShortcutCommandBridge.tsx +368 -0
  93. package/src/workbench/SplitView.tsx +136 -136
  94. package/src/workbench/StatusBar.tsx +178 -123
  95. package/src/workbench/StructuredArtifactEditor.tsx +205 -0
  96. package/src/workbench/Timeline.tsx +302 -0
  97. package/src/workbench/WorkbenchCanvasShell.tsx +941 -0
  98. package/src/workbench/WorkbenchShell.tsx +74 -70
  99. package/src/workbench/WorkbenchStandaloneShell.tsx +318 -291
  100. package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -0
  101. package/src/workbench/auth/WorkbenchLoginView.tsx +635 -0
  102. package/src/workbench/auth/index.ts +19 -0
  103. package/src/workbench/chat/ChatComposer.tsx +165 -148
  104. package/src/workbench/chat/ChatMessageItem.tsx +10 -1
  105. package/src/workbench/chat/index.ts +9 -0
  106. package/src/workbench/chat/slashCommand.ts +28 -0
  107. package/src/workbench/chat/useChatRuntimeState.ts +76 -0
  108. package/src/workbench/chat/useSlashCommandSuggest.ts +101 -0
  109. package/src/workbench/commands.ts +520 -490
  110. package/src/workbench/index.ts +443 -98
  111. package/src/workbench/keyboard.ts +53 -0
  112. package/src/workbench/schema/index.ts +48 -0
  113. package/src/workbench/schema/workbenchDocument.ts +31 -0
  114. package/src/workbench/schema/workbenchDocumentActions.ts +6 -0
  115. package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -0
  116. package/src/workbench/schema/workbenchDocumentPatch.ts +13 -0
  117. package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -0
  118. package/src/workbench/settings/NavigationPanel.tsx +47 -0
  119. package/src/workbench/settings/SchemaForm.tsx +420 -0
  120. package/src/workbench/settings/SectionedPanel.tsx +415 -0
  121. package/src/workbench/settings/StructuredDataForm.tsx +539 -0
  122. package/src/workbench/settings/StructuredDataSchemaPanel.tsx +559 -0
  123. package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -0
  124. package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -0
  125. package/src/workbench/settings/StructuredDataTableView.tsx +127 -0
  126. package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -0
  127. package/src/workbench/settings/WorkbenchSettingsModal.tsx +21 -12
  128. package/src/workbench/settings/WorkbenchSettingsNav.tsx +38 -22
  129. package/src/workbench/settings/index.ts +135 -0
  130. package/src/workbench/settings/panel-region.css +21 -0
  131. package/src/workbench/settings/sectionedPanelScrollSpy.ts +143 -0
  132. package/src/workbench/settings/structured-data-schema-panel.css +330 -0
  133. package/src/workbench/settings/structured-data-table-view.css +85 -0
  134. package/src/workbench/settings/structuredDataSchema.ts +886 -0
  135. package/src/workbench/shell.ts +15 -0
  136. package/src/workbench/shellState.ts +203 -203
  137. package/src/workbench/standalone.ts +101 -94
  138. package/src/workbench/status.ts +129 -0
  139. package/src/workbench/theme.ts +44 -0
  140. package/src/workbench/workspace/MultiProviderExplorer.tsx +625 -0
  141. package/src/workbench/workspace/WorkspaceDraftsContext.tsx +111 -0
  142. package/src/workbench/workspace/WorkspaceEditor.tsx +47 -12
  143. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +334 -353
  144. package/src/workbench/workspace/WorkspaceExplorer.tsx +532 -524
  145. package/src/workbench/workspace/WorkspaceFileIcon.tsx +32 -2
  146. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -113
  147. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -47
  148. package/src/workbench/workspace/index.ts +156 -98
  149. package/src/workbench/workspace/mimeType.ts +45 -0
  150. package/src/workbench/workspace/path.ts +10 -10
  151. package/src/workbench/workspace/search.ts +6 -6
  152. package/src/workbench/workspace/tree.ts +1 -1
  153. package/src/workbench/workspace/types.ts +10 -10
  154. package/src/workbench/workspace/useVirtualWorkspace.ts +111 -98
  155. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -0
@@ -0,0 +1,819 @@
1
+ import {
2
+ useEffect,
3
+ useId,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type ComponentPropsWithRef,
8
+ type KeyboardEvent,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import type { CommandMenuItem, ResolvedCommandMenuCommandItem } from '@workbench-kit/platform';
12
+ import { EmptyState } from '../primitives/EmptyState';
13
+ import { cxCodicon } from '../utils/codicon';
14
+ import { cx } from '../utils/cx';
15
+ import {
16
+ getWorkbenchStatusLabel,
17
+ isWorkbenchStatusBusy,
18
+ isWorkbenchStatusDisabled,
19
+ type WorkbenchStatus,
20
+ } from './status';
21
+
22
+ export type WorkbenchCommandStatus = WorkbenchStatus;
23
+
24
+ export interface WorkbenchCommandExecution {
25
+ kind: 'agent' | 'local' | 'remote' | 'composite' | (string & {});
26
+ label?: string | undefined;
27
+ }
28
+
29
+ export type WorkbenchCommandFeedback = 'none' | 'status' | 'timeline';
30
+ export type WorkbenchCommandOutput = 'none' | 'message' | 'event' | 'artifact';
31
+ export type WorkbenchCommandSideEffect = 'none' | 'workspace-write' | 'external-write';
32
+ export type WorkbenchCommandRunSource = 'grouped-list' | 'list' | 'palette' | 'suggest';
33
+ export type WorkbenchCommandGroupBy =
34
+ | 'category'
35
+ | 'danger'
36
+ | 'execution'
37
+ | 'feedback'
38
+ | 'keyword'
39
+ | 'output'
40
+ | 'sideEffect'
41
+ | 'status';
42
+
43
+ export interface WorkbenchCommandDescriptor {
44
+ category?: string | undefined;
45
+ danger?: boolean | undefined;
46
+ description?: string | undefined;
47
+ disabled?: boolean | undefined;
48
+ disabledReason?: string | undefined;
49
+ execution?: WorkbenchCommandExecution | undefined;
50
+ feedback?: WorkbenchCommandFeedback | undefined;
51
+ icon?: string | undefined;
52
+ id: string;
53
+ keywords?: readonly string[] | undefined;
54
+ label: string;
55
+ metadata?: Record<string, unknown> | undefined;
56
+ output?: WorkbenchCommandOutput | undefined;
57
+ shortcut?: string | undefined;
58
+ sideEffect?: WorkbenchCommandSideEffect | undefined;
59
+ status?: WorkbenchCommandStatus | undefined;
60
+ }
61
+
62
+ export interface WorkbenchCommandRunContext {
63
+ groupId?: string | undefined;
64
+ groupLabel?: string | undefined;
65
+ index: number;
66
+ query: string;
67
+ source: WorkbenchCommandRunSource;
68
+ }
69
+
70
+ export interface WorkbenchCommandFilterInput {
71
+ commands: readonly WorkbenchCommandDescriptor[];
72
+ limit?: number | undefined;
73
+ query?: string | undefined;
74
+ }
75
+
76
+ export interface WorkbenchCommandNavigationInput {
77
+ commands: readonly WorkbenchCommandDescriptor[];
78
+ currentIndex: number;
79
+ direction: 'next' | 'previous';
80
+ }
81
+
82
+ export interface WorkbenchCommandGroup {
83
+ commands: readonly WorkbenchCommandDescriptor[];
84
+ id: string;
85
+ label: string;
86
+ }
87
+
88
+ export interface WorkbenchCommandGroupingInput {
89
+ commands: readonly WorkbenchCommandDescriptor[];
90
+ fallbackGroupLabel?: string | undefined;
91
+ groupBy?: WorkbenchCommandGroupBy | undefined;
92
+ }
93
+
94
+ export type WorkbenchCommandDescriptorOverrides = Partial<
95
+ Omit<WorkbenchCommandDescriptor, 'danger' | 'disabled' | 'icon' | 'id' | 'label' | 'shortcut'>
96
+ >;
97
+
98
+ const commandExecutionLabels: Record<string, string> = {
99
+ agent: 'Agent',
100
+ composite: 'Composite',
101
+ local: 'Local',
102
+ remote: 'Remote',
103
+ };
104
+
105
+ const commandFeedbackLabels: Record<WorkbenchCommandFeedback, string> = {
106
+ none: 'No feedback',
107
+ status: 'Status',
108
+ timeline: 'Timeline',
109
+ };
110
+
111
+ const commandOutputLabels: Record<WorkbenchCommandOutput, string> = {
112
+ artifact: 'Artifact',
113
+ event: 'Event',
114
+ message: 'Message',
115
+ none: 'No output',
116
+ };
117
+
118
+ const commandSideEffectLabels: Record<WorkbenchCommandSideEffect, string> = {
119
+ 'external-write': 'External write',
120
+ none: 'No side effect',
121
+ 'workspace-write': 'Workspace write',
122
+ };
123
+
124
+ function normalizedSearchText(value: string) {
125
+ return value.trim().toLocaleLowerCase();
126
+ }
127
+
128
+ function commandSearchText(command: WorkbenchCommandDescriptor) {
129
+ return normalizedSearchText(
130
+ [
131
+ command.id,
132
+ command.label,
133
+ command.description,
134
+ command.category,
135
+ command.shortcut,
136
+ ...(command.keywords ?? []),
137
+ ]
138
+ .filter(Boolean)
139
+ .join(' '),
140
+ );
141
+ }
142
+
143
+ export function getWorkbenchCommandStatusLabel(status: WorkbenchCommandStatus) {
144
+ return getWorkbenchStatusLabel(status);
145
+ }
146
+
147
+ export function getWorkbenchCommandExecutionLabel(execution: WorkbenchCommandExecution) {
148
+ return execution.label ?? commandExecutionLabels[execution.kind] ?? execution.kind;
149
+ }
150
+
151
+ export function isWorkbenchCommandRunnable(command: WorkbenchCommandDescriptor) {
152
+ return !(command.disabled || (command.status && isWorkbenchStatusDisabled(command.status)));
153
+ }
154
+
155
+ function normalizeWorkbenchCommandGroupId(label: string) {
156
+ const normalized = normalizedSearchText(label)
157
+ .replace(/[^\p{L}\p{N}]+/gu, '-')
158
+ .replace(/^-+|-+$/g, '');
159
+
160
+ return normalized || 'group';
161
+ }
162
+
163
+ function getWorkbenchCommandGroupLabels({
164
+ command,
165
+ fallbackGroupLabel,
166
+ groupBy = 'category',
167
+ }: {
168
+ command: WorkbenchCommandDescriptor;
169
+ fallbackGroupLabel: string;
170
+ groupBy?: WorkbenchCommandGroupBy | undefined;
171
+ }) {
172
+ if (groupBy === 'keyword') {
173
+ return command.keywords?.length ? [...command.keywords] : [fallbackGroupLabel];
174
+ }
175
+
176
+ if (groupBy === 'category') return [command.category ?? fallbackGroupLabel];
177
+ if (groupBy === 'danger') return [command.danger ? 'Danger' : 'Default'];
178
+ if (groupBy === 'execution') {
179
+ return [
180
+ command.execution ? getWorkbenchCommandExecutionLabel(command.execution) : fallbackGroupLabel,
181
+ ];
182
+ }
183
+ if (groupBy === 'feedback')
184
+ return [command.feedback ? commandFeedbackLabels[command.feedback] : fallbackGroupLabel];
185
+ if (groupBy === 'output')
186
+ return [command.output ? commandOutputLabels[command.output] : fallbackGroupLabel];
187
+ if (groupBy === 'sideEffect') {
188
+ return [command.sideEffect ? commandSideEffectLabels[command.sideEffect] : fallbackGroupLabel];
189
+ }
190
+
191
+ const status = command.status ?? (command.disabled ? 'disabled' : undefined);
192
+ return [status ? getWorkbenchCommandStatusLabel(status) : fallbackGroupLabel];
193
+ }
194
+
195
+ export function groupWorkbenchCommands({
196
+ commands,
197
+ fallbackGroupLabel = 'Other',
198
+ groupBy = 'category',
199
+ }: WorkbenchCommandGroupingInput): WorkbenchCommandGroup[] {
200
+ const groups = new Map<string, { commands: WorkbenchCommandDescriptor[]; label: string }>();
201
+
202
+ commands.forEach((command) => {
203
+ getWorkbenchCommandGroupLabels({ command, fallbackGroupLabel, groupBy }).forEach((label) => {
204
+ const id = normalizeWorkbenchCommandGroupId(`${groupBy}-${label}`);
205
+ const group = groups.get(id) ?? { commands: [], label };
206
+
207
+ group.commands.push(command);
208
+ groups.set(id, group);
209
+ });
210
+ });
211
+
212
+ return Array.from(groups, ([id, group]) => ({
213
+ commands: group.commands,
214
+ id,
215
+ label: group.label,
216
+ }));
217
+ }
218
+
219
+ export function filterWorkbenchCommands({
220
+ commands,
221
+ limit,
222
+ query = '',
223
+ }: WorkbenchCommandFilterInput) {
224
+ const tokens = normalizedSearchText(query).split(/\s+/).filter(Boolean);
225
+ const filtered = tokens.length
226
+ ? commands.filter((command) => {
227
+ const searchText = commandSearchText(command);
228
+ return tokens.every((token) => searchText.includes(token));
229
+ })
230
+ : [...commands];
231
+
232
+ return typeof limit === 'number' ? filtered.slice(0, limit) : filtered;
233
+ }
234
+
235
+ export function getNextWorkbenchCommandIndex({
236
+ commands,
237
+ currentIndex,
238
+ direction,
239
+ }: WorkbenchCommandNavigationInput) {
240
+ if (commands.length === 0) return -1;
241
+
242
+ const step = direction === 'next' ? 1 : -1;
243
+ const startIndex =
244
+ currentIndex >= 0 && currentIndex < commands.length
245
+ ? currentIndex
246
+ : direction === 'next'
247
+ ? -1
248
+ : 0;
249
+
250
+ for (let offset = 1; offset <= commands.length; offset += 1) {
251
+ const nextIndex = (startIndex + step * offset + commands.length) % commands.length;
252
+ const command = commands[nextIndex];
253
+ if (command && isWorkbenchCommandRunnable(command)) return nextIndex;
254
+ }
255
+
256
+ return -1;
257
+ }
258
+
259
+ export function commandMenuItemToWorkbenchCommandDescriptor(
260
+ item: ResolvedCommandMenuCommandItem,
261
+ overrides: WorkbenchCommandDescriptorOverrides = {},
262
+ ): WorkbenchCommandDescriptor {
263
+ const metadata = {
264
+ menuItemId: item.id,
265
+ ...overrides.metadata,
266
+ };
267
+
268
+ return {
269
+ ...overrides,
270
+ danger: item.danger,
271
+ disabled: item.disabled,
272
+ icon: item.icon,
273
+ id: item.commandId,
274
+ label: item.label,
275
+ metadata,
276
+ shortcut: item.shortcut,
277
+ };
278
+ }
279
+
280
+ export function commandMenuItemsToWorkbenchCommandDescriptors(
281
+ items: readonly CommandMenuItem[],
282
+ overridesByCommandId: Record<string, WorkbenchCommandDescriptorOverrides> = {},
283
+ ) {
284
+ return items.flatMap((item) =>
285
+ item.type === 'command'
286
+ ? [commandMenuItemToWorkbenchCommandDescriptor(item, overridesByCommandId[item.commandId])]
287
+ : [],
288
+ );
289
+ }
290
+
291
+ function commandIcon(command: WorkbenchCommandDescriptor) {
292
+ if (!command.icon) return null;
293
+ return <i aria-hidden="true" className={cxCodicon(command.icon)} />;
294
+ }
295
+
296
+ function commandStatus(command: WorkbenchCommandDescriptor) {
297
+ const status = command.status ?? (command.disabled ? 'disabled' : 'idle');
298
+
299
+ return (
300
+ <span
301
+ aria-label={getWorkbenchCommandStatusLabel(status)}
302
+ className="ui-workbench-command-item__status"
303
+ title={getWorkbenchCommandStatusLabel(status)}
304
+ >
305
+ <span aria-hidden="true" className="ui-workbench-command-item__status-dot" />
306
+ </span>
307
+ );
308
+ }
309
+
310
+ function commandExecution(command: WorkbenchCommandDescriptor) {
311
+ if (!command.execution) return null;
312
+
313
+ return (
314
+ <span className="ui-workbench-command-item__chip">
315
+ {getWorkbenchCommandExecutionLabel(command.execution)}
316
+ </span>
317
+ );
318
+ }
319
+
320
+ export interface WorkbenchCommandListProps extends Omit<
321
+ ComponentPropsWithRef<'div'>,
322
+ 'children' | 'onSelect'
323
+ > {
324
+ activeCommandId?: string | undefined;
325
+ commands: readonly WorkbenchCommandDescriptor[];
326
+ emptyLabel?: ReactNode | undefined;
327
+ onActiveCommandChange?: ((commandId: string) => void) | undefined;
328
+ onRunCommand?:
329
+ | ((command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => void)
330
+ | undefined;
331
+ query?: string | undefined;
332
+ source?: WorkbenchCommandRunSource | undefined;
333
+ }
334
+
335
+ export function WorkbenchCommandList({
336
+ activeCommandId,
337
+ className,
338
+ commands,
339
+ emptyLabel = 'No commands available',
340
+ id,
341
+ onActiveCommandChange,
342
+ onRunCommand,
343
+ query = '',
344
+ source = 'list',
345
+ ...props
346
+ }: WorkbenchCommandListProps) {
347
+ const generatedId = useId();
348
+ const listId = id ?? generatedId;
349
+
350
+ if (commands.length === 0) {
351
+ return (
352
+ <div id={id} className={cx('ui-workbench-command-list', className)} {...props}>
353
+ <EmptyState compact icon="codicon-search">
354
+ {emptyLabel}
355
+ </EmptyState>
356
+ </div>
357
+ );
358
+ }
359
+
360
+ return (
361
+ <div
362
+ id={id}
363
+ aria-label={props['aria-label'] ?? 'Commands'}
364
+ className={cx('ui-workbench-command-list', className)}
365
+ role="listbox"
366
+ {...props}
367
+ >
368
+ {commands.map((command, index) => {
369
+ const active = command.id === activeCommandId;
370
+ const disabled = !isWorkbenchCommandRunnable(command);
371
+ const itemId = `${listId}-${command.id}`;
372
+ const descriptionId = command.description ? `${itemId}-description` : undefined;
373
+ const disabledReasonId = command.disabledReason ? `${itemId}-disabled` : undefined;
374
+
375
+ return (
376
+ <button
377
+ key={command.id}
378
+ id={itemId}
379
+ aria-busy={command.status && isWorkbenchStatusBusy(command.status) ? true : undefined}
380
+ aria-describedby={
381
+ [descriptionId, disabledReasonId].filter(Boolean).join(' ') || undefined
382
+ }
383
+ aria-selected={active}
384
+ className="ui-workbench-command-item"
385
+ data-active={active ? 'true' : undefined}
386
+ data-danger={command.danger ? 'true' : undefined}
387
+ data-status={command.status ?? (command.disabled ? 'disabled' : 'idle')}
388
+ disabled={disabled}
389
+ role="option"
390
+ type="button"
391
+ onClick={() => onRunCommand?.(command, { index, query, source })}
392
+ onMouseDown={(event) => {
393
+ if (source === 'suggest') event.preventDefault();
394
+ }}
395
+ onMouseEnter={() => onActiveCommandChange?.(command.id)}
396
+ >
397
+ <span className="ui-workbench-command-item__icon">{commandIcon(command)}</span>
398
+ <span className="ui-workbench-command-item__content">
399
+ <span className="ui-workbench-command-item__label">{command.label}</span>
400
+ {command.description ? (
401
+ <span id={descriptionId} className="ui-workbench-command-item__description">
402
+ {command.description}
403
+ </span>
404
+ ) : null}
405
+ {command.disabledReason ? (
406
+ <span id={disabledReasonId} className="ui-visually-hidden">
407
+ {command.disabledReason}
408
+ </span>
409
+ ) : null}
410
+ </span>
411
+ <span className="ui-workbench-command-item__meta">
412
+ {command.category ? (
413
+ <span className="ui-workbench-command-item__category">{command.category}</span>
414
+ ) : null}
415
+ {commandExecution(command)}
416
+ {command.shortcut ? (
417
+ <kbd className="ui-workbench-command-item__shortcut">{command.shortcut}</kbd>
418
+ ) : null}
419
+ {commandStatus(command)}
420
+ </span>
421
+ </button>
422
+ );
423
+ })}
424
+ </div>
425
+ );
426
+ }
427
+
428
+ export interface WorkbenchCommandGroupShellProps extends Omit<
429
+ ComponentPropsWithRef<'div'>,
430
+ 'children' | 'onSelect'
431
+ > {
432
+ activeCommandId?: string | undefined;
433
+ commands: readonly WorkbenchCommandDescriptor[];
434
+ emptyLabel?: ReactNode | undefined;
435
+ fallbackGroupLabel?: string | undefined;
436
+ groupBy?: WorkbenchCommandGroupBy | undefined;
437
+ groupNavLabel?: string | undefined;
438
+ onActiveCommandChange?: ((commandId: string) => void) | undefined;
439
+ onRunCommand?:
440
+ | ((command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => void)
441
+ | undefined;
442
+ query?: string | undefined;
443
+ showGroupCounts?: boolean | undefined;
444
+ showGroupNav?: boolean | undefined;
445
+ }
446
+
447
+ export function WorkbenchCommandGroupShell({
448
+ activeCommandId,
449
+ className,
450
+ commands,
451
+ emptyLabel = 'No commands available',
452
+ fallbackGroupLabel = 'Other',
453
+ groupBy = 'category',
454
+ groupNavLabel = 'Command groups',
455
+ onActiveCommandChange,
456
+ onRunCommand,
457
+ query = '',
458
+ showGroupCounts = true,
459
+ showGroupNav = true,
460
+ ...props
461
+ }: WorkbenchCommandGroupShellProps) {
462
+ const generatedId = useId().replace(/:/g, '');
463
+ const filteredCommands = useMemo(
464
+ () => filterWorkbenchCommands({ commands, query }),
465
+ [commands, query],
466
+ );
467
+ const groups = useMemo(
468
+ () => groupWorkbenchCommands({ commands: filteredCommands, fallbackGroupLabel, groupBy }),
469
+ [fallbackGroupLabel, filteredCommands, groupBy],
470
+ );
471
+
472
+ if (groups.length === 0) {
473
+ return (
474
+ <div
475
+ className={cx('ui-workbench-command-group-shell', className)}
476
+ data-show-group-nav={showGroupNav ? 'true' : 'false'}
477
+ {...props}
478
+ >
479
+ <EmptyState compact icon="codicon-symbol-keyword">
480
+ {emptyLabel}
481
+ </EmptyState>
482
+ </div>
483
+ );
484
+ }
485
+
486
+ return (
487
+ <div
488
+ className={cx('ui-workbench-command-group-shell', className)}
489
+ data-show-group-nav={showGroupNav ? 'true' : 'false'}
490
+ {...props}
491
+ >
492
+ {showGroupNav ? (
493
+ <nav aria-label={groupNavLabel} className="ui-workbench-command-group-shell__nav">
494
+ {groups.map((group) => (
495
+ <a
496
+ key={group.id}
497
+ className="ui-workbench-command-group-shell__nav-link"
498
+ href={`#${generatedId}-${group.id}`}
499
+ >
500
+ <span>{group.label}</span>
501
+ {showGroupCounts ? <em>{group.commands.length}</em> : null}
502
+ </a>
503
+ ))}
504
+ </nav>
505
+ ) : null}
506
+ <div className="ui-workbench-command-group-shell__content">
507
+ {groups.map((group) => {
508
+ const titleId = `${generatedId}-${group.id}-title`;
509
+
510
+ return (
511
+ <section
512
+ key={group.id}
513
+ id={`${generatedId}-${group.id}`}
514
+ className="ui-workbench-command-group-shell__section"
515
+ aria-labelledby={titleId}
516
+ >
517
+ <div className="ui-workbench-command-group-shell__section-header">
518
+ <h2 id={titleId}>{group.label}</h2>
519
+ {showGroupCounts ? <span>{group.commands.length}</span> : null}
520
+ </div>
521
+ <WorkbenchCommandList
522
+ activeCommandId={activeCommandId}
523
+ aria-label={`${group.label} commands`}
524
+ commands={group.commands}
525
+ emptyLabel={emptyLabel}
526
+ query={query}
527
+ source="grouped-list"
528
+ onActiveCommandChange={onActiveCommandChange}
529
+ onRunCommand={(command, context) =>
530
+ onRunCommand?.(command, {
531
+ ...context,
532
+ groupId: group.id,
533
+ groupLabel: group.label,
534
+ })
535
+ }
536
+ />
537
+ </section>
538
+ );
539
+ })}
540
+ </div>
541
+ </div>
542
+ );
543
+ }
544
+
545
+ function useControllableCommandQuery({
546
+ defaultQuery = '',
547
+ query,
548
+ onQueryChange,
549
+ }: {
550
+ defaultQuery?: string | undefined;
551
+ query?: string | undefined;
552
+ onQueryChange?: ((query: string) => void) | undefined;
553
+ }) {
554
+ const [uncontrolledQuery, setUncontrolledQuery] = useState(defaultQuery);
555
+ const resolvedQuery = query ?? uncontrolledQuery;
556
+
557
+ const setQuery = (nextQuery: string) => {
558
+ if (query === undefined) {
559
+ setUncontrolledQuery(nextQuery);
560
+ }
561
+ onQueryChange?.(nextQuery);
562
+ };
563
+
564
+ return [resolvedQuery, setQuery] as const;
565
+ }
566
+
567
+ export interface WorkbenchCommandPaletteProps extends Omit<
568
+ ComponentPropsWithRef<'div'>,
569
+ 'children' | 'onSelect' | 'title'
570
+ > {
571
+ activeCommandId?: string | undefined;
572
+ closeLabel?: string | undefined;
573
+ commands: readonly WorkbenchCommandDescriptor[];
574
+ defaultQuery?: string | undefined;
575
+ emptyLabel?: ReactNode | undefined;
576
+ onActiveCommandChange?: ((commandId: string) => void) | undefined;
577
+ onClose: () => void;
578
+ onQueryChange?: ((query: string) => void) | undefined;
579
+ onRunCommand?:
580
+ | ((command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => void)
581
+ | undefined;
582
+ open?: boolean | undefined;
583
+ placeholder?: string | undefined;
584
+ query?: string | undefined;
585
+ restoreFocusOnClose?: boolean | undefined;
586
+ title?: ReactNode | undefined;
587
+ }
588
+
589
+ export function WorkbenchCommandPalette({
590
+ activeCommandId,
591
+ className,
592
+ closeLabel = 'Close command palette',
593
+ commands,
594
+ defaultQuery,
595
+ emptyLabel = 'No commands match your search',
596
+ onActiveCommandChange,
597
+ onClose,
598
+ onQueryChange,
599
+ onRunCommand,
600
+ open = true,
601
+ placeholder = 'Search commands',
602
+ query,
603
+ restoreFocusOnClose = true,
604
+ title = 'Command Palette',
605
+ ...props
606
+ }: WorkbenchCommandPaletteProps) {
607
+ const titleId = useId();
608
+ const listId = useId();
609
+ const inputRef = useRef<HTMLInputElement>(null);
610
+ const previousFocusRef = useRef<HTMLElement | null>(null);
611
+ const [uncontrolledActiveCommandId, setUncontrolledActiveCommandId] = useState<string>();
612
+ const [resolvedQuery, setResolvedQuery] = useControllableCommandQuery({
613
+ defaultQuery,
614
+ onQueryChange,
615
+ query,
616
+ });
617
+ const filteredCommands = useMemo(
618
+ () => filterWorkbenchCommands({ commands, query: resolvedQuery }),
619
+ [commands, resolvedQuery],
620
+ );
621
+ const resolvedActiveCommandId = activeCommandId ?? uncontrolledActiveCommandId;
622
+ const activeIndex = filteredCommands.findIndex(
623
+ (command) => command.id === resolvedActiveCommandId,
624
+ );
625
+ const activeCommand =
626
+ activeIndex >= 0
627
+ ? filteredCommands[activeIndex]
628
+ : filteredCommands.find((command) => isWorkbenchCommandRunnable(command));
629
+
630
+ const updateActiveCommand = (commandId: string | undefined) => {
631
+ if (!commandId) return;
632
+ if (activeCommandId === undefined) {
633
+ setUncontrolledActiveCommandId(commandId);
634
+ }
635
+ onActiveCommandChange?.(commandId);
636
+ };
637
+
638
+ const restoreFocus = () => {
639
+ if (!restoreFocusOnClose) return;
640
+ previousFocusRef.current?.focus();
641
+ };
642
+
643
+ const closePalette = () => {
644
+ restoreFocus();
645
+ onClose();
646
+ };
647
+
648
+ const runCommand = (command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => {
649
+ if (!isWorkbenchCommandRunnable(command)) return;
650
+ onRunCommand?.(command, context);
651
+ };
652
+
653
+ useEffect(() => {
654
+ if (!open) return;
655
+
656
+ previousFocusRef.current = document.activeElement as HTMLElement | null;
657
+ const frame = window.requestAnimationFrame(() => inputRef.current?.focus());
658
+
659
+ return () => {
660
+ window.cancelAnimationFrame(frame);
661
+ restoreFocus();
662
+ };
663
+ }, [open]);
664
+
665
+ useEffect(() => {
666
+ if (!open) return;
667
+ if (filteredCommands.length === 0) return;
668
+ if (activeCommand && isWorkbenchCommandRunnable(activeCommand)) return;
669
+
670
+ const nextIndex = getNextWorkbenchCommandIndex({
671
+ commands: filteredCommands,
672
+ currentIndex: -1,
673
+ direction: 'next',
674
+ });
675
+ updateActiveCommand(filteredCommands[nextIndex]?.id);
676
+ }, [activeCommand, filteredCommands, open]);
677
+
678
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
679
+ if (event.key === 'Escape') {
680
+ event.preventDefault();
681
+ closePalette();
682
+ return;
683
+ }
684
+
685
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Enter') return;
686
+
687
+ if (event.key === 'Enter') {
688
+ if (!activeCommand) return;
689
+ event.preventDefault();
690
+ runCommand(activeCommand, {
691
+ index: filteredCommands.findIndex((command) => command.id === activeCommand.id),
692
+ query: resolvedQuery,
693
+ source: 'palette',
694
+ });
695
+ return;
696
+ }
697
+
698
+ const nextIndex = getNextWorkbenchCommandIndex({
699
+ commands: filteredCommands,
700
+ currentIndex: activeIndex,
701
+ direction: event.key === 'ArrowDown' ? 'next' : 'previous',
702
+ });
703
+
704
+ if (nextIndex >= 0) {
705
+ event.preventDefault();
706
+ updateActiveCommand(filteredCommands[nextIndex]?.id);
707
+ }
708
+ };
709
+
710
+ if (!open) return null;
711
+
712
+ return (
713
+ <div className="ui-workbench-command-palette-overlay" onClick={closePalette}>
714
+ <div
715
+ aria-labelledby={titleId}
716
+ aria-modal="true"
717
+ className={cx('ui-workbench-command-palette', className)}
718
+ role="dialog"
719
+ {...props}
720
+ onClick={(event) => event.stopPropagation()}
721
+ >
722
+ <div className="ui-workbench-command-palette__header">
723
+ <span id={titleId} className="ui-workbench-command-palette__title">
724
+ {title}
725
+ </span>
726
+ <button
727
+ aria-label={closeLabel}
728
+ className="ui-workbench-command-palette__close"
729
+ type="button"
730
+ onClick={closePalette}
731
+ >
732
+ <i aria-hidden="true" className="codicon codicon-close" />
733
+ </button>
734
+ </div>
735
+ <div className="ui-workbench-command-palette__search">
736
+ <i aria-hidden="true" className="codicon codicon-search" />
737
+ <input
738
+ ref={inputRef}
739
+ aria-controls={listId}
740
+ aria-label={placeholder}
741
+ className="ui-workbench-command-palette__input"
742
+ placeholder={placeholder}
743
+ type="search"
744
+ value={resolvedQuery}
745
+ onChange={(event) => setResolvedQuery(event.currentTarget.value)}
746
+ onKeyDown={handleKeyDown}
747
+ />
748
+ </div>
749
+ <WorkbenchCommandList
750
+ id={listId}
751
+ activeCommandId={activeCommand?.id}
752
+ aria-label="Command palette results"
753
+ commands={filteredCommands}
754
+ emptyLabel={emptyLabel}
755
+ query={resolvedQuery}
756
+ source="palette"
757
+ onActiveCommandChange={updateActiveCommand}
758
+ onRunCommand={runCommand}
759
+ />
760
+ </div>
761
+ </div>
762
+ );
763
+ }
764
+
765
+ export interface WorkbenchCommandSuggestProps extends Omit<
766
+ ComponentPropsWithRef<'div'>,
767
+ 'children' | 'onSelect'
768
+ > {
769
+ activeCommandId?: string | undefined;
770
+ commands: readonly WorkbenchCommandDescriptor[];
771
+ emptyLabel?: ReactNode | undefined;
772
+ onActiveCommandChange?: ((commandId: string) => void) | undefined;
773
+ onRunCommand?:
774
+ | ((command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => void)
775
+ | undefined;
776
+ query?: string | undefined;
777
+ visible?: boolean | undefined;
778
+ }
779
+
780
+ export function WorkbenchCommandSuggest({
781
+ activeCommandId,
782
+ className,
783
+ commands,
784
+ emptyLabel = 'No suggested commands',
785
+ onActiveCommandChange,
786
+ onRunCommand,
787
+ query = '',
788
+ visible = true,
789
+ ...props
790
+ }: WorkbenchCommandSuggestProps) {
791
+ const filteredCommands = useMemo(
792
+ () => filterWorkbenchCommands({ commands, limit: 8, query }),
793
+ [commands, query],
794
+ );
795
+ const fallbackActiveCommand = filteredCommands.find((command) =>
796
+ isWorkbenchCommandRunnable(command),
797
+ );
798
+
799
+ if (!visible) return null;
800
+
801
+ return (
802
+ <div className={cx('ui-workbench-command-suggest', className)} {...props}>
803
+ <div className="ui-workbench-command-suggest__header">
804
+ <span>Commands</span>
805
+ {query ? <span className="ui-workbench-command-suggest__query">/{query}</span> : null}
806
+ </div>
807
+ <WorkbenchCommandList
808
+ activeCommandId={activeCommandId ?? fallbackActiveCommand?.id}
809
+ aria-label="Suggested commands"
810
+ commands={filteredCommands}
811
+ emptyLabel={emptyLabel}
812
+ query={query}
813
+ source="suggest"
814
+ onActiveCommandChange={onActiveCommandChange}
815
+ onRunCommand={onRunCommand}
816
+ />
817
+ </div>
818
+ );
819
+ }