@stainless-api/docs 0.1.0-beta.11 → 0.1.0-beta.110

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 (149) hide show
  1. package/CHANGELOG.md +919 -0
  2. package/eslint-suppressions.json +27 -0
  3. package/locals.d.ts +17 -0
  4. package/package.json +50 -41
  5. package/playground-virtual-modules.d.ts +96 -0
  6. package/plugin/assets/languages/cli.svg +14 -0
  7. package/plugin/assets/languages/csharp.svg +1 -0
  8. package/plugin/assets/languages/php.svg +4 -0
  9. package/plugin/buildAlgoliaIndex.ts +40 -39
  10. package/plugin/components/MethodDescription.tsx +54 -0
  11. package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
  12. package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
  13. package/plugin/components/RequestBuilder/index.tsx +37 -0
  14. package/plugin/components/RequestBuilder/props.ts +9 -0
  15. package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
  16. package/plugin/components/RequestBuilder/styles.css +67 -0
  17. package/plugin/components/SDKSelect.astro +18 -111
  18. package/plugin/components/SnippetCode.tsx +110 -68
  19. package/plugin/components/StainlessIslands.tsx +126 -0
  20. package/plugin/components/search/SearchAlgolia.astro +46 -29
  21. package/plugin/components/search/SearchIsland.tsx +47 -29
  22. package/plugin/generateAPIReferenceLink.ts +2 -2
  23. package/plugin/globalJs/ai-dropdown-options.ts +243 -0
  24. package/plugin/globalJs/code-snippets.ts +40 -11
  25. package/plugin/globalJs/copy.ts +95 -17
  26. package/plugin/globalJs/create-playground.shim.ts +3 -0
  27. package/plugin/globalJs/method-descriptions.ts +33 -0
  28. package/plugin/globalJs/navigation.ts +12 -32
  29. package/plugin/globalJs/playground-data.shim.ts +1 -0
  30. package/plugin/globalJs/playground-data.ts +14 -0
  31. package/plugin/helpers/generateDocsRoutes.ts +59 -0
  32. package/plugin/helpers/multiSpec.ts +8 -0
  33. package/plugin/index.ts +304 -138
  34. package/plugin/languages.ts +8 -2
  35. package/plugin/loadPluginConfig.ts +251 -107
  36. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
  37. package/plugin/react/Routing.tsx +212 -141
  38. package/plugin/referencePlaceholderUtils.ts +18 -15
  39. package/plugin/replaceSidebarPlaceholderMiddleware.ts +38 -34
  40. package/plugin/routes/Docs.astro +70 -111
  41. package/plugin/routes/DocsStatic.astro +6 -5
  42. package/plugin/routes/Overview.astro +45 -21
  43. package/plugin/routes/markdown.ts +13 -12
  44. package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +49 -60
  45. package/plugin/specs/FileCache.ts +99 -0
  46. package/plugin/specs/fetchSpecSSR.ts +27 -0
  47. package/plugin/specs/generateSpec.ts +112 -0
  48. package/plugin/specs/index.ts +132 -0
  49. package/plugin/specs/inputResolver.ts +146 -0
  50. package/plugin/{cms → specs}/worker.ts +82 -5
  51. package/plugin/vendor/preview.worker.docs.js +27303 -18260
  52. package/plugin/vendor/templates/cli.md +1 -0
  53. package/plugin/vendor/templates/go.md +4 -2
  54. package/plugin/vendor/templates/java.md +5 -1
  55. package/plugin/vendor/templates/kotlin.md +5 -1
  56. package/plugin/vendor/templates/node.md +4 -2
  57. package/plugin/vendor/templates/python.md +4 -2
  58. package/plugin/vendor/templates/ruby.md +4 -2
  59. package/plugin/vendor/templates/terraform.md +1 -1
  60. package/plugin/vendor/templates/typescript.md +3 -1
  61. package/resolveSrcFile.ts +10 -0
  62. package/scripts/vendor_deps.ts +5 -5
  63. package/shared/conditionalIntegration.ts +28 -0
  64. package/shared/getProsePages.ts +41 -0
  65. package/shared/getSharedLogger.ts +15 -0
  66. package/shared/terminalUtils.ts +3 -0
  67. package/shared/virtualModule.ts +54 -1
  68. package/src/content.config.ts +9 -0
  69. package/stl-docs/components/AIDropdown.tsx +63 -0
  70. package/stl-docs/components/AiChatIsland.tsx +14 -0
  71. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
  72. package/stl-docs/components/Footer.astro +89 -0
  73. package/stl-docs/components/Head.astro +20 -0
  74. package/stl-docs/components/Header.astro +3 -10
  75. package/stl-docs/components/PageFrame.astro +34 -0
  76. package/stl-docs/components/PageSidebar.astro +11 -0
  77. package/stl-docs/components/PageTitle.astro +82 -0
  78. package/stl-docs/components/StainlessLogo.svg +4 -0
  79. package/stl-docs/components/TableOfContents.astro +34 -0
  80. package/stl-docs/components/ThemeProvider.astro +36 -0
  81. package/stl-docs/components/ThemeSelect.astro +84 -146
  82. package/stl-docs/components/TwoColumnContent.astro +2 -0
  83. package/stl-docs/components/content-panel/ContentPanel.astro +4 -64
  84. package/stl-docs/components/headers/DefaultHeader.astro +4 -6
  85. package/stl-docs/components/headers/StackedHeader.astro +8 -51
  86. package/stl-docs/components/icons/chat-gpt.tsx +2 -2
  87. package/stl-docs/components/icons/cursor.tsx +10 -0
  88. package/stl-docs/components/icons/gemini.tsx +19 -0
  89. package/stl-docs/components/icons/markdown.tsx +1 -1
  90. package/stl-docs/components/index.ts +1 -0
  91. package/stl-docs/components/mintlify-compat/Frame.astro +4 -4
  92. package/stl-docs/components/mintlify-compat/card.css +4 -4
  93. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  94. package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -75
  95. package/stl-docs/components/nav-tabs/NavTabs.astro +79 -81
  96. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -7
  97. package/stl-docs/components/nav-tabs/buildNavLinks.ts +3 -2
  98. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  99. package/stl-docs/components/pagination/Pagination.astro +177 -0
  100. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  101. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  102. package/stl-docs/components/pagination/util.ts +71 -0
  103. package/stl-docs/components/scripts.ts +1 -0
  104. package/stl-docs/components/sidebars/BaseSidebar.astro +79 -2
  105. package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
  106. package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
  107. package/stl-docs/disableCalloutSyntax.ts +36 -0
  108. package/stl-docs/fonts.ts +186 -0
  109. package/stl-docs/index.ts +171 -51
  110. package/stl-docs/loadStlDocsConfig.ts +64 -8
  111. package/stl-docs/proseDocSync.ts +314 -0
  112. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
  113. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
  114. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  115. package/stl-docs/proseSearchIndexing.ts +222 -0
  116. package/stl-docs/tabsMiddleware.ts +13 -4
  117. package/styles/code.css +53 -49
  118. package/styles/links.css +2 -37
  119. package/styles/method-descriptions.css +36 -0
  120. package/styles/overrides.css +28 -46
  121. package/styles/page.css +230 -52
  122. package/styles/sdk_select.css +9 -6
  123. package/styles/search.css +11 -21
  124. package/styles/sidebar.css +25 -211
  125. package/styles/{variables.css → sl-variables.css} +4 -8
  126. package/styles/stldocs-variables.css +6 -0
  127. package/styles/toc.css +19 -8
  128. package/theme.css +11 -9
  129. package/tsconfig.json +1 -4
  130. package/virtual-module.d.ts +65 -8
  131. package/components/variables.css +0 -112
  132. package/plugin/cms/client.ts +0 -62
  133. package/plugin/cms/server.ts +0 -268
  134. package/plugin/globalJs/ai-dropdown.ts +0 -57
  135. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
  136. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
  137. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
  138. package/stl-docs/components/mintlify-compat/Step.astro +0 -56
  139. package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
  140. package/styles/fonts.css +0 -68
  141. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  142. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  143. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  144. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  145. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  146. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  147. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  148. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  149. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
@@ -0,0 +1,37 @@
1
+ import { ReactNode } from 'react';
2
+ import * as SDKJSON from '@stainless/sdk-json';
3
+ import { useSpec } from '@stainless-api/docs-ui/contexts';
4
+ import { extractParams, Param } from './spec-helpers';
5
+ import type { SnippetStainlessIslandProps } from './props';
6
+
7
+ /** Load and process the spec on the server side to avoid inflating client bundle */
8
+ export function RequestBuilder({
9
+ className,
10
+ children,
11
+ method,
12
+ }: {
13
+ className: string;
14
+ children: ReactNode;
15
+ method: SDKJSON.Method;
16
+ }) {
17
+ let params: Param[];
18
+ const spec = useSpec();
19
+ try {
20
+ if (!spec) throw new Error('Spec is required for RequestBuilder');
21
+ params = spec && extractParams(spec, method);
22
+ } catch (e) {
23
+ console.warn(e);
24
+ return <div className={className}>{children}</div>;
25
+ }
26
+ const [httpMethod, path] = method.endpoint.split(' ') as [string, string];
27
+
28
+ return (
29
+ <stl-island component="SnippetStainlessIsland" className={className}>
30
+ {/* Pass state down to the client component. TODO: we need a better solution for this */}
31
+ <template className="request-builder-props">
32
+ {JSON.stringify({ method: httpMethod, path, params } satisfies SnippetStainlessIslandProps)}
33
+ </template>
34
+ {children}
35
+ </stl-island>
36
+ );
37
+ }
@@ -0,0 +1,9 @@
1
+ import z from 'zod';
2
+ import { ParamSchema } from './spec-helpers';
3
+
4
+ export const SnippetStainlessIslandPropsSchema = z.object({
5
+ method: z.string(),
6
+ path: z.string(),
7
+ params: z.array(ParamSchema),
8
+ });
9
+ export type SnippetStainlessIslandProps = z.infer<typeof SnippetStainlessIslandPropsSchema>;
@@ -0,0 +1,47 @@
1
+ import type * as SDKJSON from '@stainless/sdk-json';
2
+ import { printer } from '@stainless-api/docs-ui/markdown';
3
+ import z from 'zod';
4
+ import { getBodyParams } from '@stainless-api/docs-ui/utils';
5
+
6
+ export const ParamSchema = z.object({
7
+ stainlessPath: z.string(),
8
+ location: z.string(),
9
+ key: z.string(),
10
+ type: z.string(),
11
+ });
12
+
13
+ export type Param = z.infer<typeof ParamSchema>;
14
+
15
+ export function extractParams(spec: SDKJSON.Spec | undefined, method: SDKJSON.Method): Param[] {
16
+ const httpDecls = spec?.decls?.http;
17
+ if (!httpDecls) throw new Error('expected http language to be present in SDKJSON');
18
+ const decl = httpDecls?.[method.stainlessPath];
19
+ if (decl?.kind !== 'HttpDeclFunction') {
20
+ throw new Error(
21
+ 'expected HttpDeclFunction at stainlessPath "' + method.stainlessPath + '", got ' + decl?.kind,
22
+ );
23
+ }
24
+ const bodyParams = getBodyParams(decl);
25
+ const params = [
26
+ ...Object.entries(decl.paramsChildren ?? {}).map(([location, children]) => ({ location, children })),
27
+ ...(bodyParams ? [{ location: 'body', children: bodyParams.params }] : []),
28
+ ]
29
+ .filter((e) => e.children.length)
30
+ .flatMap(({ location, children }) =>
31
+ children.map((child) => {
32
+ const resolved = httpDecls[child];
33
+ if (resolved?.kind !== 'HttpDeclProperty') {
34
+ throw new Error(
35
+ 'expected HttpDeclProperty at stainlessPath "' + child + '", got ' + resolved?.kind,
36
+ );
37
+ }
38
+ return {
39
+ stainlessPath: resolved.stainlessPath,
40
+ location,
41
+ key: resolved.key,
42
+ type: (resolved.optional ? 'optional ' : '') + printer.type('http', resolved.type),
43
+ };
44
+ }),
45
+ );
46
+ return params;
47
+ }
@@ -0,0 +1,67 @@
1
+ .request-builder-container form {
2
+ display: grid;
3
+
4
+ /* prettier-ignore */
5
+ grid-template-columns: /*info button*/max-content /*label*/max-content /*colon*/max-content /*input*/1fr;
6
+ font-family: var(--stl-typography-font);
7
+ font-size: var(--stl-typography-text-body-sm);
8
+ color: var(--stl-color-foreground);
9
+
10
+ & > * {
11
+ padding-inline-start: 12px;
12
+ padding-inline-end: 8px;
13
+ }
14
+
15
+ & > h4 {
16
+ grid-column: 1 / -1;
17
+ text-transform: capitalize;
18
+ font-size: var(--stl-typography-scale-sm);
19
+ margin-top: 0;
20
+ margin-bottom: 0.25em;
21
+ color: var(--stl-color-foreground-muted);
22
+ padding-top: 0.75rem;
23
+
24
+ &:not(:first-child) {
25
+ border-top: 1px solid var(--stl-color-border);
26
+ }
27
+ }
28
+
29
+ .request-builder-param {
30
+ grid-column: 1 / -1;
31
+ display: grid;
32
+ grid-template-columns: subgrid;
33
+ align-items: center;
34
+ column-gap: 0.5rem;
35
+ padding-block: 0.35rem;
36
+
37
+ .request-builder-param-info-button {
38
+ color: var(--stl-color-foreground-muted);
39
+ margin-inline: -4px;
40
+ padding: 0;
41
+ }
42
+ .request-builder-param-label {
43
+ font-family: var(--stl-typography-font-mono);
44
+ }
45
+ .request-builder-param-colon {
46
+ font-family: var(--stl-typography-font-mono);
47
+ color: var(--stl-color-foreground-muted);
48
+ }
49
+ /* TODO: new input component that is better stylable */
50
+ .request-builder-param-value {
51
+ input {
52
+ margin: 6px 8px;
53
+ font-size: inherit;
54
+ }
55
+ }
56
+
57
+ &:has(+ h4),
58
+ &:last-child {
59
+ padding-bottom: 0.75rem;
60
+ }
61
+ }
62
+ }
63
+
64
+ .request-builder-footer .stl-ui-button--ghost .stl-ui-button__icon {
65
+ color: var(--stl-color-foreground);
66
+ opacity: var(--stl-opacity-level-040);
67
+ }
@@ -1,14 +1,14 @@
1
1
  ---
2
- import type { DocsLanguage } from '@stainless-api/docs-ui/src/routing';
3
- import { parseRoute } from '@stainless-api/docs-ui/src/routing';
4
- import { cmsClient } from '../cms/client';
5
- import { BASE_PATH, DEFAULT_LANGUAGE, EXCLUDE_LANGUAGES } from 'virtual:stl-starlight-virtual-module';
2
+ import { parseRoute } from '@stainless-api/docs-ui/routing';
3
+ import { DEFAULT_LANGUAGE } from 'virtual:stl-starlight-virtual-module';
6
4
  import { Languages } from '../languages';
7
5
  import { SDKSelectReactComponent } from '../react/Routing';
6
+ import { getDocsLanguages } from '../helpers/multiSpec';
7
+ import { API_REFERENCE_BASE_PATH } from 'virtual:stl-docs-virtual-module';
8
8
 
9
9
  const slug = `/${Astro.locals.starlightRoute.id}`;
10
10
 
11
- const basePath = BASE_PATH;
11
+ const basePath = API_REFERENCE_BASE_PATH;
12
12
  const defaultLanguage = DEFAULT_LANGUAGE;
13
13
  const { language, stainlessPath } = parseRoute(basePath, slug);
14
14
 
@@ -19,28 +19,15 @@ const data = {
19
19
  defaultLanguage,
20
20
  };
21
21
 
22
- const spec = await cmsClient.getSpec();
22
+ const options = getDocsLanguages().map((value) => ({
23
+ value,
24
+ label: Languages[value].name,
25
+ selected: data.language === value,
26
+ }));
23
27
 
24
- // TODO: should not force unwrap this
25
- const languages: DocsLanguage[] = spec.docs!.languages ?? ['http'];
26
- const options = languages
27
- .filter((language) => language !== 'terraform')
28
- .filter((language) => !EXCLUDE_LANGUAGES.includes(language))
29
- .map((value) => ({
30
- value,
31
- label: Languages[value].name,
32
- selected: data.language === value,
33
- }));
34
-
35
- const readmeSlug = language === 'http' ? BASE_PATH : `${BASE_PATH}/${language}`;
28
+ const readmeSlug = language === 'http' ? API_REFERENCE_BASE_PATH : `${API_REFERENCE_BASE_PATH}/${language}`;
36
29
  ---
37
30
 
38
- <span
39
- hidden
40
- id="stldocs-data"
41
- data-stldocs-basepath={data.basePath}
42
- data-stldocs-defaultLanguage={data.defaultLanguage}></span>
43
-
44
31
  {
45
32
  (data.stainlessPath || slug === readmeSlug) && (
46
33
  <div class="stldocs-root stl-sdk-select">
@@ -53,101 +40,21 @@ const readmeSlug = language === 'http' ? BASE_PATH : `${BASE_PATH}/${language}`;
53
40
  )
54
41
  }
55
42
 
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
43
  <script>
139
44
  import { navigate } from 'astro:transitions/client';
140
45
  import { updateSelectedLanguage } from '../languages';
141
- import { initDropdown } from '@stainless-api/docs-ui/src/components/scripts/dropdown';
142
- import { BASE_PATH } from 'virtual:stl-starlight-virtual-module';
46
+ import { initDropdown } from '@stainless-api/docs/components/scripts';
47
+ import { RESOLVED_API_REFERENCE_PATH } from 'virtual:stl-starlight-virtual-module';
143
48
  import { getPageLoadEvent } from '../helpers/getPageLoadEvent';
144
49
 
145
50
  document.addEventListener(getPageLoadEvent(), () => {
51
+ const sdkSelect = document.getElementById('sidebar-sdk-select');
52
+ if (!sdkSelect) return;
146
53
  initDropdown({
147
- dropdownId: 'sidebar-sdk-select',
54
+ root: sdkSelect,
148
55
  onSelect: (value) => {
149
- const originalLanguage = document.getElementById('sidebar-sdk-select')?.dataset.currentValue;
150
- navigate(updateSelectedLanguage(BASE_PATH, originalLanguage, value));
56
+ const originalLanguage = sdkSelect.dataset.currentValue;
57
+ navigate(updateSelectedLanguage(RESOLVED_API_REFERENCE_PATH, originalLanguage, value));
151
58
  },
152
59
  });
153
60
  });
@@ -1,14 +1,43 @@
1
- import type {
2
- SnippetCodeProps,
3
- SnippetContainerProps,
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';
1
+ import {
2
+ type SnippetCodeProps,
3
+ type SnippetContainerProps,
4
+ SnippetResponse as DocsUiSnippetResponse,
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
+ EXPERIMENTAL_REQUEST_BUILDER,
13
+ } from 'virtual:stl-starlight-virtual-module';
10
14
  import clsx from 'clsx';
11
15
  import { Button } from '@stainless-api/ui-primitives';
16
+ import { CopyIcon, PlayIcon } from 'lucide-react';
17
+ import React from 'react';
18
+ import { RequestBuilder } from './RequestBuilder';
19
+ import { Method } from '@stainless/sdk-json';
20
+
21
+ function PlaygroundIcon() {
22
+ return (
23
+ <svg
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ width={16}
26
+ height={16}
27
+ viewBox="0 0 24 24"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ strokeWidth={2}
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ className={'lucide ' + style.Icon}
34
+ aria-hidden="true"
35
+ >
36
+ <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" />
37
+ </svg>
38
+ );
39
+ }
40
+
12
41
  /*
13
42
  * This may be replaced by additional data from the sdk.
14
43
  * Without information from the sdk, we use simple heuristics per language.
@@ -54,7 +83,7 @@ function wrapFirstNSpaces($line: cheerio.Cheerio<any>, n: number) {
54
83
  const m = inner.match(new RegExp(`^( {1,${n}})`));
55
84
  if (!m) return;
56
85
 
57
- const lead = m[1];
86
+ const lead = m[1]!;
58
87
  $firstSpan.html(`<span class="leading-ws">${lead}</span>${inner.slice(lead.length)}`);
59
88
  }
60
89
 
@@ -124,63 +153,40 @@ function useIsCollapsible({ signature }: { signature?: string }): boolean {
124
153
  return Boolean(EXPERIMENTAL_COLLAPSIBLE_SNIPPETS && signature && language);
125
154
  }
126
155
 
127
- export function SnippetRequestContainer({ children, signature }: SnippetRequestContainerProps) {
128
- const isCollapsible = useIsCollapsible({ signature });
129
-
130
- return (
131
- <div className="stl-snippet-request-container">
132
- {children}
133
- {signature && isCollapsible && (
134
- <Button
135
- className={'stl-snippet-expand-button'}
136
- id="stl-snippet-expand-button"
137
- size="sm"
138
- variant="outline"
139
- >
140
- Show more
141
- </Button>
142
- )}
143
- </div>
144
- );
156
+ function isActualMethod(value: object): value is Method {
157
+ return 'kind' in value && value.kind === 'http_method';
145
158
  }
146
159
 
147
- export function SnippetContainer({ children, signature }: SnippetContainerProps) {
160
+ export function SnippetContainer({ children, signature, method }: SnippetContainerProps) {
148
161
  const isCollapsible = useIsCollapsible({ signature });
149
-
150
- return (
151
- <div
152
- className={clsx(
153
- style.Snippet,
154
- isCollapsible ? 'stl-snippet-collapsible' : 'stl-snippet-non-collapsible',
155
- )}
156
- >
162
+ const className = clsx(
163
+ style.Snippet,
164
+ isCollapsible ? 'stl-snippet-collapsible' : 'stl-snippet-non-collapsible',
165
+ );
166
+ return EXPERIMENTAL_REQUEST_BUILDER && isActualMethod(method) ? (
167
+ <RequestBuilder className={className} method={method}>
157
168
  {children}
158
- </div>
169
+ </RequestBuilder>
170
+ ) : (
171
+ <div className={className}>{children}</div>
159
172
  );
160
173
  }
161
174
 
162
- export function CondensibleSnippetCode({
163
- content,
164
- language,
165
- signature,
166
- highlighted,
167
- }: SnippetCodeProps & { signature: string; highlighted: string; language: string }) {
168
- const ranges = getCollapsedRanges(content, language, signature);
169
- const html = condensedShikiHtmlFull(highlighted, language, ranges);
170
- const offset = getCounterOffset(ranges);
171
-
175
+ export function SnippetButtons({ content }: { content: string }) {
176
+ void content;
177
+ const language = useLanguage();
172
178
  return (
173
- <div
174
- className={clsx(style.SnippetCode, 'stl-snippet-code-is-collapsed')}
175
- data-snippet-expanded-offset={offset}
176
- >
177
- <pre className={style.SnippetCodeContent} data-stldocs-copy-content>
178
- <code
179
- className={language === 'json' ? 'snippet-json' : 'snippet'}
180
- dangerouslySetInnerHTML={{ __html: html }}
181
- />
182
- </pre>
183
- </div>
179
+ <>
180
+ <Button variant="outline" data-stldocs-snippet-copy>
181
+ <CopyIcon size={16} className={style.Icon} />
182
+ </Button>
183
+ {EXPERIMENTAL_PLAYGROUNDS &&
184
+ (language === 'python' || language === 'typescript' || language === 'http') && (
185
+ <Button data-stldocs-snippet-play variant="muted" border title="Play">
186
+ <PlaygroundIcon />
187
+ </Button>
188
+ )}
189
+ </>
184
190
  );
185
191
  }
186
192
 
@@ -200,16 +206,52 @@ export function SnippetCode({ content, signature, language: forcedLanguage }: Sn
200
206
  }
201
207
 
202
208
  return (
203
- <div
204
- className={clsx(style.SnippetCode, isCollapsible && 'stl-snippet-code-is-collapsed')}
205
- data-snippet-expanded-offset={offset}
206
- >
207
- <pre className={style.SnippetCodeContent} data-stldocs-copy-content>
208
- <code
209
- className={(language as string) === 'json' ? 'snippet-json' : 'snippet'}
210
- dangerouslySetInnerHTML={{ __html: highlighted }}
211
- />
212
- </pre>
209
+ <>
210
+ <div
211
+ className={clsx(style.SnippetCode, isCollapsible && 'stl-snippet-code-is-collapsed')}
212
+ data-snippet-expanded-offset={offset}
213
+ data-stldocs-copy-content
214
+ dangerouslySetInnerHTML={{ __html: highlighted }}
215
+ />
216
+ {signature && isCollapsible && (
217
+ <Button
218
+ className={'stl-snippet-expand-button'}
219
+ id="stl-snippet-expand-button"
220
+ size="sm"
221
+ variant="outline"
222
+ >
223
+ Show more
224
+ </Button>
225
+ )}
226
+ {EXPERIMENTAL_REQUEST_BUILDER && (
227
+ <div className="request-builder-container" style={{ display: 'contents' }}></div>
228
+ )}
229
+ </>
230
+ );
231
+ }
232
+
233
+ export function SnippetFooter() {
234
+ if (!EXPERIMENTAL_REQUEST_BUILDER) return null;
235
+ return (
236
+ <div className={clsx(style.SnippetFooter, 'try-it-footer')}>
237
+ {EXPERIMENTAL_REQUEST_BUILDER && (
238
+ <div className="request-builder-footer" style={{ display: 'contents' }}></div>
239
+ )}
240
+ <Button variant="accent" className="try-it-button">
241
+ <Button.Label>Try it</Button.Label>
242
+ <Button.Icon icon={PlayIcon} />
243
+ </Button>
213
244
  </div>
214
245
  );
215
246
  }
247
+
248
+ export function SnippetResponse({ ...props }: React.ComponentProps<typeof DocsUiSnippetResponse>) {
249
+ return (
250
+ <>
251
+ <DocsUiSnippetResponse {...props} />
252
+ {EXPERIMENTAL_REQUEST_BUILDER && (
253
+ <div className="request-builder-response" style={{ display: 'contents' }} />
254
+ )}
255
+ </>
256
+ );
257
+ }
@@ -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
+ }