@redocly/theme 0.61.0-next.1 → 0.61.0-next.3

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 (97) hide show
  1. package/lib/components/Catalog/CatalogEntity/CatalogEntity.js +5 -0
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.js +9 -0
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistoryButton.d.ts +6 -0
  4. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistoryButton.js +144 -0
  5. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.d.ts +8 -0
  6. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.js +161 -0
  7. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityRevisionItem.d.ts +8 -0
  8. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityRevisionItem.js +67 -0
  9. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityVersionItem.d.ts +9 -0
  10. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityVersionItem.js +212 -0
  11. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +2 -25
  12. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +12 -1
  13. package/lib/components/Catalog/CatalogEntity/ShowMoreButton.d.ts +8 -0
  14. package/lib/components/Catalog/CatalogEntity/ShowMoreButton.js +35 -0
  15. package/lib/components/Catalog/CatalogTableView/CatalogTableViewRow.d.ts +2 -0
  16. package/lib/components/Catalog/CatalogTableView/CatalogTableViewRow.js +4 -1
  17. package/lib/components/Catalog/variables.js +112 -0
  18. package/lib/components/ColorModeSwitcher/ColorModeIcon.d.ts +2 -1
  19. package/lib/components/ColorModeSwitcher/ColorModeIcon.js +3 -2
  20. package/lib/components/ColorModeSwitcher/ColorModeSwitcher.js +1 -4
  21. package/lib/components/Menu/variables.js +1 -0
  22. package/lib/components/Product/utils.d.ts +1 -0
  23. package/lib/components/Product/utils.js +10 -0
  24. package/lib/components/Tooltip/Tooltip.js +2 -0
  25. package/lib/core/constants/catalog.d.ts +1 -0
  26. package/lib/core/constants/catalog.js +2 -1
  27. package/lib/core/constants/common.d.ts +4 -0
  28. package/lib/core/constants/common.js +5 -1
  29. package/lib/core/hooks/catalog/use-catalog-entity-details.d.ts +3 -1
  30. package/lib/core/hooks/catalog/use-catalog-entity-details.js +12 -5
  31. package/lib/core/hooks/index.d.ts +1 -0
  32. package/lib/core/hooks/index.js +1 -0
  33. package/lib/core/hooks/search/use-recent-searches.js +14 -41
  34. package/lib/core/hooks/use-color-switcher.d.ts +0 -1
  35. package/lib/core/hooks/use-color-switcher.js +19 -13
  36. package/lib/core/hooks/use-page-actions.js +37 -6
  37. package/lib/core/hooks/use-product-picker.js +12 -2
  38. package/lib/core/hooks/use-store.d.ts +17 -0
  39. package/lib/core/hooks/use-store.js +64 -0
  40. package/lib/core/hooks/use-telemetry-fallback.d.ts +2 -0
  41. package/lib/core/hooks/use-telemetry-fallback.js +2 -0
  42. package/lib/core/types/catalog.d.ts +33 -4
  43. package/lib/core/types/common.d.ts +2 -0
  44. package/lib/core/types/hooks.d.ts +14 -3
  45. package/lib/core/types/l10n.d.ts +1 -1
  46. package/lib/core/utils/build-revision-url.d.ts +1 -0
  47. package/lib/core/utils/build-revision-url.js +15 -0
  48. package/lib/core/utils/date.d.ts +14 -0
  49. package/lib/core/utils/date.js +39 -0
  50. package/lib/core/utils/index.d.ts +2 -0
  51. package/lib/core/utils/index.js +2 -0
  52. package/lib/core/utils/load-and-navigate.js +7 -2
  53. package/lib/core/utils/transform-revisions-to-version-history.d.ts +8 -0
  54. package/lib/core/utils/transform-revisions-to-version-history.js +110 -0
  55. package/lib/icons/NavaidMilitaryIcon/NavaidMilitaryIcon.d.ts +9 -0
  56. package/lib/icons/NavaidMilitaryIcon/NavaidMilitaryIcon.js +26 -0
  57. package/lib/index.d.ts +2 -0
  58. package/lib/index.js +2 -0
  59. package/package.json +5 -5
  60. package/src/components/Catalog/CatalogEntity/CatalogEntity.tsx +7 -1
  61. package/src/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.tsx +12 -0
  62. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistoryButton.tsx +147 -0
  63. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +180 -0
  64. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityRevisionItem.tsx +93 -0
  65. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityVersionItem.tsx +284 -0
  66. package/src/components/Catalog/CatalogEntity/CatalogEntityMetadata.tsx +3 -25
  67. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.tsx +15 -2
  68. package/src/components/Catalog/CatalogEntity/ShowMoreButton.tsx +47 -0
  69. package/src/components/Catalog/CatalogTableView/CatalogTableViewRow.tsx +6 -1
  70. package/src/components/Catalog/variables.ts +112 -0
  71. package/src/components/ColorModeSwitcher/ColorModeIcon.tsx +5 -3
  72. package/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx +2 -7
  73. package/src/components/Menu/variables.ts +1 -0
  74. package/src/components/Product/utils.ts +6 -0
  75. package/src/components/Tooltip/Tooltip.tsx +2 -0
  76. package/src/core/constants/catalog.ts +2 -0
  77. package/src/core/constants/common.ts +5 -0
  78. package/src/core/hooks/__mocks__/use-theme-hooks.ts +1 -0
  79. package/src/core/hooks/catalog/use-catalog-entity-details.ts +22 -6
  80. package/src/core/hooks/index.ts +1 -0
  81. package/src/core/hooks/search/use-recent-searches.ts +38 -65
  82. package/src/core/hooks/use-color-switcher.ts +29 -15
  83. package/src/core/hooks/use-page-actions.ts +63 -6
  84. package/src/core/hooks/use-product-picker.ts +12 -0
  85. package/src/core/hooks/use-store.ts +95 -0
  86. package/src/core/hooks/use-telemetry-fallback.ts +2 -0
  87. package/src/core/types/catalog.ts +38 -10
  88. package/src/core/types/common.ts +4 -0
  89. package/src/core/types/hooks.ts +23 -4
  90. package/src/core/types/l10n.ts +10 -0
  91. package/src/core/utils/build-revision-url.ts +16 -0
  92. package/src/core/utils/date.ts +33 -0
  93. package/src/core/utils/index.ts +2 -0
  94. package/src/core/utils/load-and-navigate.ts +6 -1
  95. package/src/core/utils/transform-revisions-to-version-history.ts +163 -0
  96. package/src/icons/NavaidMilitaryIcon/NavaidMilitaryIcon.tsx +43 -0
  97. package/src/index.ts +2 -0
@@ -2,7 +2,7 @@ import React from 'react';
2
2
 
3
3
  import type { JSX } from 'react';
4
4
 
5
- import { useMount, useColorSwitcher } from '@redocly/theme/core/hooks';
5
+ import { useColorSwitcher } from '@redocly/theme/core/hooks';
6
6
  import { ColorModeIcon } from '@redocly/theme/components/ColorModeSwitcher/ColorModeIcon';
7
7
  import { Button } from '@redocly/theme/components/Button/Button';
8
8
 
@@ -11,12 +11,7 @@ export type ColorModeSwitcherProps = {
11
11
  };
12
12
 
13
13
  export function ColorModeSwitcher({ className }: ColorModeSwitcherProps): JSX.Element | null {
14
- const { isSwitcherHidden, initActiveColorMode, switchColorMode, activeColorMode } =
15
- useColorSwitcher();
16
-
17
- useMount(() => {
18
- initActiveColorMode();
19
- });
14
+ const { isSwitcherHidden, switchColorMode, activeColorMode } = useColorSwitcher();
20
15
 
21
16
  if (isSwitcherHidden) {
22
17
  return null;
@@ -40,6 +40,7 @@ export const menu = css`
40
40
  --menu-item-padding-vertical: var(--spacing-unit); // @presenter Spacing
41
41
  --menu-item-padding-horizontal: var(--spacing-xxs); // @presenter Spacing
42
42
  --menu-item-nested-offset: var(--spacing-sm); // @presenter Spacing
43
+ --menu-header-container-gap: var(--spacing-sm); // @presenter Spacing
43
44
 
44
45
  /**
45
46
  * @tokens Menu item label
@@ -0,0 +1,6 @@
1
+ export function getProductClassName(productName: string): string {
2
+ return `product-${productName
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-+|-+$/g, '')}`;
6
+ }
@@ -110,6 +110,8 @@ export function TooltipComponent({
110
110
  onMouseEnter: handleOpen,
111
111
  onMouseLeave: handleClose,
112
112
  onClick: handleClose,
113
+ onFocus: handleOpen,
114
+ onBlur: handleClose,
113
115
  };
114
116
 
115
117
  return (
@@ -84,3 +84,5 @@ export enum GraphCustomNodeType {
84
84
  export enum GraphCustomEdgeType {
85
85
  CatalogEdge = 'catalogEdge',
86
86
  }
87
+
88
+ export const VERSION_NOT_SPECIFIED = 'not_specified_version';
@@ -30,3 +30,8 @@ export enum MobileMenuType {
30
30
  PRODUCT = 'PRODUCT',
31
31
  PAGE = 'PAGE',
32
32
  }
33
+
34
+ export const DEFAULT_COLOR_MODES = {
35
+ LIGHT: 'light',
36
+ DARK: 'dark',
37
+ } as const;
@@ -15,6 +15,7 @@ export const useThemeHooks = vi.fn(() => ({
15
15
  useTelemetry: vi.fn(() => ({
16
16
  send: vi.fn(),
17
17
  sendCodeSnippetReportedMessage: vi.fn(),
18
+ sendPageActionsButtonClickedMessage: vi.fn(),
18
19
  })),
19
20
  useBreadcrumbs: vi.fn().mockReturnValue({ breadcrumbs: [], siblings: undefined }),
20
21
  useBanner: vi.fn(() => ({
@@ -1,16 +1,23 @@
1
1
  import type { CatalogEntityConfig, EntitiesCatalogConfig } from '@redocly/config';
2
2
  import type { BffCatalogEntity } from '@redocly/theme/core/types';
3
3
 
4
- import { getPathPrefix } from '../../utils/urls';
4
+ import { withPathPrefix } from '../../utils/urls';
5
5
 
6
6
  type Props = {
7
7
  catalogConfig: CatalogEntityConfig;
8
8
  entitiesCatalogConfig?: EntitiesCatalogConfig;
9
+ revision?: string | null;
10
+ version?: string | null;
9
11
  };
10
12
 
11
13
  type BaseEntity = Pick<BffCatalogEntity, 'key' | 'type'>;
12
14
 
13
- export function useCatalogEntityDetails({ catalogConfig, entitiesCatalogConfig }: Props) {
15
+ export function useCatalogEntityDetails({
16
+ catalogConfig,
17
+ entitiesCatalogConfig,
18
+ revision,
19
+ version,
20
+ }: Props) {
14
21
  const getCatalogSpecificConfigByEntityTypeIncluded = (entity: BaseEntity) => {
15
22
  if (!entitiesCatalogConfig) {
16
23
  return;
@@ -22,14 +29,23 @@ export function useCatalogEntityDetails({ catalogConfig, entitiesCatalogConfig }
22
29
  };
23
30
 
24
31
  const getEntityDetailsLink = (entity: BaseEntity) => {
25
- const pathPrefix = getPathPrefix();
26
32
  const catalogSpecificConfig = getCatalogSpecificConfigByEntityTypeIncluded(entity);
27
33
 
28
- if (!catalogSpecificConfig || !entitiesCatalogConfig) {
29
- return `${pathPrefix}/catalogs/${catalogConfig.slug}/entities/${entity.key}`;
34
+ const basePath =
35
+ !catalogSpecificConfig || !entitiesCatalogConfig
36
+ ? withPathPrefix(`/catalogs/${catalogConfig.slug}/entities/${entity.key}`)
37
+ : withPathPrefix(`/catalogs/${catalogSpecificConfig.slug}/entities/${entity.key}`);
38
+
39
+ const params = new URLSearchParams();
40
+ if (revision) {
41
+ params.set('revision', revision);
42
+ }
43
+ if (version !== undefined) {
44
+ params.set('version', version ?? '');
30
45
  }
46
+ params.set('search', '');
31
47
 
32
- return `${pathPrefix}/catalogs/${catalogSpecificConfig.slug}/entities/${entity.key}`;
48
+ return `${basePath}?${params.toString()}`;
33
49
  };
34
50
 
35
51
  return { getEntityDetailsLink };
@@ -48,3 +48,4 @@ export * from './use-connect-mcp-button';
48
48
  export * from './catalog/use-catalog-entity-details';
49
49
  export * from './catalog/use-catalog-entity-schema';
50
50
  export * from './catalog/use-catalog-table-header-cell-actions';
51
+ export * from './use-store';
@@ -1,83 +1,56 @@
1
- import { useCallback, useSyncExternalStore } from 'react';
1
+ import { useCallback } from 'react';
2
2
 
3
+ import { createStore, useStore } from '../use-store';
3
4
  import { isBrowser } from '../../utils/js-utils';
4
5
 
5
6
  const RECENT_SEARCHES_KEY = 'recentSearches';
6
7
  const RECENT_SEARCHES_LIMIT = 5;
7
8
 
8
- const createRecentSearchesStore = () => {
9
- const subscribers = new Set<() => void>();
10
- let cachedSnapshot: string[];
11
-
12
- const getSnapshot = (): string[] => {
13
- if (!isBrowser()) return [];
14
-
15
- if (cachedSnapshot) return cachedSnapshot;
16
-
17
- try {
18
- const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
19
- cachedSnapshot = stored ? JSON.parse(stored) : [];
20
- return cachedSnapshot;
21
- } catch (e) {
22
- cachedSnapshot = [];
23
- return cachedSnapshot;
24
- }
25
- };
26
-
27
- const updateItems = (value: string, isAdd: boolean) => {
28
- if (!isBrowser()) return;
29
-
30
- const currentItems = getSnapshot();
31
- const valueIndex = currentItems.indexOf(value);
32
-
33
- if (valueIndex !== -1) {
34
- currentItems.splice(valueIndex, 1);
35
- }
36
-
37
- if (isAdd) {
38
- currentItems.unshift(value);
39
- }
40
-
41
- const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
42
-
43
- localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limitedItems));
44
- cachedSnapshot = limitedItems;
45
-
46
- subscribers.forEach((callback) => callback());
47
- };
48
-
49
- const subscribe = (callback: () => void) => {
50
- subscribers.add(callback);
51
- return () => subscribers.delete(callback);
52
- };
53
-
54
- return {
55
- getSnapshot,
56
- subscribe,
57
- updateItems,
58
- };
59
- };
60
-
61
- const recentSearchesStore = createRecentSearchesStore();
9
+ const recentSearchesStore = createStore<string[]>({
10
+ storageKey: RECENT_SEARCHES_KEY,
11
+ });
62
12
 
63
13
  export const useRecentSearches = (): {
64
14
  items: string[];
65
15
  addSearchHistoryItem: (value: string) => void;
66
16
  removeSearchHistoryItem: (value: string) => void;
67
17
  } => {
68
- const items = useSyncExternalStore(
69
- recentSearchesStore.subscribe,
70
- recentSearchesStore.getSnapshot,
71
- () => [],
18
+ const [items, setItems] = useStore<string[]>(recentSearchesStore, []);
19
+
20
+ const updateItems = useCallback(
21
+ (value: string, isAdd: boolean) => {
22
+ if (!isBrowser()) return;
23
+
24
+ const currentItems = [...items];
25
+ const valueIndex = currentItems.indexOf(value);
26
+ if (valueIndex !== -1) {
27
+ currentItems.splice(valueIndex, 1);
28
+ }
29
+
30
+ if (isAdd) {
31
+ currentItems.unshift(value);
32
+ }
33
+
34
+ const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
35
+
36
+ setItems(limitedItems);
37
+ },
38
+ [items, setItems],
72
39
  );
73
40
 
74
- const addSearchHistoryItem = useCallback((value: string) => {
75
- recentSearchesStore.updateItems(value, true);
76
- }, []);
41
+ const addSearchHistoryItem = useCallback(
42
+ (value: string) => {
43
+ updateItems(value, true);
44
+ },
45
+ [updateItems],
46
+ );
77
47
 
78
- const removeSearchHistoryItem = useCallback((value: string) => {
79
- recentSearchesStore.updateItems(value, false);
80
- }, []);
48
+ const removeSearchHistoryItem = useCallback(
49
+ (value: string) => {
50
+ updateItems(value, false);
51
+ },
52
+ [updateItems],
53
+ );
81
54
 
82
55
  return { items, addSearchHistoryItem, removeSearchHistoryItem };
83
56
  };
@@ -1,23 +1,38 @@
1
- import { useState } from 'react';
1
+ import { useMemo } from 'react';
2
+
3
+ import { useMount } from '@redocly/theme/core/hooks';
2
4
 
3
5
  import { useThemeConfig } from './use-theme-config';
4
6
  import { useThemeHooks } from './use-theme-hooks';
7
+ import { createStore, useStore } from './use-store';
8
+ import { DEFAULT_COLOR_MODES } from '../constants';
9
+
10
+ const COLOR_MODE_KEY = 'colorSchema';
11
+ const colorModeStore = createStore<string>({
12
+ storageKey: COLOR_MODE_KEY,
13
+ });
5
14
 
6
15
  export const useColorSwitcher = () => {
7
16
  const themeSettings = useThemeConfig();
8
17
  const { useTelemetry } = useThemeHooks();
9
18
  const telemetry = useTelemetry();
10
- const colorMode = themeSettings.colorMode;
11
- const modes = colorMode?.modes || ['light', 'dark'];
12
- const defaultColor = modes[0] || 'light';
13
- const [activeColorMode, setActiveColorMode] = useState(defaultColor);
19
+ const themeColorMode = themeSettings.colorMode;
20
+
21
+ const modes = useMemo(
22
+ () => themeColorMode?.modes || [DEFAULT_COLOR_MODES.LIGHT, DEFAULT_COLOR_MODES.DARK],
23
+ [themeColorMode],
24
+ );
25
+
26
+ const defaultMode = modes[0] || DEFAULT_COLOR_MODES.LIGHT;
14
27
 
15
- const initActiveColorMode = (): void => {
28
+ const [activeColorMode, setActiveColorMode] = useStore<string>(colorModeStore, defaultMode);
29
+
30
+ useMount(() => {
16
31
  const activeMode = Array.from(document.documentElement.classList).find((c) =>
17
32
  modes.includes(c),
18
33
  );
19
- setActiveColorMode(activeMode || defaultColor);
20
- };
34
+ setActiveColorMode(activeMode || defaultMode);
35
+ });
21
36
 
22
37
  const switchColorMode = (mode?: string): void => {
23
38
  if (mode && !modes.includes(mode)) {
@@ -25,23 +40,22 @@ export const useColorSwitcher = () => {
25
40
  }
26
41
 
27
42
  const activeIndex = modes.indexOf(activeColorMode);
28
- // If specific mode is provided, use it, otherwise cycle through modes
29
43
  const newMode = mode || (activeIndex < modes.length - 1 ? modes[activeIndex + 1] : modes[0]);
30
44
 
31
- localStorage.setItem('colorSchema', newMode);
32
- document.documentElement.className = `${newMode} notransition`;
45
+ const root = document.documentElement;
46
+
47
+ modes.forEach((mode) => root.classList.remove(mode));
48
+ root.classList.add(newMode, 'notransition');
33
49
 
34
50
  window.requestAnimationFrame(() => {
35
- document.documentElement.classList.remove('notransition');
51
+ root.classList.remove('notransition');
36
52
  });
37
53
  telemetry.sendColorModeSwitchedMessage({ from: activeColorMode, to: newMode });
38
-
39
54
  setActiveColorMode(newMode);
40
55
  };
41
56
 
42
57
  return {
43
- isSwitcherHidden: colorMode?.hide,
44
- initActiveColorMode,
58
+ isSwitcherHidden: themeColorMode?.hide,
45
59
  switchColorMode,
46
60
  activeColorMode,
47
61
  };
@@ -17,6 +17,14 @@ import { ClipboardService } from '../utils/clipboard-service';
17
17
  import { IS_BROWSER } from '../utils/dom';
18
18
  import { generateMCPDeepLink } from '../utils/mcp';
19
19
 
20
+ function createPageActionResource(pageSlug: string, pageUrl: string) {
21
+ return {
22
+ id: pageSlug,
23
+ object: 'page' as const,
24
+ uri: pageUrl,
25
+ };
26
+ }
27
+
20
28
  const DEFAULT_ENABLED_ACTIONS = [
21
29
  'copy',
22
30
  'view',
@@ -41,10 +49,12 @@ export function usePageActions(
41
49
  mcpUrl?: string,
42
50
  actions?: PageActionType[],
43
51
  ): PageAction[] {
44
- const { useTranslate, usePageData, usePageProps, usePageSharedData } = useThemeHooks();
52
+ const { useTranslate, usePageData, usePageProps, usePageSharedData, useTelemetry } =
53
+ useThemeHooks();
45
54
  const { translate } = useTranslate();
46
55
  const themeConfig = useThemeConfig();
47
56
  const pageProps = usePageProps();
57
+ const telemetry = useTelemetry();
48
58
  const openApiSharedData = usePageSharedData<
49
59
  { options: { excludeFromSearch: boolean } } | undefined
50
60
  >('openAPIDocsStore');
@@ -67,9 +77,25 @@ export function usePageActions(
67
77
  ? { serverName: mcpConfig.serverName, url: mcpUrl || '' }
68
78
  : { serverName: mcpConfig.serverName, url: mcpConfig.serverUrl || '' };
69
79
 
70
- return createMCPAction({ clientType, mcpConfig: config, translate });
80
+ const isDocsMcp = !requiresMcpUrl;
81
+
82
+ const origin = IS_BROWSER ? window.location.origin : '';
83
+ const pageUrl = `${origin}${pageSlug}`;
84
+
85
+ return createMCPAction({
86
+ clientType,
87
+ mcpConfig: config,
88
+ translate,
89
+ onClickCallback: isDocsMcp
90
+ ? () =>
91
+ telemetry.sendPageActionsButtonClickedMessage({
92
+ ...createPageActionResource(pageSlug, pageUrl),
93
+ action_type: `docs-mcp-${clientType}` as const,
94
+ })
95
+ : undefined,
96
+ });
71
97
  },
72
- [mcpUrl, mcpConfig, translate],
98
+ [mcpUrl, mcpConfig, translate, telemetry, pageSlug],
73
99
  );
74
100
 
75
101
  const result: PageAction[] = useMemo(() => {
@@ -81,13 +107,14 @@ export function usePageActions(
81
107
  ? window.location.origin
82
108
  : ((globalThis as { SSR_HOSTNAME?: string })['SSR_HOSTNAME'] ?? '');
83
109
  const normalizedSlug = pageSlug.startsWith('/') ? pageSlug : '/' + pageSlug;
110
+ const pageUrl = `${origin}${normalizedSlug}`;
84
111
  const mdPageUrl = new URL(
85
112
  origin + normalizedSlug + (normalizedSlug === '/' ? 'index.html.md' : '.md'),
86
113
  ).toString();
87
114
 
88
115
  const actionHandlers: Record<PageActionType, () => PageAction | null> = {
89
- 'docs-mcp-cursor': createMCPHandler('cursor'),
90
- 'docs-mcp-vscode': createMCPHandler('vscode'),
116
+ 'docs-mcp-cursor': createMCPHandler('cursor', false),
117
+ 'docs-mcp-vscode': createMCPHandler('vscode', false),
91
118
  'mcp-cursor': createMCPHandler('cursor', true),
92
119
  'mcp-vscode': createMCPHandler('vscode', true),
93
120
 
@@ -104,6 +131,10 @@ export function usePageActions(
104
131
  }
105
132
  const text = await result.text();
106
133
  ClipboardService.copyCustom(text);
134
+ telemetry.sendPageActionsButtonClickedMessage({
135
+ ...createPageActionResource(pageSlug, pageUrl),
136
+ action_type: 'copy',
137
+ });
107
138
  } catch (error) {
108
139
  console.error(error);
109
140
  }
@@ -116,6 +147,12 @@ export function usePageActions(
116
147
  description: translate('page.actions.viewAsMdDescription', 'Open this page as Markdown'),
117
148
  iconComponent: MarkdownFullIcon,
118
149
  link: mdPageUrl,
150
+ onClick: () => {
151
+ telemetry.sendPageActionsButtonClickedMessage({
152
+ ...createPageActionResource(pageSlug, pageUrl),
153
+ action_type: 'view',
154
+ });
155
+ },
119
156
  }),
120
157
 
121
158
  chatgpt: () => {
@@ -128,6 +165,12 @@ export function usePageActions(
128
165
  description: translate('page.actions.chatGptDescription', 'Get insights from ChatGPT'),
129
166
  iconComponent: ChatGptIcon,
130
167
  link: getExternalAiPromptLink('https://chat.openai.com', mdPageUrl),
168
+ onClick: () => {
169
+ telemetry.sendPageActionsButtonClickedMessage({
170
+ ...createPageActionResource(pageSlug, pageUrl),
171
+ action_type: 'chatgpt',
172
+ });
173
+ },
131
174
  };
132
175
  },
133
176
 
@@ -141,6 +184,12 @@ export function usePageActions(
141
184
  description: translate('page.actions.claudeDescription', 'Get insights from Claude'),
142
185
  iconComponent: ClaudeIcon,
143
186
  link: getExternalAiPromptLink('https://claude.ai/new', mdPageUrl),
187
+ onClick: () => {
188
+ telemetry.sendPageActionsButtonClickedMessage({
189
+ ...createPageActionResource(pageSlug, pageUrl),
190
+ action_type: 'claude',
191
+ });
192
+ },
144
193
  };
145
194
  },
146
195
  };
@@ -156,6 +205,7 @@ export function usePageActions(
156
205
  translate,
157
206
  isPublic,
158
207
  createMCPHandler,
208
+ telemetry,
159
209
  ]);
160
210
 
161
211
  return result;
@@ -172,12 +222,19 @@ type CreateMCPActionParams = {
172
222
  clientType: MCPClientType;
173
223
  mcpConfig: McpConnectionParams;
174
224
  translate: (key: string, defaultValue: string) => string;
225
+ onClickCallback?: () => void;
175
226
  };
176
227
 
177
- function createMCPAction({ clientType, mcpConfig, translate }: CreateMCPActionParams): PageAction {
228
+ function createMCPAction({
229
+ clientType,
230
+ mcpConfig,
231
+ translate,
232
+ onClickCallback,
233
+ }: CreateMCPActionParams): PageAction {
178
234
  const url = generateMCPDeepLink(clientType, mcpConfig);
179
235
  const sharedProps = {
180
236
  onClick: () => {
237
+ onClickCallback?.();
181
238
  window.open(url, '_blank');
182
239
  },
183
240
  };
@@ -1,5 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom';
2
2
 
3
+ import { getProductClassName } from '@redocly/theme/components/Product/utils';
4
+
3
5
  import { useThemeHooks } from './use-theme-hooks';
4
6
  import { withPathPrefix } from '../utils';
5
7
 
@@ -13,6 +15,16 @@ export function useProductPicker() {
13
15
  function setProduct(product: typeof currentProduct) {
14
16
  if (!product) return;
15
17
  telemetry.sendProductPickedMessage({ product: product.slug });
18
+ if (typeof document === 'undefined') return;
19
+
20
+ if (product.name) {
21
+ const root = document.documentElement;
22
+ Array.from(root.classList)
23
+ .filter((c) => c.startsWith('product-'))
24
+ .forEach((c) => root.classList.remove(c));
25
+ root.classList.add(getProductClassName(product.name));
26
+ }
27
+
16
28
  loadAndNavigate({ navigate, to: withPathPrefix(product.link) });
17
29
  }
18
30
  return {
@@ -0,0 +1,95 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ import { isBrowser } from '../utils/js-utils';
4
+
5
+ type Store<T> = {
6
+ getValue: (defaultValue: T) => T;
7
+ setValue: (next: T) => void;
8
+ subscribe: (callback: () => void) => Unsubscribe;
9
+ };
10
+
11
+ type Unsubscribe = () => void;
12
+
13
+ type CreateStoreProps<T> = {
14
+ storageKey: string;
15
+ storageType?: 'localStorage' | 'sessionStorage';
16
+ serializer?: {
17
+ parse: (value: string) => T;
18
+ serialize: (value: T) => string;
19
+ };
20
+ };
21
+
22
+ export function createStore<T>({
23
+ storageKey,
24
+ storageType = 'localStorage',
25
+ serializer = {
26
+ parse: (value: string) => JSON.parse(value) as T,
27
+ serialize: (value: T) => JSON.stringify(value),
28
+ },
29
+ }: CreateStoreProps<T>): Store<T> {
30
+ const subscribers = new Set<() => void>();
31
+ let cachedValue: T;
32
+
33
+ const shouldSerialize = (value: unknown) => typeof value !== 'string';
34
+
35
+ const getValue = (defaultValue: T): T => {
36
+ if (!isBrowser()) return defaultValue;
37
+ if (cachedValue !== undefined) return cachedValue;
38
+
39
+ const value = window[storageType].getItem(storageKey);
40
+ if (!value) {
41
+ cachedValue = defaultValue;
42
+ return cachedValue;
43
+ }
44
+
45
+ if (!shouldSerialize(defaultValue)) {
46
+ cachedValue = value as T;
47
+ return cachedValue;
48
+ }
49
+
50
+ try {
51
+ cachedValue = JSON.parse(value) as T;
52
+ } catch {
53
+ cachedValue = defaultValue;
54
+ }
55
+
56
+ return cachedValue;
57
+ };
58
+
59
+ const setValue = (next: T): void => {
60
+ if (!isBrowser()) return;
61
+ try {
62
+ window[storageType].setItem(
63
+ storageKey,
64
+ shouldSerialize(next) ? serializer.serialize(next) : (next as string),
65
+ );
66
+ cachedValue = next;
67
+ subscribers.forEach((callback) => callback());
68
+ } catch {
69
+ return;
70
+ }
71
+ };
72
+
73
+ const subscribe = (callback: () => void): Unsubscribe => {
74
+ subscribers.add(callback);
75
+ return () => {
76
+ subscribers.delete(callback);
77
+ };
78
+ };
79
+
80
+ return {
81
+ getValue,
82
+ setValue,
83
+ subscribe,
84
+ };
85
+ }
86
+
87
+ export function useStore<T>(store: Store<T>, defaultValue: T) {
88
+ const value = useSyncExternalStore(
89
+ store.subscribe,
90
+ () => store.getValue(defaultValue),
91
+ () => defaultValue,
92
+ );
93
+
94
+ return [value, store.setValue] as const;
95
+ }
@@ -3,6 +3,7 @@
3
3
  export const useTelemetryFallback = () => ({
4
4
  send: () => {},
5
5
  sendPageViewedMessage: () => {},
6
+ sendPageActionsButtonClickedMessage: () => {},
6
7
  sendErrorMessage: () => {},
7
8
  sendClientErrorMessage: () => {},
8
9
  sendBreadcrumbClickedMessage: () => {},
@@ -63,6 +64,7 @@ export const useTelemetryFallback = () => ({
63
64
  sendAsyncapiDocsMessageClickedMessage: () => {},
64
65
  sendAsyncapiDocsServerModalOpenedMessage: () => {},
65
66
  sendAsyncapiDocsDownloadDefinitionClickedMessage: () => {},
67
+ sendAsyncapiDocsReferencedInClickedMessage: () => {},
66
68
  sendGraphqlDocsViewedMessage: () => {},
67
69
  sendGraphqlDocsPerformanceMetricsMessage: () => {},
68
70
  sendGraphqlDocsReferencedInLinkClickedMessage: () => {},