@stainless-api/docs 0.1.0-beta.6 → 0.1.0-beta.61

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 (115) hide show
  1. package/CHANGELOG.md +486 -0
  2. package/README.md +1 -1
  3. package/eslint-suppressions.json +52 -0
  4. package/locals.d.ts +16 -0
  5. package/package.json +45 -40
  6. package/plugin/assets/languages/csharp.svg +1 -0
  7. package/plugin/buildAlgoliaIndex.ts +32 -7
  8. package/plugin/cms/server.ts +130 -58
  9. package/plugin/cms/sidebar-builder.ts +7 -26
  10. package/plugin/cms/worker.ts +83 -5
  11. package/plugin/components/MethodDescription.tsx +54 -0
  12. package/plugin/components/SDKSelect.astro +7 -87
  13. package/plugin/components/SnippetCode.tsx +53 -8
  14. package/plugin/components/search/SearchAlgolia.astro +14 -26
  15. package/plugin/components/search/SearchIsland.tsx +38 -24
  16. package/plugin/create-playground.shim.tsx +3 -0
  17. package/plugin/generateAPIReferenceLink.ts +2 -2
  18. package/plugin/globalJs/ai-dropdown-options.ts +235 -0
  19. package/plugin/globalJs/code-snippets.ts +15 -8
  20. package/plugin/globalJs/copy.ts +81 -16
  21. package/plugin/globalJs/method-descriptions.ts +33 -0
  22. package/plugin/globalJs/navigation.ts +7 -4
  23. package/plugin/index.ts +179 -35
  24. package/plugin/languages.ts +5 -2
  25. package/plugin/loadPluginConfig.ts +121 -32
  26. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +1 -1
  27. package/plugin/react/Routing.tsx +208 -104
  28. package/plugin/referencePlaceholderUtils.ts +1 -1
  29. package/plugin/replaceSidebarPlaceholderMiddleware.ts +5 -1
  30. package/plugin/routes/Docs.astro +61 -83
  31. package/plugin/routes/Overview.astro +10 -16
  32. package/plugin/routes/markdown.ts +7 -7
  33. package/plugin/vendor/preview.worker.docs.js +19768 -17702
  34. package/plugin/vendor/templates/go.md +1 -1
  35. package/plugin/vendor/templates/python.md +1 -1
  36. package/resolveSrcFile.ts +10 -0
  37. package/scripts/vendor_deps.ts +5 -5
  38. package/shared/getSharedLogger.ts +15 -0
  39. package/shared/terminalUtils.ts +3 -0
  40. package/src/content.config.ts +9 -0
  41. package/stl-docs/components/AIDropdown.tsx +63 -0
  42. package/stl-docs/components/AiChatIsland.tsx +10 -0
  43. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +10 -18
  44. package/stl-docs/components/Head.astro +16 -0
  45. package/stl-docs/components/Header.astro +6 -8
  46. package/stl-docs/components/PageFrame.astro +14 -0
  47. package/stl-docs/components/PageTitle.astro +82 -0
  48. package/stl-docs/components/TableOfContents.astro +34 -0
  49. package/stl-docs/components/ThemeSelect.astro +118 -136
  50. package/stl-docs/components/content-panel/ContentPanel.astro +16 -25
  51. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +17 -1
  52. package/stl-docs/components/headers/StackedHeader.astro +29 -24
  53. package/stl-docs/components/icons/chat-gpt.tsx +17 -0
  54. package/stl-docs/components/icons/claude.tsx +10 -0
  55. package/stl-docs/components/icons/cursor.tsx +10 -0
  56. package/stl-docs/components/icons/gemini.tsx +19 -0
  57. package/stl-docs/components/icons/markdown.tsx +10 -0
  58. package/stl-docs/components/index.ts +1 -0
  59. package/stl-docs/components/mintlify-compat/Accordion.astro +7 -5
  60. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +7 -3
  61. package/stl-docs/components/mintlify-compat/Columns.astro +40 -42
  62. package/stl-docs/components/mintlify-compat/Frame.astro +16 -18
  63. package/stl-docs/components/mintlify-compat/Step.astro +30 -32
  64. package/stl-docs/components/mintlify-compat/Steps.astro +8 -10
  65. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +1 -1
  66. package/stl-docs/components/mintlify-compat/callouts/Check.astro +1 -1
  67. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +1 -1
  68. package/stl-docs/components/mintlify-compat/callouts/Info.astro +1 -1
  69. package/stl-docs/components/mintlify-compat/callouts/Note.astro +1 -1
  70. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +1 -1
  71. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +1 -1
  72. package/stl-docs/components/mintlify-compat/card.css +33 -35
  73. package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -70
  74. package/stl-docs/components/nav-tabs/NavTabs.astro +78 -80
  75. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -8
  76. package/stl-docs/components/nav-tabs/buildNavLinks.ts +3 -2
  77. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  78. package/stl-docs/components/pagination/Pagination.astro +175 -0
  79. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  80. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  81. package/stl-docs/components/pagination/util.ts +71 -0
  82. package/stl-docs/components/scripts.ts +1 -0
  83. package/stl-docs/disableCalloutSyntax.ts +36 -0
  84. package/stl-docs/index.ts +130 -48
  85. package/stl-docs/loadStlDocsConfig.ts +44 -4
  86. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +64 -0
  87. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +34 -0
  88. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  89. package/stl-docs/proseSearchIndexing.ts +113 -0
  90. package/stl-docs/tabsMiddleware.ts +11 -3
  91. package/styles/code.css +108 -140
  92. package/styles/fonts.css +32 -17
  93. package/styles/links.css +11 -48
  94. package/styles/method-descriptions.css +36 -0
  95. package/styles/overrides.css +48 -60
  96. package/styles/page.css +92 -52
  97. package/styles/sdk_select.css +9 -7
  98. package/styles/search.css +58 -69
  99. package/styles/sidebar.css +211 -131
  100. package/styles/{variables.css → sl-variables.css} +3 -2
  101. package/styles/stldocs-variables.css +6 -0
  102. package/styles/toc.css +41 -34
  103. package/theme.css +10 -10
  104. package/tsconfig.json +2 -5
  105. package/virtual-module.d.ts +23 -3
  106. package/components/variables.css +0 -135
  107. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  108. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  109. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  110. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  111. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  112. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  113. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  114. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  115. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
- import type { DocsLanguage } from '@stainless-api/docs-ui/src/routing';
3
- import { parseRoute } from '@stainless-api/docs-ui/src/routing';
2
+ import type { DocsLanguage } from '@stainless-api/docs-ui/routing';
3
+ import { parseRoute } from '@stainless-api/docs-ui/routing';
4
4
  import { cmsClient } from '../cms/client';
5
5
  import { BASE_PATH, DEFAULT_LANGUAGE, EXCLUDE_LANGUAGES } from 'virtual:stl-starlight-virtual-module';
6
6
  import { Languages } from '../languages';
@@ -53,100 +53,20 @@ const readmeSlug = language === 'http' ? BASE_PATH : `${BASE_PATH}/${language}`;
53
53
  )
54
54
  }
55
55
 
56
- <style>
57
- @layer starlight.core {
58
- label {
59
- --sl-label-icon-size: 16px;
60
- --sl-caret-size: 1.25rem;
61
- --sl-inline-padding: 0.5rem;
62
- position: relative;
63
- display: flex;
64
- align-items: center;
65
- gap: 0.25rem;
66
- color: var(--sl-color-gray-1);
67
- }
68
-
69
- label:hover {
70
- color: var(--sl-color-gray-2);
71
- }
72
-
73
- .icon {
74
- position: absolute;
75
- top: 50%;
76
- transform: translateY(-50%);
77
- pointer-events: none;
78
- width: 16px;
79
- }
80
-
81
- select {
82
- padding-block: 0.3rem;
83
- padding-inline: calc(var(--sl-label-icon-size) + var(--sl-inline-padding) + 0.5rem)
84
- calc(var(--sl-caret-size) + var(--sl-inline-padding) + 0.25rem);
85
- margin-inline: calc(var(--sl-inline-padding) * -1);
86
- width: calc(var(--sl-select-width) + var(--sl-inline-padding) * 2);
87
- text-overflow: ellipsis;
88
- color: inherit;
89
- cursor: pointer;
90
- appearance: none;
91
- font-weight: 600;
92
- text-transform: capitalize;
93
- }
94
-
95
- select:active {
96
- font-weight: inherit;
97
- /* font-family: sans-serif;
98
- font-weight: 400; */
99
- }
100
-
101
- option {
102
- background-color: var(--sl-color-bg-nav);
103
- color: var(--sl-color-gray-1);
104
- }
105
-
106
- @media (min-width: 50rem) {
107
- select {
108
- font-size: var(--sl-text-sm);
109
- }
110
- }
111
- }
112
-
113
- @layer starlight.components {
114
- .label-icon {
115
- font-size: var(--sl-label-icon-size);
116
- inset-inline-start: 0;
117
- }
118
-
119
- .caret {
120
- font-size: var(--sl-caret-size);
121
- inset-inline-end: 0;
122
- }
123
- }
124
-
125
- .custom-select-wrapper {
126
- --sl-inline-padding: 0.5rem;
127
- position: relative;
128
- display: inline-block;
129
- /* These match the padding on the sidebar menu */
130
- padding-left: var(--sl-inline-padding);
131
- padding-right: var(--sl-inline-padding);
132
-
133
- .icon.http path {
134
- fill: var(--sl-color-text);
135
- }
136
- }
137
- </style>
138
56
  <script>
139
57
  import { navigate } from 'astro:transitions/client';
140
58
  import { updateSelectedLanguage } from '../languages';
141
- import { initDropdown } from '@stainless-api/docs-ui/src/components/scripts/dropdown';
59
+ import { initDropdown } from '@stainless-api/docs/components/scripts';
142
60
  import { BASE_PATH } from 'virtual:stl-starlight-virtual-module';
143
61
  import { getPageLoadEvent } from '../helpers/getPageLoadEvent';
144
62
 
145
63
  document.addEventListener(getPageLoadEvent(), () => {
64
+ const sdkSelect = document.getElementById('sidebar-sdk-select');
65
+ if (!sdkSelect) return;
146
66
  initDropdown({
147
- dropdownId: 'sidebar-sdk-select',
67
+ root: sdkSelect,
148
68
  onSelect: (value) => {
149
- const originalLanguage = document.getElementById('sidebar-sdk-select')?.dataset.currentValue;
69
+ const originalLanguage = sdkSelect.dataset.currentValue;
150
70
  navigate(updateSelectedLanguage(BASE_PATH, originalLanguage, value));
151
71
  },
152
72
  });
@@ -2,12 +2,38 @@ import type {
2
2
  SnippetCodeProps,
3
3
  SnippetContainerProps,
4
4
  SnippetRequestContainerProps,
5
- } from '@stainless-api/docs-ui/src/components';
6
- import { useHighlight, useLanguage } from '@stainless-api/docs-ui/src/contexts';
7
- import style from '@stainless-api/docs-ui/src/style';
5
+ } from '@stainless-api/docs-ui/components';
6
+ import { useHighlight, useLanguage } from '@stainless-api/docs-ui/contexts';
7
+ import style from '@stainless-api/docs-ui/style';
8
8
  import * as cheerio from 'cheerio/slim';
9
- import { EXPERIMENTAL_COLLAPSIBLE_SNIPPETS } from 'virtual:stl-starlight-virtual-module';
9
+ import {
10
+ EXPERIMENTAL_COLLAPSIBLE_SNIPPETS,
11
+ EXPERIMENTAL_PLAYGROUNDS,
12
+ } from 'virtual:stl-starlight-virtual-module';
10
13
  import clsx from 'clsx';
14
+ import { Button } from '@stainless-api/ui-primitives';
15
+ import { CopyIcon } from 'lucide-react';
16
+
17
+ function PlaygroundIcon() {
18
+ return (
19
+ <svg
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ width={16}
22
+ height={16}
23
+ viewBox="0 0 24 24"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ strokeWidth={2}
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ className={'lucide ' + style.Icon}
30
+ aria-hidden="true"
31
+ >
32
+ <path d="m 1,2 h 1 a 4,4 0 0 1 4,4 v 1 m 5,15 H 10 A 4,4 0 0 1 6,18 V 6 a 4,4 0 0 1 4,-4 h 1 M 1,22 H 2 A 4,4 0 0 0 6,18 V 17 M 14.029059,8.147837 A 1.2853426,1.2853426 0 0 1 15.978924,7.0437277 L 22.40178,10.8959 a 1.2853426,1.2853426 0 0 1 0,2.208219 l -6.422856,3.852172 a 1.2853426,1.2853426 0 0 1 -1.949865,-1.105395 z" />
33
+ </svg>
34
+ );
35
+ }
36
+
11
37
  /*
12
38
  * This may be replaced by additional data from the sdk.
13
39
  * Without information from the sdk, we use simple heuristics per language.
@@ -53,7 +79,7 @@ function wrapFirstNSpaces($line: cheerio.Cheerio<any>, n: number) {
53
79
  const m = inner.match(new RegExp(`^( {1,${n}})`));
54
80
  if (!m) return;
55
81
 
56
- const lead = m[1];
82
+ const lead = m[1]!;
57
83
  $firstSpan.html(`<span class="leading-ws">${lead}</span>${inner.slice(lead.length)}`);
58
84
  }
59
85
 
@@ -130,12 +156,14 @@ export function SnippetRequestContainer({ children, signature }: SnippetRequestC
130
156
  <div className="stl-snippet-request-container">
131
157
  {children}
132
158
  {signature && isCollapsible && (
133
- <button
134
- className={clsx(style.Button, style.ButtonSecondary, 'stl-snippet-expand-button')}
159
+ <Button
160
+ className={'stl-snippet-expand-button'}
135
161
  id="stl-snippet-expand-button"
162
+ size="sm"
163
+ variant="outline"
136
164
  >
137
165
  Show more
138
- </button>
166
+ </Button>
139
167
  )}
140
168
  </div>
141
169
  );
@@ -181,6 +209,23 @@ export function CondensibleSnippetCode({
181
209
  );
182
210
  }
183
211
 
212
+ export function SnippetButtons({ content }: { content: string }) {
213
+ void content;
214
+ const language = useLanguage();
215
+ return (
216
+ <>
217
+ <Button variant="outline" data-stldocs-snippet-copy>
218
+ <CopyIcon size={16} className={style.Icon} />
219
+ </Button>
220
+ {EXPERIMENTAL_PLAYGROUNDS &&
221
+ (language === 'python' || language === 'typescript' || language === 'http') && (
222
+ <Button data-stldocs-snippet-play variant="muted" border title="Play">
223
+ <PlaygroundIcon />
224
+ </Button>
225
+ )}
226
+ </>
227
+ );
228
+ }
184
229
  export function SnippetCode({ content, signature, language: forcedLanguage }: SnippetCodeProps) {
185
230
  const lang = useLanguage();
186
231
  const language = forcedLanguage || lang;
@@ -1,23 +1,24 @@
1
1
  ---
2
2
  import { Icon } from '@astrojs/starlight/components';
3
3
  import { DocsSearch } from './SearchIsland';
4
- import { SEARCH } from 'virtual:stl-starlight-virtual-module';
4
+ import { Button } from '@stainless-api/ui-primitives';
5
5
  ---
6
6
 
7
7
  <site-search>
8
- <button
9
- popovertarget="stldocs-search"
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
16
  <Icon name="magnifier" />
16
17
  <span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
17
18
  <kbd class="sl-hidden md:sl-flex" style="display: none;">
18
19
  <kbd>{Astro.locals.t('search.ctrlKey')}</kbd><kbd>K</kbd>
19
20
  </kbd>
20
- </button>
21
+ </Button>
21
22
 
22
23
  <DocsSearch
23
24
  client:only="react"
@@ -31,27 +32,6 @@ import { SEARCH } from 'virtual:stl-starlight-virtual-module';
31
32
  />
32
33
  </site-search>
33
34
 
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
35
  <script is:inline>
56
36
  function setupShortcut() {
57
37
  const openBtn = document.querySelector('button[data-open-modal]');
@@ -78,3 +58,11 @@ import { SEARCH } from 'virtual:stl-starlight-virtual-module';
78
58
  }
79
59
  });
80
60
  </script>
61
+
62
+ <style is:inline>
63
+ .default-tabs-container .stl-algolia-search {
64
+ max-width: unset;
65
+ width: unset;
66
+ flex-grow: 1;
67
+ }
68
+ </style>
@@ -1,19 +1,20 @@
1
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';
2
+ import { BASE_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,
11
+ type MarkdownContext,
12
12
  MarkdownProvider,
13
13
  NavigationProvider,
14
- SearchProvider,
15
- } from '@stainless-api/docs-ui/src/contexts';
16
- import type { SearchSettings } from '@stainless-api/docs-ui/src/search/types';
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
  let $$highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null = null;
19
20
  async function getHighlighter() {
@@ -27,7 +28,7 @@ async function getHighlighter() {
27
28
  return $$highlighter;
28
29
  }
29
30
 
30
- async function createMarkdownRenderer() {
31
+ async function createMarkdownRenderer(): Promise<MarkdownContext> {
31
32
  const highlighter = await getHighlighter();
32
33
  const markdocConfig: Markdoc.Config = {
33
34
  nodes: {
@@ -37,7 +38,10 @@ async function createMarkdownRenderer() {
37
38
  transform(node, config) {
38
39
  const attributes = node.transformAttributes(config);
39
40
  const lang = node.attributes.language === 'node' ? 'typescript' : node.attributes.language;
40
- const code = highlighter.codeToTokens(node.attributes.content, { lang, theme: 'github-dark' });
41
+ const code = highlighter.codeToTokens(node.attributes.content, {
42
+ lang,
43
+ theme: 'github-dark',
44
+ });
41
45
 
42
46
  return new Markdoc.Tag(
43
47
  'pre',
@@ -66,16 +70,25 @@ async function createMarkdownRenderer() {
66
70
  },
67
71
  };
68
72
 
69
- return (content: string) => {
70
- const ast = Markdoc.parse(content);
71
- const transformed = Markdoc.transform(ast, markdocConfig);
72
- return Markdoc.renderers.html(transformed);
73
+ return {
74
+ render: (content: string) => {
75
+ const ast = Markdoc.parse(content);
76
+ const transformed = Markdoc.transform(ast, markdocConfig);
77
+ return Markdoc.renderers.html(transformed);
78
+ },
79
+ highlight: (content: string, language: string) => {
80
+ return highlighter.codeToHtml(content, {
81
+ lang: language ?? 'javascript',
82
+ themes: HIGHLIGHT_THEMES || {},
83
+ });
84
+ },
73
85
  };
74
86
  }
75
87
 
76
88
  export function DocsSearch({ settings, currentPath }: { settings: SearchSettings; currentPath: string }) {
77
- const renderMarkdown = React.use(createMarkdownRenderer());
89
+ const markdownRenderer = React.use(createMarkdownRenderer());
78
90
  const { stainlessPath, language } = parseRoute(BASE_PATH, currentPath);
91
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
79
92
  const pageFind = import.meta.env.DEV ? undefined : '/pagefind/pagefind.js';
80
93
 
81
94
  function handleSelect(path: string) {
@@ -85,16 +98,17 @@ export function DocsSearch({ settings, currentPath }: { settings: SearchSettings
85
98
 
86
99
  return (
87
100
  <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>
101
+ <ComponentProvider>
102
+ <NavigationProvider basePath="/" selectedPath={stainlessPath}>
103
+ <MarkdownProvider {...markdownRenderer}>
104
+ <SearchProvider onSelect={handleSelect} pageFind={pageFind} settings={settings}>
105
+ <div className="stldocs-root">
106
+ <SearchModal id="stldocs-search" />
107
+ </div>
108
+ </SearchProvider>
109
+ </MarkdownProvider>
110
+ </NavigationProvider>
111
+ </ComponentProvider>
98
112
  </DocsProvider>
99
113
  );
100
114
  }
@@ -0,0 +1,3 @@
1
+ export function createPlayground(): () => Promise<void> {
2
+ return () => Promise.resolve();
3
+ }
@@ -1,8 +1,8 @@
1
1
  // This is probably temporary, but it fills in functionality needed for Mintlify imports
2
2
 
3
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';
4
+ import type * as SDKJSON from '@stainless/sdk-json';
5
+ import { walkTree } from '@stainless-api/docs-ui/routing';
6
6
 
7
7
  const INTERNAL_REFERENCE_ENTRY_MARKER = 'STL_STARLIGHT_API_REFERENCE_METHOD_LINK_PLACEHOLDER';
8
8
 
@@ -0,0 +1,235 @@
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]);
146
+ } else {
147
+ // NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
148
+ // but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
149
+ // Good news is that other than Safari, Firefox does not care about
150
+ // Clipboard API being used async in a Promise.
151
+ fetch(markdownUrl)
152
+ .then((response) => response.text())
153
+ .then((text) => navigator.clipboard.writeText(text));
154
+ }
155
+ },
156
+ icon: 'copy',
157
+ primaryAction: true,
158
+ external: false,
159
+ }),
160
+ option('View as Markdown', {
161
+ onClick: () => {
162
+ window.open(getMarkdownUrl('absolute'), '_blank');
163
+ },
164
+ icon: 'markdown',
165
+ primaryAction: false,
166
+ external: true,
167
+ }),
168
+ ],
169
+ ];
170
+
171
+ // TODO: Add support for more LLMs
172
+ // {
173
+ // label: ['Open in ', 'Gemini'],
174
+ // onClick: () => {
175
+ // openInLLM('https://gemini.google.com?prompt_action=prefill&prompt_text=');
176
+ // },
177
+ // icon: 'gemini',
178
+ // primaryAction: false,
179
+ // },
180
+
181
+ export function getAIDropdownOptions() {
182
+ const renderedOptions = aiDropdownOptions.map((group, index) => {
183
+ return {
184
+ options: group,
185
+ isLast: index === aiDropdownOptions.length - 1,
186
+ reactKey: index,
187
+ };
188
+ });
189
+
190
+ const allOptions = renderedOptions.flatMap((group) => group.options);
191
+ const primaryAction = allOptions.find((o) => o.primaryAction) ?? allOptions[0]!;
192
+
193
+ return {
194
+ primaryAction,
195
+ groups: renderedOptions,
196
+ };
197
+ }
198
+
199
+ export function wireAIDropdown() {
200
+ const { primaryAction, groups } = getAIDropdownOptions();
201
+ const flatOptions = groups.flatMap((group) => group.options);
202
+ function triggerOption(id: string) {
203
+ const option = flatOptions.find((option) => option.id === id);
204
+ if (!option) return;
205
+ option.onClick();
206
+ }
207
+
208
+ document.addEventListener(getPageLoadEvent(), () => {
209
+ // we hide the Cursor option on non-desktop devices
210
+ for (const option of flatOptions) {
211
+ if (option.clientHidden === true) {
212
+ const el = document.querySelector(
213
+ `[data-dropdown-id="ai-dropdown-button"] [data-value="${option.id}"]`,
214
+ );
215
+ if (el) {
216
+ el.remove();
217
+ }
218
+ }
219
+ }
220
+
221
+ const dropdowns = document.querySelectorAll('[data-dropdown-id="ai-dropdown-button"]');
222
+
223
+ dropdowns.forEach((dropdown) => {
224
+ initDropdownButton({
225
+ dropdown: dropdown,
226
+ onSelect: (value) => {
227
+ triggerOption(value);
228
+ },
229
+ onPrimaryAction: () => {
230
+ triggerOption(primaryAction.id);
231
+ },
232
+ });
233
+ });
234
+ });
235
+ }
@@ -43,24 +43,31 @@ document.addEventListener(getPageLoadEvent(), () => {
43
43
  // This is a bit funky, but it animates pretty smooth.
44
44
  // 1. Toggle to new state so we can measure the target height
45
45
  // 2. Reset to starting height to trigger transition
46
- const startHeight = el.scrollHeight;
46
+ el.style.height = '';
47
+ el.style.overflowY = '';
48
+ const startHeight = el.getBoundingClientRect().height;
47
49
  el.classList.toggle('stl-snippet-code-is-expanded', expand);
48
50
  el.classList.toggle('stl-snippet-code-is-collapsed', !expand);
49
51
 
50
- const endHeight = el.scrollHeight;
51
-
52
- el.style.height = `${startHeight}px`;
53
-
54
- requestAnimationFrame(() => {
55
- el.style.height = `${endHeight}px`;
56
- });
52
+ const endHeight = el.getBoundingClientRect().height;
57
53
 
58
54
  const onEnd = (e: TransitionEvent) => {
59
55
  if (e.propertyName !== 'height') return;
60
56
  el.style.height = '';
57
+ el.style.overflowY = '';
61
58
  el.removeEventListener('transitionend', onEnd);
62
59
  };
63
60
  el.addEventListener('transitionend', onEnd);
61
+
62
+ // Set initial height
63
+ el.style.height = `${startHeight}px`;
64
+ el.style.overflowY = 'hidden';
65
+
66
+ // Force layout recalculation
67
+ el.getBoundingClientRect();
68
+
69
+ // Start transition to end state
70
+ el.style.height = `${endHeight}px`;
64
71
  };
65
72
 
66
73
  if (collapsedDiv) {