@stainless-api/docs 0.1.0-beta.13 → 0.1.0-beta.130

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 (188) hide show
  1. package/CHANGELOG.md +1102 -0
  2. package/ambient.d.ts +6 -0
  3. package/eslint-suppressions.json +90 -0
  4. package/{eslint.config.js → eslint.config.ts} +0 -2
  5. package/locals.d.ts +17 -0
  6. package/package.json +62 -44
  7. package/playground-virtual-modules.d.ts +96 -0
  8. package/plugin/assets/languages/cli.svg +14 -0
  9. package/plugin/assets/languages/csharp.svg +1 -0
  10. package/plugin/assets/languages/php.svg +4 -0
  11. package/plugin/buildAlgoliaIndex.ts +40 -39
  12. package/plugin/components/MethodDescription.tsx +54 -0
  13. package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
  14. package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
  15. package/plugin/components/RequestBuilder/index.tsx +40 -0
  16. package/plugin/components/RequestBuilder/props.ts +9 -0
  17. package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
  18. package/plugin/components/RequestBuilder/styles.css +67 -0
  19. package/plugin/components/SDKSelect.astro +18 -111
  20. package/plugin/components/SnippetCode.tsx +112 -70
  21. package/plugin/components/StainlessIslands.tsx +126 -0
  22. package/plugin/components/search/SearchAlgolia.astro +46 -29
  23. package/plugin/components/search/SearchIsland.tsx +61 -37
  24. package/plugin/generateAPIReferenceLink.ts +0 -40
  25. package/plugin/globalJs/ai-dropdown-options.ts +248 -0
  26. package/plugin/globalJs/code-snippets.ts +45 -16
  27. package/plugin/globalJs/copy.ts +115 -27
  28. package/plugin/globalJs/create-playground.shim.ts +3 -0
  29. package/plugin/globalJs/method-descriptions.ts +33 -0
  30. package/plugin/globalJs/navigation.ts +24 -44
  31. package/plugin/globalJs/playground-data.shim.ts +1 -0
  32. package/plugin/globalJs/playground-data.ts +14 -0
  33. package/plugin/globalJs/summary-selection-tweak.ts +29 -0
  34. package/plugin/helpers/generateDocsRoutes.ts +59 -0
  35. package/plugin/helpers/multiSpec.ts +8 -0
  36. package/plugin/index.ts +317 -141
  37. package/plugin/languages.ts +8 -2
  38. package/plugin/loadPluginConfig.ts +284 -109
  39. package/plugin/markdown/highlighter.ts +100 -0
  40. package/plugin/markdown/index.ts +39 -0
  41. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
  42. package/plugin/react/Routing.tsx +98 -263
  43. package/plugin/referencePlaceholderUtils.ts +17 -14
  44. package/plugin/replaceSidebarPlaceholderMiddleware.ts +39 -35
  45. package/plugin/routes/Docs.astro +72 -111
  46. package/plugin/routes/DocsStatic.astro +6 -5
  47. package/plugin/routes/Overview.astro +46 -22
  48. package/plugin/routes/llms.ts +186 -0
  49. package/plugin/routes/markdown.ts +13 -12
  50. package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +84 -69
  51. package/plugin/specs/FileCache.ts +99 -0
  52. package/plugin/specs/fetchSpecSSR.ts +27 -0
  53. package/plugin/specs/generateSpec.ts +112 -0
  54. package/plugin/specs/index.ts +132 -0
  55. package/plugin/specs/inputResolver.ts +148 -0
  56. package/plugin/{cms → specs}/worker.ts +82 -5
  57. package/plugin/vendor/preview.worker.docs.js +27121 -16890
  58. package/plugin/vendor/templates/cli.md +1 -0
  59. package/plugin/vendor/templates/go.md +4 -2
  60. package/plugin/vendor/templates/java.md +5 -1
  61. package/plugin/vendor/templates/kotlin.md +5 -1
  62. package/plugin/vendor/templates/node.md +4 -2
  63. package/plugin/vendor/templates/python.md +4 -2
  64. package/plugin/vendor/templates/ruby.md +4 -2
  65. package/plugin/vendor/templates/terraform.md +1 -1
  66. package/plugin/vendor/templates/typescript.md +3 -1
  67. package/resolveSrcFile.ts +10 -0
  68. package/scripts/vendor_deps.ts +5 -5
  69. package/shared/conditionalIntegration.ts +28 -0
  70. package/shared/getProsePages.ts +41 -0
  71. package/shared/getSharedLogger.ts +15 -0
  72. package/shared/terminalUtils.ts +3 -0
  73. package/shared/virtualModule.ts +46 -1
  74. package/src/content.config.ts +9 -0
  75. package/stl-docs/aiChatExamples.ts +95 -0
  76. package/stl-docs/chat/docs-chat-handler.ts +18 -0
  77. package/stl-docs/chat/hook.ts +215 -0
  78. package/stl-docs/chat/schemas.ts +70 -0
  79. package/stl-docs/chat/stainless-handler/index.ts +126 -0
  80. package/stl-docs/chat/stream-util.ts +16 -0
  81. package/stl-docs/chat/ui/AiChat.module.css +591 -0
  82. package/stl-docs/chat/ui/AiChat.tsx +188 -0
  83. package/stl-docs/chat/ui/Trigger.tsx +154 -0
  84. package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
  85. package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
  86. package/stl-docs/chat/ui/components/ChatLog.tsx +96 -0
  87. package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
  88. package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
  89. package/stl-docs/chat/ui/components/MessageFeedback.tsx +109 -0
  90. package/stl-docs/chat/ui/components/Table.tsx +15 -0
  91. package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
  92. package/stl-docs/chat/ui/components/hljs-github.css +81 -0
  93. package/stl-docs/chat/ui/scroll-manager.ts +86 -0
  94. package/stl-docs/chat/ui/types.ts +45 -0
  95. package/stl-docs/components/AIDropdown.tsx +63 -0
  96. package/stl-docs/components/AiChatIsland.tsx +16 -0
  97. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
  98. package/stl-docs/components/ContentPanel.astro +9 -0
  99. package/stl-docs/components/Footer.astro +89 -0
  100. package/stl-docs/components/Head.astro +20 -0
  101. package/stl-docs/components/Header.astro +3 -9
  102. package/stl-docs/components/PageFrame.astro +37 -0
  103. package/stl-docs/components/PageSidebar.astro +11 -0
  104. package/stl-docs/components/PageTitle.astro +82 -0
  105. package/stl-docs/components/StainlessLogo.svg +4 -0
  106. package/stl-docs/components/ThemeProvider.astro +36 -0
  107. package/stl-docs/components/ThemeSelect.astro +84 -146
  108. package/stl-docs/components/TwoColumnContent.astro +2 -0
  109. package/stl-docs/components/headers/DefaultHeader.astro +6 -8
  110. package/stl-docs/components/headers/StackedHeader.astro +10 -53
  111. package/stl-docs/components/icons/chat-gpt.tsx +2 -2
  112. package/stl-docs/components/icons/cursor.tsx +10 -0
  113. package/stl-docs/components/icons/gemini.tsx +19 -0
  114. package/stl-docs/components/icons/markdown.tsx +1 -1
  115. package/stl-docs/components/index.ts +1 -0
  116. package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
  117. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
  118. package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
  119. package/stl-docs/components/mintlify-compat/Frame.astro +6 -6
  120. package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
  121. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
  122. package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
  123. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
  124. package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
  125. package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
  126. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
  127. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
  128. package/stl-docs/components/mintlify-compat/card.css +4 -4
  129. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  130. package/stl-docs/components/nav-tabs/NavDropdown.astro +38 -77
  131. package/stl-docs/components/nav-tabs/NavTabs.astro +81 -81
  132. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +1 -2
  133. package/stl-docs/components/nav-tabs/buildNavLinks.ts +5 -2
  134. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  135. package/stl-docs/components/pagination/Pagination.astro +177 -0
  136. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  137. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  138. package/stl-docs/components/pagination/util.ts +71 -0
  139. package/stl-docs/components/scripts.ts +1 -0
  140. package/stl-docs/components/sidebars/BaseSidebar.astro +80 -2
  141. package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
  142. package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
  143. package/stl-docs/disableCalloutSyntax.ts +36 -0
  144. package/stl-docs/fonts.ts +186 -0
  145. package/stl-docs/index.ts +176 -58
  146. package/stl-docs/loadStlDocsConfig.ts +73 -8
  147. package/stl-docs/proseDocSync.test.ts +74 -0
  148. package/stl-docs/proseDocSync.ts +344 -0
  149. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
  150. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
  151. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  152. package/stl-docs/proseSearchIndexing.ts +218 -0
  153. package/stl-docs/tabsMiddleware.ts +14 -5
  154. package/styles/code.css +53 -49
  155. package/styles/links.css +2 -37
  156. package/styles/method-descriptions.css +36 -0
  157. package/styles/overrides.css +28 -46
  158. package/styles/page.css +228 -38
  159. package/styles/sdk_select.css +9 -6
  160. package/styles/search.css +11 -21
  161. package/styles/sidebar.css +28 -215
  162. package/styles/{variables.css → sl-variables.css} +4 -8
  163. package/styles/stldocs-variables.css +6 -0
  164. package/styles/toc.css +19 -8
  165. package/theme.css +11 -9
  166. package/tsconfig.json +1 -4
  167. package/virtual-module.d.ts +66 -8
  168. package/components/variables.css +0 -112
  169. package/plugin/cms/client.ts +0 -62
  170. package/plugin/cms/server.ts +0 -268
  171. package/plugin/globalJs/ai-dropdown.ts +0 -57
  172. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
  173. package/stl-docs/components/ClientRouterHead.astro +0 -41
  174. package/stl-docs/components/content-panel/ContentPanel.astro +0 -69
  175. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
  176. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
  177. package/stl-docs/components/mintlify-compat/Step.astro +0 -56
  178. package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
  179. package/styles/fonts.css +0 -68
  180. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  181. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  182. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  183. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  184. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  185. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  186. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  187. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  188. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Technical overview:
3
+ * - `StainlessIslands.ts` is an Astro Client Island wherein we register a HTML custom element called `stl-island`
4
+ * - When a `<stl-island component="MyComponent">` is added to the DOM, we:
5
+ * 1. receive a callback from the browser
6
+ * 2. dynamic-import `MyComponent`
7
+ * 3. create an instance of `MyComponent`, to which we pass the **`stl-island` DOM node** as a prop named `parent`
8
+ * 4. register the (`stlIslandDomNode` → `ReactNode`) pair in a global map named `roots`
9
+ * - The actual Astro client island is a simple React component that renders all of the ReactNodes from the global `roots` map
10
+ * - it uses a `useSyncExternalStore` to be notified of changes to the global `roots` map by registering a callback
11
+ * - Each “stainless island” (instantiated by the custom element handler and rendered by the Astro client island) is responsible for using the `parent` prop it receives to identify one or more **portal targets** into which it can render its contents.
12
+ * - the react parent of all Stainless Islands is the global `<StainlessIslands client:load />` singleton, but the _DOM parent_ is the various portal targets of all of the stainless islands
13
+ */
14
+
15
+ import { useSyncExternalStore, ReactNode } from 'react';
16
+
17
+ type StlIslandComponent = ({ parent }: { parent: HTMLElement }) => ReactNode;
18
+
19
+ /**
20
+ * Register new Stainless Islands in this map.
21
+ * The component should be the default export and should be able to be dynamic-imported.
22
+ * The component should accept a single prop: `parent: HTMLElement`, which is the DOM node of the `<stl-island>` element.
23
+ * The component can create portals to render into the DOM subtree of the `parent`.
24
+ */
25
+ const componentsMap: Record<string, () => Promise<{ default: StlIslandComponent }>> = {
26
+ SnippetStainlessIsland: () => import('./RequestBuilder/SnippetStainlessIsland'),
27
+ };
28
+
29
+ interface State {
30
+ roots: Map<HTMLElement, ReactNode>;
31
+ onRootsChange?: (() => void) | undefined;
32
+ }
33
+
34
+ // keep state in import.meta.hot.data so that our record of our react roots is not lost across HMR
35
+ const state: State = import.meta.hot?.data?.state ?? {
36
+ roots: new Map(),
37
+ };
38
+
39
+ if (import.meta.hot?.data) {
40
+ import.meta.hot.data.state = state;
41
+ }
42
+
43
+ let key = 0;
44
+
45
+ /**
46
+ * Custom element mounts and unmounts components and gives them a reference to the parent
47
+ * so they can render in portals.
48
+ */
49
+ class StlIsland extends (globalThis?.HTMLElement ?? Object) {
50
+ connectedCallback(this: StlIsland) {
51
+ const componentName = this.getAttribute('component');
52
+ if (!componentName) {
53
+ console.error('[stl-island] missing required attribute "component"');
54
+ return;
55
+ }
56
+
57
+ if (!componentsMap[componentName]) {
58
+ console.error(`[stl-island] unknown component "${componentName}"`);
59
+ return;
60
+ }
61
+
62
+ componentsMap[componentName]().then(
63
+ ({ default: Component }) => {
64
+ state.roots = new Map(state.roots).set(this, <Component parent={this} key={key++} />);
65
+ state.onRootsChange?.();
66
+ },
67
+ (e) => {
68
+ console.error(`[stl-island] failed to load component "${componentName}":`, e);
69
+ },
70
+ );
71
+ }
72
+ connectedMoveCallback() {
73
+ // empty so we don't get disconnected/reconnected if the dom element gets moved
74
+ }
75
+ disconnectedCallback() {
76
+ state.roots = new Map(state.roots);
77
+ state.roots.delete(this);
78
+ state.onRootsChange?.();
79
+ }
80
+ }
81
+
82
+ if (typeof customElements !== 'undefined' && !customElements.get('stl-island')) {
83
+ customElements.define('stl-island', StlIsland);
84
+ }
85
+
86
+ declare global {
87
+ interface HTMLElementTagNameMap {
88
+ 'stl-island': StlIsland;
89
+ }
90
+ }
91
+ declare module 'react' {
92
+ // eslint-disable-next-line @typescript-eslint/no-namespace
93
+ namespace JSX {
94
+ interface IntrinsicElements {
95
+ // client-loaded
96
+ 'stl-island': React.DetailedHTMLProps<React.HTMLAttributes<StlIsland>, StlIsland> & {
97
+ component: keyof typeof componentsMap;
98
+ };
99
+ }
100
+ }
101
+ }
102
+
103
+ /** Renders all StainlessIslands that have been registered in the global `state.roots` map */
104
+ function StainlessIslandsInner() {
105
+ const roots = useSyncExternalStore<Map<HTMLElement, ReactNode> | null>(
106
+ (onChange) => {
107
+ state.onRootsChange = onChange;
108
+ return () => {
109
+ state.onRootsChange = undefined;
110
+ };
111
+ },
112
+ () => {
113
+ return state.roots;
114
+ },
115
+ () => null,
116
+ );
117
+ if (!roots) return null;
118
+ return [...roots.values()];
119
+ }
120
+
121
+ export function StainlessIslands() {
122
+ // Astro tries to call this function outside of react to detect if it's
123
+ // an Astro JSX or React JSX component, which causes warnings if we use hooks,
124
+ // so we use a wrapper component to avoid the hook calls.
125
+ return <StainlessIslandsInner />;
126
+ }
@@ -1,23 +1,25 @@
1
1
  ---
2
- import { Icon } from '@astrojs/starlight/components';
3
2
  import { DocsSearch } from './SearchIsland';
4
- import { SEARCH } from 'virtual:stl-starlight-virtual-module';
3
+ import { Button } from '@stainless-api/ui-primitives';
4
+ import { SearchIcon, XIcon } from 'lucide-react';
5
5
  ---
6
6
 
7
- <site-search>
8
- <button
9
- popovertarget="stldocs-search"
7
+ <site-search class={Astro.props.class}>
8
+ <Button
9
+ popoverTarget="stldocs-search"
10
10
  data-open-modal
11
11
  aria-label={Astro.locals.t('search.label')}
12
12
  aria-keyshortcuts="Control+K"
13
- class="stldocs-button stldocs-button-secondary"
13
+ variant="outline"
14
+ className="stl-algolia-search"
14
15
  >
15
- <Icon name="magnifier" />
16
+ <SearchIcon className="icon-search" size={14} />
17
+ <XIcon className="icon-close" size={14} />
16
18
  <span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
17
19
  <kbd class="sl-hidden md:sl-flex" style="display: none;">
18
20
  <kbd>{Astro.locals.t('search.ctrlKey')}</kbd><kbd>K</kbd>
19
21
  </kbd>
20
- </button>
22
+ </Button>
21
23
 
22
24
  <DocsSearch
23
25
  client:only="react"
@@ -31,27 +33,6 @@ import { SEARCH } from 'virtual:stl-starlight-virtual-module';
31
33
  />
32
34
  </site-search>
33
35
 
34
- {
35
- SEARCH?.enableAISearch === true && (
36
- <button id="chat-button" popovertarget="stldocs-chat" data-open-modal>
37
- <Icon name="comment" />
38
- </button>
39
- )
40
- }
41
-
42
- <style>
43
- #chat-button {
44
- background: var(--sl-color-bg-ui);
45
- border: 1px solid var(--sl-color-hairline);
46
- height: 2.25rem;
47
- width: 2.25rem;
48
-
49
- &:hover {
50
- border: 1px solid rgb(64, 64, 64);
51
- }
52
- }
53
- </style>
54
-
55
36
  <script is:inline>
56
37
  function setupShortcut() {
57
38
  const openBtn = document.querySelector('button[data-open-modal]');
@@ -78,3 +59,39 @@ import { SEARCH } from 'virtual:stl-starlight-virtual-module';
78
59
  }
79
60
  });
80
61
  </script>
62
+
63
+ <style is:inline>
64
+ .default-tabs-container .stl-algolia-search {
65
+ max-width: unset;
66
+ width: unset;
67
+ flex-grow: 1;
68
+ }
69
+ .stl-algolia-search {
70
+ padding: 0;
71
+ }
72
+ @media (min-width: 50rem) {
73
+ .stl-algolia-search {
74
+ padding: 8px 10px;
75
+ }
76
+ }
77
+
78
+ .stl-algolia-search {
79
+ .icon-search {
80
+ display: inline;
81
+ }
82
+
83
+ .icon-close {
84
+ display: none;
85
+ }
86
+ }
87
+
88
+ site-search:has(#stldocs-search[data-stldocs-modal-open='true']) .stl-algolia-search {
89
+ .icon-search {
90
+ display: none;
91
+ }
92
+
93
+ .icon-close {
94
+ display: inline;
95
+ }
96
+ }
97
+ </style>
@@ -1,33 +1,35 @@
1
- import * as React from 'react';
2
- import { BASE_PATH, SEARCH } from 'virtual:stl-starlight-virtual-module';
3
- import { parseRoute, generateRoute } from '@stainless-api/docs-ui/src/routing';
4
- import { SearchModal } from '@stainless-api/docs-ui/src/search/index';
5
- import { ChatModal } from '@stainless-api/docs-ui/src/components/chat';
1
+ import { useMemo } from 'react';
2
+ import { RESOLVED_API_REFERENCE_PATH, HIGHLIGHT_THEMES } from 'virtual:stl-starlight-virtual-module';
3
+ import { parseRoute, generateRoute } from '@stainless-api/docs-ui/routing';
4
+ import { SearchModal } from '@stainless-api/docs-search';
6
5
  import * as Markdoc from '@markdoc/markdoc';
7
6
  import { createHighlighter } from 'shiki';
8
7
  import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki';
9
8
 
10
9
  import {
11
10
  DocsProvider,
12
- MarkdownProvider,
11
+ type MarkdownContextValue,
13
12
  NavigationProvider,
14
- SearchProvider,
15
- } from '@stainless-api/docs-ui/src/contexts';
16
- import type { SearchSettings } from '@stainless-api/docs-ui/src/search/types';
13
+ SuspensefulMarkdownProvider,
14
+ } from '@stainless-api/docs-ui/contexts';
15
+ import { ComponentProvider } from '@stainless-api/docs-ui/contexts/component';
16
+ import { SearchProvider } from '@stainless-api/docs-search/context';
17
+ import type { SearchSettings } from '@stainless-api/docs-search/types';
17
18
 
18
- let $$highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null = null;
19
- async function getHighlighter() {
20
- if ($$highlighter === null) {
21
- $$highlighter = await createHighlighter({
19
+ declare global {
20
+ var __docsSearchShikiSingleton: Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> | undefined;
21
+ }
22
+ function getHighlighter() {
23
+ if (!globalThis.__docsSearchShikiSingleton) {
24
+ globalThis.__docsSearchShikiSingleton = createHighlighter({
22
25
  themes: ['github-dark'],
23
26
  langs: ['typescript', 'python', 'go', 'java', 'kotlin', 'ruby'],
24
27
  });
25
28
  }
26
-
27
- return $$highlighter;
29
+ return globalThis.__docsSearchShikiSingleton;
28
30
  }
29
31
 
30
- async function createMarkdownRenderer() {
32
+ async function createMarkdownRenderer(): Promise<MarkdownContextValue> {
31
33
  const highlighter = await getHighlighter();
32
34
  const markdocConfig: Markdoc.Config = {
33
35
  nodes: {
@@ -36,8 +38,15 @@ async function createMarkdownRenderer() {
36
38
  ...Markdoc.nodes.fence,
37
39
  transform(node, config) {
38
40
  const attributes = node.transformAttributes(config);
41
+ if (typeof node.attributes.language !== 'string' || typeof node.attributes.content !== 'string') {
42
+ throw new Error('Expected language and content to be strings');
43
+ }
39
44
  const lang = node.attributes.language === 'node' ? 'typescript' : node.attributes.language;
40
- const code = highlighter.codeToTokens(node.attributes.content, { lang, theme: 'github-dark' });
45
+ const code = highlighter.codeToTokens(node.attributes.content, {
46
+ // @ts-expect-error - TODO: narrowing
47
+ lang,
48
+ theme: 'github-dark',
49
+ });
41
50
 
42
51
  return new Markdoc.Tag(
43
52
  'pre',
@@ -59,42 +68,57 @@ async function createMarkdownRenderer() {
59
68
  transform(node, config) {
60
69
  const children = node.transformChildren(config);
61
70
  const attrs = node.transformAttributes(config);
62
- const href = attrs['href'].replace('docs://BASE_PATH', BASE_PATH);
71
+ if (typeof attrs['href'] !== 'string') throw new Error('Expected href to be a string');
72
+ const href = attrs['href'].replace('docs://BASE_PATH', RESOLVED_API_REFERENCE_PATH);
63
73
  return new Markdoc.Tag(this.render, { ...attrs, href }, children);
64
74
  },
65
75
  },
66
76
  },
67
77
  };
68
78
 
69
- return (content: string) => {
70
- const ast = Markdoc.parse(content);
71
- const transformed = Markdoc.transform(ast, markdocConfig);
72
- return Markdoc.renderers.html(transformed);
79
+ return {
80
+ render: (content: string) => {
81
+ const ast = Markdoc.parse(content);
82
+ const transformed = Markdoc.transform(ast, markdocConfig);
83
+ return Markdoc.renderers.html(transformed);
84
+ },
85
+ highlight: (content: string, language: string) => {
86
+ return highlighter.codeToHtml(content, {
87
+ lang: language ?? 'javascript',
88
+ themes: HIGHLIGHT_THEMES || {},
89
+ });
90
+ },
73
91
  };
74
92
  }
75
93
 
76
94
  export function DocsSearch({ settings, currentPath }: { settings: SearchSettings; currentPath: string }) {
77
- const renderMarkdown = React.use(createMarkdownRenderer());
78
- const { stainlessPath, language } = parseRoute(BASE_PATH, currentPath);
79
- const pageFind = import.meta.env.DEV ? undefined : '/pagefind/pagefind.js';
95
+ const rendererPromise = useMemo(() => createMarkdownRenderer(), []);
96
+ const { stainlessPath, language } = parseRoute(RESOLVED_API_REFERENCE_PATH, currentPath);
97
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
98
+ const pageFind = import.meta.env.DEV
99
+ ? undefined
100
+ : `${import.meta.env.BASE_URL}/pagefind/pagefind.js`.replace(/\/+/g, '/');
80
101
 
81
- function handleSelect(path: string) {
82
- const url = path.startsWith('/') ? path : generateRoute(BASE_PATH, language, path);
102
+ function handleSelect(selectedPath: string) {
103
+ const url = selectedPath.startsWith('/')
104
+ ? selectedPath
105
+ : generateRoute(RESOLVED_API_REFERENCE_PATH, language, selectedPath);
83
106
  if (url) window.location.href = url;
84
107
  }
85
108
 
86
109
  return (
87
110
  <DocsProvider spec={null} language={language}>
88
- <NavigationProvider basePath="/" selectedPath={stainlessPath}>
89
- <MarkdownProvider render={renderMarkdown}>
90
- <SearchProvider onSelect={handleSelect} pageFind={pageFind} settings={settings}>
91
- <div className="stldocs-root">
92
- <SearchModal id="stldocs-search" />
93
- {SEARCH?.enableAISearch === true && <ChatModal id="stldocs-chat" />}
94
- </div>
95
- </SearchProvider>
96
- </MarkdownProvider>
97
- </NavigationProvider>
111
+ <ComponentProvider>
112
+ <NavigationProvider basePath="/" selectedPath={stainlessPath}>
113
+ <SuspensefulMarkdownProvider value={rendererPromise}>
114
+ <SearchProvider onSelect={handleSelect} pageFind={pageFind} settings={settings}>
115
+ <div className="stldocs-root">
116
+ <SearchModal id="stldocs-search" />
117
+ </div>
118
+ </SearchProvider>
119
+ </SuspensefulMarkdownProvider>
120
+ </NavigationProvider>
121
+ </ComponentProvider>
98
122
  </DocsProvider>
99
123
  );
100
124
  }
@@ -1,47 +1,7 @@
1
1
  // This is probably temporary, but it fills in functionality needed for Mintlify imports
2
2
 
3
- import type { StarlightRouteData } from '@astrojs/starlight/route-data';
4
- import type * as SDKJSON from '~/lib/json-spec-v2/types';
5
- import { walkTree } from '@stainless-api/docs-ui/src/routing';
6
-
7
3
  const INTERNAL_REFERENCE_ENTRY_MARKER = 'STL_STARLIGHT_API_REFERENCE_METHOD_LINK_PLACEHOLDER';
8
4
 
9
- type SidebarEntry = StarlightRouteData['sidebar'][number];
10
-
11
- type SidebarLink = Extract<SidebarEntry, { href: string }>;
12
-
13
- export function getMethodFromSDKJSON(spec: SDKJSON.Spec, endpoint: string) {
14
- for (const entry of walkTree(spec)) {
15
- if (entry.data.kind === 'http_method' && entry.data.endpoint === endpoint) {
16
- return entry.data;
17
- }
18
- }
19
- throw new Error(`Endpoint ${endpoint} not found in API`);
20
- }
21
-
22
- export function recursiveReplacePlaceholderItems(
23
- sidebar: SidebarEntry[],
24
- modifyFn: (entry: SidebarLink, props: { endpoint: string; label?: string }) => void,
25
- ) {
26
- for (const entry of sidebar) {
27
- const endpoint = 'attrs' in entry && entry.attrs?.['data-stldocs-endpoint'];
28
- if (
29
- 'attrs' in entry &&
30
- entry.attrs?.about === INTERNAL_REFERENCE_ENTRY_MARKER &&
31
- endpoint &&
32
- typeof endpoint === 'string'
33
- ) {
34
- modifyFn(entry, {
35
- endpoint,
36
- label: entry.attrs?.['data-stldocs-label'],
37
- });
38
- }
39
- if ('entries' in entry) {
40
- recursiveReplacePlaceholderItems(entry.entries, modifyFn);
41
- }
42
- }
43
- }
44
-
45
5
  type GenerateProps = string | { label?: string; endpoint: string };
46
6
 
47
7
  function normalizeGenerateProps(generateProps: GenerateProps) {
@@ -0,0 +1,248 @@
1
+ import { initDropdownButton } from '@stainless-api/ui-primitives/scripts';
2
+ import { getPageLoadEvent } from '../helpers/getPageLoadEvent';
3
+
4
+ export type DropdownIcon = 'markdown' | 'copy' | 'claude' | 'chatgpt' | 'gemini' | 'cursor';
5
+
6
+ interface DropdownOptionInputProps {
7
+ onClick: () => void;
8
+ icon: DropdownIcon;
9
+ primaryAction?: boolean;
10
+ clientHidden?: () => boolean;
11
+ external: boolean;
12
+ }
13
+
14
+ function option(label: string[] | string, props: DropdownOptionInputProps) {
15
+ const labelArr = typeof label === 'string' ? [label] : label;
16
+ return {
17
+ ...props,
18
+ label: labelArr,
19
+ primaryAction: props.primaryAction ?? false,
20
+ clientHidden: props.clientHidden ?? (() => false),
21
+ id: labelArr.join('').toLowerCase().replace(/ /g, '-'),
22
+ };
23
+ }
24
+
25
+ type DropdownOption = ReturnType<typeof option>;
26
+
27
+ function getMarkdownUrl(type: 'relative' | 'absolute') {
28
+ const currentUrl = new URL(window.location.href);
29
+ const hasTrailingSlash = currentUrl.pathname.endsWith('/');
30
+
31
+ const markdownUrl = [
32
+ type === 'absolute' ? currentUrl.origin : '',
33
+ currentUrl.pathname,
34
+ hasTrailingSlash ? '' : '/',
35
+ 'index.md',
36
+ ].join('');
37
+
38
+ return markdownUrl;
39
+ }
40
+
41
+ function getURLEncodedPrompt() {
42
+ const mdUrl = getMarkdownUrl('absolute');
43
+ const aiPrompt = encodeURIComponent(
44
+ `Load the contents of ${mdUrl} into this chat's context so we can discuss it.`,
45
+ );
46
+ return aiPrompt;
47
+ }
48
+
49
+ function openDeepLink({ deepLinkUrl, fallbackUrl }: { deepLinkUrl: string; fallbackUrl: string }) {
50
+ if (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrom')) {
51
+ // safari doesn't let us detect if the deep link worked
52
+ window.open(fallbackUrl, '_blank');
53
+ return;
54
+ }
55
+
56
+ const controller = new AbortController();
57
+ let frame: HTMLIFrameElement | null;
58
+
59
+ // We are using a native deep link with a fallback web url.
60
+ if (navigator.userAgent.includes('Chrom')) {
61
+ // In Chrome, load it in a hidden frame, this shows the "Do you want to open ...?" prompt, but unlike
62
+ // top level navigation this preserves our userActivation, so we can open the fallback if it fails.
63
+ frame = Object.assign(document.createElement('iframe'), { src: deepLinkUrl });
64
+ document.head.append(frame);
65
+ } else {
66
+ // In Firefox do the opposite.
67
+ location.href = deepLinkUrl;
68
+ }
69
+
70
+ // The popup (in non-Safari browsers) fires a `blur` event.
71
+ window.addEventListener(
72
+ 'blur',
73
+ () => {
74
+ controller.abort();
75
+ },
76
+ controller,
77
+ );
78
+
79
+ // If it's been 300ms with no popup, open the fallback web url.
80
+ const timeout = setTimeout(() => {
81
+ window.open(fallbackUrl, '_blank');
82
+ }, 300);
83
+
84
+ controller.signal.addEventListener('abort', () => {
85
+ clearTimeout(timeout);
86
+ frame?.remove();
87
+ });
88
+ }
89
+
90
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop
91
+ const hasMobileUserAgent = () => navigator.userAgent.includes('Mobi');
92
+
93
+ // 2d array of dropdown options
94
+ // each sub-array is a group, separated by a horizontal rule in the UI
95
+ const aiDropdownOptions: DropdownOption[][] = [
96
+ [
97
+ option(['Open in ', 'Claude'], {
98
+ onClick: () => {
99
+ window.open(`https://claude.ai/new?q=${getURLEncodedPrompt()}`, '_blank');
100
+ },
101
+ icon: 'claude',
102
+ primaryAction: false,
103
+ external: true,
104
+ }),
105
+ option(['Open in ', 'ChatGPT'], {
106
+ onClick: () => {
107
+ window.open(`https://chatgpt.com/?hints=search&prompt=${getURLEncodedPrompt()}`, '_blank');
108
+ },
109
+ icon: 'chatgpt',
110
+ primaryAction: false,
111
+ external: true,
112
+ }),
113
+ option(['Open in ', 'Cursor'], {
114
+ onClick: () => {
115
+ const aiPrompt = getURLEncodedPrompt();
116
+ openDeepLink({
117
+ deepLinkUrl: `cursor://anysphere.cursor-deeplink/prompt?text=${aiPrompt}`,
118
+ fallbackUrl: `https://cursor.com/link/prompt?text=${aiPrompt}`,
119
+ });
120
+ },
121
+ clientHidden: hasMobileUserAgent,
122
+ icon: 'cursor',
123
+ primaryAction: false,
124
+ external: true,
125
+ }),
126
+ ],
127
+ [
128
+ option('Copy Markdown', {
129
+ onClick: () => {
130
+ // Source: https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/
131
+ const markdownUrl = getMarkdownUrl('relative');
132
+ // ClipboardItem doesn't exist in every browser
133
+ // eslint-disable-next-line no-constant-binary-expression
134
+ if (typeof ClipboardItem && navigator.clipboard.write) {
135
+ // NOTE: Safari locks down the clipboard API to only work when triggered
136
+ // by a direct user interaction. You can't use it async in a promise.
137
+ // But! You can wrap the promise in a ClipboardItem, and give that to
138
+ // the clipboard API.
139
+ // Found this on https://developer.apple.com/forums/thread/691873
140
+ const text = new ClipboardItem({
141
+ 'text/plain': fetch(markdownUrl)
142
+ .then((response) => response.text())
143
+ .then((text) => new Blob([text], { type: 'text/plain' })),
144
+ });
145
+ navigator.clipboard.write([text]).catch(() => {
146
+ console.error('Failed to copy to clipboard using ClipboardItem');
147
+ });
148
+ } else {
149
+ // NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
150
+ // but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
151
+ // Good news is that other than Safari, Firefox does not care about
152
+ // Clipboard API being used async in a Promise.
153
+ fetch(markdownUrl)
154
+ .then((response) => response.text())
155
+ .then((text) => navigator.clipboard.writeText(text))
156
+ .catch(() => {
157
+ console.error('Failed to copy to clipboard');
158
+ });
159
+ }
160
+ },
161
+ icon: 'copy',
162
+ primaryAction: true,
163
+ external: false,
164
+ }),
165
+ option('View as Markdown', {
166
+ onClick: () => {
167
+ window.open(getMarkdownUrl('absolute'), '_blank');
168
+ },
169
+ icon: 'markdown',
170
+ primaryAction: false,
171
+ external: true,
172
+ }),
173
+ ],
174
+ ];
175
+
176
+ // TODO: Add support for more LLMs
177
+ // {
178
+ // label: ['Open in ', 'Gemini'],
179
+ // onClick: () => {
180
+ // openInLLM('https://gemini.google.com?prompt_action=prefill&prompt_text=');
181
+ // },
182
+ // icon: 'gemini',
183
+ // primaryAction: false,
184
+ // },
185
+
186
+ export function getAIDropdownOptions() {
187
+ const renderedOptions = aiDropdownOptions.map((group, index) => {
188
+ return {
189
+ options: group,
190
+ isLast: index === aiDropdownOptions.length - 1,
191
+ reactKey: index,
192
+ };
193
+ });
194
+
195
+ const allOptions = renderedOptions.flatMap((group) => group.options);
196
+ const primaryAction = allOptions.find((o) => o.primaryAction) ?? allOptions[0]!;
197
+
198
+ return {
199
+ primaryAction,
200
+ groups: renderedOptions,
201
+ };
202
+ }
203
+
204
+ export function wireAIDropdown() {
205
+ const { primaryAction, groups } = getAIDropdownOptions();
206
+ const flatOptions = groups.flatMap((group) => group.options);
207
+ function triggerOption(id: string) {
208
+ const option = flatOptions.find((option) => option.id === id);
209
+ if (!option) return;
210
+ option.onClick();
211
+ }
212
+
213
+ document.addEventListener(getPageLoadEvent(), () => {
214
+ // we hide the Cursor option on non-desktop devices
215
+ for (const option of flatOptions) {
216
+ if (option.clientHidden() === true) {
217
+ const el = document.querySelector(
218
+ `[data-dropdown-id="ai-dropdown-button"] [data-value="${option.id}"]`,
219
+ );
220
+ if (el) {
221
+ el.remove();
222
+ }
223
+ }
224
+ }
225
+
226
+ const dropdowns = document.querySelectorAll('[data-dropdown-id="ai-dropdown-button"]');
227
+
228
+ dropdowns.forEach((dropdown) => {
229
+ initDropdownButton({
230
+ dropdown: dropdown,
231
+ onSelect: (value) => {
232
+ triggerOption(value);
233
+ },
234
+ onPrimaryAction: (el) => {
235
+ triggerOption(primaryAction.id);
236
+ const span = el.querySelector('[data-part="primary-action-text"]');
237
+ if (span) {
238
+ const originalContent = span.textContent;
239
+ span.textContent = 'Copied!';
240
+ setTimeout(() => {
241
+ span.textContent = originalContent;
242
+ }, 2000);
243
+ }
244
+ },
245
+ });
246
+ });
247
+ });
248
+ }