@redocly/theme 0.70.0-next.8 → 0.70.0

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 (101) hide show
  1. package/lib/components/LoadProgressBar/LoadProgressBar.d.ts +8 -0
  2. package/lib/components/LoadProgressBar/LoadProgressBar.js +122 -0
  3. package/lib/core/constants/index.d.ts +1 -0
  4. package/lib/core/constants/index.js +1 -0
  5. package/lib/core/constants/prompt.d.ts +9 -0
  6. package/lib/core/constants/prompt.js +33 -0
  7. package/lib/core/hooks/index.d.ts +3 -0
  8. package/lib/core/hooks/index.js +3 -0
  9. package/lib/core/hooks/prompt/use-prompt-actions.d.ts +7 -0
  10. package/lib/core/hooks/prompt/use-prompt-actions.js +19 -0
  11. package/lib/core/hooks/prompt/use-prompt-text-limit.d.ts +19 -0
  12. package/lib/core/hooks/prompt/use-prompt-text-limit.js +35 -0
  13. package/lib/core/hooks/use-page-actions.js +4 -8
  14. package/lib/core/hooks/use-pointer-border-highlight.d.ts +7 -0
  15. package/lib/core/hooks/use-pointer-border-highlight.js +29 -0
  16. package/lib/core/hooks/use-telemetry-fallback.d.ts +2 -0
  17. package/lib/core/hooks/use-telemetry-fallback.js +2 -0
  18. package/lib/core/styles/dark.js +14 -12
  19. package/lib/core/styles/global.js +16 -54
  20. package/lib/core/types/index.d.ts +1 -0
  21. package/lib/core/types/l10n.d.ts +1 -1
  22. package/lib/core/types/prompt.d.ts +9 -0
  23. package/lib/core/types/prompt.js +3 -0
  24. package/lib/core/utils/get-ai-prompt-link.d.ts +15 -0
  25. package/lib/core/utils/get-ai-prompt-link.js +29 -0
  26. package/lib/core/utils/index.d.ts +1 -0
  27. package/lib/core/utils/index.js +1 -0
  28. package/lib/core/utils/urls.js +1 -1
  29. package/lib/core/utils/with-load-progress.d.ts +3 -0
  30. package/lib/core/utils/with-load-progress.js +24 -19
  31. package/lib/ext/configure.d.ts +1 -1
  32. package/lib/ext/configure.js +0 -1
  33. package/lib/icons/LinkIcon/LinkIconSymbol.d.ts +12 -0
  34. package/lib/icons/LinkIcon/LinkIconSymbol.js +41 -0
  35. package/lib/index.d.ts +1 -0
  36. package/lib/index.js +1 -0
  37. package/lib/markdoc/components/Cards/Card.js +1 -1
  38. package/lib/markdoc/components/Prompt/Prompt.d.ts +15 -0
  39. package/lib/markdoc/components/Prompt/Prompt.js +163 -0
  40. package/lib/markdoc/components/Prompt/PromptActions.d.ts +13 -0
  41. package/lib/markdoc/components/Prompt/PromptActions.js +43 -0
  42. package/lib/markdoc/components/Prompt/PromptBody.d.ts +14 -0
  43. package/lib/markdoc/components/Prompt/PromptBody.js +99 -0
  44. package/lib/markdoc/components/Prompt/PromptCopyButton.d.ts +10 -0
  45. package/lib/markdoc/components/Prompt/PromptCopyButton.js +28 -0
  46. package/lib/markdoc/components/Prompt/PromptHeader.d.ts +9 -0
  47. package/lib/markdoc/components/Prompt/PromptHeader.js +57 -0
  48. package/lib/markdoc/components/Prompt/PromptShowHideButton.d.ts +7 -0
  49. package/lib/markdoc/components/Prompt/PromptShowHideButton.js +21 -0
  50. package/lib/markdoc/components/Prompt/PromptToggleLabel.d.ts +7 -0
  51. package/lib/markdoc/components/Prompt/PromptToggleLabel.js +28 -0
  52. package/lib/markdoc/components/Prompt/variables.d.ts +1 -0
  53. package/lib/markdoc/components/Prompt/variables.dark.d.ts +1 -0
  54. package/lib/markdoc/components/Prompt/variables.dark.js +8 -0
  55. package/lib/markdoc/components/Prompt/variables.js +44 -0
  56. package/lib/markdoc/components/default.d.ts +1 -0
  57. package/lib/markdoc/components/default.js +1 -0
  58. package/lib/markdoc/default.d.ts +2 -0
  59. package/lib/markdoc/default.js +2 -0
  60. package/lib/markdoc/tags/code-walkthrough.d.ts +1 -1
  61. package/lib/markdoc/tags/code-walkthrough.js +3 -5
  62. package/lib/markdoc/tags/prompt.d.ts +2 -0
  63. package/lib/markdoc/tags/prompt.js +56 -0
  64. package/package.json +2 -7
  65. package/src/components/LoadProgressBar/LoadProgressBar.tsx +113 -0
  66. package/src/core/constants/index.ts +1 -0
  67. package/src/core/constants/prompt.ts +34 -0
  68. package/src/core/hooks/__mocks__/index.ts +3 -0
  69. package/src/core/hooks/__mocks__/use-theme-hooks.ts +2 -0
  70. package/src/core/hooks/index.ts +3 -0
  71. package/src/core/hooks/prompt/use-prompt-actions.ts +28 -0
  72. package/src/core/hooks/prompt/use-prompt-text-limit.ts +59 -0
  73. package/src/core/hooks/use-page-actions.ts +4 -9
  74. package/src/core/hooks/use-pointer-border-highlight.ts +46 -0
  75. package/src/core/hooks/use-telemetry-fallback.ts +2 -0
  76. package/src/core/styles/dark.ts +2 -0
  77. package/src/core/styles/global.ts +2 -40
  78. package/src/core/types/index.ts +1 -0
  79. package/src/core/types/l10n.ts +11 -0
  80. package/src/core/types/prompt.ts +11 -0
  81. package/src/core/utils/get-ai-prompt-link.ts +30 -0
  82. package/src/core/utils/index.ts +1 -0
  83. package/src/core/utils/urls.ts +1 -1
  84. package/src/core/utils/with-load-progress.ts +23 -18
  85. package/src/ext/configure.ts +1 -1
  86. package/src/icons/LinkIcon/LinkIconSymbol.tsx +51 -0
  87. package/src/index.ts +1 -0
  88. package/src/markdoc/components/Cards/Card.tsx +1 -1
  89. package/src/markdoc/components/Prompt/Prompt.tsx +219 -0
  90. package/src/markdoc/components/Prompt/PromptActions.tsx +104 -0
  91. package/src/markdoc/components/Prompt/PromptBody.tsx +107 -0
  92. package/src/markdoc/components/Prompt/PromptCopyButton.tsx +73 -0
  93. package/src/markdoc/components/Prompt/PromptHeader.tsx +81 -0
  94. package/src/markdoc/components/Prompt/PromptShowHideButton.tsx +43 -0
  95. package/src/markdoc/components/Prompt/PromptToggleLabel.tsx +41 -0
  96. package/src/markdoc/components/Prompt/variables.dark.ts +5 -0
  97. package/src/markdoc/components/Prompt/variables.ts +41 -0
  98. package/src/markdoc/components/default.ts +1 -0
  99. package/src/markdoc/default.ts +2 -0
  100. package/src/markdoc/tags/code-walkthrough.ts +4 -12
  101. package/src/markdoc/tags/prompt.ts +58 -0
@@ -0,0 +1,34 @@
1
+ import type { AiPromptActionOption, AiPromptTarget } from '../types/prompt';
2
+
3
+ export const PROMPT_TAG = 'prompt';
4
+
5
+ export const AI_PROMPT_TARGETS = Object.freeze({
6
+ CURSOR: 'cursor',
7
+ CLAUDE: 'claude',
8
+ CHATGPT: 'chatgpt',
9
+ } as const);
10
+
11
+ /* Block width, not viewport width: a prompt in a narrow desktop column stacks like one on a
12
+ phone. The viewport `breakpoints` in core/utils/media-css.ts do not apply here. */
13
+ export const PROMPT_NARROW_BLOCK_MAX_WIDTH = '440px';
14
+
15
+ export const AI_PROMPT_ACTION_OPTIONS: Record<AiPromptTarget, AiPromptActionOption> = {
16
+ cursor: {
17
+ titleTranslationKey: 'promptTag.openIn.cursor',
18
+ titleDefault: 'Open in Cursor',
19
+ descriptionTranslationKey: 'promptTag.openIn.cursorDescription',
20
+ descriptionDefault: 'Send this prompt to Cursor',
21
+ },
22
+ claude: {
23
+ titleTranslationKey: 'promptTag.openIn.claude',
24
+ titleDefault: 'Open in Claude',
25
+ descriptionTranslationKey: 'promptTag.openIn.claudeDescription',
26
+ descriptionDefault: 'Send this prompt to Claude',
27
+ },
28
+ chatgpt: {
29
+ titleTranslationKey: 'promptTag.openIn.chatgpt',
30
+ titleDefault: 'Open in ChatGPT',
31
+ descriptionTranslationKey: 'promptTag.openIn.chatgptDescription',
32
+ descriptionDefault: 'Send this prompt to ChatGPT',
33
+ },
34
+ };
@@ -30,3 +30,6 @@ export * from './use-element-size';
30
30
  export * from './use-time-ago';
31
31
  export * from './use-input-key-commands';
32
32
  export * from '../use-tooltip-fallback-placement';
33
+ export * from '../use-pointer-border-highlight';
34
+ export * from '../prompt/use-prompt-text-limit';
35
+ export * from '../prompt/use-prompt-actions';
@@ -17,6 +17,8 @@ export const useThemeHooks = vi.fn(() => ({
17
17
  sendCodeSnippetReportedMessage: vi.fn(),
18
18
  sendConnectMcpClickedMessage: vi.fn(),
19
19
  sendMcpConnectionSnippetCopiedMessage: vi.fn(),
20
+ sendPromptTagCopiedMessage: vi.fn(),
21
+ sendPromptTagOpenedMessage: vi.fn(),
20
22
  sendPageActionsButtonClickedMessage: vi.fn(),
21
23
  sendColorModeSwitchedMessage: vi.fn(),
22
24
  sendConsentChangedMessage: vi.fn(),
@@ -62,3 +62,6 @@ export * from './use-banner-telemetry';
62
62
  export * from './use-tooltip-fallback-placement';
63
63
  export * from './use-anchor-positioning';
64
64
  export * from './use-scroll-item-into-view';
65
+ export * from './use-pointer-border-highlight';
66
+ export * from './prompt/use-prompt-text-limit';
67
+ export * from './prompt/use-prompt-actions';
@@ -0,0 +1,28 @@
1
+ import type { AsyncApiRealmUI } from '@redocly/realm-asyncapi-sdk';
2
+ import type { AiPromptTarget } from '../../types/prompt';
3
+
4
+ import { useThemeHooks } from '../use-theme-hooks';
5
+ import { getAiPromptLink } from '../../utils/get-ai-prompt-link';
6
+ import { getBaseDataAttributes } from '../../utils/telemetry/get-base-data-attributes';
7
+
8
+ type PromptTagData = Parameters<AsyncApiRealmUI.Telemetry['sendPromptTagCopiedMessage']>[0][0];
9
+
10
+ export type PromptActionHandlers = {
11
+ reportCopy: () => void;
12
+ /** Opens the prompt in the AI tool and reports it. */
13
+ openIn: (target: AiPromptTarget) => void;
14
+ };
15
+
16
+ export function usePromptActions(prompt: string): PromptActionHandlers {
17
+ const { useTelemetry } = useThemeHooks();
18
+ const telemetry = useTelemetry();
19
+ const tagData: PromptTagData = getBaseDataAttributes('prompt', 'markdocTag');
20
+
21
+ return {
22
+ reportCopy: () => telemetry.sendPromptTagCopiedMessage([tagData]),
23
+ openIn: (target) => {
24
+ telemetry.sendPromptTagOpenedMessage([{ ...tagData, target }]);
25
+ window.open(getAiPromptLink(target, prompt), '_blank', 'noopener,noreferrer');
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,59 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react';
2
+
3
+ import type { RefObject } from 'react';
4
+
5
+ export type PromptTextLimit = 'none' | 'oneLine' | 'maxLines';
6
+
7
+ export type UsePromptTextLimitParams = {
8
+ prompt: string;
9
+ singleLine: boolean;
10
+ isPromptVisible: boolean;
11
+ };
12
+
13
+ export type PromptTextLimitState = {
14
+ textRef: RefObject<HTMLDivElement | null>;
15
+ textLimit: PromptTextLimit;
16
+ isExpanded: boolean;
17
+ hasHiddenText: boolean;
18
+ toggleExpanded: () => void;
19
+ };
20
+
21
+ /**
22
+ * Tracks the line limit of the prompt text (one line or the maximum lines) and whether the
23
+ * reader expanded it. `hasHiddenText` is true only when the limit hides some text.
24
+ */
25
+ export function usePromptTextLimit({
26
+ prompt,
27
+ singleLine,
28
+ isPromptVisible,
29
+ }: UsePromptTextLimitParams): PromptTextLimitState {
30
+ const textRef = useRef<HTMLDivElement>(null);
31
+ const [isExpanded, setIsExpanded] = useState(false);
32
+ const [hasHiddenText, setHasHiddenText] = useState(false);
33
+
34
+ /* The text mounts late with hidePrompt and re-wraps on resize, so the observer is
35
+ attached each time the text (re)appears. */
36
+ useLayoutEffect(() => {
37
+ const text = textRef.current;
38
+ if (!text || isExpanded) return;
39
+
40
+ const measure = () =>
41
+ setHasHiddenText(
42
+ text.scrollHeight > text.clientHeight || text.scrollWidth > text.clientWidth,
43
+ );
44
+ measure();
45
+
46
+ if (typeof ResizeObserver === 'undefined') return;
47
+ const observer = new ResizeObserver(measure);
48
+ observer.observe(text);
49
+ return () => observer.disconnect();
50
+ }, [prompt, singleLine, isExpanded, isPromptVisible]);
51
+
52
+ return {
53
+ textRef,
54
+ textLimit: isExpanded ? 'none' : singleLine ? 'oneLine' : 'maxLines',
55
+ isExpanded,
56
+ hasHiddenText,
57
+ toggleExpanded: () => setIsExpanded((expanded) => !expanded),
58
+ };
59
+ }
@@ -23,6 +23,7 @@ import {
23
23
  withoutPathPrefix,
24
24
  } from '../utils/urls';
25
25
  import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
26
+ import { getAiPromptLink } from '../utils/get-ai-prompt-link';
26
27
 
27
28
  const DEFAULT_ENABLED_ACTIONS = [
28
29
  'copy',
@@ -112,6 +113,7 @@ export function usePageActions(
112
113
  const mdPageUrl = isRoot
113
114
  ? combineUrls(origin, pathname, 'index.html.md')
114
115
  : combineUrls(origin, removeTrailingSlash(pathname) + '.md');
116
+ const externalAiPrompt = `Read ${mdPageUrl} and answer questions based on the content.`;
115
117
 
116
118
  const actionHandlers: Record<PageActionType, () => PageAction> = {
117
119
  'docs-mcp-cursor': createMCPHandler('cursor', false),
@@ -162,7 +164,7 @@ export function usePageActions(
162
164
  }),
163
165
 
164
166
  chatgpt: () => {
165
- const link = getExternalAiPromptLink('https://chat.openai.com', mdPageUrl);
167
+ const link = getAiPromptLink('chatgpt', externalAiPrompt);
166
168
  return {
167
169
  buttonText: translate('page.actions.chatGptButtonText', 'Open in ChatGPT'),
168
170
  title: translate('page.actions.chatGptTitle', 'Open in ChatGPT'),
@@ -182,7 +184,7 @@ export function usePageActions(
182
184
  },
183
185
 
184
186
  claude: () => {
185
- const link = getExternalAiPromptLink('https://claude.ai/new', mdPageUrl);
187
+ const link = getAiPromptLink('claude', externalAiPrompt);
186
188
  return {
187
189
  buttonText: translate('page.actions.claudeButtonText', 'Open in Claude'),
188
190
  title: translate('page.actions.claudeTitle', 'Open in Claude'),
@@ -223,13 +225,6 @@ export function usePageActions(
223
225
  return result;
224
226
  }
225
227
 
226
- function getExternalAiPromptLink(baseUrl: string, mdPageUrl: string): string {
227
- const externalAiPrompt = `Read ${mdPageUrl} and answer questions based on the content.`;
228
- const url = new URL(baseUrl);
229
- url.searchParams.set('q', externalAiPrompt);
230
- return url.toString();
231
- }
232
-
233
228
  type CreateMCPActionParams = {
234
229
  clientType: MCPClientType;
235
230
  mcpConfig: McpConnectionParams;
@@ -0,0 +1,46 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ import type { PointerEvent, RefObject } from 'react';
4
+
5
+ const EDGE_PROXIMITY_DISTANCE = 80;
6
+
7
+ export type PointerBorderHighlight<T extends HTMLElement> = {
8
+ ref: RefObject<T | null>;
9
+ onPointerMove: (event: PointerEvent<T>) => void;
10
+ onPointerLeave: () => void;
11
+ };
12
+
13
+ /* Writes `--pointer-x`, `--pointer-y` and `--pointer-edge-proximity` (1 at the border, 0 at
14
+ `distance` px inside) to the element's inline style, so the highlight moves without re-renders. */
15
+ export function usePointerBorderHighlight<T extends HTMLElement>(
16
+ distance = EDGE_PROXIMITY_DISTANCE,
17
+ ): PointerBorderHighlight<T> {
18
+ const ref = useRef<T | null>(null);
19
+
20
+ const onPointerMove = useCallback(
21
+ (event: PointerEvent<T>) => {
22
+ const element = ref.current;
23
+ if (!element) return;
24
+
25
+ const rect = element.getBoundingClientRect();
26
+ const x = event.clientX - rect.left;
27
+ const y = event.clientY - rect.top;
28
+ const edgeDistance = Math.min(x, y, rect.width - x, rect.height - y);
29
+ const proximity = Math.min(1, Math.max(0, 1 - edgeDistance / distance));
30
+
31
+ element.style.setProperty('--pointer-x', `${x}px`);
32
+ element.style.setProperty('--pointer-y', `${y}px`);
33
+ element.style.setProperty(
34
+ '--pointer-edge-proximity',
35
+ String(Math.round(proximity * 100) / 100),
36
+ );
37
+ },
38
+ [distance],
39
+ );
40
+
41
+ const onPointerLeave = useCallback(() => {
42
+ ref.current?.style.setProperty('--pointer-edge-proximity', '0');
43
+ }, []);
44
+
45
+ return { ref, onPointerMove, onPointerLeave };
46
+ }
@@ -38,6 +38,8 @@ export const useTelemetryFallback = () => ({
38
38
  sendCodeSnippetReportedMessage: () => {},
39
39
  sendConnectMcpClickedMessage: () => {},
40
40
  sendMcpConnectionSnippetCopiedMessage: () => {},
41
+ sendPromptTagCopiedMessage: () => {},
42
+ sendPromptTagOpenedMessage: () => {},
41
43
  sendMobileMenuClosedMessage: () => {},
42
44
  sendMobileMenuOpenedMessage: () => {},
43
45
  sendPageViewedMessage: () => {},
@@ -10,6 +10,7 @@ import { tagDarkMode } from '@redocly/theme/components/Tag/variables.dark';
10
10
  import { statusCodeDarkMode } from '@redocly/theme/components/StatusCode/variables.dark';
11
11
  import { switcherDarkMode } from '@redocly/theme/components/Switch/variables.dark';
12
12
  import { cardsDarkMode } from '@redocly/theme/markdoc/components/Cards/variables.dark';
13
+ import { promptTagDarkMode } from '@redocly/theme/markdoc/components/Prompt/variables.dark';
13
14
  import { catalogDarkMode } from '@redocly/theme/components/Catalog/variables.dark';
14
15
  import { pageActionsDarkMode } from '@redocly/theme/components/PageActions/variables.dark';
15
16
  import { tooltipDarkMode } from '@redocly/theme/components/Tooltip/variables.dark';
@@ -357,6 +358,7 @@ export const darkMode = css`
357
358
  ${replayDarkMode}
358
359
  ${switcherDarkMode}
359
360
  ${cardsDarkMode}
361
+ ${promptTagDarkMode}
360
362
  ${catalogDarkMode}
361
363
  ${pageActionsDarkMode}
362
364
  ${tooltipDarkMode}
@@ -43,6 +43,7 @@ import { versionPicker } from '@redocly/theme/components/VersionPicker/variables
43
43
  import { datePicker } from '@redocly/theme/components/DatePicker/variables'
44
44
  import { switcher } from '@redocly/theme/components/Switch/variables';
45
45
  import { cards } from '@redocly/theme/markdoc/components/Cards/variables';
46
+ import { promptTag } from '@redocly/theme/markdoc/components/Prompt/variables';
46
47
  import { codeWalkthrough } from '@redocly/theme/markdoc/components/CodeWalkthrough/variables';
47
48
  import { skipContent } from '@redocly/theme/components/SkipContent/variables';
48
49
  import { pageActions } from '@redocly/theme/components/PageActions/variables';
@@ -872,46 +873,6 @@ const loadProgressBar = css`
872
873
  --load-progress-bar-height: 2px; // @presenter Height
873
874
 
874
875
  // @tokens End
875
-
876
- /* Make clicks pass-through */
877
- #nprogress {
878
- pointer-events: none;
879
- }
880
-
881
- #nprogress .bar {
882
- background: var(--load-progress-bar-color);
883
-
884
- position: fixed;
885
- z-index: var(--z-index-popover);
886
- top: 0;
887
- left: 0;
888
-
889
- width: 100%;
890
- height: var(--load-progress-bar-height);
891
- }
892
-
893
- #nprogress .peg {
894
- display: block;
895
- position: absolute;
896
- right: 0;
897
- width: 100px;
898
- height: 100%;
899
- box-shadow: var(--load-progress-bar-shadow);
900
- opacity: 1;
901
-
902
- -webkit-transform: rotate(3deg) translate(0px, -4px);
903
- -ms-transform: rotate(3deg) translate(0px, -4px);
904
- transform: rotate(3deg) translate(0px, -4px);
905
- }
906
-
907
- .nprogress-custom-parent {
908
- overflow: hidden;
909
- position: relative;
910
- }
911
-
912
- .nprogress-custom-parent #nprogress .bar {
913
- position: absolute;
914
- }
915
876
  `;
916
877
 
917
878
  const inputs = css`
@@ -1345,6 +1306,7 @@ export const tokens = css`
1345
1306
  ${aiAssistantButton}
1346
1307
  ${connectMcpButton}
1347
1308
  ${cards}
1309
+ ${promptTag}
1348
1310
  ${catalog}
1349
1311
  ${catalogClassic}
1350
1312
  ${code}
@@ -26,3 +26,4 @@ export type * from './toast';
26
26
  export type * from './link';
27
27
  export type * from './button-group';
28
28
  export type * from './dropdown';
29
+ export type * from './prompt';
@@ -310,6 +310,17 @@ export type TranslationKey =
310
310
  | 'page.actions.connectMcp.copyConfigDescription'
311
311
  | 'page.actions.connectMcp.moreOptions'
312
312
  | 'page.actions.moreActions'
313
+ | 'promptTag.copy.buttonText'
314
+ | 'promptTag.showPrompt'
315
+ | 'promptTag.hidePrompt'
316
+ | 'promptTag.showFullPrompt'
317
+ | 'promptTag.showLess'
318
+ | 'promptTag.openIn.cursor'
319
+ | 'promptTag.openIn.cursorDescription'
320
+ | 'promptTag.openIn.claude'
321
+ | 'promptTag.openIn.claudeDescription'
322
+ | 'promptTag.openIn.chatgpt'
323
+ | 'promptTag.openIn.chatgptDescription'
313
324
  | 'openapi.download.description.title'
314
325
  | 'openapi.info.title'
315
326
  | 'openapi.info.contact.url'
@@ -0,0 +1,11 @@
1
+ import type { AI_PROMPT_TARGETS } from '../constants/prompt';
2
+ import type { TranslationKey } from './l10n';
3
+
4
+ export type AiPromptTarget = (typeof AI_PROMPT_TARGETS)[keyof typeof AI_PROMPT_TARGETS];
5
+
6
+ export type AiPromptActionOption = {
7
+ titleTranslationKey: TranslationKey;
8
+ titleDefault: string;
9
+ descriptionTranslationKey: TranslationKey;
10
+ descriptionDefault: string;
11
+ };
@@ -0,0 +1,30 @@
1
+ import type { AiPromptTarget } from '../types/prompt';
2
+
3
+ type AiPromptEndpoint = { baseUrl: string; queryParam: string };
4
+
5
+ const AI_PROMPT_ENDPOINTS: Record<AiPromptTarget, AiPromptEndpoint> = {
6
+ chatgpt: { baseUrl: 'https://chat.openai.com', queryParam: 'q' },
7
+ claude: { baseUrl: 'https://claude.ai/new', queryParam: 'q' },
8
+ // Encoding: https://cursor.com/docs/integrations/deeplinks
9
+ cursor: { baseUrl: 'https://cursor.com/link/prompt', queryParam: 'text' },
10
+ };
11
+
12
+ /**
13
+ * Builds a link that opens an AI tool with the prompt prefilled.
14
+ *
15
+ * @param target - The AI tool to open.
16
+ * @param prompt - The plain-text prompt to prefill.
17
+ * @returns The absolute URL with the encoded prompt in the query string.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * getAiPromptLink('claude', 'Summarize this page');
22
+ * // 'https://claude.ai/new?q=Summarize+this+page'
23
+ * ```
24
+ */
25
+ export function getAiPromptLink(target: AiPromptTarget, prompt: string): string {
26
+ const { baseUrl, queryParam } = AI_PROMPT_ENDPOINTS[target];
27
+ const url = new URL(baseUrl);
28
+ url.searchParams.set(queryParam, prompt);
29
+ return url.toString();
30
+ }
@@ -53,3 +53,4 @@ export * from './RenderWithLink';
53
53
  export * from './consent-settings-link-placement';
54
54
  export * from './assign-ref';
55
55
  export * from './dropdown';
56
+ export * from './get-ai-prompt-link';
@@ -4,7 +4,7 @@ import { DEFAULT_LOCALE_PLACEHOLDER } from '../constants/common';
4
4
 
5
5
  export function combineUrls(baseURL: string, ...relativeURLs: (string | undefined)[]) {
6
6
  let res = baseURL;
7
- for (let relativeURL of relativeURLs) {
7
+ for (const relativeURL of relativeURLs) {
8
8
  res = relativeURL
9
9
  ? res.replace(/(?<![/\\])[/\\]+$/, '') + '/' + relativeURL.replace(/^[/\\]+/, '')
10
10
  : res;
@@ -1,25 +1,30 @@
1
- import nprogress from 'nprogress';
1
+ let latestLoad: symbol | null = null;
2
+ const listeners = new Set<() => void>();
2
3
 
3
- nprogress.configure({
4
- trickleSpeed: 100,
5
- template: '<div class="bar" role="bar"><div class="peg"></div></div>',
6
- });
7
-
8
- // Only the last caller should be able to call nprogress.done()
9
- let lastCalledBy: symbol;
4
+ function setLatestLoad(load: symbol | null): void {
5
+ latestLoad = load;
6
+ // e2e suites wait for this class, named after the progress library it replaced, to clear.
7
+ document.documentElement.classList.toggle('nprogress-busy', load !== null);
8
+ listeners.forEach((listener) => listener());
9
+ }
10
10
 
11
+ /** Shows the load progress bar until the most recent `withLoadProgress` load settles. */
11
12
  export async function withLoadProgress<T>(loadTarget: Promise<T>): Promise<T> {
12
- const callIdentifier = Symbol();
13
- lastCalledBy = callIdentifier;
14
-
15
- // Cancel any existing progress and restart
16
- nprogress.done();
17
- nprogress.start();
13
+ const load = Symbol();
14
+ setLatestLoad(load);
18
15
 
19
16
  return loadTarget.finally(() => {
20
- if (lastCalledBy === callIdentifier) {
21
- nprogress.done();
22
- nprogress.remove();
23
- }
17
+ if (latestLoad === load) setLatestLoad(null);
24
18
  });
25
19
  }
20
+
21
+ export function subscribeToLoadProgress(listener: () => void): () => void {
22
+ listeners.add(listener);
23
+ return () => {
24
+ listeners.delete(listener);
25
+ };
26
+ }
27
+
28
+ export function isLoadInProgress(): boolean {
29
+ return latestLoad !== null;
30
+ }
@@ -1,6 +1,6 @@
1
1
  import type { UserClaims, OpenAPIServer } from '@redocly/theme/core/types';
2
2
 
3
- export * from '@redocly/theme/ext/configure-helpers';
3
+ export type * from '@redocly/theme/ext/configure-helpers';
4
4
  export * from '@redocly/theme/ext/helpers/is-direct-configure-request-values';
5
5
 
6
6
  import type {
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+
3
+ import type { JSX } from 'react';
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ import { getCssColorVariable } from '@redocly/theme/core/utils';
7
+
8
+ const LINK_ICON_SYMBOL_ID = 'redocly-link-icon';
9
+
10
+ /**
11
+ * The link icon drawing as a hidden SVG symbol, which every `SharedLinkIcon` refers to. Render it
12
+ * once per page that shows a `SharedLinkIcon`; without it the icon is empty.
13
+ */
14
+ export function LinkIconSymbol(): JSX.Element {
15
+ return (
16
+ <svg
17
+ data-component-name="icons/LinkIcon/LinkIconSymbol"
18
+ display="none"
19
+ aria-hidden="true"
20
+ focusable="false"
21
+ >
22
+ <symbol id={LINK_ICON_SYMBOL_ID} viewBox="0 0 20 20">
23
+ <path
24
+ d="M0 6C0 2.68629 2.68629 0 6 0H14C17.3137 0 20 2.68629 20 6V14C20 17.3137 17.3137 20 14 20H6C2.68629 20 0 17.3137 0 14V6Z"
25
+ fill="var(--link-icon-bg-color, var(--layer-color))"
26
+ />
27
+ <path d="M7.37318 9.56229H12.6232C12.7392 9.56229 12.8505 9.60838 12.9325 9.69043C13.0146 9.77247 13.0607 9.88375 13.0607 9.99979C13.0607 10.1158 13.0146 10.2271 12.9325 10.3091C12.8505 10.3912 12.7392 10.4373 12.6232 10.4373H7.37318C7.25714 10.4373 7.14586 10.3912 7.06382 10.3091C6.98177 10.2271 6.93568 10.1158 6.93568 9.99979C6.93568 9.88375 6.98177 9.77247 7.06382 9.69043C7.14586 9.60838 7.25714 9.56229 7.37318 9.56229ZM8.6853 12.1875H6.4978C5.91764 12.1875 5.36124 11.957 4.95101 11.5468C4.54077 11.1366 4.3103 10.5802 4.3103 10C4.3103 9.41984 4.54077 8.86344 4.95101 8.4532C5.36124 8.04297 5.91764 7.8125 6.4978 7.8125H8.6853C8.80133 7.8125 8.91262 7.76641 8.99466 7.68436C9.07671 7.60231 9.1228 7.49103 9.1228 7.375C9.1228 7.25897 9.07671 7.14769 8.99466 7.06564C8.91262 6.98359 8.80133 6.9375 8.6853 6.9375H6.4978C5.68558 6.9375 4.90662 7.26016 4.33229 7.83449C3.75796 8.40882 3.4353 9.18777 3.4353 10C3.4353 10.8122 3.75796 11.5912 4.33229 12.1655C4.90662 12.7398 5.68558 13.0625 6.4978 13.0625H8.6853C8.80133 13.0625 8.91262 13.0164 8.99466 12.9344C9.07671 12.8523 9.1228 12.741 9.1228 12.625C9.1228 12.509 9.07671 12.3977 8.99466 12.3156C8.91262 12.2336 8.80133 12.1875 8.6853 12.1875ZM13.4982 6.9375H11.3107C11.1946 6.9375 11.0834 6.98359 11.0013 7.06564C10.9193 7.14769 10.8732 7.25897 10.8732 7.375C10.8732 7.49103 10.9193 7.60231 11.0013 7.68436C11.0834 7.76641 11.1946 7.8125 11.3107 7.8125H13.4982C14.0783 7.8125 14.6347 8.04297 15.045 8.4532C15.4552 8.86344 15.6857 9.41984 15.6857 10C15.6857 10.5802 15.4552 11.1366 15.045 11.5468C14.6347 11.957 14.0783 12.1875 13.4982 12.1875H11.3107C11.1946 12.1875 11.0834 12.2336 11.0013 12.3156C10.9193 12.3977 10.8732 12.509 10.8732 12.625C10.8732 12.741 10.9193 12.8523 11.0013 12.9344C11.0834 13.0164 11.1946 13.0625 11.3107 13.0625H13.4982C14.3104 13.0625 15.0894 12.7398 15.6637 12.1655C16.238 11.5912 16.5607 10.8122 16.5607 10C16.5607 9.18777 16.238 8.40882 15.6637 7.83449C15.0894 7.26016 14.3104 6.9375 13.4982 6.9375Z" />
28
+ </symbol>
29
+ </svg>
30
+ );
31
+ }
32
+
33
+ /**
34
+ * `LinkIcon` drawn from the page's `LinkIconSymbol`, so a page with many of them carries one copy.
35
+ * `color` fills the glyph; `--link-icon-bg-color` overrides the background.
36
+ */
37
+ export function SharedLinkIcon({ color, size = '20px', ...props }: IconProps): JSX.Element {
38
+ return (
39
+ <svg
40
+ data-component-name="icons/LinkIcon/SharedLinkIcon"
41
+ width={size}
42
+ height={size}
43
+ viewBox="0 0 20 20"
44
+ fill={getCssColorVariable(color)}
45
+ xmlns="http://www.w3.org/2000/svg"
46
+ {...props}
47
+ >
48
+ <use href={`#${LINK_ICON_SYMBOL_ID}`} />
49
+ </svg>
50
+ );
51
+ }
package/src/index.ts CHANGED
@@ -256,6 +256,7 @@ export * from '@redocly/theme/icons/MoonIcon/MoonIcon';
256
256
  export * from '@redocly/theme/icons/SunIcon/SunIcon';
257
257
  export * from '@redocly/theme/icons/ContrastIcon/ContrastIcon';
258
258
  export * from '@redocly/theme/icons/LinkIcon/LinkIcon';
259
+ export * from '@redocly/theme/icons/LinkIcon/LinkIconSymbol';
259
260
  export * from '@redocly/theme/icons/LaunchIcon/LaunchIcon';
260
261
  export * from '@redocly/theme/icons/MinimizeIcon/MinimizeIcon';
261
262
  export * from '@redocly/theme/icons/MaximizeIcon/MaximizeIcon';
@@ -276,7 +276,7 @@ const CardWrapper = styled.div.attrs<{
276
276
  `;
277
277
 
278
278
  const getCardWrapperClass = ($isCardLink: boolean, $variant?: string) => {
279
- let classes = [];
279
+ const classes = [];
280
280
 
281
281
  classes.push(`card-variant-${$variant}`);
282
282