@redocly/theme 0.70.0-next.4 → 0.70.0-next.6

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 (113) hide show
  1. package/lib/components/Buttons/ConnectMCPButton.js +8 -13
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.d.ts +1 -1
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.js +1 -1
  4. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +30 -11
  5. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +2 -1
  6. package/lib/components/Dropdown/Dropdown.d.ts +4 -2
  7. package/lib/components/Dropdown/Dropdown.js +28 -8
  8. package/lib/components/Dropdown/DropdownMenu.d.ts +1 -0
  9. package/lib/components/Dropdown/DropdownMenu.js +1 -1
  10. package/lib/components/Feedback/Reasons.js +3 -2
  11. package/lib/components/Filter/FilterCheckboxes.js +30 -11
  12. package/lib/components/Filter/FilterDateRange.js +36 -2
  13. package/lib/components/Image/Image.js +24 -4
  14. package/lib/components/Search/SearchAiActivity.d.ts +10 -0
  15. package/lib/components/Search/SearchAiActivity.js +278 -0
  16. package/lib/components/Search/SearchAiDialog.js +5 -1
  17. package/lib/components/Search/SearchAiMessage.d.ts +2 -1
  18. package/lib/components/Search/SearchAiMessage.js +20 -108
  19. package/lib/components/Search/SearchItem.js +1 -8
  20. package/lib/components/Search/variables.js +15 -2
  21. package/lib/components/Segmented/Segmented.js +23 -3
  22. package/lib/components/Select/Select.js +10 -3
  23. package/lib/components/Select/SelectInput.d.ts +1 -0
  24. package/lib/components/Select/SelectInput.js +5 -11
  25. package/lib/components/SplitButton/SplitButton.js +37 -3
  26. package/lib/core/constants/mcp.d.ts +2 -2
  27. package/lib/core/constants/mcp.js +3 -8
  28. package/lib/core/constants/search.js +5 -1
  29. package/lib/core/hooks/index.d.ts +2 -0
  30. package/lib/core/hooks/index.js +2 -0
  31. package/lib/core/hooks/menu/use-collapse.js +1 -1
  32. package/lib/core/hooks/search/use-search-ai-activity.d.ts +11 -0
  33. package/lib/core/hooks/search/use-search-ai-activity.js +15 -0
  34. package/lib/core/hooks/use-connect-mcp-button.js +2 -1
  35. package/lib/core/hooks/use-full-height.d.ts +1 -1
  36. package/lib/core/hooks/use-full-height.js +8 -8
  37. package/lib/core/hooks/use-tab-list.d.ts +31 -0
  38. package/lib/core/hooks/use-tab-list.js +49 -0
  39. package/lib/core/styles/global.d.ts +2 -0
  40. package/lib/core/styles/global.js +80 -76
  41. package/lib/core/styles/index.d.ts +1 -0
  42. package/lib/core/styles/index.js +3 -1
  43. package/lib/core/styles/search-ai-appear.d.ts +1 -0
  44. package/lib/core/styles/search-ai-appear.js +18 -0
  45. package/lib/core/types/dropdown.d.ts +1 -0
  46. package/lib/core/types/dropdown.js +3 -0
  47. package/lib/core/types/index.d.ts +1 -0
  48. package/lib/core/types/l10n.d.ts +1 -1
  49. package/lib/core/types/mcp.d.ts +2 -1
  50. package/lib/core/types/search.d.ts +23 -0
  51. package/lib/core/utils/Dynamic.d.ts +8 -1
  52. package/lib/core/utils/Dynamic.js +34 -3
  53. package/lib/core/utils/assign-ref.d.ts +2 -0
  54. package/lib/core/utils/assign-ref.js +12 -0
  55. package/lib/core/utils/content-segments.d.ts +12 -1
  56. package/lib/core/utils/content-segments.js +90 -5
  57. package/lib/core/utils/dropdown.d.ts +2 -0
  58. package/lib/core/utils/dropdown.js +12 -0
  59. package/lib/core/utils/index.d.ts +2 -0
  60. package/lib/core/utils/index.js +2 -0
  61. package/lib/core/utils/mcp.d.ts +2 -1
  62. package/lib/core/utils/mcp.js +5 -0
  63. package/lib/core/utils/search-ai-activity.d.ts +4 -0
  64. package/lib/core/utils/search-ai-activity.js +83 -0
  65. package/lib/markdoc/tags/connect-mcp.js +15 -0
  66. package/package.json +2 -2
  67. package/src/components/Buttons/ConnectMCPButton.tsx +8 -14
  68. package/src/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.tsx +3 -1
  69. package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +37 -22
  70. package/src/components/Catalog/CatalogFilter/CatalogFilterDateRange.tsx +3 -2
  71. package/src/components/Dropdown/Dropdown.tsx +28 -6
  72. package/src/components/Dropdown/DropdownMenu.tsx +7 -1
  73. package/src/components/Feedback/Reasons.tsx +3 -2
  74. package/src/components/Filter/FilterCheckboxes.tsx +33 -18
  75. package/src/components/Filter/FilterDateRange.tsx +3 -2
  76. package/src/components/Image/Image.tsx +26 -5
  77. package/src/components/Search/SearchAiActivity.tsx +318 -0
  78. package/src/components/Search/SearchAiDialog.tsx +4 -3
  79. package/src/components/Search/SearchAiMessage.tsx +27 -135
  80. package/src/components/Search/SearchItem.tsx +2 -9
  81. package/src/components/Search/variables.ts +15 -2
  82. package/src/components/Segmented/Segmented.tsx +25 -16
  83. package/src/components/Select/Select.tsx +12 -6
  84. package/src/components/Select/SelectInput.tsx +10 -18
  85. package/src/components/SplitButton/SplitButton.tsx +8 -2
  86. package/src/core/constants/mcp.ts +3 -9
  87. package/src/core/constants/search.ts +5 -1
  88. package/src/core/hooks/__mocks__/index.ts +1 -0
  89. package/src/core/hooks/__mocks__/use-theme-hooks.ts +1 -0
  90. package/src/core/hooks/index.ts +2 -0
  91. package/src/core/hooks/menu/use-collapse.ts +1 -1
  92. package/src/core/hooks/search/use-search-ai-activity.ts +31 -0
  93. package/src/core/hooks/use-connect-mcp-button.ts +7 -2
  94. package/src/core/hooks/use-full-height.ts +8 -8
  95. package/src/core/hooks/use-tab-list.ts +63 -0
  96. package/src/core/styles/global.ts +80 -75
  97. package/src/core/styles/index.ts +2 -1
  98. package/src/core/styles/search-ai-appear.ts +16 -0
  99. package/src/core/types/dropdown.ts +1 -0
  100. package/src/core/types/index.ts +1 -0
  101. package/src/core/types/l10n.ts +1 -1
  102. package/src/core/types/mcp.ts +3 -1
  103. package/src/core/types/search.ts +21 -0
  104. package/src/core/utils/Dynamic.tsx +67 -35
  105. package/src/core/utils/assign-ref.ts +9 -0
  106. package/src/core/utils/content-segments.ts +98 -6
  107. package/src/core/utils/dropdown.ts +14 -0
  108. package/src/core/utils/index.ts +2 -0
  109. package/src/core/utils/mcp.ts +7 -1
  110. package/src/core/utils/search-ai-activity.ts +110 -0
  111. package/src/icons/CheckboxIcon/CheckboxIcon.tsx +1 -0
  112. package/src/icons/ChevronSortIcon/ChevronSortIcon.tsx +1 -0
  113. package/src/markdoc/tags/connect-mcp.ts +16 -1
@@ -5,34 +5,43 @@ import type { ForwardedRef, ReactElement } from 'react';
5
5
  import type { SegmentedProps } from '@redocly/theme/core/types';
6
6
 
7
7
  import { typedMemo } from '@redocly/theme/core/hoc/typed-memo';
8
+ import { useTabList } from '@redocly/theme/core/hooks';
8
9
 
9
10
  function SegmentedComponent<T>(
10
11
  { options, onChange, value, className = '', size = 'regular' }: SegmentedProps<T>,
11
12
  ref?: ForwardedRef<HTMLDivElement>,
12
13
  ): ReactElement {
14
+ const { tabListProps, getTabProps } = useTabList({
15
+ count: options.length,
16
+ activeIndex: options.findIndex((opt) => value == opt.value),
17
+ onChange: (index) => onChange(options[index]),
18
+ });
19
+
13
20
  return (
14
21
  <SegmentedGroup
15
22
  ref={ref}
16
23
  data-component-name="Segmented/Segmented"
17
24
  className={`tag-grey ${size} ${className}`}
18
- role="tablist"
25
+ {...tabListProps}
19
26
  $size={size}
20
27
  >
21
- {options.map((opt, index) => (
22
- <React.Fragment key={index}>
23
- {opt.divider}
24
- <SegmentedItem
25
- key={opt.label}
26
- role="tab"
27
- title={opt.label}
28
- onClick={() => onChange(opt)}
29
- $isActive={value == opt.value}
30
- $size={size}
31
- >
32
- {opt.element || opt.label}
33
- </SegmentedItem>
34
- </React.Fragment>
35
- ))}
28
+ {options.map((opt, index) => {
29
+ const { 'aria-controls': _noPanel, ...tabProps } = getTabProps(index);
30
+ return (
31
+ <React.Fragment key={index}>
32
+ {opt.divider}
33
+ <SegmentedItem
34
+ key={opt.label}
35
+ {...tabProps}
36
+ title={opt.label}
37
+ $isActive={value == opt.value}
38
+ $size={size}
39
+ >
40
+ {opt.element || opt.label}
41
+ </SegmentedItem>
42
+ </React.Fragment>
43
+ );
44
+ })}
36
45
  </SegmentedGroup>
37
46
  );
38
47
  }
@@ -63,6 +63,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
63
63
  const [filteredOptions, setFilteredOptions] = useState<SelectOption<T>[]>(options);
64
64
  const [stickyInputValue, setStickyInputValue] = useState<string>(placeholder || '');
65
65
  const inputId = useId();
66
+ const listboxId = `${inputId}-listbox`;
66
67
 
67
68
  useOutsideClick(selectRef, () => {
68
69
  setDropdownActive(false);
@@ -189,7 +190,8 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
189
190
  return (
190
191
  <SelectInput
191
192
  id={inputId}
192
- listboxId={inputId}
193
+ listboxId={listboxId}
194
+ expanded={dropdownActive}
193
195
  selectedOptions={selectedOptions}
194
196
  searchValue={searchValue}
195
197
  placeholder={placeholder}
@@ -223,10 +225,14 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
223
225
  setDropdownActive(!dropdownActive);
224
226
  }
225
227
  }}
226
- tabIndex={0}
227
- aria-haspopup="listbox"
228
- aria-expanded={dropdownActive}
229
- aria-label="Select option"
228
+ tabIndex={searchable ? undefined : 0}
229
+ {...(!searchable && {
230
+ role: 'combobox',
231
+ 'aria-haspopup': 'listbox' as const,
232
+ 'aria-expanded': dropdownActive,
233
+ 'aria-controls': listboxId,
234
+ 'aria-label': 'Select option',
235
+ })}
230
236
  onFocus={(e) => {
231
237
  if (e.target === selectRef.current) {
232
238
  selectInputRef.current?.focus();
@@ -245,7 +251,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
245
251
  alignment={alignment}
246
252
  active={!renderInput ? dropdownActive : undefined}
247
253
  >
248
- <SelectDropdownMenu role="listbox" footer={footer}>
254
+ <SelectDropdownMenu id={listboxId} role="listbox" footer={footer}>
249
255
  {filteredOptions.length === 0 ? (
250
256
  <DropdownMenuItem disabled>
251
257
  {translate('select.noResults', 'No results')}
@@ -6,6 +6,7 @@ import type { SelectOption } from '@redocly/theme/core/types';
6
6
  import { Tag } from '@redocly/theme/components/Tag/Tag';
7
7
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
8
8
  import { Button } from '@redocly/theme/components/Button/Button';
9
+ import { assignRef } from '@redocly/theme/core/utils';
9
10
 
10
11
  type SelectInputProps<T> = {
11
12
  id?: string;
@@ -22,6 +23,7 @@ type SelectInputProps<T> = {
22
23
  placeholder?: string;
23
24
  multiple?: boolean;
24
25
  searchable?: boolean;
26
+ expanded?: boolean;
25
27
  clearable?: boolean;
26
28
  inputRef?: React.ForwardedRef<HTMLInputElement>;
27
29
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -48,6 +50,7 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
48
50
  stickyValue,
49
51
  multiple,
50
52
  searchable,
53
+ expanded,
51
54
  clearable,
52
55
  clearHandler,
53
56
  searchHandler,
@@ -123,17 +126,15 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
123
126
  if (!input) return;
124
127
 
125
128
  inputRef.current = input;
126
-
127
- if (!forwardedInputRef) return;
128
-
129
- if (typeof forwardedInputRef === 'function') {
130
- forwardedInputRef(input);
131
- } else {
132
- forwardedInputRef.current = input;
133
- }
129
+ assignRef(forwardedInputRef, input);
134
130
  }}
135
131
  $width={multiple ? (!searchValue && selectedOptions.length ? '10px' : 'auto') : '100%'}
136
132
  tabIndex={0}
133
+ role="combobox"
134
+ aria-expanded={!!expanded}
135
+ aria-controls={listboxId}
136
+ aria-autocomplete="list"
137
+ aria-label="Select option"
137
138
  onFocus={(e) => {
138
139
  e.stopPropagation();
139
140
  }}
@@ -156,16 +157,7 @@ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
156
157
  );
157
158
 
158
159
  return (
159
- <SelectInputWrapper
160
- id={id}
161
- onFocus={onFocusHandler}
162
- onClick={onClickHandler}
163
- role="button"
164
- aria-haspopup="listbox"
165
- aria-controls={listboxId}
166
- aria-owns={listboxId}
167
- aria-label="Select option"
168
- >
160
+ <SelectInputWrapper id={id} onFocus={onFocusHandler} onClick={onClickHandler}>
169
161
  {!onlyIcon && (
170
162
  <>
171
163
  <SelectInputValue>
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useRef } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { ComponentProps, JSX } from 'react';
@@ -45,9 +45,14 @@ export function SplitButton(props: SplitButtonProps): JSX.Element {
45
45
  } = props;
46
46
 
47
47
  const hasMenu = typeof children === 'function' || React.Children.toArray(children).length > 0;
48
+ const wrapperRef = useRef<HTMLDivElement>(null);
48
49
 
49
50
  return (
50
- <SplitButtonWrapper data-component-name="SplitButton/SplitButton" className={className}>
51
+ <SplitButtonWrapper
52
+ data-component-name="SplitButton/SplitButton"
53
+ className={className}
54
+ ref={wrapperRef}
55
+ >
51
56
  <ButtonGroup variant={variant} size={size}>
52
57
  {button}
53
58
  {hasMenu ? (
@@ -56,6 +61,7 @@ export function SplitButton(props: SplitButtonProps): JSX.Element {
56
61
  placement={placement}
57
62
  alignment={alignment}
58
63
  portalled={portalled}
64
+ alignmentAnchorRef={wrapperRef}
59
65
  withArrow
60
66
  onOpen={onOpen}
61
67
  onClose={onClose}
@@ -1,15 +1,9 @@
1
- import type { MCPOption } from '../types/mcp';
2
-
3
1
  export const DEFAULT_MCP_SERVER_NAME = 'mcp-server';
4
2
 
5
3
  export const MCP_ROUTE = '/mcp';
6
4
 
7
5
  export const PUBLIC_MCP_ROUTE = '/mcp-public';
8
6
 
9
- export const DEFAULT_MCP_OPTIONS: MCPOption[] = [
10
- 'cursor',
11
- 'vscode',
12
- 'claude-code',
13
- 'codex',
14
- 'copy',
15
- ];
7
+ export const MCP_OPTIONS = ['cursor', 'vscode', 'claude-code', 'codex', 'copy'] as const;
8
+
9
+ export const DEFAULT_MCP_OPTIONS = [...MCP_OPTIONS];
@@ -53,7 +53,7 @@ export const TOOL_CALL_DISPLAY_TEXT: Partial<
53
53
  > = {
54
54
  [ToolCallName.SearchDocumentation]: {
55
55
  inProgressText: 'Searching documentation...',
56
- completedText: 'Search completed',
56
+ completedText: 'Searched the documentation',
57
57
  },
58
58
  [ToolCallName.ListApis]: {
59
59
  inProgressText: 'Browsing APIs...',
@@ -75,6 +75,10 @@ export const TOOL_CALL_DISPLAY_TEXT: Partial<
75
75
  inProgressText: 'Reading API description...',
76
76
  completedText: 'Read API description',
77
77
  },
78
+ [ToolCallName.Execute]: {
79
+ inProgressText: 'Running code...',
80
+ completedText: 'Ran code',
81
+ },
78
82
  [ToolCallName.DescribeTools]: {
79
83
  inProgressText: 'Discovering available tools...',
80
84
  completedText: 'Discovered available tools',
@@ -23,6 +23,7 @@ export * from '../menu/use-mobile-menu-levels';
23
23
  export * from '../feedback/use-report-dialog';
24
24
  export * from '../use-product-picker';
25
25
  export * from '../search/use-search-dialog';
26
+ export * from '../search/use-search-ai-activity';
26
27
  export * from '../use-language-picker';
27
28
  export * from './use-element-size';
28
29
  export * from './use-time-ago';
@@ -20,6 +20,7 @@ export const useThemeHooks = vi.fn(() => ({
20
20
  sendPageActionsButtonClickedMessage: vi.fn(),
21
21
  sendColorModeSwitchedMessage: vi.fn(),
22
22
  sendConsentChangedMessage: vi.fn(),
23
+ sendFilterCheckboxToggledMessage: vi.fn(),
23
24
  })),
24
25
  useBreadcrumbs: vi.fn().mockReturnValue({ breadcrumbs: [], siblings: undefined }),
25
26
  useBanner: vi.fn(() => ({
@@ -21,6 +21,7 @@ export * from './menu/use-mobile-menu-levels';
21
21
  export * from './menu/use-collapse';
22
22
  export * from './feedback/use-report-dialog';
23
23
  export * from './use-tabs';
24
+ export * from './use-tab-list';
24
25
  export * from './use-focus-trap';
25
26
  export * from './use-consent-dialog';
26
27
  export * from './use-consent-telemetry';
@@ -29,6 +30,7 @@ export * from './use-product-picker';
29
30
  export * from './search/use-search-dialog';
30
31
  export * from './search/use-search-filter';
31
32
  export * from './search/use-feedback-tooltip';
33
+ export * from './search/use-search-ai-activity';
32
34
  export * from './use-controlled-state';
33
35
  export * from './use-codeblock-tabs-controls';
34
36
  export * from './code-walkthrough/use-code-walkthrough';
@@ -207,5 +207,5 @@ export function useEvent<T extends (...args: any[]) => any>(callback?: T): T {
207
207
  ref.current = callback;
208
208
  });
209
209
 
210
- return useCallback(((...args: Parameters<T>) => ref.current?.(...args)) as T, []);
210
+ return useCallback((...args: Parameters<T>) => ref.current?.(...args), []) as T;
211
211
  }
@@ -0,0 +1,31 @@
1
+ import type { SearchAiActivityStatus, SearchAiActivityStep, ToolCall } from '../../types/search';
2
+
3
+ import { useThemeHooks } from '../use-theme-hooks';
4
+ import { getSearchAiActivityStatus, getSearchAiActivityStep } from '../../utils/search-ai-activity';
5
+
6
+ type UseSearchAiActivityParams = {
7
+ toolCalls: ToolCall[];
8
+ isActive: boolean;
9
+ };
10
+
11
+ type UseSearchAiActivityReturn = {
12
+ status: SearchAiActivityStatus;
13
+ steps: SearchAiActivityStep[];
14
+ };
15
+
16
+ export function useSearchAiActivity({
17
+ toolCalls,
18
+ isActive,
19
+ }: UseSearchAiActivityParams): UseSearchAiActivityReturn {
20
+ const { useTranslate } = useThemeHooks();
21
+ const { translate } = useTranslate();
22
+
23
+ const runningToolCall = isActive ? toolCalls.find((toolCall) => !toolCall.result) : undefined;
24
+
25
+ return {
26
+ status: getSearchAiActivityStatus(toolCalls, isActive, runningToolCall, translate),
27
+ steps: toolCalls.map((toolCall) =>
28
+ getSearchAiActivityStep(toolCall, isActive && !toolCall.result, translate),
29
+ ),
30
+ };
31
+ }
@@ -7,7 +7,7 @@ import { useThemeHooks } from './use-theme-hooks';
7
7
  import { useMCPConfig } from './use-mcp-config';
8
8
  import { ClipboardService } from '../utils/clipboard-service';
9
9
  import { DEFAULT_MCP_OPTIONS } from '../constants/mcp';
10
- import { generateClaudeCodeCommand, generateCodexCommand } from '../utils/mcp';
10
+ import { generateClaudeCodeCommand, generateCodexCommand, isMCPOption } from '../utils/mcp';
11
11
  import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
12
12
 
13
13
  export type McpButtonOptions = {
@@ -80,7 +80,12 @@ export function useConnectMCPButton({
80
80
  [translate],
81
81
  );
82
82
 
83
- const visibleOptions = useMemo(() => options.filter(Boolean), [options]);
83
+ // Markdoc reports attribute errors but still renders the tag, so `options` can be any value.
84
+ const visibleOptions = useMemo(
85
+ () =>
86
+ Array.isArray(options) ? [...new Set(options.filter(isMCPOption))] : [...DEFAULT_MCP_OPTIONS],
87
+ [options],
88
+ );
84
89
 
85
90
  return {
86
91
  copiedOption,
@@ -3,9 +3,9 @@ import throttle from 'lodash.throttle';
3
3
 
4
4
  import type { RefObject } from 'react';
5
5
 
6
- export function useFullHeight(element: RefObject<HTMLElement | null>): void {
6
+ export function useFullHeight(elementRef: RefObject<HTMLElement | null>): void {
7
7
  const handler = useCallback(() => {
8
- if (!element.current) {
8
+ if (!elementRef.current) {
9
9
  return;
10
10
  }
11
11
 
@@ -14,19 +14,19 @@ export function useFullHeight(element: RefObject<HTMLElement | null>): void {
14
14
  return;
15
15
  }
16
16
 
17
- const elementTop = element.current.getBoundingClientRect().top;
17
+ const elementTop = elementRef.current.getBoundingClientRect().top;
18
18
  const footerOffset = footer.offsetTop - window.scrollY - elementTop;
19
19
  if (footerOffset > window.innerHeight) {
20
- element.current.style.maxHeight = `${window.innerHeight - elementTop}px`;
20
+ elementRef.current.style.maxHeight = `${window.innerHeight - elementTop}px`;
21
21
  } else {
22
- element.current.style.maxHeight = `${footerOffset}px`;
22
+ elementRef.current.style.maxHeight = `${footerOffset}px`;
23
23
  }
24
- }, [element]);
24
+ }, [elementRef]);
25
25
 
26
26
  const throttledHandler = useMemo(() => throttle(handler, 150), [handler]);
27
27
 
28
28
  useEffect(() => {
29
- if (typeof window === 'undefined' || !element.current) {
29
+ if (typeof window === 'undefined' || !elementRef.current) {
30
30
  return () => {
31
31
  // nope
32
32
  };
@@ -45,5 +45,5 @@ export function useFullHeight(element: RefObject<HTMLElement | null>): void {
45
45
  window.removeEventListener('scroll', throttledHandler);
46
46
  window.removeEventListener('resize', throttledHandler);
47
47
  };
48
- }, [throttledHandler, element, handler]);
48
+ }, [throttledHandler, elementRef, handler]);
49
49
  }
@@ -0,0 +1,63 @@
1
+ import { useCallback, useId, useRef } from 'react';
2
+
3
+ import type { KeyboardEvent } from 'react';
4
+
5
+ type UseTabListProps = {
6
+ count: number;
7
+ activeIndex: number;
8
+ onChange: (index: number) => void;
9
+ };
10
+
11
+ /**
12
+ * Props for a WAI-ARIA tab list with automatic activation: only the active tab is a tab stop,
13
+ * and Left/Right/Home/End select and focus another tab.
14
+ */
15
+ export function useTabList({ count, activeIndex, onChange }: UseTabListProps) {
16
+ const idPrefix = useId();
17
+ const focusableIndex = activeIndex >= 0 ? activeIndex : 0;
18
+ const tabRefs = useRef<(HTMLElement | null)[]>([]);
19
+
20
+ const select = useCallback(
21
+ (index: number) => {
22
+ onChange(index);
23
+ tabRefs.current[index]?.focus();
24
+ },
25
+ [onChange],
26
+ );
27
+
28
+ const onKeyDown = useCallback(
29
+ (event: KeyboardEvent, index: number) => {
30
+ const target = (<Record<string, number>>{
31
+ ArrowRight: (index + 1) % count,
32
+ ArrowLeft: (index - 1 + count) % count,
33
+ Home: 0,
34
+ End: count - 1,
35
+ })[event.key];
36
+ if (target === undefined) return;
37
+ event.preventDefault();
38
+ select(target);
39
+ },
40
+ [count, select],
41
+ );
42
+
43
+ const getTabProps = (index: number) => ({
44
+ id: `${idPrefix}-tab-${index}`,
45
+ role: 'tab',
46
+ 'aria-selected': index === activeIndex,
47
+ 'aria-controls': `${idPrefix}-panel-${index}`,
48
+ tabIndex: index === focusableIndex ? 0 : -1,
49
+ ref: (element: HTMLElement | null) => {
50
+ tabRefs.current[index] = element;
51
+ },
52
+ onClick: () => select(index),
53
+ onKeyDown: (event: KeyboardEvent) => onKeyDown(event, index),
54
+ });
55
+
56
+ const getTabPanelProps = (index: number) => ({
57
+ id: `${idPrefix}-panel-${index}`,
58
+ role: 'tabpanel',
59
+ 'aria-labelledby': `${idPrefix}-tab-${index}`,
60
+ });
61
+
62
+ return { tabListProps: { role: 'tablist' }, getTabProps, getTabPanelProps };
63
+ }
@@ -1330,83 +1330,88 @@ const replay = css`
1330
1330
  `;
1331
1331
 
1332
1332
 
1333
+ /** The light theme tokens without a selector, for a host that sets them on one element. */
1334
+ export const tokens = css`
1335
+ ${accordion}
1336
+ ${admonition}
1337
+ ${apiReferenceDocs}
1338
+ ${apiReferencePanels}
1339
+ ${badges}
1340
+ ${banner}
1341
+ ${borders}
1342
+ ${consent}
1343
+ ${breadcrumbs}
1344
+ ${button}
1345
+ ${aiAssistantButton}
1346
+ ${cards}
1347
+ ${catalog}
1348
+ ${catalogClassic}
1349
+ ${code}
1350
+ ${codeWalkthrough}
1351
+ ${docsDropdown}
1352
+ ${dropdown}
1353
+ ${error}
1354
+ ${filter}
1355
+ ${footer}
1356
+ ${headingsTypography}
1357
+ ${httpTag}
1358
+ ${image}
1359
+ ${inputs}
1360
+ ${languagePicker}
1361
+ ${lastUpdated}
1362
+ ${links}
1363
+ ${loadProgressBar}
1364
+ ${logo}
1365
+ ${markdown}
1366
+ ${markdownAccordion}
1367
+ ${markdownTabs}
1368
+ ${markdownNumberedList}
1369
+ ${diagram}
1370
+ ${menu}
1371
+ ${mobileMenu}
1372
+ ${modal}
1373
+ ${navbar}
1374
+ ${pages}
1375
+ ${productPicker}
1376
+ ${responsePanelColors}
1377
+ ${search}
1378
+ ${navigationTabs}
1379
+ ${select}
1380
+ ${sidebar}
1381
+ ${sizeAndSpace}
1382
+ ${tag}
1383
+ ${themeColors}
1384
+ ${activeBrandPaletteLight}
1385
+ ${tile}
1386
+ ${toc}
1387
+ ${tooltip}
1388
+ ${typography}
1389
+ ${userMenu}
1390
+ ${versionPicker}
1391
+ ${zIndexDepth}
1392
+ ${scorecardColors}
1393
+ ${statusCode}
1394
+ ${tab}
1395
+ ${icon}
1396
+ ${tree}
1397
+ ${segmented}
1398
+ ${switcher}
1399
+ ${checkbox}
1400
+ ${feedback}
1401
+ ${scorecard}
1402
+ ${datePicker}
1403
+ ${replay}
1404
+ ${skipContent}
1405
+ ${pageActions}
1406
+ ${svgViewer}
1407
+ ${toast}
1408
+ ${pageNavigation}
1409
+
1410
+ `;
1411
+
1333
1412
  export const styles = css<{ palette?: string }>`
1334
1413
  :root {
1335
- ${accordion}
1336
- ${admonition}
1337
- ${apiReferenceDocs}
1338
- ${apiReferencePanels}
1339
- ${badges}
1340
- ${banner}
1341
- ${borders}
1342
- ${consent}
1343
- ${breadcrumbs}
1344
- ${button}
1345
- ${aiAssistantButton}
1346
- ${cards}
1347
- ${catalog}
1348
- ${catalogClassic}
1349
- ${code}
1350
- ${codeWalkthrough}
1351
- ${docsDropdown}
1352
- ${dropdown}
1353
- ${error}
1354
- ${filter}
1355
- ${footer}
1356
- ${headingsTypography}
1357
- ${httpTag}
1358
- ${image}
1359
- ${inputs}
1360
- ${languagePicker}
1361
- ${lastUpdated}
1362
- ${links}
1363
- ${loadProgressBar}
1364
- ${logo}
1365
- ${markdown}
1366
- ${markdownAccordion}
1367
- ${markdownTabs}
1368
- ${markdownNumberedList}
1369
- ${diagram}
1370
- ${menu}
1371
- ${mobileMenu}
1372
- ${modal}
1373
- ${navbar}
1374
- ${pages}
1375
- ${productPicker}
1376
- ${responsePanelColors}
1377
- ${search}
1378
- ${navigationTabs}
1379
- ${select}
1380
- ${sidebar}
1381
- ${sizeAndSpace}
1382
- ${tag}
1383
- ${themeColors}
1384
- ${activeBrandPaletteLight}
1385
- ${tile}
1386
- ${toc}
1387
- ${tooltip}
1388
- ${typography}
1389
- ${userMenu}
1390
- ${versionPicker}
1391
- ${zIndexDepth}
1392
- ${scorecardColors}
1393
- ${statusCode}
1394
- ${tab}
1395
- ${icon}
1396
- ${tree}
1397
- ${segmented}
1398
- ${switcher}
1399
- ${checkbox}
1400
- ${feedback}
1401
- ${scorecard}
1402
- ${datePicker}
1403
- ${replay}
1404
- ${skipContent}
1405
- ${pageActions}
1406
- ${svgViewer}
1407
- ${toast}
1408
- ${pageNavigation}
1409
-
1414
+ ${tokens}
1410
1415
  background-color: var(--bg-color);
1411
1416
  color: var(--text-color-primary);
1412
1417
  font-family: var(--font-family-base);
@@ -1,3 +1,4 @@
1
1
  export * from './global';
2
2
  export * from './dark';
3
- export { xyflow } from './xyflow';
3
+ export { xyflow } from './xyflow';
4
+ export { searchAiAppearAnimation } from './search-ai-appear';
@@ -0,0 +1,16 @@
1
+ import { css, keyframes } from 'styled-components';
2
+
3
+ const appear = keyframes`
4
+ from {
5
+ opacity: 0;
6
+ transform: translateY(4px);
7
+ }
8
+ `;
9
+
10
+ export const searchAiAppearAnimation = css`
11
+ animation: ${appear} var(--search-ai-activity-fade-duration) ease-out;
12
+
13
+ @media (prefers-reduced-motion: reduce) {
14
+ animation: none;
15
+ }
16
+ `;
@@ -0,0 +1 @@
1
+ export type DropdownAlignment = 'start' | 'end';
@@ -25,3 +25,4 @@ export type * from './tooltip';
25
25
  export type * from './toast';
26
26
  export type * from './link';
27
27
  export type * from './button-group';
28
+ export type * from './dropdown';
@@ -113,7 +113,7 @@ export type TranslationKey =
113
113
  | 'search.ai.feedback.thanks'
114
114
  | 'search.ai.toolCall.executed'
115
115
  | 'search.ai.toolCall.executing'
116
- | 'search.ai.toolCall.withArgs'
116
+ | 'search.ai.toolCall.usedTools'
117
117
  | 'search.ai.button'
118
118
  | 'search.ai.label'
119
119
  | 'search.ai.disclaimer'
@@ -1,6 +1,8 @@
1
+ import type { MCP_OPTIONS } from '../constants/mcp';
2
+
1
3
  export type MCPClientType = 'cursor' | 'vscode';
2
4
 
3
- export type MCPOption = 'cursor' | 'vscode' | 'claude-code' | 'codex' | 'copy';
5
+ export type MCPOption = (typeof MCP_OPTIONS)[number];
4
6
 
5
7
  export type McpConnectionParams = {
6
8
  serverName: string;