@redocly/theme 0.67.0-next.4 → 0.67.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 (86) hide show
  1. package/lib/components/Breadcrumbs/BreadcrumbDropdown.js +2 -6
  2. package/lib/components/Breadcrumbs/Breadcrumbs.js +3 -18
  3. package/lib/components/Buttons/AIAssistantButton.js +2 -6
  4. package/lib/components/Buttons/EditPageButton.js +2 -1
  5. package/lib/components/Catalog/CatalogCardView/CatalogCard.js +1 -1
  6. package/lib/components/CatalogClassic/CatalogClassicActions.js +3 -1
  7. package/lib/components/CatalogClassic/CatalogClassicCard.js +1 -1
  8. package/lib/components/CatalogClassic/CatalogClassicInfoBlock.js +2 -1
  9. package/lib/components/Feedback/ReportDialog.js +4 -1
  10. package/lib/components/Filter/FilterCheckboxes.js +3 -1
  11. package/lib/components/Footer/FooterItem.js +1 -1
  12. package/lib/components/LanguagePicker/LanguagePicker.js +3 -1
  13. package/lib/components/Logo/Logo.js +2 -1
  14. package/lib/components/Menu/MenuContainer.js +1 -0
  15. package/lib/components/Menu/MenuItem.js +21 -7
  16. package/lib/components/Navbar/Navbar.js +6 -2
  17. package/lib/components/Navbar/NavbarItem.js +3 -1
  18. package/lib/components/Search/SearchDialog.js +5 -33
  19. package/lib/components/Search/SearchInput.js +4 -1
  20. package/lib/components/Search/SearchRecent.js +3 -1
  21. package/lib/components/SidebarActions/SidebarActions.js +10 -3
  22. package/lib/components/TableOfContent/TableOfContent.js +1 -1
  23. package/lib/components/UserMenu/LoginButton.js +2 -1
  24. package/lib/components/UserMenu/LogoutMenuItem.js +2 -1
  25. package/lib/core/constants/search.js +4 -0
  26. package/lib/core/hooks/search/use-search-dialog.js +7 -2
  27. package/lib/core/hooks/use-banner-telemetry.js +2 -11
  28. package/lib/core/hooks/use-color-switcher.js +2 -1
  29. package/lib/core/hooks/use-page-actions.js +29 -36
  30. package/lib/core/hooks/use-product-picker.js +14 -4
  31. package/lib/core/hooks/use-telemetry-fallback.d.ts +45 -37
  32. package/lib/core/hooks/use-telemetry-fallback.js +47 -39
  33. package/lib/core/styles/global.js +33 -0
  34. package/lib/core/types/l10n.d.ts +1 -1
  35. package/lib/core/types/search.d.ts +2 -1
  36. package/lib/core/types/search.js +1 -0
  37. package/lib/core/utils/index.d.ts +2 -0
  38. package/lib/core/utils/index.js +2 -0
  39. package/lib/core/utils/telemetry/generate-before-after-context.d.ts +37 -0
  40. package/lib/core/utils/telemetry/generate-before-after-context.js +32 -0
  41. package/lib/core/utils/telemetry/generate-resource-urn.d.ts +7 -0
  42. package/lib/core/utils/telemetry/generate-resource-urn.js +13 -0
  43. package/lib/core/utils/telemetry/get-base-data-attributes.d.ts +14 -0
  44. package/lib/core/utils/telemetry/get-base-data-attributes.js +17 -0
  45. package/lib/layouts/McpNotAllowed.d.ts +2 -0
  46. package/lib/layouts/McpNotAllowed.js +67 -0
  47. package/package.json +3 -3
  48. package/src/components/Breadcrumbs/BreadcrumbDropdown.tsx +2 -1
  49. package/src/components/Breadcrumbs/Breadcrumbs.tsx +4 -4
  50. package/src/components/Buttons/AIAssistantButton.tsx +2 -3
  51. package/src/components/Buttons/EditPageButton.tsx +4 -1
  52. package/src/components/Catalog/CatalogCardView/CatalogCard.tsx +2 -2
  53. package/src/components/CatalogClassic/CatalogClassicActions.tsx +4 -2
  54. package/src/components/CatalogClassic/CatalogClassicCard.tsx +9 -2
  55. package/src/components/CatalogClassic/CatalogClassicInfoBlock.tsx +2 -1
  56. package/src/components/Feedback/ReportDialog.tsx +4 -1
  57. package/src/components/Filter/FilterCheckboxes.tsx +4 -2
  58. package/src/components/Footer/FooterItem.tsx +4 -2
  59. package/src/components/LanguagePicker/LanguagePicker.tsx +9 -2
  60. package/src/components/Logo/Logo.tsx +7 -1
  61. package/src/components/Menu/MenuContainer.tsx +1 -0
  62. package/src/components/Menu/MenuItem.tsx +35 -4
  63. package/src/components/Navbar/Navbar.tsx +7 -3
  64. package/src/components/Navbar/NavbarItem.tsx +7 -1
  65. package/src/components/Search/SearchDialog.tsx +6 -13
  66. package/src/components/Search/SearchInput.tsx +4 -1
  67. package/src/components/Search/SearchRecent.tsx +4 -2
  68. package/src/components/SidebarActions/SidebarActions.tsx +10 -3
  69. package/src/components/TableOfContent/TableOfContent.tsx +7 -2
  70. package/src/components/UserMenu/LoginButton.tsx +4 -1
  71. package/src/components/UserMenu/LogoutMenuItem.tsx +2 -1
  72. package/src/core/constants/search.ts +4 -0
  73. package/src/core/hooks/search/use-search-dialog.ts +13 -2
  74. package/src/core/hooks/use-banner-telemetry.ts +5 -4
  75. package/src/core/hooks/use-color-switcher.ts +7 -1
  76. package/src/core/hooks/use-page-actions.ts +38 -44
  77. package/src/core/hooks/use-product-picker.ts +19 -5
  78. package/src/core/hooks/use-telemetry-fallback.ts +47 -39
  79. package/src/core/styles/global.ts +33 -0
  80. package/src/core/types/l10n.ts +2 -0
  81. package/src/core/types/search.ts +1 -0
  82. package/src/core/utils/index.ts +2 -0
  83. package/src/core/utils/telemetry/generate-before-after-context.ts +59 -0
  84. package/src/core/utils/telemetry/generate-resource-urn.ts +9 -0
  85. package/src/core/utils/telemetry/get-base-data-attributes.ts +27 -0
  86. package/src/layouts/McpNotAllowed.tsx +81 -0
@@ -3,7 +3,7 @@ import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
5
 
6
- import { breakpoints } from '@redocly/theme/core/utils';
6
+ import { breakpoints, getBaseDataAttributes } from '@redocly/theme/core/utils';
7
7
  import { useThemeHooks, useRecentSearches } from '@redocly/theme/core/hooks';
8
8
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
9
9
  import { RecentlyViewedIcon } from '@redocly/theme/icons/RecentlyViewedIcon/RecentlyViewedIcon';
@@ -25,7 +25,9 @@ export function SearchRecent({ onSelect, className }: SearchRecentProps): JSX.El
25
25
  const handleOnRemove = (e: React.MouseEvent<SVGSVGElement, MouseEvent>, item: string) => {
26
26
  e.stopPropagation();
27
27
  removeSearchHistoryItem(item);
28
- telemetry.sendSearchRecentRemoveButtonClickedMessage();
28
+ telemetry.sendSearchRecentRemoveClickedMessage([
29
+ getBaseDataAttributes('searchRecentRemove', 'search'),
30
+ ]);
29
31
  };
30
32
 
31
33
  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, item: string, index: number) => {
@@ -11,6 +11,7 @@ import {
11
11
  ControlsWrapChangeLayoutButtons,
12
12
  } from '@redocly/theme/components/SidebarActions/styled';
13
13
  import { Tooltip } from '@redocly/theme/components/Tooltip/Tooltip';
14
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
14
15
 
15
16
  export { LayoutVariant };
16
17
 
@@ -57,9 +58,13 @@ export const SidebarActions = ({
57
58
  onClick={() => {
58
59
  onChangeCollapseSidebarClick();
59
60
  if (collapsedSidebar) {
60
- telemetry.sendSidebarItemExpandedMessage();
61
+ telemetry.sendSidebarExpandedMessage([
62
+ getBaseDataAttributes('sidebarExpand', 'button'),
63
+ ]);
61
64
  } else {
62
- telemetry.sendSidebarItemCollapsedMessage();
65
+ telemetry.sendSidebarCollapsedMessage([
66
+ getBaseDataAttributes('sidebarCollapse', 'button'),
67
+ ]);
63
68
  }
64
69
  }}
65
70
  size="small"
@@ -76,7 +81,9 @@ export const SidebarActions = ({
76
81
  layout={layout}
77
82
  onClick={() => {
78
83
  onChangeViewClick();
79
- telemetry.sendChangeLayoutButtonClickedMessage();
84
+ telemetry.sendChangeLayoutClickedMessage([
85
+ getBaseDataAttributes('changeLayout', 'button'),
86
+ ]);
80
87
  }}
81
88
  />
82
89
  </ControlsWrapChangeLayoutButtons>
@@ -10,7 +10,12 @@ import {
10
10
  useThemeConfig,
11
11
  useFullHeight,
12
12
  } from '@redocly/theme/core/hooks';
13
- import { breakpoints, getDisplayedHeadings, getLeastDepth } from '@redocly/theme/core/utils';
13
+ import {
14
+ breakpoints,
15
+ getDisplayedHeadings,
16
+ getLeastDepth,
17
+ getBaseDataAttributes,
18
+ } from '@redocly/theme/core/utils';
14
19
 
15
20
  export type TableOfContentProps = {
16
21
  headings?: Array<MdHeading | null> | null | undefined;
@@ -62,7 +67,7 @@ export function TableOfContent(props: TableOfContentProps): JSX.Element | null {
62
67
  data-testid={`toc-${heading.value}`}
63
68
  onClick={(e) => {
64
69
  e.preventDefault();
65
- telemetry.sendTocItemClickedMessage();
70
+ telemetry.sendTocItemClickedMessage([getBaseDataAttributes('tocItem', 'toc')]);
66
71
  handleHeadingClick(heading.id);
67
72
  }}
68
73
  />
@@ -4,6 +4,7 @@ import type { ButtonVariant, ButtonSize } from '@redocly/theme/components/Button
4
4
 
5
5
  import { useThemeHooks } from '@redocly/theme/core/hooks';
6
6
  import { Button } from '@redocly/theme/components/Button/Button';
7
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
7
8
 
8
9
  export type LoginButtonProps = {
9
10
  href: string;
@@ -36,7 +37,9 @@ export function LoginButton({
36
37
  data-translation-key={label ? undefined : labelTranslationKey}
37
38
  to={href}
38
39
  languageInsensitive
39
- onClick={() => telemetry.sendLoginButtonClickedMessage()}
40
+ onClick={() =>
41
+ telemetry.sendLoginClickedMessage([getBaseDataAttributes('login', 'button')])
42
+ }
40
43
  data-testid="login-btn"
41
44
  extraClass={className}
42
45
  variant={variant}
@@ -5,6 +5,7 @@ import type { JSX } from 'react';
5
5
  import { useThemeHooks } from '@redocly/theme/core/hooks';
6
6
  import { LogoutIcon } from '@redocly/theme/icons/LogoutIcon/LogoutIcon';
7
7
  import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
8
+ import { getBaseDataAttributes } from '@redocly/theme/core/utils';
8
9
 
9
10
  export type LogoutMenuItemProps = {
10
11
  iconOnly?: boolean;
@@ -18,7 +19,7 @@ export function LogoutMenuItem({ iconOnly, className }: LogoutMenuItemProps): JS
18
19
  const { translate } = useTranslate();
19
20
 
20
21
  const handleClick = () => {
21
- telemetry.sendLogoutMenuItemClickedMessage();
22
+ telemetry.sendLogoutClickedMessage([getBaseDataAttributes('logoutItem', 'userMenu')]);
22
23
  handleLogout();
23
24
  };
24
25
 
@@ -73,4 +73,8 @@ export const TOOL_CALL_DISPLAY_TEXT: Partial<
73
73
  inProgressText: 'Reading API description...',
74
74
  completedText: 'Read API description',
75
75
  },
76
+ [ToolCallName.DescribeTools]: {
77
+ inProgressText: 'Discovering available tools...',
78
+ completedText: 'Discovered available tools',
79
+ },
76
80
  };
@@ -5,6 +5,7 @@ import hotkeys from 'hotkeys-js';
5
5
  import { useThemeHooks } from '../use-theme-hooks';
6
6
  import { useThemeConfig } from '../use-theme-config';
7
7
  import { useSearchSession } from '../../contexts';
8
+ import { getBaseDataAttributes } from '../../utils';
8
9
 
9
10
  export function useSearchDialog() {
10
11
  const [isOpen, setIsOpen] = useState(false);
@@ -20,7 +21,12 @@ export function useSearchDialog() {
20
21
  if (hotKeys) {
21
22
  hotkeys(hotKeys, (ev) => {
22
23
  setIsOpen(true);
23
- telemetry.sendSearchOpenedMessage([{ object: 'search', method: 'shortcut' }]);
24
+ telemetry.sendSearchOpenedMessage([
25
+ {
26
+ ...getBaseDataAttributes('searchDialogId', 'search'),
27
+ method: 'shortcut',
28
+ },
29
+ ]);
24
30
  ev.preventDefault();
25
31
  });
26
32
 
@@ -30,7 +36,12 @@ export function useSearchDialog() {
30
36
  }, [hotKeys]);
31
37
 
32
38
  const onOpen = useCallback(function () {
33
- telemetry.sendSearchOpenedMessage([{ object: 'search', method: 'click' }]);
39
+ telemetry.sendSearchOpenedMessage([
40
+ {
41
+ ...getBaseDataAttributes('searchDialogId', 'search'),
42
+ method: 'click',
43
+ },
44
+ ]);
34
45
  setIsOpen(true);
35
46
  // eslint-disable-next-line react-hooks/exhaustive-deps
36
47
  }, []);
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
3
3
  import type { DisplayBanner } from '@redocly/theme/components/Banner/Banner';
4
4
 
5
5
  import { useThemeHooks } from './use-theme-hooks';
6
+ import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
6
7
 
7
8
  const noop = (): void => {};
8
9
  const noopLink = (_href: string): void => {};
@@ -28,11 +29,11 @@ export function useBannerTelemetry(
28
29
  };
29
30
  }
30
31
 
31
- const bannerUri = 'urn:redocly:realm:ui:banner:banner-id';
32
32
  const payload = {
33
- id: 'banner-id' as const,
34
- object: 'banner' as const,
35
- uri: bannerUri,
33
+ ...getBaseDataAttributes<'bannerId', 'banner', 'urn:redocly:realm:ui:banner:bannerId'>(
34
+ 'bannerId',
35
+ 'banner',
36
+ ),
36
37
  trackingId: displayBanner.trackingId,
37
38
  hash: displayBanner.hash,
38
39
  color: displayBanner.color,
@@ -6,6 +6,7 @@ import { useThemeConfig } from './use-theme-config';
6
6
  import { useThemeHooks } from './use-theme-hooks';
7
7
  import { createStore, useStore } from './use-store';
8
8
  import { DEFAULT_COLOR_MODES } from '../constants';
9
+ import { generateBeforeAfterContext } from '../utils';
9
10
 
10
11
  const COLOR_MODE_KEY = 'colorSchema';
11
12
  const colorModeStore = createStore<string>({
@@ -51,7 +52,12 @@ export const useColorSwitcher = () => {
51
52
  root.classList.remove('notransition');
52
53
  });
53
54
  telemetry.sendColorModeSwitchedMessage([
54
- { object: 'color_mode', from: activeColorMode, to: newMode },
55
+ ...generateBeforeAfterContext<'sendColorModeSwitchedMessage'>(
56
+ 'colorMode',
57
+ 'button',
58
+ { mode: activeColorMode },
59
+ { mode: newMode },
60
+ ),
55
61
  ]);
56
62
  setActiveColorMode(newMode);
57
63
  };
@@ -22,14 +22,7 @@ import {
22
22
  removeTrailingSlash,
23
23
  withoutPathPrefix,
24
24
  } from '../utils/urls';
25
-
26
- function createPageActionResource(pageSlug: string, pageUrl: string) {
27
- return {
28
- id: pageSlug,
29
- object: 'page' as const,
30
- uri: pageUrl,
31
- };
32
- }
25
+ import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
33
26
 
34
27
  const DEFAULT_ENABLED_ACTIONS = [
35
28
  'copy',
@@ -47,6 +40,7 @@ export type PageActionType =
47
40
  | 'claude'
48
41
  | 'docs-mcp-cursor'
49
42
  | 'docs-mcp-vscode'
43
+ // Used in OpenAPI docs x-mcp
50
44
  | 'mcp-cursor'
51
45
  | 'mcp-vscode';
52
46
 
@@ -78,25 +72,22 @@ export function usePageActions(
78
72
 
79
73
  const isDocsMcp = !requiresMcpUrl;
80
74
 
81
- const origin = IS_BROWSER ? window.location.origin : '';
82
- const pageUrl = `${origin}${pageSlug}`;
83
-
84
75
  return createMCPAction({
85
76
  clientType,
86
77
  mcpConfig: config,
87
78
  translate,
88
79
  onClickCallback: isDocsMcp
89
80
  ? () =>
90
- telemetry.sendPageActionsButtonClickedMessage([
81
+ telemetry.sendPageActionsClickedMessage([
91
82
  {
92
- ...createPageActionResource(pageSlug, pageUrl),
93
- action_type: `docs-mcp-${clientType}` as const,
83
+ ...getBaseDataAttributes('pageActions', 'button'),
84
+ actionType: `docs-mcp-${clientType}` as const,
94
85
  },
95
86
  ])
96
87
  : undefined,
97
88
  });
98
89
  },
99
- [mcpUrl, mcpConfig, translate, telemetry, pageSlug],
90
+ [mcpUrl, mcpConfig, translate, telemetry],
100
91
  );
101
92
 
102
93
  const result: PageAction[] = useMemo(() => {
@@ -113,7 +104,6 @@ export function usePageActions(
113
104
  ? window.location.origin
114
105
  : ((globalThis as { SSR_HOSTNAME?: string })['SSR_HOSTNAME'] ?? '');
115
106
  const pathname = addTrailingSlash(pageSlug);
116
- const pageUrl = combineUrls(origin, pathname);
117
107
  const isRoot = withoutPathPrefix(pathname) === '/';
118
108
  const mdPageUrl = isRoot
119
109
  ? combineUrls(origin, pathname, 'index.html.md')
@@ -138,10 +128,10 @@ export function usePageActions(
138
128
  }
139
129
  const text = await result.text();
140
130
  ClipboardService.copyCustom(text);
141
- telemetry.sendPageActionsButtonClickedMessage([
131
+ telemetry.sendPageActionsClickedMessage([
142
132
  {
143
- ...createPageActionResource(pageSlug, pageUrl),
144
- action_type: 'copy',
133
+ ...getBaseDataAttributes('pageActions', 'button'),
134
+ actionType: 'copy',
145
135
  },
146
136
  ]);
147
137
  } catch (error) {
@@ -157,10 +147,10 @@ export function usePageActions(
157
147
  iconComponent: MarkdownFullIcon,
158
148
  link: mdPageUrl,
159
149
  onClick: () => {
160
- telemetry.sendPageActionsButtonClickedMessage([
150
+ telemetry.sendPageActionsClickedMessage([
161
151
  {
162
- ...createPageActionResource(pageSlug, pageUrl),
163
- action_type: 'view',
152
+ ...getBaseDataAttributes('pageActions', 'button'),
153
+ actionType: 'view',
164
154
  },
165
155
  ]);
166
156
  },
@@ -175,10 +165,10 @@ export function usePageActions(
175
165
  iconComponent: ChatGptIcon,
176
166
  link,
177
167
  onClick: () => {
178
- telemetry.sendPageActionsButtonClickedMessage([
168
+ telemetry.sendPageActionsClickedMessage([
179
169
  {
180
- ...createPageActionResource(pageSlug, pageUrl),
181
- action_type: 'chatgpt',
170
+ ...getBaseDataAttributes('pageActions', 'button'),
171
+ actionType: 'chatgpt',
182
172
  },
183
173
  ]);
184
174
  window.location.href = link;
@@ -195,10 +185,10 @@ export function usePageActions(
195
185
  iconComponent: ClaudeIcon,
196
186
  link,
197
187
  onClick: () => {
198
- telemetry.sendPageActionsButtonClickedMessage([
188
+ telemetry.sendPageActionsClickedMessage([
199
189
  {
200
- ...createPageActionResource(pageSlug, pageUrl),
201
- action_type: 'claude',
190
+ ...getBaseDataAttributes('pageActions', 'button'),
191
+ actionType: 'claude',
202
192
  },
203
193
  ]);
204
194
  window.location.href = link;
@@ -300,28 +290,32 @@ function shouldHideAction(
300
290
  isMcpDisabled,
301
291
  }: ShouldHideActionContext,
302
292
  ): boolean {
303
- // Can't use any actions if search is globally disabled (markdown files are not generated)
304
- if (themeConfig.search?.hide) {
305
- return true;
306
- }
307
- // Can't use any actions if no markdown files are generated for LLMs
308
- if (pageProps?.seo?.llmstxt?.hide) {
309
- return true;
310
- }
311
293
  // Page actions are explicitly disabled in config
312
294
  if (themeConfig.navigation?.actions?.hide) {
313
295
  return true;
314
296
  }
315
297
 
316
- const isOpenApiPage =
317
- pageProps?.metadata?.type === 'openapi' || pageProps?.metadata?.subType === 'openapi-operation';
318
- const isPageExcludedFromSearch =
319
- pageProps?.frontmatter?.excludeFromSearch || (isOpenApiPage && openapiExcludeFromSearch);
298
+ const actionDependsOnLlmsTxt = ['view', 'copy', 'chatgpt', 'claude'].includes(action);
320
299
 
321
- // Page excluded from search: only explicit MCP connect actions remain visible,
322
- // since they don't depend on the page's markdown.
323
- if (isPageExcludedFromSearch && action !== 'mcp-cursor' && action !== 'mcp-vscode') {
324
- return true;
300
+ if (actionDependsOnLlmsTxt) {
301
+ // Can't use any actions if search is globally disabled (markdown files are not generated)
302
+ if (themeConfig.search?.hide) {
303
+ return true;
304
+ }
305
+ // Can't use any actions if no markdown files are generated for LLMs
306
+ if (pageProps?.seo?.llmstxt?.hide) {
307
+ return true;
308
+ }
309
+
310
+ const isOpenApiPage =
311
+ pageProps?.metadata?.type === 'openapi' ||
312
+ pageProps?.metadata?.subType === 'openapi-operation';
313
+ const isPageExcludedFromSearch =
314
+ pageProps?.frontmatter?.excludeFromSearch || (isOpenApiPage && openapiExcludeFromSearch);
315
+
316
+ if (isPageExcludedFromSearch) {
317
+ return true;
318
+ }
325
319
  }
326
320
 
327
321
  return shouldHideActionByType(action, { isPublic, mcpUrl, isMcpDisabled });
@@ -1,9 +1,10 @@
1
+ import { useRef } from 'react';
1
2
  import { useNavigate } from 'react-router-dom';
2
3
 
3
4
  import { getProductClassName } from '@redocly/theme/components/Product/utils';
4
5
 
5
6
  import { useThemeHooks } from './use-theme-hooks';
6
- import { withPathPrefix } from '../utils';
7
+ import { withPathPrefix, generateBeforeAfterContext } from '../utils';
7
8
 
8
9
  export function useProductPicker() {
9
10
  const { useCurrentProduct, useProducts, useTelemetry, useLoadAndNavigate } = useThemeHooks();
@@ -12,20 +13,33 @@ export function useProductPicker() {
12
13
  const telemetry = useTelemetry();
13
14
  const navigate = useNavigate();
14
15
  const loadAndNavigate = useLoadAndNavigate();
16
+ // An open dropdown can fire a menu item's onAction from an earlier render, so `product.link`
17
+ // may be stale (e.g. after a locale switch); resolve the live product via a ref.
18
+ const productsRef = useRef(products);
19
+ productsRef.current = products;
15
20
  function setProduct(product: typeof currentProduct) {
16
21
  if (!product) return;
17
- telemetry.sendProductPickedMessage([{ object: 'product', product: product.slug }]);
22
+ // match on `name` (locale-stable; link/slug are re-localized)
23
+ const freshProduct = productsRef.current.find((p) => p.name === product.name) ?? product;
24
+ telemetry.sendProductPickerChangedMessage([
25
+ ...generateBeforeAfterContext<'sendProductPickerChangedMessage'>(
26
+ 'productPicker',
27
+ 'dropdown',
28
+ { product: currentProduct?.slug || '' },
29
+ { product: freshProduct.slug },
30
+ ),
31
+ ]);
18
32
  if (typeof document === 'undefined') return;
19
33
 
20
- if (product.name) {
34
+ if (freshProduct.name) {
21
35
  const root = document.documentElement;
22
36
  Array.from(root.classList)
23
37
  .filter((c) => c.startsWith('product-'))
24
38
  .forEach((c) => root.classList.remove(c));
25
- root.classList.add(getProductClassName(product.name));
39
+ root.classList.add(getProductClassName(freshProduct.name));
26
40
  }
27
41
 
28
- loadAndNavigate({ navigate, to: withPathPrefix(product.link) });
42
+ loadAndNavigate({ navigate, to: withPathPrefix(freshProduct.link) });
29
43
  }
30
44
  return {
31
45
  currentProduct,
@@ -1,50 +1,61 @@
1
- // eslint-disable-next-line no-warning-comments
2
- // TODO we need to create some common way to do this.
1
+ // Fallback no-op telemetry used by `useThemeHooks` when no ThemeDataContext
2
+ // provider supplies real hooks.
3
3
  export const useTelemetryFallback = () => ({
4
4
  send: () => {},
5
- sendPageViewedMessage: () => {},
6
- sendPageActionsButtonClickedMessage: () => {},
7
5
  sendErrorMessage: () => {},
8
6
  sendClientErrorMessage: () => {},
7
+ sendCatalogEntitiesFilterCheckboxToggledMessage: () => {},
8
+ sendCatalogEntitiesCopyDataSchemaClickedMessage: () => {},
9
+ sendCatalogEntitiesViewModeChangedMessage: () => {},
10
+ sendCatalogEntitiesRelatedEntitiesListSearchQueryMessage: () => {},
11
+ sendCatalogEntitiesRelatedEntitiesListSearchResultClickMessage: () => {},
12
+ sendCatalogEntitiesListSearchQueryMessage: () => {},
13
+ sendCatalogEntitiesListSearchResultClickedMessage: () => {},
14
+ sendRedirectMessage: () => {},
15
+ sendCustomMessage: () => {},
16
+ sendSearchAIQueryMessage: () => {},
17
+ sendSearchAiOpenedMessage: () => {},
18
+ sendSearchAIFeedbackMessage: () => {},
19
+ sendLoginClickedMessage: () => {},
20
+ sendLoginProviderClickedMessage: () => {},
21
+ sendLogoutClickedMessage: () => {},
22
+ sendBannerViewedMessage: () => {},
23
+ sendBannerLinkClickedMessage: () => {},
24
+ sendBannerDismissedMessage: () => {},
9
25
  sendBreadcrumbClickedMessage: () => {},
10
- sendColorModeSwitchedMessage: () => {},
11
- sendSidebarItemClickedMessage: () => {},
12
- sendSidebarItemExpandedMessage: () => {},
13
- sendSidebarItemCollapsedMessage: () => {},
14
- sendChangeLayoutButtonClickedMessage: () => {},
15
- sendEditPageLinkClickedMessage: () => {},
16
- sendCodeSnippetReportedMessage: () => {},
17
- sendNavbarMenuItemClickedMessage: () => {},
18
- sendLoginButtonClickedMessage: () => {},
19
- sendLoginProviderButtonClickedMessage: () => {},
20
- sendLogoutMenuItemClickedMessage: () => {},
21
- sendLogoClickedMessage: () => {},
22
- sendTocItemClickedMessage: () => {},
26
+ sendCatalogActionsClickedMessage: () => {},
23
27
  sendCatalogFilterChangedMessage: () => {},
24
28
  sendCatalogItemClickedMessage: () => {},
25
29
  sendScorecardLinkClickedMessage: () => {},
26
- sendBackToCatalogButtonClickedMessage: () => {},
27
- sendSidebarDrilldownBackButtonClickedMessage: () => {},
30
+ sendFilterCheckboxToggledMessage: () => {},
31
+ sendFeedbackMessage: () => {},
28
32
  sendFooterItemClickedMessage: () => {},
29
- sendCatalogActionsButtonClickedMessage: () => {},
30
- sendMobileMenuButtonCloseClickedMessage: () => {},
31
- sendMobileMenuButtonOpenClickedMessage: () => {},
32
- sendSearchInputResetButtonClickedMessage: () => {},
33
- sendSearchRecentRemoveButtonClickedMessage: () => {},
33
+ sendNavbarMenuItemClickedMessage: () => {},
34
+ sendColorModeSwitchedMessage: () => {},
35
+ sendLanguagePickerChangedMessage: () => {},
36
+ sendProductPickerChangedMessage: () => {},
37
+ sendLogoClickedMessage: () => {},
38
+ sendCodeSnippetReportedMessage: () => {},
39
+ sendMobileMenuClosedMessage: () => {},
40
+ sendMobileMenuOpenedMessage: () => {},
41
+ sendPageViewedMessage: () => {},
42
+ sendPageTimeMessage: () => {},
43
+ sendPageActionsClickedMessage: () => {},
44
+ sendPageEditClickedMessage: () => {},
45
+ sendSidebarDrilldownBackClickedMessage: () => {},
46
+ sendSidebarItemClickedMessage: () => {},
47
+ sendSidebarExpandedMessage: () => {},
48
+ sendSidebarCollapsedMessage: () => {},
49
+ sendChangeLayoutClickedMessage: () => {},
50
+ sendRequestApiAccessClickedMessage: () => {},
51
+ sendVersionPickerChangedMessage: () => {},
52
+ sendSearchInputResetClickedMessage: () => {},
53
+ sendSearchResultClickedMessage: () => {},
54
+ sendSearchRecentRemoveClickedMessage: () => {},
34
55
  sendSearchRecentClickedMessage: () => {},
35
- sendRequestApiAccessButtonClickedMessage: () => {},
36
- sendVersionPickerSelectionChangeMessage: () => {},
37
- sendProductPickedMessage: () => {},
38
- sendFilterCheckboxToggledMessage: () => {},
39
- sendLanguagePickerLocaleChangedMessage: () => {},
40
56
  sendSearchOpenedMessage: () => {},
41
57
  sendSearchQueryMessage: () => {},
42
- sendSearchAiOpenedMessage: () => {},
43
- sendSearchAIQueryMessage: () => {},
44
- sendSearchAIFeedbackMessage: () => {},
45
- sendFeedbackMessage: () => {},
46
- sendSearchResultClickedMessage: () => {},
47
- sendRedirectMessage: () => {},
58
+ sendTocItemClickedMessage: () => {},
48
59
  sendOpenapiDocsMessage: () => {},
49
60
  sendCopyCodeSnippetClickedMessage: () => {},
50
61
  sendViewedMessage: () => {},
@@ -67,10 +78,7 @@ export const useTelemetryFallback = () => ({
67
78
  sendAsyncapiDocsReferencedInClickedMessage: () => {},
68
79
  sendGraphqlDocsViewedMessage: () => {},
69
80
  sendGraphqlDocsPerformanceMetricsMessage: () => {},
81
+ sendGraphqlDocsDownloadDefinitionClickedMessage: () => {},
70
82
  sendGraphqlDocsReferencedInLinkClickedMessage: () => {},
71
83
  sendGraphqlDocsRequiredScopesModalOpenedMessage: () => {},
72
- sendGraphqlDocsDownloadDefinitionClickedMessage: () => {},
73
- sendBannerViewedMessage: () => {},
74
- sendBannerLinkClickedMessage: () => {},
75
- sendBannerDismissedMessage: () => {},
76
84
  });
@@ -1026,6 +1026,39 @@ const pages = css`
1026
1026
 
1027
1027
  // @tokens End
1028
1028
 
1029
+ /**
1030
+ * @tokens 405 MCP Page
1031
+ * @presenter Color
1032
+ */
1033
+ --page-mcp-405-font-family: var(--font-family-base); // @presenter FontFamily
1034
+ --page-mcp-405-margin-vertical: var(--spacing-xl);
1035
+ --page-mcp-405-margin-horizontal: calc(var(--spacing-xxl) * 2);
1036
+ --page-mcp-405-gap: var(--spacing-lg);
1037
+ --page-mcp-405-max-width: 680px;
1038
+ --page-mcp-405-margin-vertical-mobile: var(--spacing-xxl);
1039
+ --page-mcp-405-margin-horizontal-mobile: 1rem;
1040
+
1041
+ --page-mcp-405-status-text-color: var(--text-color-helper);
1042
+ --page-mcp-405-status-font-size: var(--font-size-lg); // @presenter FontSize
1043
+ --page-mcp-405-status-line-height: var(--line-height-lg); // @presenter LineHeight
1044
+ --page-mcp-405-status-font-weight: var(--font-weight-semibold); // @presenter FontWeight
1045
+
1046
+ --page-mcp-405-title-text-color: var(--text-color-primary);
1047
+ --page-mcp-405-title-font-size: 42px; // @presenter FontSize
1048
+ --page-mcp-405-title-font-weight: var(--font-weight-bold); // @presenter FontWeight
1049
+ --page-mcp-405-title-line-height: 50px; // @presenter LineHeight
1050
+ --page-mcp-405-title-font-size-mobile: var(--h3-font-size);
1051
+ --page-mcp-405-title-line-height-mobile: var(--h3-line-height);
1052
+
1053
+ --page-mcp-405-description-text-color: var(--text-color-secondary);
1054
+ --page-mcp-405-description-font-size: var(--font-size-xl); // @presenter FontSize
1055
+ --page-mcp-405-description-font-weight: var(--font-weight-regular); // @presenter FontWeight
1056
+ --page-mcp-405-description-line-height: var(--line-height-xl);
1057
+ --page-mcp-405-description-font-size-mobile: var(--font-size-lg);
1058
+ --page-mcp-405-description-line-height-mobile: var(--line-height-lg);
1059
+
1060
+ // @tokens End
1061
+
1029
1062
  /**
1030
1063
  * @tokens 500 Page
1031
1064
  * @presenter Color
@@ -134,6 +134,8 @@ export type TranslationKey =
134
134
  | 'page.forbidden.description'
135
135
  | 'page.notFound.title'
136
136
  | 'page.notFound.description'
137
+ | 'page.mcpNotAllowed.title'
138
+ | 'page.mcpNotAllowed.description'
137
139
  | 'page.lastUpdated.timeago'
138
140
  | 'page.lastUpdated.on'
139
141
  | 'catalog.filters.placeholder'
@@ -133,4 +133,5 @@ export enum ToolCallName {
133
133
  GetSecuritySchemes = 'get-security-schemes',
134
134
  GetFullApiDescription = 'get-full-api-description',
135
135
  Execute = 'execute',
136
+ DescribeTools = 'describe-tools',
136
137
  }
@@ -46,3 +46,5 @@ export * from './content-segments';
46
46
  export * from './custom-catalog-options-casing';
47
47
  export * from './get-auto-dismiss-duration';
48
48
  export * from './tooltip-placement';
49
+ export * from './telemetry/get-base-data-attributes';
50
+ export * from './telemetry/generate-before-after-context';
@@ -0,0 +1,59 @@
1
+ import type { AsyncApiRealmUI } from '@redocly/realm-asyncapi-sdk';
2
+
3
+ import { getBaseDataAttributes } from './get-base-data-attributes';
4
+
5
+ type UITelemetryClient = AsyncApiRealmUI.Telemetry;
6
+
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ type UITelemetryClientMethods = {
9
+ [K in keyof UITelemetryClient]: UITelemetryClient[K] extends (...args: any) => any
10
+ ? UITelemetryClient[K]
11
+ : never;
12
+ };
13
+ type UITelemetryClientMethodKeys = {
14
+ [K in keyof UITelemetryClientMethods]: Parameters<
15
+ UITelemetryClientMethods[K]
16
+ >[0] extends readonly [any, any, ...any[]]
17
+ ? K
18
+ : never;
19
+ }[keyof UITelemetryClientMethods];
20
+
21
+ type EventSchemaProps<T extends UITelemetryClientMethodKeys> = Parameters<
22
+ UITelemetryClientMethods[T]
23
+ >[0];
24
+
25
+ type EventAfterContext<T extends UITelemetryClientMethodKeys> = EventSchemaProps<T>[0];
26
+ type EventBeforeContext<T extends UITelemetryClientMethodKeys> = EventSchemaProps<T>[1];
27
+
28
+ /**
29
+ * Builds a typed `[after, before]` tuple for Realm UI telemetry events whose AsyncAPI
30
+ * contract lists the updated state first, then the prior state (per CloudEvents styleguide).
31
+ *
32
+ * Each slice merges {@link getBaseDataAttributes} (`id`, `object`, `uri`) with the
33
+ * corresponding partial payload and sets `context` to `'after'` or `'before'`.
34
+ *
35
+ * @template T - Telemetry client method key whose `data` tuple is
36
+ * `[after, before, ...]` (for example `'sendLanguagePickerChangedMessage'`).
37
+ * @param id - Stable resource identifier passed to {@link getBaseDataAttributes}
38
+ * and included on both payloads.
39
+ * @param object - Resource kind passed to {@link getBaseDataAttributes}
40
+ * and included on both payloads (for example `'dropdown'` or `'button'`).
41
+ * @param before - Event-specific fields for the prior state, excluding `id`, `object`,
42
+ * `uri`, and `context` (filled by this helper).
43
+ * @param after - Event-specific fields for the updated state, excluding `id`, `object`,
44
+ * `uri`, and `context` (filled by this helper).
45
+ * @returns A two-tuple in send order: the fully merged after payload, then the fully
46
+ * merged before payload. Spread into the matching `send*Message` call.
47
+ */
48
+ export function generateBeforeAfterContext<T extends UITelemetryClientMethodKeys>(
49
+ id: EventAfterContext<T>['id'],
50
+ object: EventAfterContext<T>['object'],
51
+ before: Omit<EventBeforeContext<T>, 'id' | 'object' | 'uri' | 'context'>,
52
+ after: Omit<EventAfterContext<T>, 'id' | 'object' | 'uri' | 'context'>,
53
+ ): [EventAfterContext<T> & { context: 'after' }, EventBeforeContext<T> & { context: 'before' }] {
54
+ const baseAttributes = getBaseDataAttributes(id, object);
55
+ return [
56
+ { ...baseAttributes, context: 'after' as const, ...after },
57
+ { ...baseAttributes, context: 'before' as const, ...before },
58
+ ];
59
+ }