@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.
- package/CHANGELOG.md +1102 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +90 -0
- package/{eslint.config.js → eslint.config.ts} +0 -2
- package/locals.d.ts +17 -0
- package/package.json +62 -44
- package/playground-virtual-modules.d.ts +96 -0
- package/plugin/assets/languages/cli.svg +14 -0
- package/plugin/assets/languages/csharp.svg +1 -0
- package/plugin/assets/languages/php.svg +4 -0
- package/plugin/buildAlgoliaIndex.ts +40 -39
- package/plugin/components/MethodDescription.tsx +54 -0
- package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
- package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
- package/plugin/components/RequestBuilder/index.tsx +40 -0
- package/plugin/components/RequestBuilder/props.ts +9 -0
- package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
- package/plugin/components/RequestBuilder/styles.css +67 -0
- package/plugin/components/SDKSelect.astro +18 -111
- package/plugin/components/SnippetCode.tsx +112 -70
- package/plugin/components/StainlessIslands.tsx +126 -0
- package/plugin/components/search/SearchAlgolia.astro +46 -29
- package/plugin/components/search/SearchIsland.tsx +61 -37
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/globalJs/ai-dropdown-options.ts +248 -0
- package/plugin/globalJs/code-snippets.ts +45 -16
- package/plugin/globalJs/copy.ts +115 -27
- package/plugin/globalJs/create-playground.shim.ts +3 -0
- package/plugin/globalJs/method-descriptions.ts +33 -0
- package/plugin/globalJs/navigation.ts +24 -44
- package/plugin/globalJs/playground-data.shim.ts +1 -0
- package/plugin/globalJs/playground-data.ts +14 -0
- package/plugin/globalJs/summary-selection-tweak.ts +29 -0
- package/plugin/helpers/generateDocsRoutes.ts +59 -0
- package/plugin/helpers/multiSpec.ts +8 -0
- package/plugin/index.ts +317 -141
- package/plugin/languages.ts +8 -2
- package/plugin/loadPluginConfig.ts +284 -109
- package/plugin/markdown/highlighter.ts +100 -0
- package/plugin/markdown/index.ts +39 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
- package/plugin/react/Routing.tsx +98 -263
- package/plugin/referencePlaceholderUtils.ts +17 -14
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +39 -35
- package/plugin/routes/Docs.astro +72 -111
- package/plugin/routes/DocsStatic.astro +6 -5
- package/plugin/routes/Overview.astro +46 -22
- package/plugin/routes/llms.ts +186 -0
- package/plugin/routes/markdown.ts +13 -12
- package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +84 -69
- package/plugin/specs/FileCache.ts +99 -0
- package/plugin/specs/fetchSpecSSR.ts +27 -0
- package/plugin/specs/generateSpec.ts +112 -0
- package/plugin/specs/index.ts +132 -0
- package/plugin/specs/inputResolver.ts +148 -0
- package/plugin/{cms → specs}/worker.ts +82 -5
- package/plugin/vendor/preview.worker.docs.js +27121 -16890
- package/plugin/vendor/templates/cli.md +1 -0
- package/plugin/vendor/templates/go.md +4 -2
- package/plugin/vendor/templates/java.md +5 -1
- package/plugin/vendor/templates/kotlin.md +5 -1
- package/plugin/vendor/templates/node.md +4 -2
- package/plugin/vendor/templates/python.md +4 -2
- package/plugin/vendor/templates/ruby.md +4 -2
- package/plugin/vendor/templates/terraform.md +1 -1
- package/plugin/vendor/templates/typescript.md +3 -1
- package/resolveSrcFile.ts +10 -0
- package/scripts/vendor_deps.ts +5 -5
- package/shared/conditionalIntegration.ts +28 -0
- package/shared/getProsePages.ts +41 -0
- package/shared/getSharedLogger.ts +15 -0
- package/shared/terminalUtils.ts +3 -0
- package/shared/virtualModule.ts +46 -1
- package/src/content.config.ts +9 -0
- package/stl-docs/aiChatExamples.ts +95 -0
- package/stl-docs/chat/docs-chat-handler.ts +18 -0
- package/stl-docs/chat/hook.ts +215 -0
- package/stl-docs/chat/schemas.ts +70 -0
- package/stl-docs/chat/stainless-handler/index.ts +126 -0
- package/stl-docs/chat/stream-util.ts +16 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +188 -0
- package/stl-docs/chat/ui/Trigger.tsx +154 -0
- package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
- package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
- package/stl-docs/chat/ui/components/ChatLog.tsx +96 -0
- package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
- package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +109 -0
- package/stl-docs/chat/ui/components/Table.tsx +15 -0
- package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
- package/stl-docs/chat/ui/components/hljs-github.css +81 -0
- package/stl-docs/chat/ui/scroll-manager.ts +86 -0
- package/stl-docs/chat/ui/types.ts +45 -0
- package/stl-docs/components/AIDropdown.tsx +63 -0
- package/stl-docs/components/AiChatIsland.tsx +16 -0
- package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
- package/stl-docs/components/ContentPanel.astro +9 -0
- package/stl-docs/components/Footer.astro +89 -0
- package/stl-docs/components/Head.astro +20 -0
- package/stl-docs/components/Header.astro +3 -9
- package/stl-docs/components/PageFrame.astro +37 -0
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/PageTitle.astro +82 -0
- package/stl-docs/components/StainlessLogo.svg +4 -0
- package/stl-docs/components/ThemeProvider.astro +36 -0
- package/stl-docs/components/ThemeSelect.astro +84 -146
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/headers/DefaultHeader.astro +6 -8
- package/stl-docs/components/headers/StackedHeader.astro +10 -53
- package/stl-docs/components/icons/chat-gpt.tsx +2 -2
- package/stl-docs/components/icons/cursor.tsx +10 -0
- package/stl-docs/components/icons/gemini.tsx +19 -0
- package/stl-docs/components/icons/markdown.tsx +1 -1
- package/stl-docs/components/index.ts +1 -0
- package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
- package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
- package/stl-docs/components/mintlify-compat/Frame.astro +6 -6
- package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
- package/stl-docs/components/mintlify-compat/card.css +4 -4
- package/stl-docs/components/mintlify-compat/index.ts +2 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +38 -77
- package/stl-docs/components/nav-tabs/NavTabs.astro +81 -81
- package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +1 -2
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +5 -2
- package/stl-docs/components/pagination/HomeLink.astro +10 -0
- package/stl-docs/components/pagination/Pagination.astro +177 -0
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
- package/stl-docs/components/pagination/util.ts +71 -0
- package/stl-docs/components/scripts.ts +1 -0
- package/stl-docs/components/sidebars/BaseSidebar.astro +80 -2
- package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
- package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
- package/stl-docs/disableCalloutSyntax.ts +36 -0
- package/stl-docs/fonts.ts +186 -0
- package/stl-docs/index.ts +176 -58
- package/stl-docs/loadStlDocsConfig.ts +73 -8
- package/stl-docs/proseDocSync.test.ts +74 -0
- package/stl-docs/proseDocSync.ts +344 -0
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
- package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
- package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
- package/stl-docs/proseSearchIndexing.ts +218 -0
- package/stl-docs/tabsMiddleware.ts +14 -5
- package/styles/code.css +53 -49
- package/styles/links.css +2 -37
- package/styles/method-descriptions.css +36 -0
- package/styles/overrides.css +28 -46
- package/styles/page.css +228 -38
- package/styles/sdk_select.css +9 -6
- package/styles/search.css +11 -21
- package/styles/sidebar.css +28 -215
- package/styles/{variables.css → sl-variables.css} +4 -8
- package/styles/stldocs-variables.css +6 -0
- package/styles/toc.css +19 -8
- package/theme.css +11 -9
- package/tsconfig.json +1 -4
- package/virtual-module.d.ts +66 -8
- package/components/variables.css +0 -112
- package/plugin/cms/client.ts +0 -62
- package/plugin/cms/server.ts +0 -268
- package/plugin/globalJs/ai-dropdown.ts +0 -57
- package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
- package/stl-docs/components/ClientRouterHead.astro +0 -41
- package/stl-docs/components/content-panel/ContentPanel.astro +0 -69
- package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
- package/stl-docs/components/mintlify-compat/Step.astro +0 -56
- package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
- package/styles/fonts.css +0 -68
- /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
- /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 {
|
|
3
|
+
import { Button } from '@stainless-api/ui-primitives';
|
|
4
|
+
import { SearchIcon, XIcon } from 'lucide-react';
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<site-search>
|
|
8
|
-
<
|
|
9
|
-
|
|
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
|
-
|
|
13
|
+
variant="outline"
|
|
14
|
+
className="stl-algolia-search"
|
|
14
15
|
>
|
|
15
|
-
<
|
|
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
|
-
</
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import { parseRoute, generateRoute } from '@stainless-api/docs-ui/
|
|
4
|
-
import { SearchModal } from '@stainless-api/docs-
|
|
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
|
-
|
|
11
|
+
type MarkdownContextValue,
|
|
13
12
|
NavigationProvider,
|
|
14
|
-
|
|
15
|
-
} from '@stainless-api/docs-ui/
|
|
16
|
-
import
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
78
|
-
const { stainlessPath, language } = parseRoute(
|
|
79
|
-
|
|
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(
|
|
82
|
-
const url =
|
|
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
|
-
<
|
|
89
|
-
<
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|