@workbench-kit/react 0.0.2-prototype.0.2.2 → 0.0.2-prototype.0.2.21

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 (169) hide show
  1. package/README.md +125 -0
  2. package/package.json +27 -20
  3. package/src/index.ts +60 -29
  4. package/src/jdw/JsonCodeEditorPane.tsx +7 -6
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +1 -3
  6. package/src/jdw/createBuiltinJdwRegistry.ts +1 -4
  7. package/src/jdw/cssRenderBackend.tsx +1 -4
  8. package/src/json-config/JsonConfigWorkbench.tsx +1 -1
  9. package/src/layout/index.ts +11 -0
  10. package/src/layout/sidebar/SideBarTree.tsx +301 -0
  11. package/src/layout/sidebar/SidebarActionIconBar.tsx +3 -3
  12. package/src/layout/sidebar/WorkbenchSidebarActions.tsx +1 -1
  13. package/src/layout/sidebar/index.ts +13 -0
  14. package/src/modal/Modal.tsx +55 -25
  15. package/src/modal/ModalTitlebar.tsx +8 -8
  16. package/src/modal/modalPosition.ts +3 -6
  17. package/src/modal/useModalFocusTrap.ts +142 -0
  18. package/src/modal/useModalWindowFrame.ts +2 -2
  19. package/src/overlay/ContextMenu.tsx +112 -5
  20. package/src/overlay/context-menu.css +4 -2
  21. package/src/overlay/measureAnchoredOverlayPanel.ts +4 -6
  22. package/src/overlay/usePointerPassthroughRegion.ts +2 -0
  23. package/src/primitives/absolute-box/AbsoluteBox.tsx +4 -4
  24. package/src/primitives/button/Button.tsx +4 -4
  25. package/src/primitives/catalog-browse-card/CatalogBrowseCard.tsx +6 -6
  26. package/src/primitives/catalog-browse-pane/CatalogBrowsePane.tsx +15 -15
  27. package/src/primitives/catalog-filter-overlay/CatalogFilterOverlay.tsx +2 -2
  28. package/src/primitives/checkbox/Checkbox.tsx +1 -1
  29. package/src/primitives/chip/Chip.tsx +2 -2
  30. package/src/primitives/codicon/Codicon.tsx +1 -1
  31. package/src/primitives/file-icon/FileIcon.tsx +1 -1
  32. package/src/primitives/icon-button/IconButton.tsx +1 -1
  33. package/src/primitives/index.ts +5 -0
  34. package/src/primitives/library-detail-layout/LibraryDetailLayout.tsx +123 -9
  35. package/src/primitives/library-detail-layout/index.ts +5 -0
  36. package/src/primitives/library-detail-layout/library-detail-layout.css +57 -0
  37. package/src/primitives/library-detail-layout/resolve-hero-cover-media.ts +80 -0
  38. package/src/primitives/library-facet-filter-panel/LibraryFacetFilterPanel.tsx +12 -12
  39. package/src/primitives/library-facet-filter-strip/LibraryFacetFilterStrip.tsx +4 -4
  40. package/src/primitives/number-input/NumberInput.tsx +5 -5
  41. package/src/primitives/record-media-hero/RecordMediaHero.tsx +10 -6
  42. package/src/primitives/scroll-area/ScrollArea.tsx +5 -5
  43. package/src/primitives/scroll-area-infinite-load/useScrollAreaInfiniteLoad.ts +5 -5
  44. package/src/primitives/searchable-multi-select/SearchableMultiSelect.tsx +7 -7
  45. package/src/primitives/select/Select.tsx +1 -13
  46. package/src/primitives/text-area/TextArea.tsx +1 -1
  47. package/src/primitives/text-input/TextInput.tsx +1 -1
  48. package/src/primitives/workbench-media-slot/WorkbenchMediaPlaceholder.tsx +2 -2
  49. package/src/primitives/workbench-media-slot/WorkbenchMediaSlot.tsx +16 -10
  50. package/src/primitives/workbench-thumbnail/WorkbenchThumbnail.tsx +4 -4
  51. package/src/styles.css +1 -2
  52. package/src/test-utils/workbenchMonacoMock.tsx +20 -0
  53. package/src/utils/clamp.ts +4 -0
  54. package/src/utils/normalizeKeyToken.ts +13 -0
  55. package/src/utils/readNumber.ts +4 -0
  56. package/src/widget-tree/WidgetTreeCanvasPreview.tsx +1 -4
  57. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +2 -2
  58. package/src/workbench/chat/ChatCommandProposalCard.tsx +4 -4
  59. package/src/workbench/chat/ChatConversationBar.tsx +3 -3
  60. package/src/workbench/chat/ChatHistoryMenu.tsx +1 -1
  61. package/src/workbench/chat/ChatMessageCollapsible.tsx +4 -0
  62. package/src/workbench/chat/ChatMessageItem.tsx +68 -10
  63. package/src/workbench/chat/ChatMessageList.tsx +1 -1
  64. package/src/workbench/chat/ChatPanel.tsx +32 -23
  65. package/src/workbench/chat/ChatPhasedRunProgress.tsx +86 -13
  66. package/src/workbench/chat/ChatRenameDialog.tsx +1 -1
  67. package/src/workbench/chat/chat-conversation.css +2 -2
  68. package/src/workbench/chat/chat-ui.css +8 -0
  69. package/src/workbench/chat/chatConversation.ts +2 -2
  70. package/src/workbench/chat/chatMessageMeta.tsx +4 -4
  71. package/src/workbench/chat/index.ts +1 -0
  72. package/src/workbench/chat/types.ts +7 -7
  73. package/src/workbench/chat/useCancelRuntimeOnUnmount.ts +4 -4
  74. package/src/workbench/chat/useChatPrependPagination.ts +10 -10
  75. package/src/workbench/chat/useSlashCommandSuggest.ts +1 -1
  76. package/src/workbench/{WorkbenchModalPortal.tsx → chrome/WorkbenchModalPortal.tsx} +1 -1
  77. package/src/workbench/chrome/workbench-chrome.css +4 -0
  78. package/src/workbench/{CommandGroupShell.tsx → commands/CommandGroupShell.tsx} +2 -2
  79. package/src/workbench/{CommandList.tsx → commands/CommandList.tsx} +5 -5
  80. package/src/workbench/{CommandPalette.tsx → commands/CommandPalette.tsx} +16 -42
  81. package/src/workbench/{ConfirmationFlow.tsx → commands/ConfirmationFlow.tsx} +5 -5
  82. package/src/workbench/{ShortcutCommandBridge.tsx → commands/ShortcutCommandBridge.tsx} +1 -13
  83. package/src/workbench/commands/WorkbenchQuickOpen.tsx +462 -0
  84. package/src/workbench/{command-model.ts → commands/command-model.ts} +5 -1
  85. package/src/workbench/commands/command-ui.ts +3 -0
  86. package/src/workbench/{commands.ts → commands/commands.ts} +1 -1
  87. package/src/workbench/commands/createWorkspaceFilesQuickOpenProvider.ts +86 -0
  88. package/src/workbench/commands/quick-open-model.ts +66 -0
  89. package/src/workbench/{WorkbenchEditorTabs.tsx → editor/WorkbenchEditorTabs.tsx} +1 -1
  90. package/src/workbench/{editor-tabs-dnd.ts → editor/editor-tabs-dnd.ts} +1 -1
  91. package/src/workbench/{editorTabContextMenu.ts → editor/editorTabContextMenu.ts} +2 -2
  92. package/src/workbench/{useEditorTabsStripDnd.ts → editor/useEditorTabsStripDnd.ts} +1 -1
  93. package/src/workbench/{useWorkbenchEditorTabContextMenu.tsx → editor/useWorkbenchEditorTabContextMenu.tsx} +1 -1
  94. package/src/workbench/editor-tabs/index.ts +9 -9
  95. package/src/workbench/index.ts +110 -74
  96. package/src/workbench/management/ExtensionManagementPanel.tsx +32 -36
  97. package/src/workbench/management/ExtensionManagementSidebar.tsx +19 -35
  98. package/src/workbench/management/IntegrationSettingsSurface.tsx +2 -5
  99. package/src/workbench/management/IntegrationsShell.tsx +1 -1
  100. package/src/workbench/management/KeybindingCaptureField.tsx +1 -9
  101. package/src/workbench/management/WorkbenchDialogFrame.tsx +2 -2
  102. package/src/workbench/management/WorkbenchNotice.tsx +2 -5
  103. package/src/workbench/management/createWorkbenchNotify.tsx +88 -0
  104. package/src/workbench/management/extension-install-approval.ts +72 -0
  105. package/src/workbench/management/extension-management-filters.ts +38 -0
  106. package/src/workbench/management/index.ts +14 -0
  107. package/src/workbench/management/management.css +28 -0
  108. package/src/workbench/management/types.ts +14 -1
  109. package/src/workbench/management/useWorkbenchNotify.ts +10 -0
  110. package/src/workbench/{MarkdownPreview.tsx → markdown/MarkdownPreview.tsx} +22 -7
  111. package/src/workbench/markdown/markdownRemarkPlugins.ts +12 -0
  112. package/src/workbench/markdown/sanitizeMarkdownHref.ts +29 -0
  113. package/src/workbench/settings/StructuredDataForm.tsx +1 -4
  114. package/src/workbench/settings/structuredDataFormModel.ts +2 -1
  115. package/src/workbench/{ActivityBar.tsx → shell/ActivityBar.tsx} +8 -8
  116. package/src/workbench/{ArtifactShell.tsx → shell/ArtifactShell.tsx} +25 -25
  117. package/src/workbench/{SplitView.tsx → shell/SplitView.tsx} +3 -3
  118. package/src/workbench/{StatusBar.tsx → shell/StatusBar.tsx} +8 -8
  119. package/src/workbench/{StructuredArtifactEditor.tsx → shell/StructuredArtifactEditor.tsx} +9 -9
  120. package/src/workbench/{Timeline.tsx → shell/Timeline.tsx} +77 -20
  121. package/src/workbench/{WorkbenchCanvasShell.defaults.ts → shell/WorkbenchCanvasShell.defaults.ts} +1 -1
  122. package/src/workbench/{WorkbenchCanvasShell.model.ts → shell/WorkbenchCanvasShell.model.ts} +5 -5
  123. package/src/workbench/{WorkbenchCanvasShell.tsx → shell/WorkbenchCanvasShell.tsx} +6 -6
  124. package/src/workbench/{WorkbenchDesktopTitleBar.tsx → shell/WorkbenchDesktopTitleBar.tsx} +15 -15
  125. package/src/workbench/{WorkbenchShell.tsx → shell/WorkbenchShell.tsx} +45 -6
  126. package/src/workbench/{WorkbenchShellTitleBarLayoutControls.tsx → shell/WorkbenchShellTitleBarLayoutControls.tsx} +16 -19
  127. package/src/workbench/{WorkbenchStandaloneShell.tsx → shell/WorkbenchStandaloneShell.tsx} +4 -4
  128. package/src/workbench/{WorkbenchViewEditor.tsx → shell/WorkbenchViewEditor.tsx} +7 -7
  129. package/src/workbench/{WorkbenchViewSidebar.tsx → shell/WorkbenchViewSidebar.tsx} +7 -7
  130. package/src/workbench/{WorkbenchWindowChromeControls.tsx → shell/WorkbenchWindowChromeControls.tsx} +18 -15
  131. package/src/workbench/shell/activity-bar.css +47 -0
  132. package/src/workbench/{shell.ts → shell/shell.ts} +18 -12
  133. package/src/workbench/{shellViewModel.ts → shell/shellViewModel.ts} +3 -3
  134. package/src/workbench/{standalone.ts → shell/standalone.ts} +14 -2
  135. package/src/workbench/{useDeferredWorkbenchMount.ts → shell/useDeferredWorkbenchMount.ts} +3 -3
  136. package/src/workbench/{useWorkbenchAppearanceDocumentSync.tsx → shell/useWorkbenchAppearanceDocumentSync.tsx} +1 -1
  137. package/src/workbench/{useWorkbenchSidebarActionBarDnd.ts → shell/useWorkbenchSidebarActionBarDnd.ts} +1 -1
  138. package/src/workbench/{useWorkbenchSidebarViewPlacementDropZone.ts → shell/useWorkbenchSidebarViewPlacementDropZone.ts} +2 -2
  139. package/src/workbench/{workbenchViewRouteState.ts → shell/workbenchViewRouteState.ts} +4 -4
  140. package/src/workbench/{WorkbenchThemeProvider.tsx → theme/WorkbenchThemeProvider.tsx} +4 -4
  141. package/src/workbench/{theme.ts → theme/theme.ts} +1 -1
  142. package/src/workbench/{themePresets.ts → theme/themePresets.ts} +3 -2
  143. package/src/workbench/views/view-hosts.css +3 -3
  144. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +1 -1
  145. package/src/workbench/workspace/WorkspaceExplorer.tsx +104 -3
  146. package/src/workbench/workspace/workspaceExplorerKeyboard.ts +109 -0
  147. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +6 -5
  148. package/src/workbench/jdw/jdw-editors.css +0 -4
  149. package/src/workbench/jdw/jdw.css +0 -2
  150. package/src/workbench/markdownRemarkPlugins.ts +0 -4
  151. package/src/workbench/workbench-chrome.css +0 -4
  152. /package/src/workbench/{WorkbenchPlatformContext.tsx → chrome/WorkbenchPlatformContext.tsx} +0 -0
  153. /package/src/workbench/{workbenchModalViewState.ts → chrome/workbenchModalViewState.ts} +0 -0
  154. /package/src/workbench/{workbenchOverlaysContext.tsx → chrome/workbenchOverlaysContext.tsx} +0 -0
  155. /package/src/workbench/{workbenchPlatformChrome.ts → chrome/workbenchPlatformChrome.ts} +0 -0
  156. /package/src/workbench/{command-execution-policy.ts → commands/command-execution-policy.ts} +0 -0
  157. /package/src/workbench/{keyboard.ts → commands/keyboard.ts} +0 -0
  158. /package/src/workbench/{workbenchContextMenu.ts → commands/workbenchContextMenu.ts} +0 -0
  159. /package/src/workbench/{activityBarOrder.ts → shell/activityBarOrder.ts} +0 -0
  160. /package/src/workbench/{appearanceLabels.ts → shell/appearanceLabels.ts} +0 -0
  161. /package/src/workbench/{hardReset.ts → shell/hardReset.ts} +0 -0
  162. /package/src/workbench/{shellPresets.ts → shell/shellPresets.ts} +0 -0
  163. /package/src/workbench/{shellState.ts → shell/shellState.ts} +0 -0
  164. /package/src/workbench/{sidebarViewPlacementDnd.ts → shell/sidebarViewPlacementDnd.ts} +0 -0
  165. /package/src/workbench/{status.ts → shell/status.ts} +0 -0
  166. /package/src/workbench/{useWorkbenchSidebarLayout.tsx → shell/useWorkbenchSidebarLayout.tsx} +0 -0
  167. /package/src/workbench/{useWorkbenchStandaloneShellStateSync.tsx → shell/useWorkbenchStandaloneShellStateSync.tsx} +0 -0
  168. /package/src/workbench/{workbenchSidebarLayoutContext.ts → shell/workbenchSidebarLayoutContext.ts} +0 -0
  169. /package/src/workbench/{workbenchStandaloneShellReactContext.tsx → shell/workbenchStandaloneShellReactContext.tsx} +0 -0
@@ -0,0 +1,142 @@
1
+ import { useEffect, type RefObject } from 'react';
2
+
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'a[href]',
5
+ 'button:not([disabled])',
6
+ 'textarea:not([disabled])',
7
+ 'input:not([disabled]):not([type="hidden"])',
8
+ 'select:not([disabled])',
9
+ '[tabindex]:not([tabindex="-1"])',
10
+ ].join(',');
11
+
12
+ function isFocusable(element: HTMLElement): boolean {
13
+ if (element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true') {
14
+ return false;
15
+ }
16
+ if (element.getAttribute('aria-hidden') === 'true') {
17
+ return false;
18
+ }
19
+ // Explicit tabindex="-1" removes the node from the tab order.
20
+ // Do not use element.tabIndex >= 0: jsdom reports -1 for native buttons.
21
+ if (element.getAttribute('tabindex') === '-1') {
22
+ return false;
23
+ }
24
+ const style = window.getComputedStyle(element);
25
+ return style.display !== 'none' && style.visibility !== 'hidden';
26
+ }
27
+
28
+ function listFocusable(container: HTMLElement): HTMLElement[] {
29
+ return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
30
+ isFocusable,
31
+ );
32
+ }
33
+
34
+ export interface UseModalFocusTrapOptions {
35
+ readonly closeOnEscape?: boolean;
36
+ readonly containerRef: RefObject<HTMLElement | null>;
37
+ /** When false, skip trap/restore (for dialogs that stay mounted while closed). Default true. */
38
+ readonly enabled?: boolean;
39
+ readonly initialFocusRef?: RefObject<HTMLElement | null>;
40
+ readonly onClose: () => void;
41
+ readonly restoreFocusOnClose?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Trap Tab focus inside a mounted modal dialog and restore focus on unmount.
46
+ * Escape dismisses when `closeOnEscape` is true (default).
47
+ */
48
+ export function useModalFocusTrap({
49
+ closeOnEscape = true,
50
+ containerRef,
51
+ enabled = true,
52
+ initialFocusRef,
53
+ onClose,
54
+ restoreFocusOnClose = true,
55
+ }: UseModalFocusTrapOptions): void {
56
+ useEffect(() => {
57
+ if (!enabled) {
58
+ return;
59
+ }
60
+
61
+ const previousFocus =
62
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
63
+
64
+ const focusInitial = () => {
65
+ const container = containerRef.current;
66
+ if (!container) {
67
+ return;
68
+ }
69
+
70
+ const preferred = initialFocusRef?.current;
71
+ if (preferred && container.contains(preferred)) {
72
+ preferred.focus();
73
+ return;
74
+ }
75
+
76
+ const focusables = listFocusable(container);
77
+ if (focusables[0]) {
78
+ focusables[0].focus();
79
+ return;
80
+ }
81
+
82
+ if (!container.hasAttribute('tabindex')) {
83
+ container.tabIndex = -1;
84
+ }
85
+ container.focus();
86
+ };
87
+
88
+ const frame = window.requestAnimationFrame(focusInitial);
89
+
90
+ const handleKeyDown = (event: KeyboardEvent) => {
91
+ if (event.key === 'Escape' && closeOnEscape) {
92
+ event.preventDefault();
93
+ event.stopPropagation();
94
+ onClose();
95
+ return;
96
+ }
97
+
98
+ if (event.key !== 'Tab') {
99
+ return;
100
+ }
101
+
102
+ const container = containerRef.current;
103
+ if (!container) {
104
+ return;
105
+ }
106
+
107
+ const focusables = listFocusable(container);
108
+ if (focusables.length === 0) {
109
+ event.preventDefault();
110
+ container.focus();
111
+ return;
112
+ }
113
+
114
+ const first = focusables[0]!;
115
+ const last = focusables[focusables.length - 1]!;
116
+ const active = document.activeElement;
117
+
118
+ if (event.shiftKey) {
119
+ if (active === first || !container.contains(active)) {
120
+ event.preventDefault();
121
+ last.focus();
122
+ }
123
+ return;
124
+ }
125
+
126
+ if (active === last || !container.contains(active)) {
127
+ event.preventDefault();
128
+ first.focus();
129
+ }
130
+ };
131
+
132
+ window.addEventListener('keydown', handleKeyDown, true);
133
+
134
+ return () => {
135
+ window.cancelAnimationFrame(frame);
136
+ window.removeEventListener('keydown', handleKeyDown, true);
137
+ if (restoreFocusOnClose && previousFocus?.isConnected) {
138
+ previousFocus.focus();
139
+ }
140
+ };
141
+ }, [closeOnEscape, containerRef, enabled, initialFocusRef, onClose, restoreFocusOnClose]);
142
+ }
@@ -9,8 +9,8 @@ import { useModalResize } from './useModalResize';
9
9
 
10
10
  export interface UseModalWindowFrameOptions {
11
11
  defaultMaximized: boolean;
12
- defaultHeight?: number | undefined;
13
- defaultWidth?: number | undefined;
12
+ defaultHeight?: number;
13
+ defaultWidth?: number;
14
14
  minHeight: number;
15
15
  minWidth: number;
16
16
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, type ReactNode } from 'react';
1
+ import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
2
2
  import { Button } from '../primitives/button';
3
3
  import { cxCodicon } from '../utils/codicon';
4
4
  import { cx } from '../utils/cx';
@@ -44,6 +44,47 @@ function menuHasShortcuts(items: ContextMenuItem[]): boolean {
44
44
  );
45
45
  }
46
46
 
47
+ function isEnabledMenuItem(
48
+ item: ContextMenuItem,
49
+ ): item is Extract<ContextMenuItem, { onSelect: () => void }> {
50
+ return item.type !== 'separator' && !item.disabled;
51
+ }
52
+
53
+ function getEnabledItemIndexes(items: readonly ContextMenuItem[]): number[] {
54
+ return items.reduce<number[]>((indexes, item, index) => {
55
+ if (isEnabledMenuItem(item)) {
56
+ indexes.push(index);
57
+ }
58
+ return indexes;
59
+ }, []);
60
+ }
61
+
62
+ function stepEnabledIndex(
63
+ enabledIndexes: readonly number[],
64
+ currentIndex: number,
65
+ direction: 1 | -1,
66
+ ): number {
67
+ if (enabledIndexes.length === 0) {
68
+ return -1;
69
+ }
70
+
71
+ const position = enabledIndexes.indexOf(currentIndex);
72
+ if (position < 0) {
73
+ return direction === 1 ? enabledIndexes[0]! : enabledIndexes[enabledIndexes.length - 1]!;
74
+ }
75
+
76
+ const nextPosition = (position + direction + enabledIndexes.length) % enabledIndexes.length;
77
+ return enabledIndexes[nextPosition]!;
78
+ }
79
+
80
+ /**
81
+ * WAI-ARIA menu keyboard model (no nested submenus in this slice):
82
+ * - ArrowUp/ArrowDown move highlight (skip disabled + separators)
83
+ * - Home/End jump to first/last enabled item
84
+ * - Enter/Space activate the highlighted item
85
+ * - Escape closes via {@link useFixedOverlayDismiss}
86
+ * - Roving tabindex: only the highlighted enabled item is tabIndex=0
87
+ */
47
88
  export function ContextMenu({
48
89
  ariaLabel = 'Context menu',
49
90
  className,
@@ -56,6 +97,8 @@ export function ContextMenu({
56
97
  const position = useClampedFixedOverlayPosition(ref, { x, y }, items.length);
57
98
  const hasIcons = menuHasIcons(items);
58
99
  const hasShortcuts = menuHasShortcuts(items);
100
+ const enabledIndexes = useMemo(() => getEnabledItemIndexes(items), [items]);
101
+ const [highlightedIndex, setHighlightedIndex] = useState(() => enabledIndexes[0] ?? -1);
59
102
 
60
103
  useFixedOverlayDismiss({ containerRef: ref, onClose });
61
104
 
@@ -73,8 +116,64 @@ export function ContextMenu({
73
116
  }, [onClose]);
74
117
 
75
118
  useEffect(() => {
76
- ref.current?.querySelector<HTMLButtonElement>('.ui-context-menu__item:not(:disabled)')?.focus();
77
- }, []);
119
+ setHighlightedIndex((current) =>
120
+ enabledIndexes.includes(current) ? current : (enabledIndexes[0] ?? -1),
121
+ );
122
+ }, [enabledIndexes]);
123
+
124
+ useEffect(() => {
125
+ if (highlightedIndex < 0) {
126
+ return;
127
+ }
128
+
129
+ const item = ref.current?.querySelector<HTMLButtonElement>(
130
+ `[data-menu-index="${highlightedIndex}"]`,
131
+ );
132
+ item?.focus();
133
+ }, [highlightedIndex]);
134
+
135
+ const activateIndex = (index: number) => {
136
+ const item = items[index];
137
+ if (!item || !isEnabledMenuItem(item)) {
138
+ return;
139
+ }
140
+ item.onSelect();
141
+ onClose();
142
+ };
143
+
144
+ const handleMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
145
+ if (event.key === 'ArrowDown') {
146
+ event.preventDefault();
147
+ setHighlightedIndex((current) => stepEnabledIndex(enabledIndexes, current, 1));
148
+ return;
149
+ }
150
+
151
+ if (event.key === 'ArrowUp') {
152
+ event.preventDefault();
153
+ setHighlightedIndex((current) => stepEnabledIndex(enabledIndexes, current, -1));
154
+ return;
155
+ }
156
+
157
+ if (event.key === 'Home') {
158
+ event.preventDefault();
159
+ setHighlightedIndex(enabledIndexes[0] ?? -1);
160
+ return;
161
+ }
162
+
163
+ if (event.key === 'End') {
164
+ event.preventDefault();
165
+ setHighlightedIndex(enabledIndexes[enabledIndexes.length - 1] ?? -1);
166
+ return;
167
+ }
168
+
169
+ if (event.key === 'Enter' || event.key === ' ') {
170
+ if (highlightedIndex < 0) {
171
+ return;
172
+ }
173
+ event.preventDefault();
174
+ activateIndex(highlightedIndex);
175
+ }
176
+ };
78
177
 
79
178
  if (items.length === 0) return null;
80
179
 
@@ -91,6 +190,7 @@ export function ContextMenu({
91
190
  top: position.y,
92
191
  }}
93
192
  onContextMenu={(event) => event.preventDefault()}
193
+ onKeyDown={handleMenuKeyDown}
94
194
  >
95
195
  {items.map((item, index) =>
96
196
  item.type === 'separator' ? (
@@ -100,11 +200,18 @@ export function ContextMenu({
100
200
  key={itemKey(item, index)}
101
201
  className="ui-context-menu__item"
102
202
  data-danger={item.danger ? 'true' : undefined}
203
+ data-highlighted={highlightedIndex === index ? 'true' : undefined}
204
+ data-menu-index={index}
103
205
  disabled={item.disabled}
104
206
  role="menuitem"
207
+ tabIndex={highlightedIndex === index ? 0 : -1}
105
208
  onClick={() => {
106
- item.onSelect();
107
- onClose();
209
+ activateIndex(index);
210
+ }}
211
+ onMouseEnter={() => {
212
+ if (!item.disabled) {
213
+ setHighlightedIndex(index);
214
+ }
108
215
  }}
109
216
  >
110
217
  {hasIcons ? (
@@ -72,7 +72,8 @@
72
72
  }
73
73
 
74
74
  .ui-context-menu__item:hover,
75
- .ui-context-menu__item:focus-visible {
75
+ .ui-context-menu__item:focus-visible,
76
+ .ui-context-menu__item[data-highlighted='true'] {
76
77
  color: var(--color-text);
77
78
  background: var(--color-surface-hover);
78
79
  outline: none;
@@ -92,7 +93,8 @@
92
93
  }
93
94
 
94
95
  .ui-context-menu__item[data-danger='true']:hover,
95
- .ui-context-menu__item[data-danger='true']:focus-visible {
96
+ .ui-context-menu__item[data-danger='true']:focus-visible,
97
+ .ui-context-menu__item[data-danger='true'][data-highlighted='true'] {
96
98
  color: #fff;
97
99
  background: var(--color-danger);
98
100
  }
@@ -1,3 +1,5 @@
1
+ import { clampNumber } from '../utils/clamp';
2
+
1
3
  export type AnchoredOverlayPlacement = 'side' | 'below' | 'above';
2
4
 
3
5
  export interface MeasureAnchoredOverlayPanelOptions {
@@ -26,10 +28,6 @@ const SIDE_MIN_VISIBLE_HEIGHT_PX = 240;
26
28
  /** Prefer below when at least this many pixels are available under the trigger. */
27
29
  const BELOW_MIN_SPACE_PX = 12;
28
30
 
29
- function clamp(value: number, min: number, max: number): number {
30
- return Math.min(Math.max(value, min), max);
31
- }
32
-
33
31
  /**
34
32
  * Measure a panel-sized overlay anchored to a trigger element.
35
33
  * Prefers a side flyout when the trigger sits in the left portion of the
@@ -65,7 +63,7 @@ export function measureAnchoredOverlayPanel(
65
63
 
66
64
  if (preferSide) {
67
65
  const minVisibleHeight = Math.min(SIDE_MIN_VISIBLE_HEIGHT_PX, maxHeight);
68
- const top = clamp(
66
+ const top = clampNumber(
69
67
  triggerRect.top,
70
68
  viewportPaddingPx,
71
69
  viewportHeight - viewportPaddingPx - minVisibleHeight,
@@ -80,7 +78,7 @@ export function measureAnchoredOverlayPanel(
80
78
  }
81
79
 
82
80
  // Align the panel's right edge toward the trigger's right, then clamp.
83
- const left = clamp(
81
+ const left = clampNumber(
84
82
  triggerRect.right - width,
85
83
  viewportPaddingPx,
86
84
  viewportWidth - viewportPaddingPx - width,
@@ -80,6 +80,8 @@ export function usePointerPassthroughRegion(options: UsePointerPassthroughRegion
80
80
  if (frame !== 0) {
81
81
  window.cancelAnimationFrame(frame);
82
82
  }
83
+ // Do not leave the host window stuck in click-through after unmount.
84
+ void portRef.current.setPointerPassthrough(false);
83
85
  };
84
86
  }, [enabled, rootRef]);
85
87
  }
@@ -9,12 +9,12 @@ export interface WorkbenchRect {
9
9
  }
10
10
 
11
11
  export interface AbsoluteBoxProps {
12
- background?: string | undefined;
12
+ background?: string;
13
13
  children?: ReactNode;
14
- className?: string | undefined;
15
- overflow?: CSSProperties['overflow'] | undefined;
14
+ className?: string;
15
+ overflow?: CSSProperties['overflow'];
16
16
  rect: WorkbenchRect;
17
- style?: CSSProperties | undefined;
17
+ style?: CSSProperties;
18
18
  }
19
19
 
20
20
  export function AbsoluteBox({
@@ -6,10 +6,10 @@ import { cx } from '../../utils/cx';
6
6
  type ButtonVariant = 'default' | 'primary' | 'danger';
7
7
 
8
8
  export interface ButtonProps extends ComponentPropsWithRef<'button'> {
9
- block?: boolean | undefined;
10
- compact?: boolean | undefined;
11
- icon?: string | undefined;
12
- secondary?: boolean | undefined;
9
+ block?: boolean;
10
+ compact?: boolean;
11
+ icon?: string;
12
+ secondary?: boolean;
13
13
  variant?: ButtonVariant;
14
14
  }
15
15
 
@@ -11,9 +11,9 @@ export interface CatalogBrowseCardProps extends Omit<
11
11
  'children' | 'title'
12
12
  > {
13
13
  description?: ReactNode;
14
- icon?: string | undefined;
15
- imageAlt?: string | undefined;
16
- imageUrl?: string | null | undefined;
14
+ icon?: string;
15
+ imageAlt?: string;
16
+ imageUrl?: string | null;
17
17
  label: ReactNode;
18
18
  /**
19
19
  * Custom media content for the icon/cover region. When set, replaces the
@@ -23,15 +23,15 @@ export interface CatalogBrowseCardProps extends Omit<
23
23
  /** Overlay content inside the media region (badges, status chips, etc.). */
24
24
  mediaOverlay?: ReactNode;
25
25
  meta?: ReactNode;
26
- selected?: boolean | undefined;
26
+ selected?: boolean;
27
27
  /**
28
28
  * Secondary actions outside the primary hit target (for example Assign).
29
29
  * When set, the root becomes a non-button container and click/drag handlers
30
30
  * apply to the main control only.
31
31
  */
32
32
  trailing?: ReactNode;
33
- tooltip?: string | undefined;
34
- variant?: CatalogBrowseCardVariant | undefined;
33
+ tooltip?: string;
34
+ variant?: CatalogBrowseCardVariant;
35
35
  }
36
36
 
37
37
  export function CatalogBrowseCard({
@@ -33,12 +33,12 @@ export type CatalogBrowseViewMode = 'grid' | 'list';
33
33
  * Hosts with richer cards should pass `renderGridItem` / `renderListItem`.
34
34
  */
35
35
  export interface CatalogBrowseItem {
36
- readonly description?: string | null | undefined;
36
+ readonly description?: string | null;
37
37
  readonly id: string;
38
- readonly imageAlt?: string | undefined;
39
- readonly imageUrl?: string | null | undefined;
38
+ readonly imageAlt?: string;
39
+ readonly imageUrl?: string | null;
40
40
  readonly label: string;
41
- readonly meta?: string | null | undefined;
41
+ readonly meta?: string | null;
42
42
  }
43
43
 
44
44
  export interface CatalogBrowseItemRenderState {
@@ -48,17 +48,17 @@ export interface CatalogBrowseItemRenderState {
48
48
  export interface CatalogBrowsePaneProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {
49
49
  readonly clearSearchLabel: string;
50
50
  readonly emptyMessage: string;
51
- readonly errorMessage?: string | null | undefined;
51
+ readonly errorMessage?: string | null;
52
52
  /** Slot between search and sort/view controls (facet trigger, chips, etc.). */
53
53
  readonly facetStrip?: ReactNode;
54
- readonly gridAriaLabel?: string | undefined;
54
+ readonly gridAriaLabel?: string;
55
55
  /** Visual label for the grid view-mode segment (string or icon node). */
56
56
  readonly gridLabel?: ReactNode;
57
57
  readonly hasMore: boolean;
58
58
  readonly isLoading: boolean;
59
59
  readonly isLoadingMore: boolean;
60
60
  readonly items: ReadonlyArray<CatalogBrowseItem>;
61
- readonly listAriaLabel?: string | undefined;
61
+ readonly listAriaLabel?: string;
62
62
  /** Visual label for the list view-mode segment (string or icon node). */
63
63
  readonly listLabel?: ReactNode;
64
64
  readonly loadingMessage: string;
@@ -66,11 +66,11 @@ export interface CatalogBrowsePaneProps extends Omit<ComponentPropsWithRef<'div'
66
66
  ((itemId: string, itemLabel: string, event: ReactMouseEvent<HTMLElement>) => void) | undefined;
67
67
  readonly onLoadMore: () => void;
68
68
  readonly onOpenItem: (itemId: string) => void;
69
- readonly onRefresh?: (() => void) | null | undefined;
69
+ readonly onRefresh?: (() => void) | null;
70
70
  readonly onSearchQueryChange: (query: string) => void;
71
- readonly onSortChange?: ((sort: string) => void) | undefined;
71
+ readonly onSortChange?: (sort: string) => void;
72
72
  readonly onViewModeChange: (viewMode: CatalogBrowseViewMode) => void;
73
- readonly refreshLabel?: string | null | undefined;
73
+ readonly refreshLabel?: string | null;
74
74
  readonly renderGridItem?:
75
75
  ((item: CatalogBrowseItem, state: CatalogBrowseItemRenderState) => ReactNode) | undefined;
76
76
  readonly renderListItem?:
@@ -78,14 +78,14 @@ export interface CatalogBrowsePaneProps extends Omit<ComponentPropsWithRef<'div'
78
78
  readonly searchAriaLabel: string;
79
79
  readonly searchPlaceholder: string;
80
80
  readonly searchQuery: string;
81
- readonly selectedItemId?: string | null | undefined;
82
- readonly sort?: string | undefined;
83
- readonly sortAriaLabel?: string | undefined;
84
- readonly sortOptions?: ReadonlyArray<{ label: string; value: string }> | undefined;
81
+ readonly selectedItemId?: string | null;
82
+ readonly sort?: string;
83
+ readonly sortAriaLabel?: string;
84
+ readonly sortOptions?: ReadonlyArray<{ label: string; value: string }>;
85
85
  /** Extra trailing controls after built-in sort / view mode (before refresh). */
86
86
  readonly toolbarTrailing?: ReactNode;
87
87
  readonly viewMode: CatalogBrowseViewMode;
88
- readonly viewModeAriaLabel?: string | undefined;
88
+ readonly viewModeAriaLabel?: string;
89
89
  }
90
90
 
91
91
  export function CatalogBrowsePane({
@@ -13,10 +13,10 @@ export interface CatalogFilterOverlayProps extends Omit<
13
13
  * When true, Clear stays mounted and sized but is not actionable.
14
14
  * Keeps the title row height stable across selection changes.
15
15
  */
16
- readonly clearDisabled?: boolean | undefined;
16
+ readonly clearDisabled?: boolean;
17
17
  readonly clearLabel: string;
18
18
  readonly onClear: () => void;
19
- readonly ref?: Ref<HTMLDivElement> | undefined;
19
+ readonly ref?: Ref<HTMLDivElement>;
20
20
  readonly title: ReactNode;
21
21
  readonly titleId: string;
22
22
  }
@@ -3,7 +3,7 @@ import type { ChangeEvent, ComponentPropsWithRef, ReactNode } from 'react';
3
3
  import { cx } from '../../utils/cx';
4
4
 
5
5
  export interface CheckboxProps extends Omit<ComponentPropsWithRef<'input'>, 'type'> {
6
- label?: ReactNode | undefined;
6
+ label?: ReactNode;
7
7
  onCheckedChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
8
8
  }
9
9
 
@@ -4,9 +4,9 @@ import { Button } from '../button';
4
4
  import { cx } from '../../utils/cx';
5
5
 
6
6
  export interface ChipProps extends Omit<ComponentPropsWithRef<'button'>, 'children'> {
7
- count?: number | undefined;
7
+ count?: number;
8
8
  label: ReactNode;
9
- onDismiss?: (() => void) | undefined;
9
+ onDismiss?: () => void;
10
10
  }
11
11
 
12
12
  /**
@@ -4,7 +4,7 @@ import { cxCodicon } from '../../utils/codicon';
4
4
  export interface CodiconProps extends Omit<ComponentPropsWithRef<'i'>, 'children'> {
5
5
  icon: string;
6
6
  /** When set, exposes the icon to assistive tech instead of aria-hidden. */
7
- label?: string | undefined;
7
+ label?: string;
8
8
  }
9
9
 
10
10
  /** VS Code codicon glyph (`codicon` font). Requires `@vscode/codicons` CSS in the app entry. Prefer `WorkbenchIcon` for new code. */
@@ -6,7 +6,7 @@ export const UI_FILE_ICON_CLASS = 'ui-file-icon';
6
6
 
7
7
  export interface FileIconProps extends Omit<ComponentPropsWithRef<'i'>, 'children'> {
8
8
  icon: string;
9
- kind?: FileIconKind | undefined;
9
+ kind?: FileIconKind;
10
10
  }
11
11
 
12
12
  export function FileIcon({ className, icon, kind, ...props }: FileIconProps) {
@@ -7,7 +7,7 @@ import { cx } from '../../utils/cx';
7
7
  type IconButtonVariant = 'default' | 'danger';
8
8
 
9
9
  export interface IconButtonProps extends Omit<ComponentPropsWithRef<'button'>, 'children'> {
10
- compact?: boolean | undefined;
10
+ compact?: boolean;
11
11
  icon: WorkbenchIconInput;
12
12
  label: string;
13
13
  variant?: IconButtonVariant;
@@ -110,6 +110,11 @@ export type {
110
110
  LibraryDetailLayoutMode,
111
111
  LibraryDetailLayoutProps,
112
112
  } from './library-detail-layout/LibraryDetailLayout';
113
+ export { resolveLibraryDetailHeroCoverMedia } from './library-detail-layout/resolve-hero-cover-media.js';
114
+ export type {
115
+ ResolveLibraryDetailHeroCoverMediaInput,
116
+ ResolvedLibraryDetailHeroCoverMedia,
117
+ } from './library-detail-layout/resolve-hero-cover-media.js';
113
118
  export { ScrollAreaInfiniteSentinel } from './scroll-area-infinite-load/ScrollAreaInfiniteSentinel';
114
119
  export type { ScrollAreaInfiniteSentinelProps } from './scroll-area-infinite-load/ScrollAreaInfiniteSentinel';
115
120
  export { useScrollAreaInfiniteLoad } from './scroll-area-infinite-load/useScrollAreaInfiniteLoad';