@gitbook/react-openapi 1.1.9 → 1.2.0

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 (177) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/InteractiveSection.d.ts +4 -0
  3. package/dist/InteractiveSection.jsx +14 -13
  4. package/dist/OpenAPICodeSample.d.ts +3 -2
  5. package/dist/OpenAPICodeSample.jsx +8 -12
  6. package/dist/OpenAPICodeSampleInteractive.d.ts +3 -0
  7. package/dist/OpenAPICodeSampleInteractive.jsx +37 -49
  8. package/dist/OpenAPICodeSampleSelector.d.ts +14 -0
  9. package/dist/OpenAPICodeSampleSelector.jsx +44 -0
  10. package/dist/OpenAPICopyButton.d.ts +2 -0
  11. package/dist/OpenAPICopyButton.jsx +5 -2
  12. package/dist/OpenAPIDisclosure.d.ts +4 -3
  13. package/dist/OpenAPIDisclosure.jsx +8 -11
  14. package/dist/OpenAPIDisclosureGroup.d.ts +7 -3
  15. package/dist/OpenAPIDisclosureGroup.jsx +18 -18
  16. package/dist/OpenAPIExample.d.ts +16 -0
  17. package/dist/OpenAPIExample.jsx +36 -0
  18. package/dist/OpenAPIMediaType.d.ts +21 -0
  19. package/dist/OpenAPIMediaType.jsx +61 -0
  20. package/dist/OpenAPIOperation.d.ts +3 -2
  21. package/dist/OpenAPIOperation.jsx +9 -72
  22. package/dist/OpenAPIOperationDescription.d.ts +9 -0
  23. package/dist/OpenAPIOperationDescription.jsx +22 -0
  24. package/dist/OpenAPIOperationStability.d.ts +9 -0
  25. package/dist/OpenAPIOperationStability.jsx +27 -0
  26. package/dist/OpenAPIPath.d.ts +12 -2
  27. package/dist/OpenAPIPath.jsx +10 -4
  28. package/dist/OpenAPIRequestBody.d.ts +3 -1
  29. package/dist/OpenAPIRequestBody.jsx +4 -3
  30. package/dist/OpenAPIResponse.d.ts +1 -1
  31. package/dist/OpenAPIResponse.jsx +1 -1
  32. package/dist/OpenAPIResponseExample.d.ts +4 -3
  33. package/dist/OpenAPIResponseExample.jsx +24 -154
  34. package/dist/OpenAPIResponseExampleContent.d.ts +19 -0
  35. package/dist/OpenAPIResponseExampleContent.jsx +57 -0
  36. package/dist/OpenAPIResponses.d.ts +1 -1
  37. package/dist/OpenAPIResponses.jsx +49 -36
  38. package/dist/OpenAPISchema.d.ts +1 -1
  39. package/dist/OpenAPISchema.jsx +121 -20
  40. package/dist/OpenAPISchemaName.d.ts +2 -0
  41. package/dist/OpenAPISchemaName.jsx +21 -17
  42. package/dist/OpenAPISchemaServer.d.ts +1 -1
  43. package/dist/OpenAPISecurities.d.ts +2 -1
  44. package/dist/OpenAPISecurities.jsx +11 -10
  45. package/dist/OpenAPISelect.d.ts +22 -0
  46. package/dist/OpenAPISelect.jsx +43 -0
  47. package/dist/OpenAPISpec.d.ts +3 -2
  48. package/dist/OpenAPISpec.jsx +11 -9
  49. package/dist/OpenAPITabs.jsx +9 -9
  50. package/dist/OpenAPIWebhook.d.ts +10 -0
  51. package/dist/OpenAPIWebhook.jsx +23 -0
  52. package/dist/OpenAPIWebhookExample.d.ts +6 -0
  53. package/dist/OpenAPIWebhookExample.jsx +41 -0
  54. package/dist/ScalarApiButton.d.ts +2 -0
  55. package/dist/ScalarApiButton.jsx +4 -3
  56. package/dist/StaticSection.d.ts +4 -1
  57. package/dist/StaticSection.jsx +13 -4
  58. package/dist/code-samples.js +57 -39
  59. package/dist/common/OpenAPIColumnSpec.d.ts +6 -0
  60. package/dist/common/OpenAPIColumnSpec.jsx +20 -0
  61. package/dist/common/OpenAPIOperationDescription.d.ts +6 -0
  62. package/dist/common/OpenAPIOperationDescription.jsx +19 -0
  63. package/dist/common/OpenAPIStability.d.ts +4 -0
  64. package/dist/common/OpenAPIStability.jsx +15 -0
  65. package/dist/common/OpenAPISummary.d.ts +6 -0
  66. package/dist/common/OpenAPISummary.jsx +30 -0
  67. package/dist/context.d.ts +75 -0
  68. package/dist/context.js +43 -0
  69. package/dist/generateSchemaExample.js +4 -0
  70. package/dist/getOrCreateStoreByKey.d.ts +10 -0
  71. package/dist/getOrCreateStoreByKey.js +19 -0
  72. package/dist/index.d.ts +5 -1
  73. package/dist/index.js +3 -0
  74. package/dist/resolveOpenAPIOperation.js +10 -5
  75. package/dist/resolveOpenAPIWebhook.d.ts +11 -0
  76. package/dist/resolveOpenAPIWebhook.js +127 -0
  77. package/dist/schemas/OpenAPISchemas.d.ts +5 -6
  78. package/dist/schemas/OpenAPISchemas.jsx +52 -49
  79. package/dist/schemas/resolveOpenAPISchemas.d.ts +4 -3
  80. package/dist/schemas/resolveOpenAPISchemas.js +0 -1
  81. package/dist/stringifyOpenAPI.d.ts +1 -1
  82. package/dist/stringifyOpenAPI.js +6 -3
  83. package/dist/translate.d.ts +10 -0
  84. package/dist/translate.jsx +75 -0
  85. package/dist/translations/de.d.ts +37 -0
  86. package/dist/translations/de.js +37 -0
  87. package/dist/translations/en.d.ts +37 -0
  88. package/dist/translations/en.js +37 -0
  89. package/dist/translations/es.d.ts +37 -0
  90. package/dist/translations/es.js +37 -0
  91. package/dist/translations/fr.d.ts +37 -0
  92. package/dist/translations/fr.js +37 -0
  93. package/dist/translations/index.d.ts +341 -0
  94. package/dist/translations/index.js +27 -0
  95. package/dist/translations/ja.d.ts +37 -0
  96. package/dist/translations/ja.js +37 -0
  97. package/dist/translations/nl.d.ts +37 -0
  98. package/dist/translations/nl.js +37 -0
  99. package/dist/translations/no.d.ts +37 -0
  100. package/dist/translations/no.js +37 -0
  101. package/dist/translations/pt-br.d.ts +37 -0
  102. package/dist/translations/pt-br.js +37 -0
  103. package/dist/translations/types.d.ts +5 -0
  104. package/dist/translations/types.js +1 -0
  105. package/dist/translations/zh.d.ts +37 -0
  106. package/dist/translations/zh.js +37 -0
  107. package/dist/tsconfig.build.tsbuildinfo +1 -1
  108. package/dist/types.d.ts +12 -48
  109. package/dist/util/example.d.ts +35 -0
  110. package/dist/util/example.jsx +103 -0
  111. package/dist/utils.d.ts +18 -0
  112. package/dist/utils.js +57 -0
  113. package/package.json +3 -3
  114. package/src/InteractiveSection.tsx +22 -18
  115. package/src/OpenAPICodeSample.tsx +26 -15
  116. package/src/OpenAPICodeSampleInteractive.tsx +67 -70
  117. package/src/OpenAPICodeSampleSelector.tsx +94 -0
  118. package/src/OpenAPICopyButton.tsx +7 -2
  119. package/src/OpenAPIDisclosure.tsx +20 -22
  120. package/src/OpenAPIDisclosureGroup.tsx +40 -22
  121. package/src/OpenAPIExample.tsx +55 -0
  122. package/src/OpenAPIMediaType.tsx +139 -0
  123. package/src/OpenAPIOperation.tsx +11 -104
  124. package/src/OpenAPIOperationDescription.tsx +34 -0
  125. package/src/OpenAPIOperationStability.tsx +39 -0
  126. package/src/OpenAPIPath.tsx +26 -6
  127. package/src/OpenAPIRequestBody.tsx +9 -4
  128. package/src/OpenAPIResponse.tsx +2 -2
  129. package/src/OpenAPIResponseExample.tsx +41 -215
  130. package/src/OpenAPIResponseExampleContent.tsx +123 -0
  131. package/src/OpenAPIResponses.tsx +83 -62
  132. package/src/OpenAPISchema.test.ts +80 -0
  133. package/src/OpenAPISchema.tsx +149 -25
  134. package/src/OpenAPISchemaName.tsx +28 -19
  135. package/src/OpenAPISchemaServer.tsx +1 -1
  136. package/src/OpenAPISecurities.tsx +46 -12
  137. package/src/OpenAPISelect.tsx +96 -0
  138. package/src/OpenAPISpec.tsx +21 -10
  139. package/src/OpenAPITabs.tsx +9 -9
  140. package/src/OpenAPIWebhook.tsx +33 -0
  141. package/src/OpenAPIWebhookExample.tsx +60 -0
  142. package/src/ScalarApiButton.tsx +6 -6
  143. package/src/StaticSection.tsx +37 -5
  144. package/src/code-samples.test.ts +3 -1
  145. package/src/code-samples.ts +67 -54
  146. package/src/common/OpenAPIColumnSpec.tsx +31 -0
  147. package/src/common/OpenAPIOperationDescription.tsx +31 -0
  148. package/src/common/OpenAPIStability.tsx +23 -0
  149. package/src/common/OpenAPISummary.tsx +45 -0
  150. package/src/context.ts +99 -0
  151. package/src/generateSchemaExample.test.ts +1020 -0
  152. package/src/generateSchemaExample.ts +5 -0
  153. package/src/getOrCreateStoreByKey.ts +33 -0
  154. package/src/index.ts +5 -1
  155. package/src/resolveOpenAPIOperation.ts +14 -3
  156. package/src/resolveOpenAPIWebhook.ts +99 -0
  157. package/src/schemas/OpenAPISchemas.tsx +76 -71
  158. package/src/schemas/resolveOpenAPISchemas.ts +4 -5
  159. package/src/stringifyOpenAPI.ts +11 -3
  160. package/src/translate.tsx +80 -0
  161. package/src/translations/de.ts +37 -0
  162. package/src/translations/en.ts +37 -0
  163. package/src/translations/es.ts +37 -0
  164. package/src/translations/fr.ts +37 -0
  165. package/src/translations/index.ts +33 -0
  166. package/src/translations/ja.ts +37 -0
  167. package/src/translations/nl.ts +37 -0
  168. package/src/translations/no.ts +37 -0
  169. package/src/translations/pt-br.ts +37 -0
  170. package/src/translations/types.ts +7 -0
  171. package/src/translations/zh.ts +37 -0
  172. package/src/types.ts +11 -46
  173. package/src/util/example.tsx +129 -0
  174. package/src/utils.ts +67 -0
  175. package/dist/useSyncedTabsGlobalState.d.ts +0 -10
  176. package/dist/useSyncedTabsGlobalState.js +0 -20
  177. package/src/useSyncedTabsGlobalState.ts +0 -35
@@ -1,86 +1,70 @@
1
1
  'use client';
2
2
  import clsx from 'clsx';
3
- import { useCallback } from 'react';
4
- import { useStore } from 'zustand';
5
3
  import type { MediaTypeRenderer } from './OpenAPICodeSample';
6
- import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
7
-
8
- type MediaTypeState = {
9
- mediaType: string;
10
- setMediaType: (mediaType: string) => void;
11
- };
12
-
13
- function useMediaTypeState(
14
- data: { method: string; path: string },
15
- defaultKey: string
16
- ): MediaTypeState {
17
- const { method, path } = data;
18
- const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey));
19
- if (typeof store.tabKey !== 'string') {
20
- throw new Error('Media type key is not a string');
21
- }
22
- return {
23
- mediaType: store.tabKey,
24
- setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]),
25
- };
26
- }
27
-
28
- function useMediaTypeSampleIndexState(data: { method: string; path: string }, mediaType: string) {
29
- const { method, path } = data;
30
- const store = useStore(
31
- getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
32
- );
33
- if (typeof store.tabKey !== 'number') {
34
- throw new Error('Example key is not a number');
35
- }
36
- return {
37
- index: store.tabKey,
38
- setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]),
39
- };
40
- }
4
+ import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
5
+ import { createStateKey } from './utils';
41
6
 
42
7
  export function OpenAPIMediaTypeExamplesSelector(props: {
43
8
  method: string;
44
9
  path: string;
45
10
  renderers: MediaTypeRenderer[];
11
+ selectIcon?: React.ReactNode;
12
+ blockKey?: string;
46
13
  }) {
47
- const { method, path, renderers } = props;
14
+ const { method, path, renderers, selectIcon, blockKey } = props;
48
15
  if (!renderers[0]) {
49
16
  throw new Error('No renderers provided');
50
17
  }
51
- const state = useMediaTypeState({ method, path }, renderers[0].mediaType);
52
- const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0];
18
+ const stateKey = createStateKey('request-body-media-type', blockKey);
19
+ const state = useSelectState(stateKey, renderers[0].mediaType);
20
+ const selected = renderers.find((r) => r.mediaType === state.key) || renderers[0];
53
21
 
54
22
  return (
55
23
  <div className="openapi-codesample-selectors">
56
- <MediaTypeSelector state={state} renderers={renderers} />
57
- <ExamplesSelector method={method} path={path} renderer={selected} />
24
+ <MediaTypeSelector selectIcon={selectIcon} stateKey={stateKey} renderers={renderers} />
25
+ <ExamplesSelector
26
+ selectIcon={selectIcon}
27
+ method={method}
28
+ path={path}
29
+ renderer={selected}
30
+ />
58
31
  </div>
59
32
  );
60
33
  }
61
34
 
62
35
  function MediaTypeSelector(props: {
63
- state: MediaTypeState;
36
+ stateKey: string;
64
37
  renderers: MediaTypeRenderer[];
38
+ selectIcon?: React.ReactNode;
65
39
  }) {
66
- const { renderers, state } = props;
40
+ const { renderers, stateKey, selectIcon } = props;
67
41
 
68
42
  if (renderers.length < 2) {
69
43
  return null;
70
44
  }
71
45
 
46
+ const items = renderers.map((renderer) => ({
47
+ key: renderer.mediaType,
48
+ label: renderer.mediaType,
49
+ }));
50
+
72
51
  return (
73
- <select
52
+ <OpenAPISelect
74
53
  className={clsx('openapi-select')}
75
- value={state.mediaType}
76
- onChange={(e) => state.setMediaType(e.target.value)}
54
+ items={renderers.map((renderer) => ({
55
+ key: renderer.mediaType,
56
+ label: renderer.mediaType,
57
+ }))}
58
+ icon={selectIcon}
59
+ stateKey={stateKey}
60
+ placement="bottom start"
77
61
  >
78
- {renderers.map((renderer) => (
79
- <option key={renderer.mediaType} value={renderer.mediaType}>
80
- {renderer.mediaType}
81
- </option>
62
+ {items.map((item) => (
63
+ <OpenAPISelectItem key={item.key} id={item.key} value={item}>
64
+ {item.label}
65
+ </OpenAPISelectItem>
82
66
  ))}
83
- </select>
67
+ </OpenAPISelect>
84
68
  );
85
69
  }
86
70
 
@@ -88,25 +72,31 @@ function ExamplesSelector(props: {
88
72
  method: string;
89
73
  path: string;
90
74
  renderer: MediaTypeRenderer;
75
+ selectIcon?: React.ReactNode;
91
76
  }) {
92
- const { method, path, renderer } = props;
93
- const state = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
77
+ const { method, path, renderer, selectIcon } = props;
94
78
  if (renderer.examples.length < 2) {
95
79
  return null;
96
80
  }
97
81
 
82
+ const items = renderer.examples.map((example, index) => ({
83
+ key: index,
84
+ label: example.example.summary || `Example ${index + 1}`,
85
+ }));
86
+
98
87
  return (
99
- <select
100
- className={clsx('openapi-select')}
101
- value={String(state.index)}
102
- onChange={(e) => state.setIndex(Number(e.target.value))}
88
+ <OpenAPISelect
89
+ items={items}
90
+ icon={selectIcon}
91
+ stateKey={`media-type-sample-${renderer.mediaType}-${method}-${path}`}
92
+ placement="bottom start"
103
93
  >
104
- {renderer.examples.map((example, index) => (
105
- <option key={index} value={index}>
106
- {example.example.summary || `Example ${index + 1}`}
107
- </option>
94
+ {items.map((item) => (
95
+ <OpenAPISelectItem key={item.key} id={item.key} value={item}>
96
+ {item.label}
97
+ </OpenAPISelectItem>
108
98
  ))}
109
- </select>
99
+ </OpenAPISelect>
110
100
  );
111
101
  }
112
102
 
@@ -114,14 +104,18 @@ export function OpenAPIMediaTypeExamplesBody(props: {
114
104
  method: string;
115
105
  path: string;
116
106
  renderers: MediaTypeRenderer[];
107
+ blockKey?: string;
117
108
  }) {
118
- const { renderers, method, path } = props;
109
+ const { renderers, method, path, blockKey } = props;
119
110
  if (!renderers[0]) {
120
111
  throw new Error('No renderers provided');
121
112
  }
122
- const mediaTypeState = useMediaTypeState({ method, path }, renderers[0].mediaType);
123
- const selected =
124
- renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0];
113
+
114
+ const mediaTypeState = useSelectState(
115
+ createStateKey('request-body-media-type', blockKey),
116
+ renderers[0].mediaType
117
+ );
118
+ const selected = renderers.find((r) => r.mediaType === mediaTypeState.key) ?? renderers[0];
125
119
  if (selected.examples.length === 0) {
126
120
  return selected.element;
127
121
  }
@@ -130,10 +124,13 @@ export function OpenAPIMediaTypeExamplesBody(props: {
130
124
 
131
125
  function ExamplesBody(props: { method: string; path: string; renderer: MediaTypeRenderer }) {
132
126
  const { method, path, renderer } = props;
133
- const exampleState = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
134
- const example = renderer.examples[exampleState.index] ?? renderer.examples[0];
127
+ const exampleState = useSelectState(
128
+ `media-type-sample-${renderer.mediaType}-${method}-${path}`,
129
+ renderer.mediaType
130
+ );
131
+ const example = renderer.examples[Number(exampleState.key)] ?? renderer.examples[0];
135
132
  if (!example) {
136
- throw new Error(`No example found for index ${exampleState.index}`);
133
+ throw new Error(`No example found for key ${exampleState.key}`);
137
134
  }
138
135
  return example.element;
139
136
  }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import type { Key } from 'react-aria';
5
+ import { useStore } from 'zustand';
6
+ import { OpenAPIPath } from './OpenAPIPath';
7
+ import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
8
+ import { StaticSection } from './StaticSection';
9
+ import type { OpenAPIClientContext } from './context';
10
+ import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
11
+ import type { OpenAPIOperationData } from './types';
12
+
13
+ function useCodeSampleState(initialKey: Key = 'default') {
14
+ const store = useStore(getOrCreateStoreByKey('codesample', initialKey));
15
+ return {
16
+ key: store.key,
17
+ setKey: useCallback((key: Key) => store.setKey(key), [store.setKey]),
18
+ };
19
+ }
20
+
21
+ type CodeSampleItem = OpenAPISelectItem & {
22
+ body: React.ReactNode;
23
+ footer?: React.ReactNode;
24
+ };
25
+
26
+ function OpenAPICodeSampleHeader(props: {
27
+ items: CodeSampleItem[];
28
+ data: OpenAPIOperationData;
29
+ selectIcon?: React.ReactNode;
30
+ context: OpenAPIClientContext;
31
+ }) {
32
+ const { data, items, selectIcon, context } = props;
33
+
34
+ return (
35
+ <>
36
+ <OpenAPIPath context={context} canCopy={false} withServer={false} data={data} />
37
+ {items.length > 1 ? (
38
+ <OpenAPISelect
39
+ icon={selectIcon}
40
+ items={items}
41
+ stateKey="codesample"
42
+ placement="bottom end"
43
+ >
44
+ {items.map((item) => (
45
+ <OpenAPISelectItem key={item.key} id={item.key} value={item}>
46
+ {item.label}
47
+ </OpenAPISelectItem>
48
+ ))}
49
+ </OpenAPISelect>
50
+ ) : items[0] ? (
51
+ <span className="openapi-codesample-label">{items[0].label}</span>
52
+ ) : null}
53
+ </>
54
+ );
55
+ }
56
+
57
+ export function OpenAPICodeSampleBody(props: {
58
+ items: CodeSampleItem[];
59
+ data: OpenAPIOperationData;
60
+ selectIcon?: React.ReactNode;
61
+ context: OpenAPIClientContext;
62
+ }) {
63
+ const { items, data, selectIcon, context } = props;
64
+ if (!items[0]) {
65
+ throw new Error('No items provided');
66
+ }
67
+
68
+ const state = useCodeSampleState(items[0]?.key);
69
+
70
+ const selected = items.find((item) => item.key === state.key) || items[0];
71
+
72
+ if (!selected) {
73
+ return null;
74
+ }
75
+
76
+ return (
77
+ <StaticSection
78
+ header={
79
+ <OpenAPICodeSampleHeader
80
+ context={context}
81
+ selectIcon={selectIcon}
82
+ data={data}
83
+ items={items}
84
+ />
85
+ }
86
+ className="openapi-codesample"
87
+ >
88
+ <div id={selected.key as string} className="openapi-codesample-panel">
89
+ {selected.body ? selected.body : null}
90
+ {selected.footer ? selected.footer : null}
91
+ </div>
92
+ </StaticSection>
93
+ );
94
+ }
@@ -2,11 +2,14 @@
2
2
 
3
3
  import { useState } from 'react';
4
4
  import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
5
+ import type { OpenAPIClientContext } from './context';
6
+ import { t } from './translate';
5
7
 
6
8
  export function OpenAPICopyButton(
7
9
  props: ButtonProps & {
8
10
  value: string;
9
11
  children: React.ReactNode;
12
+ context: OpenAPIClientContext;
10
13
  label?: string;
11
14
  /**
12
15
  * Whether to show a tooltip.
@@ -15,7 +18,7 @@ export function OpenAPICopyButton(
15
18
  withTooltip?: boolean;
16
19
  }
17
20
  ) {
18
- const { value, label, children, onPress, className, withTooltip = true } = props;
21
+ const { value, label, children, onPress, className, context, withTooltip = true } = props;
19
22
  const [copied, setCopied] = useState(false);
20
23
  const [isOpen, setIsOpen] = useState(false);
21
24
 
@@ -60,7 +63,9 @@ export function OpenAPICopyButton(
60
63
  offset={4}
61
64
  className="openapi-tooltip"
62
65
  >
63
- {copied ? 'Copied' : label || 'Copy to clipboard'}
66
+ {copied
67
+ ? t(context.translation, 'copied')
68
+ : label || t(context.translation, 'copy_to_clipboard')}
64
69
  </Tooltip>
65
70
  </TooltipTrigger>
66
71
  );
@@ -1,41 +1,39 @@
1
1
  'use client';
2
+ import clsx from 'clsx';
3
+ import type React from 'react';
2
4
  import { useState } from 'react';
3
- import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components';
4
- import type { OpenAPIClientContext } from './types';
5
+ import { Button, Disclosure, DisclosurePanel } from 'react-aria-components';
5
6
 
6
7
  /**
7
8
  * Display an interactive OpenAPI disclosure.
8
9
  */
9
10
  export function OpenAPIDisclosure(props: {
10
- context: OpenAPIClientContext;
11
+ icon: React.ReactNode;
11
12
  children: React.ReactNode;
12
- label: string;
13
+ label: string | ((isExpanded: boolean) => string);
14
+ className?: string;
13
15
  }): React.JSX.Element {
14
- const { context, children, label } = props;
16
+ const { icon, children, label, className } = props;
15
17
  const [isExpanded, setIsExpanded] = useState(false);
16
18
 
17
19
  return (
18
20
  <Disclosure
19
- className="openapi-disclosure"
21
+ className={clsx('openapi-disclosure', className)}
20
22
  isExpanded={isExpanded}
21
23
  onExpandedChange={setIsExpanded}
22
24
  >
23
- <Heading>
24
- <Button
25
- slot="trigger"
26
- className="openapi-disclosure-trigger"
27
- style={({ isFocusVisible }) => ({
28
- outline: isFocusVisible
29
- ? '2px solid rgb(var(--primary-color-500) / 0.4)'
30
- : 'none',
31
- })}
32
- >
33
- {context.icons.plus}
34
- <span>
35
- {isExpanded ? 'Hide' : 'Show'} {label}
36
- </span>
37
- </Button>
38
- </Heading>
25
+ <Button
26
+ slot="trigger"
27
+ className="openapi-disclosure-trigger"
28
+ style={({ isFocusVisible }) => ({
29
+ outline: isFocusVisible
30
+ ? '2px solid rgb(var(--primary-color-500) / 0.4)'
31
+ : 'none',
32
+ })}
33
+ >
34
+ {icon}
35
+ <span>{typeof label === 'function' ? label(isExpanded) : label}</span>
36
+ </Button>
39
37
  <DisclosurePanel className="openapi-disclosure-panel">
40
38
  {isExpanded ? children : null}
41
39
  </DisclosurePanel>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useRef, useState } from 'react';
3
+ import { createContext, useContext, useRef } from 'react';
4
4
  import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
5
5
  import {
6
6
  type DisclosureGroupProps,
@@ -8,18 +8,23 @@ import {
8
8
  useDisclosureGroupState,
9
9
  useDisclosureState,
10
10
  } from 'react-stately';
11
+ import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
11
12
 
12
13
  interface Props {
13
14
  groups: TDisclosureGroup[];
14
15
  icon?: React.ReactNode;
16
+ /** State key to use with a store */
17
+ selectStateKey?: string;
18
+ /** Icon to display for the select */
19
+ selectIcon?: React.ReactNode;
15
20
  }
16
21
 
17
22
  type TDisclosureGroup = {
18
- id: string;
23
+ key: string;
19
24
  label: string | React.ReactNode;
20
25
  tabs?: {
21
- id: string;
22
- label?: string | React.ReactNode;
26
+ key: string;
27
+ label: string | React.ReactNode;
23
28
  body?: React.ReactNode;
24
29
  }[];
25
30
  };
@@ -30,24 +35,35 @@ const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(n
30
35
  * Display an interactive OpenAPI disclosure group.
31
36
  */
32
37
  export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
33
- const { icon, groups } = props;
38
+ const { icon, groups, selectStateKey, selectIcon } = props;
34
39
 
35
40
  const state = useDisclosureGroupState(props);
36
41
 
37
42
  return (
38
43
  <DisclosureGroupStateContext.Provider value={state}>
39
44
  {groups.map((group) => (
40
- <DisclosureItem icon={icon} key={group.id} group={group} />
45
+ <DisclosureItem
46
+ selectStateKey={selectStateKey}
47
+ selectIcon={selectIcon}
48
+ icon={icon}
49
+ key={group.key}
50
+ group={group}
51
+ />
41
52
  ))}
42
53
  </DisclosureGroupStateContext.Provider>
43
54
  );
44
55
  }
45
56
 
46
- function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }) {
47
- const { icon, group } = props;
57
+ function DisclosureItem(props: {
58
+ group: TDisclosureGroup;
59
+ icon?: React.ReactNode;
60
+ selectStateKey?: string;
61
+ selectIcon?: React.ReactNode;
62
+ }) {
63
+ const { icon, group, selectStateKey, selectIcon } = props;
48
64
 
49
65
  const defaultId = useId();
50
- const id = group.id || defaultId;
66
+ const id = group.key || defaultId;
51
67
  const groupState = useContext(DisclosureGroupStateContext);
52
68
  const isExpanded = groupState?.expandedKeys.has(id) || false;
53
69
  const state = useDisclosureState({
@@ -74,9 +90,9 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
74
90
  const { buttonProps } = useButton(triggerProps, triggerRef);
75
91
  const { isFocusVisible, focusProps } = useFocusRing();
76
92
 
77
- const defaultTab = group.tabs?.[0]?.id || '';
78
- const [selectedTabKey, setSelectedTabKey] = useState(defaultTab);
79
- const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
93
+ const defaultTab = group.tabs?.[0]?.key || '';
94
+ const store = useSelectState(selectStateKey, defaultTab);
95
+ const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0];
80
96
 
81
97
  return (
82
98
  <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
@@ -104,23 +120,25 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
104
120
  {group.label}
105
121
  </button>
106
122
  {group.tabs ? (
107
- <div className="openapi-disclosure-group-mediatype">
123
+ <div
124
+ className="openapi-disclosure-group-mediatype"
125
+ onClick={(e) => e.stopPropagation()}
126
+ >
108
127
  {group.tabs?.length > 1 ? (
109
- <select
110
- className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
111
- onClick={(event) => event.stopPropagation()}
112
- value={selectedTab?.id}
113
- onChange={(event) => {
114
- setSelectedTabKey(event.target.value);
128
+ <OpenAPISelect
129
+ icon={selectIcon}
130
+ stateKey={selectStateKey}
131
+ onSelectionChange={() => {
115
132
  state.expand();
116
133
  }}
134
+ items={group.tabs}
117
135
  >
118
136
  {group.tabs.map((tab) => (
119
- <option key={tab.id} value={tab.id}>
137
+ <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
120
138
  {tab.label}
121
- </option>
139
+ </OpenAPISelectItem>
122
140
  ))}
123
- </select>
141
+ </OpenAPISelect>
124
142
  ) : group.tabs[0]?.label ? (
125
143
  <span>{group.tabs[0].label}</span>
126
144
  ) : null}
@@ -0,0 +1,55 @@
1
+ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
+ import type { OpenAPIContext, OpenAPIUniversalContext } from './context';
3
+ import { json2xml } from './json2xml';
4
+ import { stringifyOpenAPI } from './stringifyOpenAPI';
5
+ import { t } from './translate';
6
+
7
+ /**
8
+ * Display an example.
9
+ */
10
+ export function OpenAPIExample(props: {
11
+ example: OpenAPIV3.ExampleObject;
12
+ context: OpenAPIContext;
13
+ syntax: string;
14
+ }) {
15
+ const { example, context, syntax } = props;
16
+ const code = stringifyExample({ example, xml: syntax === 'xml' });
17
+
18
+ if (code === null) {
19
+ return <OpenAPIEmptyExample context={context} />;
20
+ }
21
+
22
+ return context.renderCodeBlock({ code, syntax });
23
+ }
24
+
25
+ function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
26
+ const { example, xml } = args;
27
+
28
+ if (!example.value) {
29
+ return null;
30
+ }
31
+
32
+ if (typeof example.value === 'string') {
33
+ return example.value;
34
+ }
35
+
36
+ if (xml) {
37
+ return json2xml(example.value);
38
+ }
39
+
40
+ return stringifyOpenAPI(example.value, null, 2);
41
+ }
42
+
43
+ /**
44
+ * Empty response example.
45
+ */
46
+ export function OpenAPIEmptyExample(props: {
47
+ context: OpenAPIUniversalContext;
48
+ }) {
49
+ const { context } = props;
50
+ return (
51
+ <pre className="openapi-example-empty">
52
+ <p>{t(context.translation, 'no_content')}</p>
53
+ </pre>
54
+ );
55
+ }