zudoku 0.3.0-dev.99 → 0.3.1-dev.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 (202) hide show
  1. package/dist/app/demo.js +5 -9
  2. package/dist/app/demo.js.map +1 -1
  3. package/dist/app/main.js +9 -7
  4. package/dist/app/main.js.map +1 -1
  5. package/dist/app/standalone.js +12 -8
  6. package/dist/app/standalone.js.map +1 -1
  7. package/dist/cli/dev/handler.js +1 -1
  8. package/dist/config/validators/SidebarSchema.js +9 -4
  9. package/dist/config/validators/SidebarSchema.js.map +1 -1
  10. package/dist/lib/components/ClientOnly.d.ts +3 -0
  11. package/dist/lib/components/ClientOnly.js +7 -0
  12. package/dist/lib/components/ClientOnly.js.map +1 -0
  13. package/dist/lib/components/Header.js +3 -2
  14. package/dist/lib/components/Header.js.map +1 -1
  15. package/dist/lib/components/Markdown.js +3 -2
  16. package/dist/lib/components/Markdown.js.map +1 -1
  17. package/dist/lib/components/Search.js +5 -8
  18. package/dist/lib/components/Search.js.map +1 -1
  19. package/dist/lib/components/SlotletProvider.d.ts +2 -2
  20. package/dist/lib/components/SlotletProvider.js +7 -2
  21. package/dist/lib/components/SlotletProvider.js.map +1 -1
  22. package/dist/lib/components/navigation/SidebarCategory.js +6 -2
  23. package/dist/lib/components/navigation/SidebarCategory.js.map +1 -1
  24. package/dist/lib/components/navigation/SidebarItem.js +3 -2
  25. package/dist/lib/components/navigation/SidebarItem.js.map +1 -1
  26. package/dist/lib/components/navigation/SidebarWrapper.js +2 -3
  27. package/dist/lib/components/navigation/SidebarWrapper.js.map +1 -1
  28. package/dist/lib/core/plugins.d.ts +1 -1
  29. package/dist/lib/core/plugins.js +1 -1
  30. package/dist/lib/core/plugins.js.map +1 -1
  31. package/dist/lib/oas/graphql/index.js +18 -3
  32. package/dist/lib/oas/graphql/index.js.map +1 -1
  33. package/dist/lib/plugins/markdown/MdxPage.js +1 -1
  34. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  35. package/dist/lib/plugins/openapi/OperationListItem.js +5 -5
  36. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  37. package/dist/lib/plugins/openapi/ParameterList.js +3 -1
  38. package/dist/lib/plugins/openapi/ParameterList.js.map +1 -1
  39. package/dist/lib/plugins/openapi/ResponsesSidecarBox.d.ts +3 -1
  40. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js +5 -9
  41. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js.map +1 -1
  42. package/dist/lib/plugins/openapi/Sidecar.d.ts +3 -1
  43. package/dist/lib/plugins/openapi/Sidecar.js +11 -5
  44. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  45. package/dist/lib/plugins/openapi/index.js +1 -1
  46. package/dist/lib/plugins/openapi/index.js.map +1 -1
  47. package/dist/lib/plugins/openapi/playground/Playground.js +2 -3
  48. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  49. package/dist/lib/plugins/openapi/playground/createUrl.js +5 -1
  50. package/dist/lib/plugins/openapi/playground/createUrl.js.map +1 -1
  51. package/dist/lib/plugins/openapi/schema/SchemaComponents.d.ts +13 -0
  52. package/dist/lib/plugins/openapi/schema/SchemaComponents.js +28 -0
  53. package/dist/lib/plugins/openapi/schema/SchemaComponents.js.map +1 -0
  54. package/dist/lib/plugins/openapi/schema/SchemaView.d.ts +6 -0
  55. package/dist/lib/plugins/openapi/schema/SchemaView.js +58 -0
  56. package/dist/lib/plugins/openapi/schema/SchemaView.js.map +1 -0
  57. package/dist/lib/plugins/openapi/schema/utils.d.ts +3 -0
  58. package/dist/lib/plugins/openapi/schema/utils.js +6 -0
  59. package/dist/lib/plugins/openapi/schema/utils.js.map +1 -0
  60. package/dist/lib/plugins/search-inkeep/index.js +3 -5
  61. package/dist/lib/plugins/search-inkeep/index.js.map +1 -1
  62. package/dist/lib/themeToggle.d.ts +1 -0
  63. package/dist/lib/themeToggle.js +7 -0
  64. package/dist/lib/themeToggle.js.map +1 -0
  65. package/dist/lib/util/MdxComponents.js +4 -4
  66. package/dist/lib/util/MdxComponents.js.map +1 -1
  67. package/dist/vite/config.d.ts +4 -5
  68. package/dist/vite/config.js +7 -4
  69. package/dist/vite/config.js.map +1 -1
  70. package/dist/vite/config.test.js +2 -2
  71. package/dist/vite/config.test.js.map +1 -1
  72. package/dist/vite/dev-server.d.ts +0 -1
  73. package/dist/vite/dev-server.js +2 -21
  74. package/dist/vite/dev-server.js.map +1 -1
  75. package/dist/vite/html.js +2 -11
  76. package/dist/vite/html.js.map +1 -1
  77. package/dist/vite/plugin-api-keys.d.ts +3 -3
  78. package/dist/vite/plugin-api-keys.js +2 -1
  79. package/dist/vite/plugin-api-keys.js.map +1 -1
  80. package/dist/vite/plugin-api.d.ts +3 -3
  81. package/dist/vite/plugin-api.js +2 -1
  82. package/dist/vite/plugin-api.js.map +1 -1
  83. package/dist/vite/plugin-auth.d.ts +3 -3
  84. package/dist/vite/plugin-auth.js +2 -1
  85. package/dist/vite/plugin-auth.js.map +1 -1
  86. package/dist/vite/plugin-component.d.ts +3 -3
  87. package/dist/vite/plugin-component.js +17 -14
  88. package/dist/vite/plugin-component.js.map +1 -1
  89. package/dist/vite/plugin-config-reload.d.ts +4 -0
  90. package/dist/vite/plugin-config-reload.js +24 -0
  91. package/dist/vite/plugin-config-reload.js.map +1 -0
  92. package/dist/vite/plugin-config.d.ts +2 -2
  93. package/dist/vite/plugin-config.js.map +1 -1
  94. package/dist/vite/plugin-custom-css.d.ts +3 -3
  95. package/dist/vite/plugin-custom-css.js +2 -1
  96. package/dist/vite/plugin-custom-css.js.map +1 -1
  97. package/dist/vite/plugin-docs.d.ts +3 -3
  98. package/dist/vite/plugin-docs.js +3 -2
  99. package/dist/vite/plugin-docs.js.map +1 -1
  100. package/dist/vite/plugin-html-transform.d.ts +2 -0
  101. package/dist/vite/plugin-html-transform.js +15 -0
  102. package/dist/vite/plugin-html-transform.js.map +1 -0
  103. package/dist/vite/plugin-mdx.d.ts +3 -3
  104. package/dist/vite/plugin-mdx.js +2 -1
  105. package/dist/vite/plugin-mdx.js.map +1 -1
  106. package/dist/vite/plugin-metadata.d.ts +1 -1
  107. package/dist/vite/plugin-redirect.d.ts +3 -3
  108. package/dist/vite/plugin-redirect.js +2 -1
  109. package/dist/vite/plugin-redirect.js.map +1 -1
  110. package/dist/vite/plugin-sidebar.d.ts +3 -3
  111. package/dist/vite/plugin-sidebar.js +5 -4
  112. package/dist/vite/plugin-sidebar.js.map +1 -1
  113. package/dist/vite/plugin.d.ts +3 -2
  114. package/dist/vite/plugin.js +16 -11
  115. package/dist/vite/plugin.js.map +1 -1
  116. package/lib/{CategoryHeading-BWq12Bfa.js → CategoryHeading-z15xh7Jb.js} +2 -2
  117. package/lib/{CategoryHeading-BWq12Bfa.js.map → CategoryHeading-z15xh7Jb.js.map} +1 -1
  118. package/lib/{Combination-D-9IH0zy.js → Combination-DTfV-c98.js} +2 -2
  119. package/lib/{Combination-D-9IH0zy.js.map → Combination-DTfV-c98.js.map} +1 -1
  120. package/lib/{Input-HmAaR6kw.js → Input-DB9VROFR.js} +3 -3
  121. package/lib/{Input-HmAaR6kw.js.map → Input-DB9VROFR.js.map} +1 -1
  122. package/lib/Markdown-CEccPMI_.js +20508 -0
  123. package/lib/Markdown-CEccPMI_.js.map +1 -0
  124. package/lib/{MdxPage-oN3huD58.js → MdxPage-CnqOoqvp.js} +12 -15
  125. package/lib/MdxPage-CnqOoqvp.js.map +1 -0
  126. package/lib/OperationList-Cxiw2Z-v.js +457 -0
  127. package/lib/OperationList-Cxiw2Z-v.js.map +1 -0
  128. package/lib/{Route-DAF15JAU.js → Route-DfAFiR7v.js} +2 -2
  129. package/lib/{Route-DAF15JAU.js.map → Route-DfAFiR7v.js.map} +1 -1
  130. package/lib/SlotletProvider-ByLSCZQa.js +262 -0
  131. package/lib/SlotletProvider-ByLSCZQa.js.map +1 -0
  132. package/lib/{Spinner-BCz1kNGw.js → Spinner-BT_AYFrA.js} +3 -3
  133. package/lib/{Spinner-BCz1kNGw.js.map → Spinner-BT_AYFrA.js.map} +1 -1
  134. package/lib/assets/{worker-CR7aeKop.js → worker-CzHUifWA.js} +710 -703
  135. package/lib/assets/{worker-CR7aeKop.js.map → worker-CzHUifWA.js.map} +1 -1
  136. package/lib/{index-CtKkHGcd.js → index-D-9zqIOh.js} +1159 -1147
  137. package/lib/index-D-9zqIOh.js.map +1 -0
  138. package/lib/{index-D-9Z7HSn.js → index-Dz4LyXZI.js} +3 -3
  139. package/lib/{index-D-9Z7HSn.js.map → index-Dz4LyXZI.js.map} +1 -1
  140. package/lib/zudoku.components.js +775 -759
  141. package/lib/zudoku.components.js.map +1 -1
  142. package/lib/zudoku.openapi-worker.js +734 -727
  143. package/lib/zudoku.openapi-worker.js.map +1 -1
  144. package/lib/zudoku.plugin-api-keys.js +4 -4
  145. package/lib/zudoku.plugin-custom-page.js +1 -1
  146. package/lib/zudoku.plugin-markdown.js +1 -1
  147. package/lib/zudoku.plugin-openapi.js +3 -3
  148. package/lib/zudoku.plugin-search-inkeep.js +23 -22
  149. package/lib/zudoku.plugin-search-inkeep.js.map +1 -1
  150. package/package.json +99 -78
  151. package/src/app/demo-cdn.html +1 -1
  152. package/src/app/demo.html +1 -1
  153. package/src/app/demo.tsx +7 -9
  154. package/src/app/main.css +22 -0
  155. package/src/app/main.tsx +11 -8
  156. package/src/app/standalone.html +1 -1
  157. package/src/app/standalone.tsx +13 -8
  158. package/src/lib/components/ClientOnly.tsx +13 -0
  159. package/src/lib/components/Header.tsx +3 -0
  160. package/src/lib/components/Markdown.tsx +3 -2
  161. package/src/lib/components/Search.tsx +7 -7
  162. package/src/lib/components/SlotletProvider.tsx +10 -5
  163. package/src/lib/components/navigation/SidebarCategory.tsx +13 -2
  164. package/src/lib/components/navigation/SidebarItem.tsx +3 -2
  165. package/src/lib/components/navigation/SidebarWrapper.tsx +20 -18
  166. package/src/lib/core/plugins.ts +2 -2
  167. package/src/lib/oas/graphql/index.ts +26 -7
  168. package/src/lib/plugins/markdown/MdxPage.tsx +0 -1
  169. package/src/lib/plugins/openapi/OperationListItem.tsx +22 -17
  170. package/src/lib/plugins/openapi/ParameterList.tsx +10 -8
  171. package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +51 -39
  172. package/src/lib/plugins/openapi/Sidecar.tsx +34 -19
  173. package/src/lib/plugins/openapi/index.tsx +1 -1
  174. package/src/lib/plugins/openapi/playground/Playground.tsx +3 -6
  175. package/src/lib/plugins/openapi/playground/createUrl.ts +6 -5
  176. package/src/lib/plugins/openapi/schema/SchemaComponents.tsx +126 -0
  177. package/src/lib/plugins/openapi/schema/SchemaView.tsx +172 -0
  178. package/src/lib/plugins/openapi/schema/utils.ts +10 -0
  179. package/src/lib/plugins/search-inkeep/index.tsx +11 -9
  180. package/src/lib/themeToggle.ts +7 -0
  181. package/src/lib/util/MdxComponents.tsx +12 -12
  182. package/LICENSE.md +0 -21
  183. package/dist/lib/plugins/openapi/SchemaListView.d.ts +0 -7
  184. package/dist/lib/plugins/openapi/SchemaListView.js +0 -27
  185. package/dist/lib/plugins/openapi/SchemaListView.js.map +0 -1
  186. package/dist/lib/plugins/openapi/SchemaListViewItem.d.ts +0 -8
  187. package/dist/lib/plugins/openapi/SchemaListViewItem.js +0 -25
  188. package/dist/lib/plugins/openapi/SchemaListViewItem.js.map +0 -1
  189. package/dist/lib/plugins/openapi/SchemaListViewItemGroup.d.ts +0 -8
  190. package/dist/lib/plugins/openapi/SchemaListViewItemGroup.js +0 -17
  191. package/dist/lib/plugins/openapi/SchemaListViewItemGroup.js.map +0 -1
  192. package/lib/Markdown-B_Gax7at.js +0 -14108
  193. package/lib/Markdown-B_Gax7at.js.map +0 -1
  194. package/lib/MdxPage-oN3huD58.js.map +0 -1
  195. package/lib/OperationList-Ctj0ihBN.js +0 -448
  196. package/lib/OperationList-Ctj0ihBN.js.map +0 -1
  197. package/lib/SlotletProvider-CzMAO73_.js +0 -82
  198. package/lib/SlotletProvider-CzMAO73_.js.map +0 -1
  199. package/lib/index-CtKkHGcd.js.map +0 -1
  200. package/src/lib/plugins/openapi/SchemaListView.tsx +0 -75
  201. package/src/lib/plugins/openapi/SchemaListViewItem.tsx +0 -125
  202. package/src/lib/plugins/openapi/SchemaListViewItemGroup.tsx +0 -63
@@ -1,15 +1,13 @@
1
1
  import { SearchIcon } from "lucide-react";
2
- import { useCallback, useEffect, useState } from "react";
2
+ import { Suspense, useCallback, useEffect, useState } from "react";
3
3
  import { isSearchPlugin } from "../core/plugins.js";
4
4
  import { useZudoku } from "./context/ZudokuContext.js";
5
5
 
6
6
  export const Search = () => {
7
7
  const ctx = useZudoku();
8
- const [isOpen, setIsOpen] = useState(true);
8
+ const [isOpen, setIsOpen] = useState(false);
9
9
 
10
- const onClose = useCallback(() => {
11
- setIsOpen(false);
12
- }, []);
10
+ const onClose = useCallback(() => setIsOpen(false), []);
13
11
 
14
12
  useEffect(() => {
15
13
  if (isOpen) {
@@ -40,6 +38,7 @@ export const Search = () => {
40
38
  <>
41
39
  <button
42
40
  type="button"
41
+ onClick={() => setIsOpen(true)}
43
42
  className="flex items-center border border-input hover:bg-accent hover:text-accent-foreground p-4 relative h-8 justify-start rounded-lg bg-background text-sm text-muted-foreground shadow-none w-40 sm:w-72"
44
43
  >
45
44
  <div className="flex items-center gap-2 flex-grow">
@@ -50,11 +49,12 @@ export const Search = () => {
50
49
  ⌘K
51
50
  </kbd>
52
51
  </button>
53
- {isOpen &&
54
- searchPlugin.onRequestSearch({
52
+ <Suspense fallback={null}>
53
+ {searchPlugin.renderSearch({
55
54
  isOpen,
56
55
  onClose,
57
56
  })}
57
+ </Suspense>
58
58
  </>
59
59
  );
60
60
  };
@@ -1,6 +1,6 @@
1
- import React, { ReactNode, useContext } from "react";
2
-
3
- export type Slotlets = Record<string, ReactNode>;
1
+ import React, { type ReactElement, ReactNode, useContext } from "react";
2
+ import { isValidElementType } from "react-is";
3
+ export type Slotlets = Record<string, ReactNode | ReactElement>;
4
4
 
5
5
  const SlotletContext = React.createContext<Slotlets | undefined>({});
6
6
 
@@ -19,7 +19,12 @@ export const SlotletProvider = ({
19
19
  };
20
20
 
21
21
  export const Slotlet = ({ name }: { name: string }) => {
22
- const x = useContext(SlotletContext);
22
+ const context = useContext(SlotletContext);
23
+ const componentOrElement = context?.[name];
24
+
25
+ if (isValidElementType(componentOrElement)) {
26
+ return React.createElement(componentOrElement);
27
+ }
23
28
 
24
- return x?.[name];
29
+ return componentOrElement;
25
30
  };
@@ -18,6 +18,7 @@ export const SidebarCategory = ({
18
18
  }) => {
19
19
  const topNavItem = useTopNavigationItem();
20
20
  const isCategoryOpen = useIsCategoryOpen(category);
21
+ const [hasInteracted, setHasInteracted] = useState(false);
21
22
 
22
23
  const isCollapsible = category.collapsible ?? true;
23
24
  const isCollapsed = category.collapsed ?? true;
@@ -40,11 +41,15 @@ export const SidebarCategory = ({
40
41
  onClick={(e) => {
41
42
  e.preventDefault();
42
43
  setOpen((prev) => !prev);
44
+ setHasInteracted(true);
43
45
  }}
44
46
  >
45
47
  <ChevronRightIcon
46
48
  size={16}
47
- className="transition shrink-0 group-data-[state=open]:rotate-90"
49
+ className={cn(
50
+ hasInteracted && "transition",
51
+ "shrink-0 group-data-[state=open]:rotate-90",
52
+ )}
48
53
  />
49
54
  </button>
50
55
  );
@@ -88,7 +93,13 @@ export const SidebarCategory = ({
88
93
  </div>
89
94
  )}
90
95
  </Collapsible.Trigger>
91
- <Collapsible.Content className="CollapsibleContent ms-[calc(var(--padding-nav-item)*1.125)]">
96
+ <Collapsible.Content
97
+ className={cn(
98
+ // CollapsibleContent class is used to animate and it should only be applied when the user has triggered the toggle
99
+ hasInteracted && "CollapsibleContent",
100
+ "ms-[calc(var(--padding-nav-item)*1.125)]",
101
+ )}
102
+ >
92
103
  <ul className="mt-1 border-l ps-2">
93
104
  {category.items.map((item) => (
94
105
  <SidebarItem
@@ -1,6 +1,6 @@
1
1
  import { cva } from "class-variance-authority";
2
2
  import { ExternalLinkIcon } from "lucide-react";
3
- import { NavLink } from "react-router-dom";
3
+ import { NavLink, useSearchParams } from "react-router-dom";
4
4
 
5
5
  import type { SidebarItem as SidebarItemType } from "../../../config/validators/SidebarSchema.js";
6
6
  import { cn } from "../../util/cn.js";
@@ -42,6 +42,7 @@ export const SidebarItem = ({
42
42
  }) => {
43
43
  const topNavItem = useTopNavigationItem();
44
44
  const { activeAnchor } = useViewportAnchor();
45
+ const [searchParams] = useSearchParams();
45
46
 
46
47
  switch (item.type) {
47
48
  case "category":
@@ -69,7 +70,7 @@ export const SidebarItem = ({
69
70
  case "link":
70
71
  return item.href.startsWith("#") ? (
71
72
  <AnchorLink
72
- to={item.href}
73
+ to={{ hash: item.href, search: searchParams.toString() }}
73
74
  {...{ [DATA_ANCHOR_ATTR]: item.href.slice(1) }}
74
75
  className={cn(
75
76
  "flex gap-2.5 justify-between",
@@ -4,21 +4,23 @@ import { cn } from "../../util/cn.js";
4
4
  export const SidebarWrapper = forwardRef<
5
5
  HTMLDivElement,
6
6
  PropsWithChildren<{ pushMainContent?: boolean; className?: string }>
7
- >(function SideNavigation({ children, className, pushMainContent }, ref) {
8
- return (
9
- <nav
10
- // this data attribute is used in `Layout.tsx` to determine if side navigation
11
- // is present for the current page so the main content is pushed to the right
12
- // it's also important to set `peer` class here.
13
- // maybe this could be simplified by adjusting the layout
14
- data-navigation={String(pushMainContent)}
15
- className={cn(
16
- "peer hidden lg:flex flex-col fixed text-sm overflow-y-auto shrink-0 p-[--padding-nav-item] -mx-[--padding-nav-item] pb-20 pt-[--padding-content-top] w-[--side-nav-width] h-[calc(100%-var(--header-height))] scroll-pt-2 gap-2",
17
- className,
18
- )}
19
- ref={ref}
20
- >
21
- {children}
22
- </nav>
23
- );
24
- });
7
+ >(({ children, className, pushMainContent }, ref) => (
8
+ <nav
9
+ // this data attribute is used in `Layout.tsx` to determine if side navigation
10
+ // is present for the current page so the main content is pushed to the right
11
+ // it's also important to set `peer` class here.
12
+ // maybe this could be simplified by adjusting the layout
13
+ data-navigation={String(pushMainContent)}
14
+ className={cn(
15
+ "scrollbar peer hidden lg:flex flex-col fixed text-sm overflow-y-auto shrink-0",
16
+ "px-[--padding-nav-item] -mx-[--padding-nav-item] pb-20 mt-[--padding-content-top]",
17
+ "w-[--side-nav-width] h-[calc(100%-var(--header-height))] scroll-pt-2 gap-2",
18
+ className,
19
+ )}
20
+ ref={ref}
21
+ >
22
+ {children}
23
+ </nav>
24
+ ));
25
+
26
+ SidebarWrapper.displayName = "SidebarWrapper";
@@ -21,7 +21,7 @@ export interface ApiIdentityPlugin {
21
21
  }
22
22
 
23
23
  export interface SearchProviderPlugin {
24
- onRequestSearch: (o: {
24
+ renderSearch: (o: {
25
25
  isOpen: boolean;
26
26
  onClose: () => void;
27
27
  }) => React.JSX.Element | null;
@@ -59,7 +59,7 @@ export const isNavigationPlugin = (
59
59
  export const isSearchPlugin = (
60
60
  obj: DevPortalPlugin,
61
61
  ): obj is SearchProviderPlugin =>
62
- "onRequestSearch" in obj && typeof obj.onRequestSearch === "function";
62
+ "renderSearch" in obj && typeof obj.renderSearch === "function";
63
63
 
64
64
  export const needsInitialization = (
65
65
  obj: DevPortalPlugin,
@@ -68,6 +68,25 @@ const builder = new SchemaBuilder<{
68
68
  const JSONScalar = builder.addScalarType("JSON", GraphQLJSON);
69
69
  const JSONObjectScalar = builder.addScalarType("JSONObject", GraphQLJSONObject);
70
70
 
71
+ const getAllTags = (schema: OpenAPIDocument): TagObject[] => {
72
+ const tags = schema.tags ?? [];
73
+
74
+ // Extract tags from operations
75
+ const operationTags = Object.values(schema.paths ?? {})
76
+ .flatMap((path) => Object.values(path ?? {}))
77
+ .flatMap((operation) =>
78
+ typeof operation === "object" && "tags" in operation
79
+ ? operation.tags ?? []
80
+ : [],
81
+ );
82
+
83
+ // Remove duplicates and tags that appear in the schema
84
+ const uniqueOperationTags = [...new Set(operationTags)].filter(
85
+ (tag) => !tags.some((rootTag) => rootTag.name === tag),
86
+ );
87
+ return [...tags, ...uniqueOperationTags.map((tag) => ({ name: tag }))];
88
+ };
89
+
71
90
  const getAllOperations = (paths?: PathsObject, tag?: string) => {
72
91
  return Object.entries(paths ?? {}).flatMap(([path, value]) =>
73
92
  HttpMethods.flatMap((method) => {
@@ -117,7 +136,7 @@ const SchemaTag = builder.objectRef<TagObject>("SchemaTag").implement({
117
136
  operations: t.field({
118
137
  type: [OperationItem],
119
138
  resolve: (parent, _args, ctx) => {
120
- const rootTags = ctx.schema.tags?.map((tag) => tag.name) ?? [];
139
+ const rootTags = getAllTags(ctx.schema).map((tag) => tag.name);
121
140
 
122
141
  return getAllOperations(ctx.schema.paths, parent.name).filter((item) =>
123
142
  parent.name
@@ -246,7 +265,7 @@ const RequestBodyObject = builder
246
265
  const ResponseItem = builder
247
266
  .objectRef<{
248
267
  statusCode: string;
249
- description: string;
268
+ description?: string;
250
269
  content: Array<{
251
270
  mediaType: string;
252
271
  schema: any;
@@ -258,7 +277,7 @@ const ResponseItem = builder
258
277
  .implement({
259
278
  fields: (t) => ({
260
279
  statusCode: t.exposeString("statusCode"),
261
- description: t.exposeString("description"),
280
+ description: t.exposeString("description", { nullable: true }),
262
281
  content: t.expose("content", { type: [MediaTypeItem], nullable: true }),
263
282
  headers: t.expose("headers", { type: JSONScalar, nullable: true }),
264
283
  links: t.expose("links", { type: JSONScalar, nullable: true }),
@@ -363,10 +382,10 @@ const Schema = builder.objectRef<OpenAPIDocument>("Schema").implement({
363
382
  name: t.arg.string(),
364
383
  },
365
384
  type: [SchemaTag],
366
- resolve: (root, args) =>
367
- [...(root.tags ?? []), { name: "" }].filter(
368
- (tag) => !args.name || args.name === tag.name,
369
- ),
385
+ resolve: (root, args) => {
386
+ const tags = [...getAllTags(root), { name: "" }];
387
+ return args.name ? tags.filter((tag) => tag.name === args.name) : tags;
388
+ },
370
389
  }),
371
390
  operations: t.field({
372
391
  type: [OperationItem],
@@ -23,7 +23,6 @@ const MarkdownHeadings = {
23
23
  ),
24
24
  h3: ({ children, id }) => (
25
25
  <Heading level={3} id={id} registerSidebarAnchor>
26
- {" "}
27
26
  {children}
28
27
  </Heading>
29
28
  ),
@@ -1,14 +1,14 @@
1
+ import { useState } from "react";
1
2
  import { Heading } from "../../components/Heading.js";
2
3
  import { Markdown } from "../../components/Markdown.js";
3
- import { Card } from "../../ui/Card.js";
4
4
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/Tabs.js";
5
5
  import { groupBy } from "../../util/groupBy.js";
6
6
  import { renderIf } from "../../util/renderIf.js";
7
7
  import { OperationsFragment } from "./OperationList.js";
8
8
  import { ParameterList } from "./ParameterList.js";
9
- import { SchemaListView } from "./SchemaListView.js";
10
9
  import { Sidecar } from "./Sidecar.js";
11
10
  import { FragmentType, useFragment } from "./graphql/index.js";
11
+ import { SchemaView } from "./schema/SchemaView.js";
12
12
  import { SchemaProseClasses } from "./util/prose.js";
13
13
 
14
14
  export const PARAM_GROUPS = ["path", "query", "header", "cookie"] as const;
@@ -26,6 +26,8 @@ export const OperationListItem = ({
26
26
  );
27
27
 
28
28
  const first = operation.responses.at(0);
29
+ const [selectedResponse, setSelectedResponse] = useState(first?.statusCode);
30
+
29
31
  return (
30
32
  <div
31
33
  key={operation.operationId}
@@ -62,7 +64,7 @@ export const OperationListItem = ({
62
64
  <Heading level={3} className="capitalize">
63
65
  Request Body
64
66
  </Heading>
65
- <SchemaListView schema={schema} />
67
+ <SchemaView schema={schema} />
66
68
  </div>
67
69
  ))}
68
70
  {operation.responses.length > 0 && (
@@ -70,12 +72,15 @@ export const OperationListItem = ({
70
72
  <Heading level={3} className="capitalize mt-8 pt-8 border-t">
71
73
  Responses
72
74
  </Heading>
73
- <Tabs defaultValue={`${first?.statusCode}${first?.description}`}>
75
+ <Tabs
76
+ onValueChange={(value) => setSelectedResponse(value)}
77
+ value={selectedResponse}
78
+ >
74
79
  {operation.responses.length > 1 && (
75
80
  <TabsList>
76
81
  {operation.responses.map((response) => (
77
82
  <TabsTrigger
78
- value={response.statusCode + response.description}
83
+ value={response.statusCode}
79
84
  key={response.statusCode}
80
85
  title={response.description}
81
86
  >
@@ -87,19 +92,15 @@ export const OperationListItem = ({
87
92
  <ul className="list-none m-0 px-0 overflow-hidden">
88
93
  {operation.responses.map((response) => (
89
94
  <TabsContent
90
- value={response.statusCode + response.description}
95
+ value={response.statusCode}
91
96
  key={response.statusCode}
92
97
  >
93
- {renderIf(
94
- response.content?.find((content) => content.schema),
95
- (content) => {
96
- return <SchemaListView schema={content.schema} />;
97
- },
98
- ) ?? (
99
- <Card className="font-mono text-sm p-4">
100
- No response body
101
- </Card>
102
- )}
98
+ <SchemaView
99
+ schema={
100
+ response.content?.find((content) => content.schema)
101
+ ?.schema
102
+ }
103
+ />
103
104
  </TabsContent>
104
105
  ))}
105
106
  </ul>
@@ -108,7 +109,11 @@ export const OperationListItem = ({
108
109
  )}
109
110
  </div>
110
111
 
111
- <Sidecar operation={operation} />
112
+ <Sidecar
113
+ selectedResponse={selectedResponse}
114
+ onSelectResponse={setSelectedResponse}
115
+ operation={operation}
116
+ />
112
117
  </div>
113
118
  );
114
119
  };
@@ -21,14 +21,16 @@ export const ParameterList = ({
21
21
  </Heading>
22
22
  <Card>
23
23
  <ul className="list-none m-0 px-0 divide-y ">
24
- {parameters.map((parameter) => (
25
- <ParameterListItem
26
- key={`${parameter.name}-${parameter.in}`}
27
- parameter={parameter}
28
- id={id}
29
- group={group}
30
- />
31
- ))}
24
+ {parameters
25
+ .sort((a, b) => (a.required === b.required ? 0 : a.required ? -1 : 1))
26
+ .map((parameter) => (
27
+ <ParameterListItem
28
+ key={`${parameter.name}-${parameter.in}`}
29
+ parameter={parameter}
30
+ id={id}
31
+ group={group}
32
+ />
33
+ ))}
32
34
  </ul>
33
35
  </Card>
34
36
  </>
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import * as Tabs from "@radix-ui/react-tabs";
2
2
  import { SyntaxHighlight } from "../../components/SyntaxHighlight.js";
3
3
  import { type SchemaObject } from "../../oas/graphql/index.js";
4
4
  import { cn } from "../../util/cn.js";
@@ -9,52 +9,64 @@ import { generateSchemaExample } from "./util/generateSchemaExample.js";
9
9
  type Responses = OperationListItemResult["responses"];
10
10
  export const ResponsesSidecarBox = ({
11
11
  responses,
12
+ selectedResponse,
13
+ onSelectResponse,
12
14
  }: {
13
15
  responses: Responses;
14
- }) => {
15
- const [tabIndex, setTabIndex] = useState(0);
16
-
17
- const activeTab = responses[tabIndex];
18
- const schema = activeTab.content?.[0]?.schema as SchemaObject | undefined;
19
-
20
- return (
21
- <SidecarBox.Root>
22
- <SidecarBox.Head className="text-xs grid grid-rows-2 pb-0">
16
+ selectedResponse?: string;
17
+ onSelectResponse: (response: string) => void;
18
+ }) => (
19
+ <SidecarBox.Root>
20
+ <Tabs.Root
21
+ defaultValue={responses[0]?.statusCode}
22
+ value={selectedResponse}
23
+ onValueChange={(value) => onSelectResponse(value)}
24
+ >
25
+ <SidecarBox.Head className="text-xs flex flex-col gap-2 pb-0">
23
26
  <span className="font-mono">Example Responses</span>
24
- <div className="flex gap-2">
25
- {responses.map((response, index) => (
26
- <div
27
+ <Tabs.List className="flex gap-2">
28
+ {responses.map((response) => (
29
+ <Tabs.Trigger
27
30
  key={response.statusCode}
28
- onClick={() => setTabIndex(index)}
31
+ value={response.statusCode}
29
32
  className={cn(
30
33
  "text-xs font-mono px-1.5 py-1 pb-px translate-y-px border-b-2 border-transparent rounded-t cursor-pointer",
31
- tabIndex === index
32
- ? "text-primary dark:text-inherit border-primary"
33
- : "hover:border-accent-foreground/25",
34
+ "data-[state=active]:text-primary data-[state=active]:dark:text-inherit data-[state=active]:border-primary",
35
+ "hover:border-accent-foreground/25",
34
36
  )}
35
37
  >
36
38
  {response.statusCode}
37
- </div>
39
+ </Tabs.Trigger>
38
40
  ))}
39
- </div>
41
+ </Tabs.List>
40
42
  </SidecarBox.Head>
41
- <SidecarBox.Body>
42
- {schema ? (
43
- <SyntaxHighlight
44
- language="json"
45
- noBackground
46
- className="text-xs"
47
- code={JSON.stringify(generateSchemaExample(schema), null, 2)}
48
- />
49
- ) : (
50
- <span className="text-muted-foreground font-mono italic text-xs">
51
- Empty Response
52
- </span>
53
- )}
54
- </SidecarBox.Body>
55
- <SidecarBox.Footer className="flex justify-end text-xs">
56
- {responses[tabIndex].description}
57
- </SidecarBox.Footer>
58
- </SidecarBox.Root>
59
- );
60
- };
43
+ {responses.map((response) => {
44
+ const schema = response.content?.[0]?.schema as
45
+ | SchemaObject
46
+ | undefined;
47
+
48
+ return (
49
+ <Tabs.Content key={response.statusCode} value={response.statusCode}>
50
+ <SidecarBox.Body>
51
+ {schema ? (
52
+ <SyntaxHighlight
53
+ language="json"
54
+ noBackground
55
+ className="text-xs"
56
+ code={JSON.stringify(generateSchemaExample(schema), null, 2)}
57
+ />
58
+ ) : (
59
+ <span className="text-muted-foreground font-mono italic text-xs">
60
+ Empty Response
61
+ </span>
62
+ )}
63
+ </SidecarBox.Body>
64
+ <SidecarBox.Footer className="flex justify-end text-xs">
65
+ {response.description}
66
+ </SidecarBox.Footer>
67
+ </Tabs.Content>
68
+ );
69
+ })}
70
+ </Tabs.Root>
71
+ </SidecarBox.Root>
72
+ );
@@ -84,8 +84,12 @@ const methodToColor = {
84
84
 
85
85
  export const Sidecar = ({
86
86
  operation,
87
+ selectedResponse,
88
+ onSelectResponse,
87
89
  }: {
88
90
  operation: OperationListItemResult;
91
+ selectedResponse?: string;
92
+ onSelectResponse: (response: string) => void;
89
93
  }) => {
90
94
  const oasConfig = useOasConfig();
91
95
  const [result] = useQuery({
@@ -105,24 +109,31 @@ export const Sidecar = ({
105
109
 
106
110
  const requestBodyContent = operation.requestBody?.content;
107
111
 
108
- const path = operation.path.split("/").map((part) => (
109
- <Fragment key={part}>
110
- {part.startsWith("{") && part.endsWith("}") ? (
111
- <ColorizedParam
112
- name={part.slice(1, -1)}
113
- backgroundOpacity="0"
114
- // same as in `ParameterListItem`
115
- slug={operation.slug + "-" + part.slice(1, -1).toLocaleLowerCase()}
116
- >
117
- {part}
118
- </ColorizedParam>
119
- ) : (
120
- part
121
- )}
122
- /
123
- <wbr />
124
- </Fragment>
125
- ));
112
+ const path = operation.path.split("/").map((part, i, arr) => {
113
+ const isParam =
114
+ (part.startsWith("{") && part.endsWith("}")) || part.startsWith(":");
115
+ const paramName = isParam ? part.replace(/[:{}]/g, "") : undefined;
116
+
117
+ return (
118
+ // eslint-disable-next-line react/no-array-index-key
119
+ <Fragment key={part + i}>
120
+ {paramName ? (
121
+ <ColorizedParam
122
+ name={paramName}
123
+ backgroundOpacity="0"
124
+ // same as in `ParameterListItem`
125
+ slug={`${operation.slug}-${paramName.toLocaleLowerCase()}`}
126
+ >
127
+ {part}
128
+ </ColorizedParam>
129
+ ) : (
130
+ part
131
+ )}
132
+ {i < arr.length - 1 ? "/" : null}
133
+ <wbr />
134
+ </Fragment>
135
+ );
136
+ });
126
137
 
127
138
  const code = useMemo(() => {
128
139
  const example = requestBodyContent?.[0]?.schema
@@ -200,7 +211,11 @@ export const Sidecar = ({
200
211
  <RequestBodySidecarBox content={requestBodyContent} />
201
212
  )}
202
213
  {operation.responses.length > 0 && (
203
- <ResponsesSidecarBox responses={operation.responses} />
214
+ <ResponsesSidecarBox
215
+ selectedResponse={selectedResponse}
216
+ onSelectResponse={onSelectResponse}
217
+ responses={operation.responses}
218
+ />
204
219
  )}
205
220
  </aside>
206
221
  );
@@ -164,7 +164,7 @@ export const openApiPlugin = (
164
164
  .map<SidebarItem>((tag) => ({
165
165
  type: "category",
166
166
  label: tag.name ?? "",
167
- collapsible: false,
167
+ collapsible: true,
168
168
  collapsed: false,
169
169
  items: tag.operations.map((operation) => ({
170
170
  type: "link",
@@ -164,8 +164,9 @@ export const Playground = ({
164
164
  });
165
165
 
166
166
  const path = url.split("/").map((part, i, arr) => {
167
- const isPathParam = part.startsWith("{") && part.endsWith("}");
168
- const replaced = part.replace(/[{}]/g, "");
167
+ const isPathParam =
168
+ (part.startsWith("{") && part.endsWith("}")) || part.startsWith(":");
169
+ const replaced = part.replace(/[:{}]/g, "");
169
170
  const value = formState.pathParams.find((p) => p.name === replaced)?.value;
170
171
 
171
172
  const pathParamValue = value ? (
@@ -191,10 +192,6 @@ export const Playground = ({
191
192
  );
192
193
  });
193
194
 
194
- const lang = mimeTypeToLanguage(
195
- queryMutation.data?.headers.get("Content-Type") ?? "",
196
- );
197
-
198
195
  const headerEntries = Array.from(queryMutation.data?.headers.entries() ?? []);
199
196
 
200
197
  const urlQueryParams = formState.queryParams
@@ -1,11 +1,12 @@
1
1
  import type { PlaygroundForm } from "./Playground.js";
2
2
 
3
3
  export const createUrl = (host: string, path: string, data: PlaygroundForm) => {
4
- const filledPath = path.replace(
5
- /\{(\w+)}/g,
6
- (_, key) =>
7
- data.pathParams.find((part) => part.name === key)?.value || `{${key}}`,
8
- );
4
+ const filledPath = path.replace(/(:\w+|\{\w+})/g, (match) => {
5
+ const key = match.replace(/[:{}]/g, "");
6
+ const value = data.pathParams.find((part) => part.name === key)?.value;
7
+
8
+ return value ?? match;
9
+ });
9
10
 
10
11
  const url = new URL(filledPath, host);
11
12