@gitbook/react-openapi 0.7.1 → 1.0.1

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 (118) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/InteractiveSection.d.ts +4 -8
  3. package/dist/InteractiveSection.jsx +60 -0
  4. package/dist/Markdown.d.ts +1 -2
  5. package/dist/Markdown.jsx +5 -0
  6. package/dist/OpenAPICodeSample.d.ts +2 -4
  7. package/dist/OpenAPICodeSample.jsx +141 -0
  8. package/dist/OpenAPIDisclosure.d.ts +12 -0
  9. package/dist/OpenAPIDisclosure.jsx +32 -0
  10. package/dist/OpenAPIDisclosureGroup.d.ts +19 -0
  11. package/dist/OpenAPIDisclosureGroup.jsx +81 -0
  12. package/dist/OpenAPIOperation.d.ts +2 -4
  13. package/dist/OpenAPIOperation.jsx +51 -0
  14. package/dist/OpenAPIOperationContext.d.ts +16 -0
  15. package/dist/OpenAPIOperationContext.jsx +26 -0
  16. package/dist/OpenAPIPath.d.ts +8 -0
  17. package/dist/OpenAPIPath.jsx +54 -0
  18. package/dist/OpenAPIRequestBody.d.ts +4 -5
  19. package/dist/OpenAPIRequestBody.jsx +22 -0
  20. package/dist/OpenAPIResponse.d.ts +4 -4
  21. package/dist/OpenAPIResponse.jsx +39 -0
  22. package/dist/OpenAPIResponseExample.d.ts +2 -4
  23. package/dist/OpenAPIResponseExample.jsx +108 -0
  24. package/dist/OpenAPIResponses.d.ts +3 -4
  25. package/dist/OpenAPIResponses.jsx +35 -0
  26. package/dist/OpenAPISchema.d.ts +11 -8
  27. package/dist/OpenAPISchema.jsx +285 -0
  28. package/dist/OpenAPISchemaName.d.ts +12 -0
  29. package/dist/OpenAPISchemaName.jsx +15 -0
  30. package/dist/OpenAPISecurities.d.ts +2 -4
  31. package/dist/OpenAPISecurities.jsx +55 -0
  32. package/dist/OpenAPIServerURL.d.ts +2 -3
  33. package/dist/OpenAPIServerURL.jsx +67 -0
  34. package/dist/OpenAPIServerURLVariable.d.ts +2 -3
  35. package/dist/OpenAPIServerURLVariable.jsx +8 -0
  36. package/dist/OpenAPISpec.d.ts +3 -4
  37. package/dist/OpenAPISpec.jsx +91 -0
  38. package/dist/OpenAPITabs.d.ts +26 -0
  39. package/dist/OpenAPITabs.jsx +103 -0
  40. package/dist/ScalarApiButton.d.ts +3 -3
  41. package/dist/ScalarApiButton.jsx +51 -0
  42. package/dist/code-samples.d.ts +4 -0
  43. package/dist/code-samples.js +103 -38
  44. package/dist/generateSchemaExample.d.ts +2 -2
  45. package/dist/generateSchemaExample.js +29 -102
  46. package/dist/index.d.ts +3 -2
  47. package/dist/index.js +2 -1
  48. package/dist/resolveOpenAPIOperation.d.ts +11 -0
  49. package/dist/resolveOpenAPIOperation.js +194 -0
  50. package/dist/stringifyOpenAPI.d.ts +4 -0
  51. package/dist/stringifyOpenAPI.js +6 -0
  52. package/dist/tsconfig.build.tsbuildinfo +1 -0
  53. package/dist/types.d.ts +11 -12
  54. package/dist/useSyncedTabsGlobalState.d.ts +1 -0
  55. package/dist/useSyncedTabsGlobalState.js +16 -0
  56. package/dist/utils.d.ts +6 -2
  57. package/dist/utils.js +13 -6
  58. package/package.json +12 -10
  59. package/src/InteractiveSection.tsx +90 -86
  60. package/src/Markdown.tsx +2 -3
  61. package/src/OpenAPICodeSample.tsx +43 -31
  62. package/src/OpenAPIDisclosure.tsx +50 -0
  63. package/src/OpenAPIDisclosureGroup.tsx +136 -0
  64. package/src/OpenAPIOperation.tsx +36 -42
  65. package/src/OpenAPIOperationContext.tsx +45 -0
  66. package/src/OpenAPIPath.tsx +65 -0
  67. package/src/OpenAPIRequestBody.tsx +10 -17
  68. package/src/OpenAPIResponse.tsx +27 -45
  69. package/src/OpenAPIResponseExample.tsx +89 -31
  70. package/src/OpenAPIResponses.tsx +48 -17
  71. package/src/OpenAPISchema.test.ts +1 -1
  72. package/src/OpenAPISchema.tsx +129 -108
  73. package/src/OpenAPISchemaName.tsx +27 -0
  74. package/src/OpenAPISecurities.tsx +45 -24
  75. package/src/OpenAPIServerURL.tsx +17 -10
  76. package/src/OpenAPIServerURLVariable.tsx +2 -4
  77. package/src/OpenAPISpec.tsx +58 -58
  78. package/src/OpenAPITabs.tsx +153 -0
  79. package/src/ScalarApiButton.tsx +84 -7
  80. package/src/code-samples.test.ts +51 -0
  81. package/src/code-samples.ts +95 -31
  82. package/src/generateSchemaExample.ts +26 -153
  83. package/src/index.ts +3 -2
  84. package/src/resolveOpenAPIOperation.test.ts +177 -0
  85. package/src/resolveOpenAPIOperation.ts +164 -0
  86. package/src/stringifyOpenAPI.ts +6 -0
  87. package/src/types.ts +17 -10
  88. package/src/useSyncedTabsGlobalState.ts +23 -0
  89. package/src/utils.ts +14 -7
  90. package/dist/InteractiveSection.js +0 -47
  91. package/dist/Markdown.js +0 -6
  92. package/dist/OpenAPICodeSample.js +0 -110
  93. package/dist/OpenAPIOperation.js +0 -38
  94. package/dist/OpenAPIRequestBody.js +0 -18
  95. package/dist/OpenAPIResponse.js +0 -32
  96. package/dist/OpenAPIResponseExample.js +0 -54
  97. package/dist/OpenAPIResponses.js +0 -18
  98. package/dist/OpenAPISchema.js +0 -235
  99. package/dist/OpenAPISchema.test.d.ts +0 -1
  100. package/dist/OpenAPISchema.test.js +0 -91
  101. package/dist/OpenAPISecurities.js +0 -42
  102. package/dist/OpenAPIServerURL.js +0 -51
  103. package/dist/OpenAPIServerURLVariable.js +0 -10
  104. package/dist/OpenAPISpec.js +0 -70
  105. package/dist/ScalarApiButton.js +0 -14
  106. package/dist/fetchOpenAPIOperation.d.ts +0 -72
  107. package/dist/fetchOpenAPIOperation.js +0 -124
  108. package/dist/fetchOpenAPIOperation.test.d.ts +0 -1
  109. package/dist/fetchOpenAPIOperation.test.js +0 -152
  110. package/dist/resolveOpenAPIPath.d.ts +0 -7
  111. package/dist/resolveOpenAPIPath.js +0 -112
  112. package/dist/resolveOpenAPIPath.test.d.ts +0 -1
  113. package/dist/resolveOpenAPIPath.test.js +0 -39
  114. package/dist/tsconfig.tsbuildinfo +0 -1
  115. package/src/fetchOpenAPIOperation.test.ts +0 -185
  116. package/src/fetchOpenAPIOperation.ts +0 -230
  117. package/src/resolveOpenAPIPath.test.ts +0 -60
  118. package/src/resolveOpenAPIPath.ts +0 -145
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import classNames from 'classnames';
4
- import React from 'react';
5
- import { atom, useRecoilState } from 'recoil';
3
+ import clsx from 'clsx';
4
+ import { useRef, useState } from 'react';
5
+ import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
6
+ import { useDisclosureState } from 'react-stately';
6
7
 
7
8
  interface InteractiveSectionTab {
8
9
  key: string;
@@ -10,11 +11,6 @@ interface InteractiveSectionTab {
10
11
  body: React.ReactNode;
11
12
  }
12
13
 
13
- const syncedTabsAtom = atom<Record<string, string>>({
14
- key: 'syncedTabState',
15
- default: {},
16
- });
17
-
18
14
  /**
19
15
  * To optimize rendering, most of the components are server-components,
20
16
  * and the interactiveness is mainly handled by a few key components like this one.
@@ -27,21 +23,18 @@ export function InteractiveSection(props: {
27
23
  toggeable?: boolean;
28
24
  /** Default state of the toggle */
29
25
  defaultOpened?: boolean;
30
- /** Icons to display for the toggle */
31
- toggleOpenIcon?: React.ReactNode;
32
- toggleCloseIcon?: React.ReactNode;
26
+ /** Icon to display for the toggle */
27
+ toggleIcon?: React.ReactNode;
33
28
  /** Tabs of content to display */
34
29
  tabs?: Array<InteractiveSectionTab>;
35
30
  /** Default tab to have opened */
36
31
  defaultTab?: string;
37
32
  /** Content of the header */
38
- header: React.ReactNode;
33
+ header?: React.ReactNode;
39
34
  /** Body of the section */
40
35
  children?: React.ReactNode;
41
36
  /** Children to display within the container */
42
37
  overlay?: React.ReactNode;
43
- /** An optional key referencing a value in global state */
44
- stateKey?: string;
45
38
  }) {
46
39
  const {
47
40
  id,
@@ -53,99 +46,110 @@ export function InteractiveSection(props: {
53
46
  header,
54
47
  children,
55
48
  overlay,
56
- toggleOpenIcon = '▶',
57
- toggleCloseIcon = '▼',
58
- stateKey,
49
+ toggleIcon = '▶',
59
50
  } = props;
60
- const [syncedTabs, setSyncedTabs] = useRecoilState(syncedTabsAtom);
61
- const tabFromState =
62
- stateKey && stateKey in syncedTabs
63
- ? tabs.find((tab) => tab.key === syncedTabs[stateKey])
64
- : undefined;
65
51
 
66
- const [opened, setOpened] = React.useState(defaultOpened);
67
- const [selectedTabKey, setSelectedTab] = React.useState(tabFromState?.key ?? defaultTab);
52
+ const [selectedTabKey, setSelectedTab] = useState(defaultTab);
68
53
  const selectedTab: InteractiveSectionTab | undefined =
69
- tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
54
+ tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
55
+
56
+ const state = useDisclosureState({
57
+ defaultExpanded: defaultOpened,
58
+ });
59
+ const panelRef = useRef<HTMLDivElement | null>(null);
60
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
61
+ const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
62
+ const { buttonProps } = useButton(triggerProps, triggerRef);
63
+ const { isFocusVisible, focusProps } = useFocusRing();
70
64
 
71
65
  return (
72
66
  <div
73
67
  id={id}
74
- className={classNames(
68
+ className={clsx(
75
69
  'openapi-section',
76
70
  toggeable ? 'openapi-section-toggeable' : null,
77
71
  className,
78
- toggeable ? `${className}-${opened ? 'opened' : 'closed'}` : null,
72
+ toggeable ? `${className}-${state.isExpanded ? 'opened' : 'closed'}` : null,
79
73
  )}
80
74
  >
81
- <div
82
- onClick={() => {
83
- if (toggeable) {
84
- setOpened(!opened);
85
- }
86
- }}
87
- className={classNames('openapi-section-header', `${className}-header`)}
88
- >
75
+ {header ? (
89
76
  <div
90
- className={classNames(
91
- 'openapi-section-header-content',
92
- `${className}-header-content`,
93
- )}
77
+ onClick={() => {
78
+ if (toggeable) {
79
+ state.toggle();
80
+ }
81
+ }}
82
+ className={clsx('openapi-section-header', `${className}-header`)}
94
83
  >
95
- {header}
84
+ <div
85
+ className={clsx(
86
+ 'openapi-section-header-content',
87
+ `${className}-header-content`,
88
+ )}
89
+ >
90
+ {(children || selectedTab?.body) && toggeable ? (
91
+ <button
92
+ {...mergeProps(buttonProps, focusProps)}
93
+ ref={triggerRef}
94
+ className={clsx('openapi-section-toggle', `${className}-toggle`)}
95
+ style={{
96
+ outline: isFocusVisible
97
+ ? '2px solid rgb(var(--primary-color-500) / 0.4)'
98
+ : 'none',
99
+ }}
100
+ >
101
+ {toggleIcon}
102
+ </button>
103
+ ) : null}
104
+ {header}
105
+ </div>
106
+ <div
107
+ className={clsx(
108
+ 'openapi-section-header-controls',
109
+ `${className}-header-controls`,
110
+ )}
111
+ onClick={(event) => {
112
+ event.stopPropagation();
113
+ }}
114
+ >
115
+ {tabs.length > 1 ? (
116
+ <select
117
+ className={clsx(
118
+ 'openapi-section-select',
119
+ 'openapi-select',
120
+ `${className}-tabs-select`,
121
+ )}
122
+ value={selectedTab?.key ?? ''}
123
+ onChange={(event) => {
124
+ setSelectedTab(event.target.value);
125
+ state.expand();
126
+ }}
127
+ >
128
+ {tabs.map((tab) => (
129
+ <option key={tab.key} value={tab.key}>
130
+ {tab.label}
131
+ </option>
132
+ ))}
133
+ </select>
134
+ ) : null}
135
+ </div>
96
136
  </div>
137
+ ) : null}
138
+ {(!toggeable || state.isExpanded) && (children || selectedTab?.body) ? (
97
139
  <div
98
- className={classNames(
99
- 'openapi-section-header-controls',
100
- `${className}-header-controls`,
101
- )}
102
- onClick={(event) => {
103
- event.stopPropagation();
104
- }}
140
+ ref={panelRef}
141
+ {...panelProps}
142
+ className={clsx('openapi-section-body', `${className}-body`)}
105
143
  >
106
- {tabs.length ? (
107
- <select
108
- className={classNames(
109
- 'openapi-section-select',
110
- 'openapi-select',
111
- `${className}-tabs-select`,
112
- )}
113
- value={selectedTab.key}
114
- onChange={(event) => {
115
- setSelectedTab(event.target.value);
116
- if (stateKey) {
117
- setSyncedTabs((state) => ({
118
- ...state,
119
- [stateKey]: event.target.value,
120
- }));
121
- }
122
- setOpened(true);
123
- }}
124
- >
125
- {tabs.map((tab) => (
126
- <option key={tab.key} value={tab.key}>
127
- {tab.label}
128
- </option>
129
- ))}
130
- </select>
131
- ) : null}
132
- {(children || selectedTab?.body) && toggeable ? (
133
- <button
134
- className={classNames('openapi-section-toggle', `${className}-toggle`)}
135
- onClick={() => setOpened(!opened)}
136
- >
137
- {opened ? toggleCloseIcon : toggleOpenIcon}
138
- </button>
139
- ) : null}
140
- </div>
141
- </div>
142
- {(!toggeable || opened) && (children || selectedTab?.body) ? (
143
- <div className={classNames('openapi-section-body', `${className}-body`)}>
144
144
  {children}
145
145
  {selectedTab?.body}
146
146
  </div>
147
147
  ) : null}
148
- {overlay}
148
+ {overlay ? (
149
+ <div className={clsx('openapi-section-overlay', `${className}-overlay`)}>
150
+ {overlay}
151
+ </div>
152
+ ) : null}
149
153
  </div>
150
154
  );
151
155
  }
package/src/Markdown.tsx CHANGED
@@ -1,12 +1,11 @@
1
- import * as React from 'react';
2
- import classNames from 'classnames';
1
+ import clsx from 'clsx';
3
2
 
4
3
  export function Markdown(props: { source: string; className?: string }) {
5
4
  const { source, className } = props;
6
5
 
7
6
  return (
8
7
  <div
9
- className={classNames('openapi-markdown', className)}
8
+ className={clsx('openapi-markdown', className)}
10
9
  dangerouslySetInnerHTML={{ __html: source }}
11
10
  />
12
11
  );
@@ -1,13 +1,12 @@
1
- import * as React from 'react';
2
-
3
1
  import { CodeSampleInput, codeSampleGenerators } from './code-samples';
4
- import { OpenAPIOperationData } from './fetchOpenAPIOperation';
5
2
  import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
6
3
  import { InteractiveSection } from './InteractiveSection';
7
4
  import { getServersURL } from './OpenAPIServerURL';
8
- import { ScalarApiButton } from './ScalarApiButton';
9
- import { OpenAPIContextProps } from './types';
10
- import { noReference } from './utils';
5
+ import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
6
+ import { createStateKey } from './utils';
7
+ import { stringifyOpenAPI } from './stringifyOpenAPI';
8
+ import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
9
+ import { checkIsReference } from './utils';
11
10
 
12
11
  /**
13
12
  * Display code samples to execute the operation.
@@ -22,25 +21,20 @@ export function OpenAPICodeSample(props: {
22
21
  const searchParams = new URLSearchParams();
23
22
  const headersObject: { [k: string]: string } = {};
24
23
 
25
- data.operation.parameters?.forEach((rawParam) => {
26
- const param = noReference(rawParam);
24
+ data.operation.parameters?.forEach((param) => {
27
25
  if (!param) {
28
26
  return;
29
27
  }
30
28
 
31
29
  if (param.in === 'header' && param.required) {
32
- const example = param.schema
33
- ? generateSchemaExample(noReference(param.schema))
34
- : undefined;
35
- if (example !== undefined) {
30
+ const example = param.schema ? generateSchemaExample(param.schema) : undefined;
31
+ if (example !== undefined && param.name) {
36
32
  headersObject[param.name] =
37
- typeof example !== 'string' ? JSON.stringify(example) : example;
33
+ typeof example !== 'string' ? stringifyOpenAPI(example) : example;
38
34
  }
39
35
  } else if (param.in === 'query' && param.required) {
40
- const example = param.schema
41
- ? generateSchemaExample(noReference(param.schema))
42
- : undefined;
43
- if (example !== undefined) {
36
+ const example = param.schema ? generateSchemaExample(param.schema) : undefined;
37
+ if (example !== undefined && param.name) {
44
38
  searchParams.append(
45
39
  param.name,
46
40
  String(Array.isArray(example) ? example[0] : example),
@@ -49,8 +43,13 @@ export function OpenAPICodeSample(props: {
49
43
  }
50
44
  });
51
45
 
52
- const requestBody = noReference(data.operation.requestBody);
53
- const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
46
+ const requestBody = !checkIsReference(data.operation.requestBody)
47
+ ? data.operation.requestBody
48
+ : undefined;
49
+ const requestBodyContentEntries = requestBody?.content
50
+ ? Object.entries(requestBody.content)
51
+ : undefined;
52
+ const requestBodyContent = requestBodyContentEntries?.[0];
54
53
 
55
54
  const input: CodeSampleInput = {
56
55
  url:
@@ -108,21 +107,17 @@ export function OpenAPICodeSample(props: {
108
107
  const codeSamplesDisabled =
109
108
  data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false;
110
109
  const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []);
110
+
111
111
  if (samples.length === 0) {
112
112
  return null;
113
113
  }
114
114
 
115
115
  return (
116
- <InteractiveSection
117
- header="Request"
118
- className="openapi-codesample"
119
- tabs={samples}
120
- overlay={
121
- data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
122
- <ScalarApiButton method={data.method} path={data.path} />
123
- )
124
- }
125
- />
116
+ <OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
117
+ <InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample">
118
+ <OpenAPITabsPanels />
119
+ </InteractiveSection>
120
+ </OpenAPITabs>
126
121
  );
127
122
  }
128
123
 
@@ -130,6 +125,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
130
125
  [key: string]: string;
131
126
  } {
132
127
  const security = securities[0];
128
+
133
129
  if (!security) {
134
130
  return {};
135
131
  }
@@ -137,12 +133,28 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
137
133
  switch (security[1].type) {
138
134
  case 'http': {
139
135
  let scheme = security[1].scheme;
140
- if (scheme === 'bearer') {
136
+ let format = security[1].bearerFormat ?? 'YOUR_SECRET_TOKEN';
137
+
138
+ if (scheme?.includes('bearer')) {
141
139
  scheme = 'Bearer';
140
+ } else if (scheme?.includes('basic')) {
141
+ scheme = 'Basic';
142
+ format = 'username:password';
143
+ } else if (scheme?.includes('token')) {
144
+ scheme = 'Token';
142
145
  }
143
146
 
144
147
  return {
145
- Authorization: scheme + ' ' + (security[1].bearerFormat ?? '<token>'),
148
+ Authorization: scheme + ' ' + format,
149
+ };
150
+ }
151
+ case 'apiKey': {
152
+ if (security[1].in !== 'header') return {};
153
+
154
+ const name = security[1].name ?? 'Authorization';
155
+
156
+ return {
157
+ [name]: 'YOUR_API_KEY',
146
158
  };
147
159
  }
148
160
  default: {
@@ -0,0 +1,50 @@
1
+ import { useRef } from 'react';
2
+ import type { OpenAPIClientContext } from './types';
3
+ import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
4
+ import { useDisclosureState } from 'react-stately';
5
+
6
+ interface Props {
7
+ context: OpenAPIClientContext;
8
+ children: React.ReactNode;
9
+ label?: string;
10
+ }
11
+
12
+ /**
13
+ * Display an interactive OpenAPI disclosure.
14
+ * The label is optional and defaults to "child attributes".
15
+ */
16
+ export function OpenAPIDisclosure({ context, children, label }: Props): JSX.Element {
17
+ const state = useDisclosureState({});
18
+ const panelRef = useRef<HTMLDivElement | null>(null);
19
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
20
+ const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
21
+ const { buttonProps } = useButton(triggerProps, triggerRef);
22
+ const { isFocusVisible, focusProps } = useFocusRing();
23
+
24
+ return (
25
+ <div className="openapi-disclosure">
26
+ <button
27
+ ref={triggerRef}
28
+ {...mergeProps(buttonProps, focusProps)}
29
+ slot="trigger"
30
+ className="openapi-disclosure-trigger"
31
+ style={{
32
+ outline: isFocusVisible
33
+ ? '2px solid rgb(var(--primary-color-500) / 0.4)'
34
+ : 'none',
35
+ }}
36
+ >
37
+ {context.icons.plus}
38
+ <span>
39
+ {`${state.isExpanded ? 'Hide' : 'Show'} ${label ? label : `child attributes`}`}
40
+ </span>
41
+ </button>
42
+
43
+ {state.isExpanded && (
44
+ <div ref={panelRef} {...panelProps} className="openapi-disclosure-panel">
45
+ {children}
46
+ </div>
47
+ )}
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,136 @@
1
+ interface Props {
2
+ groups: TDisclosureGroup[];
3
+ icon?: React.ReactNode;
4
+ }
5
+
6
+ type TDisclosureGroup = {
7
+ id: string;
8
+ label: string | React.ReactNode;
9
+ tabs?: {
10
+ id: string;
11
+ label: string | React.ReactNode;
12
+ body?: React.ReactNode;
13
+ }[];
14
+ };
15
+
16
+ import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
17
+ import {
18
+ DisclosureGroupProps,
19
+ DisclosureGroupState,
20
+ useDisclosureGroupState,
21
+ useDisclosureState,
22
+ } from 'react-stately';
23
+ import { createContext, useContext, useRef, useState } from 'react';
24
+
25
+ const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);
26
+
27
+ /**
28
+ * Display an interactive OpenAPI disclosure group.
29
+ */
30
+ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
31
+ const { icon, groups } = props;
32
+
33
+ const state = useDisclosureGroupState(props);
34
+
35
+ return (
36
+ <DisclosureGroupStateContext.Provider value={state}>
37
+ {groups.map((group) => (
38
+ <DisclosureItem icon={icon} key={group.id} group={group} />
39
+ ))}
40
+ </DisclosureGroupStateContext.Provider>
41
+ );
42
+ }
43
+
44
+ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }) {
45
+ const { icon, group } = props;
46
+
47
+ const defaultId = useId();
48
+ const id = group.id || defaultId;
49
+ const groupState = useContext(DisclosureGroupStateContext);
50
+ const isExpanded = groupState?.expandedKeys.has(id) || false;
51
+ const state = useDisclosureState({
52
+ isExpanded,
53
+ onExpandedChange() {
54
+ if (groupState) {
55
+ groupState.toggleKey(id);
56
+ }
57
+ },
58
+ });
59
+
60
+ const panelRef = useRef<HTMLDivElement | null>(null);
61
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
62
+ const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
63
+ const { buttonProps: triggerProps, panelProps } = useDisclosure(
64
+ {
65
+ ...props,
66
+ isExpanded,
67
+ isDisabled,
68
+ },
69
+ state,
70
+ panelRef,
71
+ );
72
+ const { buttonProps } = useButton(triggerProps, triggerRef);
73
+ const { isFocusVisible, focusProps } = useFocusRing();
74
+
75
+ const defaultTab = group.tabs?.[0]?.id || '';
76
+ const [selectedTabKey, setSelectedTabKey] = useState(defaultTab);
77
+ const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
78
+
79
+ return (
80
+ <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
81
+ <div className="openapi-disclosure-group-header">
82
+ <button
83
+ slot="trigger"
84
+ ref={triggerRef}
85
+ {...mergeProps(buttonProps, focusProps)}
86
+ disabled={isDisabled}
87
+ style={{
88
+ outline: isFocusVisible
89
+ ? '2px solid rgb(var(--primary-color-500)/0.4)'
90
+ : 'none',
91
+ }}
92
+ className="openapi-disclosure-group-trigger"
93
+ >
94
+ <div className="openapi-disclosure-group-icon">
95
+ {icon || (
96
+ <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
97
+ <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
98
+ </svg>
99
+ )}
100
+ </div>
101
+
102
+ {group.label}
103
+ </button>
104
+ {group.tabs ? (
105
+ <div className="openapi-disclosure-group-mediatype">
106
+ {group.tabs?.length > 1 ? (
107
+ <select
108
+ className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
109
+ onClick={(event) => event.stopPropagation()}
110
+ value={selectedTab?.id}
111
+ onChange={(event) => {
112
+ setSelectedTabKey(event.target.value);
113
+ state.expand();
114
+ }}
115
+ >
116
+ {group.tabs.map((tab) => (
117
+ <option key={tab.id} value={tab.id}>
118
+ {tab.label}
119
+ </option>
120
+ ))}
121
+ </select>
122
+ ) : !!group.tabs[0] ? (
123
+ <span>{group.tabs[0].label}</span>
124
+ ) : null}
125
+ </div>
126
+ ) : null}
127
+ </div>
128
+
129
+ {state.isExpanded && selectedTab && (
130
+ <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}>
131
+ {selectedTab.body}
132
+ </div>
133
+ )}
134
+ </div>
135
+ );
136
+ }
@@ -1,14 +1,12 @@
1
- import * as React from 'react';
2
- import classNames from 'classnames';
3
- import { ApiClientModalProvider } from '@scalar/api-client-react';
1
+ import clsx from 'clsx';
4
2
 
5
- import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
6
3
  import { Markdown } from './Markdown';
7
4
  import { OpenAPICodeSample } from './OpenAPICodeSample';
8
5
  import { OpenAPIResponseExample } from './OpenAPIResponseExample';
9
- import { OpenAPIServerURL } from './OpenAPIServerURL';
10
6
  import { OpenAPISpec } from './OpenAPISpec';
11
- import { OpenAPIClientContext, OpenAPIContextProps } from './types';
7
+ import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types';
8
+ import { OpenAPIPath } from './OpenAPIPath';
9
+ import { resolveDescription } from './utils';
12
10
 
13
11
  /**
14
12
  * Display an interactive OpenAPI operation.
@@ -19,7 +17,7 @@ export function OpenAPIOperation(props: {
19
17
  context: OpenAPIContextProps;
20
18
  }) {
21
19
  const { className, data, context } = props;
22
- const { operation, servers, method, path } = data;
20
+ const { operation } = data;
23
21
 
24
22
  const clientContext: OpenAPIClientContext = {
25
23
  defaultInteractiveOpened: context.defaultInteractiveOpened,
@@ -27,46 +25,42 @@ export function OpenAPIOperation(props: {
27
25
  blockKey: context.blockKey,
28
26
  };
29
27
 
28
+ const description = resolveDescription(operation)?.trim();
29
+
30
30
  return (
31
- <ApiClientModalProvider
32
- configuration={{ spec: { url: context.specUrl } }}
33
- initialRequest={{ path: data.path, method: data.method }}
34
- >
35
- <div className={classNames('openapi-operation', className)}>
36
- <div className="openapi-intro">
37
- <h2 className="openapi-summary" id={context.id}>
38
- {operation.summary}
39
- </h2>
40
- {operation.description ? (
41
- <Markdown className="openapi-description" source={operation.description} />
31
+ <div className={clsx('openapi-operation', className)}>
32
+ <div className="openapi-summary" id={context.id}>
33
+ <h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
34
+ {operation.summary}
35
+ </h2>
36
+ {operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
37
+ </div>
38
+ <div className="openapi-columns">
39
+ <div className="openapi-column-spec">
40
+ {operation['x-deprecated-sunset'] ? (
41
+ <div className="openapi-deprecated-sunset openapi-description openapi-markdown">
42
+ This operation is deprecated and will be sunset on{' '}
43
+ <span className="openapi-deprecated-sunset-date">
44
+ {operation['x-deprecated-sunset']}
45
+ </span>
46
+ {`.`}
47
+ </div>
42
48
  ) : null}
43
- <div className="openapi-target">
44
- <span
45
- className={classNames(
46
- 'openapi-method',
47
- `openapi-method-${method.toLowerCase()}`,
48
- )}
49
- >
50
- {method.toUpperCase()}
51
- </span>
52
- <span className="openapi-url">
53
- <OpenAPIServerURL servers={servers} />
54
- {path}
55
- </span>
56
- </div>
57
- </div>
58
- <div className={classNames('openapi-columns')}>
59
- <div className={classNames('openapi-column-spec')}>
60
- <OpenAPISpec rawData={toJSON(data)} context={clientContext} />
61
- </div>
62
- <div className={classNames('openapi-column-preview')}>
63
- <div className={classNames('openapi-column-preview-body')}>
64
- <OpenAPICodeSample {...props} />
65
- <OpenAPIResponseExample {...props} />
49
+ {description ? (
50
+ <div className="openapi-intro">
51
+ <Markdown className="openapi-description" source={description} />
66
52
  </div>
53
+ ) : null}
54
+ <OpenAPIPath data={data} context={context} />
55
+ <OpenAPISpec data={data} context={clientContext} />
56
+ </div>
57
+ <div className="openapi-column-preview">
58
+ <div className="openapi-column-preview-body">
59
+ <OpenAPICodeSample {...props} />
60
+ <OpenAPIResponseExample {...props} />
67
61
  </div>
68
62
  </div>
69
63
  </div>
70
- </ApiClientModalProvider>
64
+ </div>
71
65
  );
72
66
  }