zudoku 0.25.0 → 0.25.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 (82) hide show
  1. package/dist/config/validators/InputSidebarSchema.d.ts +15 -0
  2. package/dist/config/validators/InputSidebarSchema.js +1 -0
  3. package/dist/config/validators/InputSidebarSchema.js.map +1 -1
  4. package/dist/config/validators/common.d.ts +45 -34
  5. package/dist/config/validators/common.js +2 -1
  6. package/dist/config/validators/common.js.map +1 -1
  7. package/dist/config/validators/validate.d.ts +19 -14
  8. package/dist/lib/authentication/components/SignOut.js +1 -1
  9. package/dist/lib/authentication/components/SignOut.js.map +1 -1
  10. package/dist/lib/authentication/providers/clerk.js +29 -6
  11. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  12. package/dist/lib/components/navigation/SidebarBadge.d.ts +11 -1
  13. package/dist/lib/components/navigation/SidebarBadge.js +11 -2
  14. package/dist/lib/components/navigation/SidebarBadge.js.map +1 -1
  15. package/dist/lib/components/navigation/SidebarCategory.js +2 -2
  16. package/dist/lib/components/navigation/SidebarCategory.js.map +1 -1
  17. package/dist/lib/components/navigation/SidebarWrapper.js +1 -1
  18. package/dist/lib/components/navigation/SidebarWrapper.js.map +1 -1
  19. package/dist/lib/oas/graphql/index.js +16 -3
  20. package/dist/lib/oas/graphql/index.js.map +1 -1
  21. package/dist/lib/oas/parser/upgrade/index.js +17 -3
  22. package/dist/lib/oas/parser/upgrade/index.js.map +1 -1
  23. package/dist/lib/plugins/openapi/CollapsibleCode.js +1 -1
  24. package/dist/lib/plugins/openapi/Endpoint.d.ts +1 -1
  25. package/dist/lib/plugins/openapi/Endpoint.js +2 -0
  26. package/dist/lib/plugins/openapi/Endpoint.js.map +1 -1
  27. package/dist/lib/plugins/openapi/ExampleDisplay.d.ts +12 -0
  28. package/dist/lib/plugins/openapi/ExampleDisplay.js +78 -0
  29. package/dist/lib/plugins/openapi/ExampleDisplay.js.map +1 -0
  30. package/dist/lib/plugins/openapi/OperationListItem.js +1 -1
  31. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  32. package/dist/lib/plugins/openapi/ParameterListItem.js +1 -1
  33. package/dist/lib/plugins/openapi/ParameterListItem.js.map +1 -1
  34. package/dist/lib/plugins/openapi/RequestBodySidecarBox.d.ts +2 -4
  35. package/dist/lib/plugins/openapi/RequestBodySidecarBox.js +6 -13
  36. package/dist/lib/plugins/openapi/RequestBodySidecarBox.js.map +1 -1
  37. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js +11 -13
  38. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js.map +1 -1
  39. package/dist/lib/plugins/openapi/Sidecar.js +1 -1
  40. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  41. package/dist/lib/plugins/openapi/index.js +1 -0
  42. package/dist/lib/plugins/openapi/index.js.map +1 -1
  43. package/dist/vite/build.js +10 -8
  44. package/dist/vite/build.js.map +1 -1
  45. package/dist/vite/config.js +10 -0
  46. package/dist/vite/config.js.map +1 -1
  47. package/lib/{AuthenticationPlugin-DVLEc6cm.js → AuthenticationPlugin-D7G3me8L.js} +16 -16
  48. package/lib/AuthenticationPlugin-D7G3me8L.js.map +1 -0
  49. package/lib/{OperationList-c6V_vcgz.js → OperationList-BLdHAQ39.js} +1594 -1525
  50. package/lib/OperationList-BLdHAQ39.js.map +1 -0
  51. package/lib/assets/{worker-DV9Ecqy9.js → worker-Cbp2r2BQ.js} +26 -14
  52. package/lib/assets/{worker-DV9Ecqy9.js.map → worker-Cbp2r2BQ.js.map} +1 -1
  53. package/lib/{createServer-C4C0OO0m.js → createServer-Bf5_6o6G.js} +796 -784
  54. package/lib/{createServer-C4C0OO0m.js.map → createServer-Bf5_6o6G.js.map} +1 -1
  55. package/lib/{index-DkwDHnit.js → index-BNx95gkf.js} +5 -4
  56. package/lib/{index-DkwDHnit.js.map → index-BNx95gkf.js.map} +1 -1
  57. package/lib/zudoku.auth-clerk.js +80 -52
  58. package/lib/zudoku.auth-clerk.js.map +1 -1
  59. package/lib/zudoku.auth-openid.js +1 -1
  60. package/lib/zudoku.components.js +89 -85
  61. package/lib/zudoku.components.js.map +1 -1
  62. package/lib/zudoku.openapi-worker.js +1 -1
  63. package/lib/zudoku.plugin-openapi.js +1 -1
  64. package/package.json +4 -4
  65. package/src/lib/authentication/components/SignOut.tsx +2 -1
  66. package/src/lib/authentication/providers/clerk.tsx +38 -7
  67. package/src/lib/components/navigation/SidebarBadge.tsx +13 -1
  68. package/src/lib/components/navigation/SidebarCategory.tsx +3 -7
  69. package/src/lib/components/navigation/SidebarWrapper.tsx +1 -0
  70. package/src/lib/oas/graphql/index.ts +16 -7
  71. package/src/lib/oas/parser/upgrade/index.ts +19 -4
  72. package/src/lib/plugins/openapi/CollapsibleCode.tsx +1 -1
  73. package/src/lib/plugins/openapi/Endpoint.tsx +2 -0
  74. package/src/lib/plugins/openapi/ExampleDisplay.tsx +163 -0
  75. package/src/lib/plugins/openapi/OperationListItem.tsx +5 -3
  76. package/src/lib/plugins/openapi/ParameterListItem.tsx +1 -1
  77. package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +11 -37
  78. package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +49 -63
  79. package/src/lib/plugins/openapi/Sidecar.tsx +2 -4
  80. package/src/lib/plugins/openapi/index.tsx +1 -0
  81. package/lib/AuthenticationPlugin-DVLEc6cm.js.map +0 -1
  82. package/lib/OperationList-c6V_vcgz.js.map +0 -1
@@ -2,7 +2,7 @@ const o = () => {
2
2
  const r = new SharedWorker(
3
3
  new URL(
4
4
  /* @vite-ignore */
5
- "./assets/worker-DV9Ecqy9.js",
5
+ "./assets/worker-Cbp2r2BQ.js",
6
6
  import.meta.url
7
7
  ),
8
8
  { type: "module" }
@@ -1,6 +1,6 @@
1
1
  import "./jsx-runtime-Dx-03ztt.js";
2
2
  import "./chunk-D52XG6IA-Dl7HLe6j.js";
3
- import { o as a } from "./index-DkwDHnit.js";
3
+ import { o as a } from "./index-BNx95gkf.js";
4
4
  import "./ZudokuContext-hmLMUdf2.js";
5
5
  import "lucide-react";
6
6
  import "./hook-CHq7pFyz.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "type": "module",
5
5
  "homepage": "https://zudoku.dev",
6
6
  "repository": {
@@ -147,7 +147,7 @@
147
147
  "@sentry/node": "8.42.0",
148
148
  "@sindresorhus/slugify": "2.2.1",
149
149
  "@stefanprobst/rehype-extract-toc": "2.2.0",
150
- "@tailwindcss/typography": "0.5.15",
150
+ "@tailwindcss/typography": "0.5.16",
151
151
  "@tanstack/react-query": "5.62.3",
152
152
  "@types/react": "19.0.1",
153
153
  "@types/react-dom": "19.0.1",
@@ -240,7 +240,7 @@
240
240
  "mdast-util-mdx": "3.0.0",
241
241
  "react": "19.0.0",
242
242
  "react-dom": "19.0.0",
243
- "rollup-plugin-visualizer": "5.12.0",
243
+ "rollup-plugin-visualizer": "5.14.0",
244
244
  "typescript": "5.7.2",
245
245
  "vitest": "2.1.8"
246
246
  },
@@ -250,7 +250,7 @@
250
250
  },
251
251
  "optionalDependencies": {
252
252
  "@clerk/clerk-js": "^5.43.4",
253
- "@sentry/react": "^8.48.0"
253
+ "@sentry/react": "^8.50.0"
254
254
  },
255
255
  "scripts": {
256
256
  "build": "tsc --project tsconfig.json",
@@ -5,9 +5,10 @@ import { useZudoku } from "../../components/context/ZudokuContext.js";
5
5
  export const SignOut = () => {
6
6
  const context = useZudoku();
7
7
  const navigate = useNavigate();
8
+
8
9
  useEffect(() => {
9
10
  void context.authentication?.signOut().then(() => navigate("/"));
10
- }, [navigate, context.authentication]);
11
+ }, []);
11
12
 
12
13
  return null;
13
14
  };
@@ -16,14 +16,19 @@ class ClerkAuthPlugin extends AuthenticationPlugin {
16
16
  }
17
17
 
18
18
  if (clerk.session) {
19
+ const verifiedEmail = clerk.session.user.emailAddresses.find(
20
+ (email) => email.verification.status === "verified",
21
+ );
19
22
  useAuthState.setState({
20
23
  isAuthenticated: true,
21
24
  isPending: false,
22
25
  profile: {
23
26
  sub: clerk.session.user.id,
24
27
  name: clerk.session.user.fullName ?? undefined,
25
- email: clerk.session.user.emailAddresses[0]?.emailAddress,
26
- emailVerified: false, // TODO: Check this
28
+ email:
29
+ verifiedEmail?.emailAddress ??
30
+ clerk.session.user.emailAddresses[0]?.emailAddress,
31
+ emailVerified: verifiedEmail !== undefined,
27
32
  pictureUrl: clerk.session.user.imageUrl,
28
33
  },
29
34
  });
@@ -45,19 +50,39 @@ const clerkAuth: AuthenticationProviderInitializer<
45
50
  redirectToAfterSignUp = "/",
46
51
  redirectToAfterSignIn = "/",
47
52
  }) => {
48
- let clerkApi: Clerk;
53
+ let clerkApi: Clerk | undefined;
49
54
  const ensureLoaded = (async () => {
50
55
  if (typeof window === "undefined") return;
51
56
  const { Clerk } = await import("@clerk/clerk-js");
52
57
  clerkApi = new Clerk(clerkPubKey);
53
58
 
54
59
  await clerkApi.load();
60
+
61
+ if (clerkApi.user) {
62
+ const verifiedEmail = clerkApi.user.emailAddresses.find(
63
+ (email) => email.verification.status === "verified",
64
+ );
65
+ useAuthState.setState({
66
+ isAuthenticated: true,
67
+ isPending: false,
68
+ profile: {
69
+ sub: clerkApi.user.id,
70
+ name: clerkApi.user.fullName ?? undefined,
71
+ email:
72
+ verifiedEmail?.emailAddress ??
73
+ clerkApi.user.emailAddresses[0]?.emailAddress,
74
+ emailVerified: verifiedEmail !== undefined,
75
+ pictureUrl: clerkApi.user.imageUrl,
76
+ },
77
+ });
78
+ }
79
+
55
80
  return clerkApi;
56
81
  })();
57
82
 
58
83
  async function getAccessToken() {
59
84
  await ensureLoaded;
60
- if (!clerkApi.session) {
85
+ if (!clerkApi?.session) {
61
86
  throw new Error("No session available");
62
87
  }
63
88
  const response = await clerkApi.session.getToken();
@@ -71,20 +96,26 @@ const clerkAuth: AuthenticationProviderInitializer<
71
96
  getAccessToken,
72
97
  signOut: async () => {
73
98
  await ensureLoaded;
74
- await clerkApi.signOut({
99
+ await clerkApi?.signOut({
75
100
  redirectUrl: window.location.origin + redirectToAfterSignOut,
76
101
  });
102
+ useAuthState.setState({
103
+ isAuthenticated: false,
104
+ isPending: false,
105
+ profile: null,
106
+ providerData: null,
107
+ });
77
108
  },
78
109
  signIn: async () => {
79
110
  await ensureLoaded;
80
- await clerkApi.redirectToSignIn({
111
+ await clerkApi?.redirectToSignIn({
81
112
  signInForceRedirectUrl: window.location.origin + redirectToAfterSignIn,
82
113
  signUpForceRedirectUrl: window.location.origin + redirectToAfterSignUp,
83
114
  });
84
115
  },
85
116
  signUp: async () => {
86
117
  await ensureLoaded;
87
- await clerkApi.redirectToSignUp({
118
+ await clerkApi?.redirectToSignUp({
88
119
  signInForceRedirectUrl: window.location.origin + redirectToAfterSignIn,
89
120
  signUpForceRedirectUrl: window.location.origin + redirectToAfterSignUp,
90
121
  });
@@ -10,20 +10,32 @@ export const ColorMap = {
10
10
  gray: "bg-gray-400 dark:bg-gray-600",
11
11
  };
12
12
 
13
+ export const ColorMapInvert = {
14
+ green: "text-green-500 dark:text-green-600",
15
+ blue: "text-sky-400 dark:text-sky-600",
16
+ yellow: "text-yellow-400 dark:text-yellow-600",
17
+ red: "text-red-400 dark:text-red-600",
18
+ purple: "text-purple-400 dark:text-purple-600",
19
+ indigo: "text-indigo-400 dark:text-indigo-600",
20
+ gray: "text-gray-400 dark:text-gray-600",
21
+ };
22
+
13
23
  export const SidebarBadge = ({
14
24
  color,
15
25
  label,
16
26
  className,
27
+ invert,
17
28
  }: {
18
29
  color: keyof typeof ColorMap;
19
30
  label: string;
20
31
  className?: string;
32
+ invert?: boolean;
21
33
  }) => {
22
34
  return (
23
35
  <span
24
36
  className={cn(
25
37
  "mt-0.5 flex items-center duration-200 transition-opacity text-center uppercase font-mono text-[0.65rem] font-bold rounded text-background dark:text-zinc-50 h-4 px-1",
26
- ColorMap[color],
38
+ invert ? ColorMapInvert[color] : ColorMap[color],
27
39
  className,
28
40
  )}
29
41
  >
@@ -66,7 +66,7 @@ export const SidebarCategory = ({
66
66
  className={navigationListItem({
67
67
  isActive: false,
68
68
  className: [
69
- "text-start",
69
+ "text-start font-medium",
70
70
  isCollapsible
71
71
  ? "cursor-pointer"
72
72
  : "cursor-default hover:bg-transparent",
@@ -112,14 +112,10 @@ export const SidebarCategory = ({
112
112
  className={cn(
113
113
  // CollapsibleContent class is used to animate and it should only be applied when the user has triggered the toggle
114
114
  hasInteracted && "CollapsibleContent",
115
- "ms-6 my-1",
115
+ "my-1",
116
116
  )}
117
117
  >
118
- <ul
119
- className={
120
- "relative after:absolute after:-left-[--padding-nav-item] after:translate-x-[1.5px] after:top-0 after:bottom-0 after:w-px after:bg-border"
121
- }
122
- >
118
+ <ul className={"relative"}>
123
119
  {category.items.map((item) => (
124
120
  <SidebarItem
125
121
  key={
@@ -15,6 +15,7 @@ export const SidebarWrapper = forwardRef<
15
15
  "scrollbar peer hidden lg:flex flex-col fixed text-sm overflow-y-auto shrink-0 border-r pr-10",
16
16
  "-mx-[--padding-nav-item] pb-20 pt-[--padding-content-top]",
17
17
  "w-[--side-nav-width] h-[calc(100%-var(--header-height))] scroll-pt-2 gap-2",
18
+ !pushMainContent && "border-r-0",
18
19
  className,
19
20
  )}
20
21
  ref={ref}
@@ -329,9 +329,12 @@ const OperationItem = builder
329
329
  ([mediaType, content]) => ({
330
330
  mediaType,
331
331
  schema: content.schema,
332
- examples: Object.entries(content.examples ?? {}).map(
333
- ([name, value]) => ({ name, ...value }),
334
- ),
332
+ examples: content.examples
333
+ ? Object.entries(content.examples).map(([name, value]) => ({
334
+ name,
335
+ ...(typeof value === "string" ? { value } : value),
336
+ }))
337
+ : [],
335
338
  encoding: Object.entries(content.encoding ?? {}).map(
336
339
  ([name, value]) => ({ name, ...value }),
337
340
  ),
@@ -351,9 +354,12 @@ const OperationItem = builder
351
354
  ([mediaType, { schema, examples }]) => ({
352
355
  mediaType,
353
356
  schema,
354
- examples: Object.entries(examples ?? {}).map(
355
- ([name, value]) => ({ name, ...value }),
356
- ),
357
+ examples: examples
358
+ ? Object.entries(examples).map(([name, value]) => ({
359
+ name,
360
+ ...(typeof value === "string" ? { value } : value),
361
+ }))
362
+ : [],
357
363
  }),
358
364
  ),
359
365
  headers: response.headers,
@@ -379,7 +385,10 @@ const OperationItem = builder
379
385
  const Schema = builder.objectRef<OpenAPIDocument>("Schema").implement({
380
386
  fields: (t) => ({
381
387
  openapi: t.string({ resolve: (root) => root.openapi }),
382
- url: t.string({ resolve: (root) => root.servers?.at(0)?.url ?? "/" }),
388
+ url: t.string({
389
+ resolve: (root) => root.servers?.at(0)?.url,
390
+ nullable: true,
391
+ }),
383
392
  servers: t.field({
384
393
  type: [ServerItem],
385
394
  resolve: (root) => root.servers ?? [],
@@ -42,12 +42,27 @@ export const upgradeSchema = (schema: RecordAny): OpenAPIDocument => {
42
42
 
43
43
  schema = traverse(schema, (sub) => {
44
44
  if (sub.example !== undefined) {
45
- sub.examples = {
46
- default: sub.example,
47
- };
45
+ const isExampleObject =
46
+ typeof sub.example === "object" &&
47
+ (sub.example.summary !== undefined ||
48
+ sub.example.description !== undefined ||
49
+ sub.example.value !== undefined ||
50
+ sub.example.externalValue !== undefined);
51
+
52
+ const exampleValue = isExampleObject
53
+ ? sub.example
54
+ : { value: sub.example };
55
+
56
+ if (!sub.examples) {
57
+ sub.examples = { default: exampleValue };
58
+ } else {
59
+ sub.examples = {
60
+ default: exampleValue,
61
+ ...sub.examples,
62
+ };
63
+ }
48
64
  delete sub.example;
49
65
  }
50
-
51
66
  return sub;
52
67
  });
53
68
 
@@ -49,7 +49,7 @@ export const CollapsibleCode = ({
49
49
  )}
50
50
  >
51
51
  {!open && isOverflowing && (
52
- <div className=" absolute inset-0 bg-gradient-to-b from-transparent to-zinc-50/90 dark:to-zinc-800/90 z-10 group-hover:to-transparent"></div>
52
+ <div className=" absolute inset-0 bg-gradient-to-b from-transparent to-zinc-50/60 dark:to-zinc-950/90 z-10 group-hover:to-transparent"></div>
53
53
  )}
54
54
  <div ref={contentRef}>{children}</div>
55
55
  {!open && isOverflowing && (
@@ -52,6 +52,8 @@ export const Endpoint = () => {
52
52
 
53
53
  const { servers } = result.data.schema;
54
54
 
55
+ if (servers.length === 0) return null;
56
+
55
57
  if (servers.length === 1) {
56
58
  return (
57
59
  <div className="flex items-center gap-2">
@@ -0,0 +1,163 @@
1
+ import { useState } from "react";
2
+ import { SyntaxHighlight } from "../../components/SyntaxHighlight.js";
3
+ import { type SchemaObject } from "../../oas/graphql/index.js";
4
+ import { CollapsibleCode } from "./CollapsibleCode.js";
5
+ import type { OperationListItemResult } from "./OperationList.js";
6
+ import * as SidecarBox from "./SidecarBox.js";
7
+ import { SimpleSelect } from "./SimpleSelect.js";
8
+ import { generateSchemaExample } from "./util/generateSchemaExample.js";
9
+
10
+ export type Content = NonNullable<
11
+ NonNullable<OperationListItemResult["requestBody"]>["content"]
12
+ >;
13
+ export type Example = NonNullable<
14
+ NonNullable<Content[number]["examples"]>
15
+ >[number];
16
+
17
+ const formatExample = (example: unknown) => {
18
+ if (example == null) return "No example";
19
+
20
+ if (typeof example === "string" || typeof example !== "object") {
21
+ return String(example).trim();
22
+ }
23
+
24
+ return JSON.stringify(example, null, 2);
25
+ };
26
+
27
+ const getLanguageForContentType = (mediaType?: string) =>
28
+ mediaType
29
+ ? ({
30
+ "application/json": "json",
31
+ "application/xml": "xml",
32
+ "application/x-yaml": "yaml",
33
+ "text/csv": "csv",
34
+ "application/javascript": "javascript",
35
+ "application/graphql": "graphql",
36
+ "text/plain": "plain",
37
+ "application/x-www-form-urlencoded": "plain",
38
+ "multipart/form-data": "plain",
39
+ "application/x-protobuf": "plain",
40
+ }[mediaType] ?? "plain")
41
+ : "plain";
42
+
43
+ const getExampleName = (example: Example) => {
44
+ if (example.summary) return example.summary;
45
+ if (example.name) return example.name;
46
+ if (example.description) return example.description;
47
+ };
48
+
49
+ const getExampleValue = (example?: Example) => {
50
+ if (!example) return undefined;
51
+
52
+ if (example.value !== undefined) return example.value;
53
+ if (example.externalValue) return example.externalValue;
54
+ if (example.name) return example;
55
+ };
56
+
57
+ export interface UseExampleDisplayProps {
58
+ content: Content;
59
+ description?: string;
60
+ }
61
+
62
+ export const useSidecarExamples = ({
63
+ content,
64
+ description,
65
+ }: UseExampleDisplayProps) => {
66
+ const [selectedContentTypeIndex, setSelectedContentTypeIndex] = useState(0);
67
+ const [selectedExampleIndex, setSelectedExampleIndex] = useState(0);
68
+
69
+ const selectedContent = content[selectedContentTypeIndex];
70
+ const examples = (selectedContent?.examples ?? []) as Example[];
71
+ const hasExamples = examples.length > 0;
72
+
73
+ const selectedExample = hasExamples
74
+ ? examples[selectedExampleIndex]
75
+ : undefined;
76
+
77
+ const example = hasExamples
78
+ ? getExampleValue(selectedExample)
79
+ : selectedContent?.schema
80
+ ? generateSchemaExample(selectedContent.schema as SchemaObject)
81
+ : undefined;
82
+
83
+ const formattedExample = formatExample(example);
84
+ const language = getLanguageForContentType(selectedContent?.mediaType);
85
+
86
+ const SidecarBody = () => (
87
+ <SidecarBox.Body className="p-0">
88
+ {selectedExample?.externalValue ? (
89
+ <div className="p-2">
90
+ <a
91
+ href={selectedExample.externalValue}
92
+ target="_blank"
93
+ rel="noopener noreferrer"
94
+ className="text-xs text-primary hover:underline"
95
+ >
96
+ View External Example →
97
+ </a>
98
+ </div>
99
+ ) : (
100
+ <CollapsibleCode>
101
+ <SyntaxHighlight
102
+ language={language}
103
+ noBackground
104
+ copyable
105
+ className="[--scrollbar-color:gray] text-xs max-h-[500px] p-2"
106
+ code={formattedExample}
107
+ />
108
+ </CollapsibleCode>
109
+ )}
110
+ {selectedExample?.description && (
111
+ <div className="border-t text-xs px-2 py-1">
112
+ {selectedExample.description}
113
+ </div>
114
+ )}
115
+ </SidecarBox.Body>
116
+ );
117
+
118
+ const SidebarFooter = () => (
119
+ <SidecarBox.Footer className="flex items-center text-xs gap-2 justify-between py-1">
120
+ <div className="flex items-center gap-2 min-w-0">
121
+ {content.length > 1 ? (
122
+ <div className="flex items-center gap-1">
123
+ <SimpleSelect
124
+ className="max-w-[200px]"
125
+ value={selectedContentTypeIndex.toString()}
126
+ onChange={(e) =>
127
+ setSelectedContentTypeIndex(Number(e.target.value))
128
+ }
129
+ options={content.map((c, index) => ({
130
+ value: index.toString(),
131
+ label: c.mediaType,
132
+ }))}
133
+ />
134
+ </div>
135
+ ) : (
136
+ <span className="font-mono text-[11px]">{content[0]?.mediaType}</span>
137
+ )}
138
+ {description && (
139
+ <span className="text-muted-foreground truncate">{description}</span>
140
+ )}
141
+ </div>
142
+ {examples.length > 1 && (
143
+ <div className="flex items-center gap-1">
144
+ <SimpleSelect
145
+ className="max-w-[180px]"
146
+ value={selectedExampleIndex.toString()}
147
+ onChange={(e) => setSelectedExampleIndex(Number(e.target.value))}
148
+ options={examples.map((example, index) => ({
149
+ value: index.toString(),
150
+ label: getExampleName(example) ?? `Example ${index + 1}`,
151
+ }))}
152
+ />
153
+ </div>
154
+ )}
155
+ </SidecarBox.Footer>
156
+ );
157
+
158
+ return {
159
+ SidecarBody,
160
+ SidebarFooter,
161
+ hasContent: hasExamples || content.length > 0,
162
+ };
163
+ };
@@ -57,9 +57,11 @@ export const OperationListItem = ({
57
57
  }
58
58
  }}
59
59
  >
60
- <div className="text-neutral-400 dark:text-neutral-500 truncate">
61
- {serverUrl}
62
- </div>
60
+ {serverUrl && (
61
+ <div className="text-neutral-400 dark:text-neutral-500 truncate">
62
+ {serverUrl}
63
+ </div>
64
+ )}
63
65
  <div className="text-neutral-900 dark:text-neutral-200">
64
66
  {operation.path}
65
67
  </div>
@@ -45,7 +45,7 @@ export const ParameterListItem = ({
45
45
  )}
46
46
  </code>
47
47
  {parameter.required && (
48
- <span className="py-px px-1.5 font-medium bg-primary/75 text-muted rounded-lg">
48
+ <span className="py-px px-1.5 font-medium bg-primary/75 text-primary-foreground rounded-lg">
49
49
  required
50
50
  </span>
51
51
  )}
@@ -1,44 +1,18 @@
1
- import { SyntaxHighlight } from "../../components/SyntaxHighlight.js";
2
- import { type SchemaObject } from "../../oas/graphql/index.js";
3
- import { CollapsibleCode } from "./CollapsibleCode.js";
4
- import type { OperationListItemResult } from "./OperationList.js";
1
+ import { Content, useSidecarExamples } from "./ExampleDisplay.js";
5
2
  import * as SidecarBox from "./SidecarBox.js";
6
- import { generateSchemaExample } from "./util/generateSchemaExample.js";
7
3
 
8
- type Content = NonNullable<
9
- NonNullable<OperationListItemResult["requestBody"]>["content"]
10
- >;
11
-
12
- // @todo should we handle multiple content types?
13
4
  export const RequestBodySidecarBox = ({ content }: { content: Content }) => {
14
- if (!content.length) return null;
15
-
16
- const firstContent = content.at(0);
17
-
18
- const example =
19
- firstContent?.examples?.at(0)?.value ??
20
- (firstContent?.schema
21
- ? generateSchemaExample(firstContent.schema as SchemaObject)
22
- : "");
5
+ const { SidecarBody, SidebarFooter, hasContent } = useSidecarExamples({
6
+ content,
7
+ });
23
8
 
24
9
  return (
25
- <>
26
- <SidecarBox.Root>
27
- <SidecarBox.Head className="text-xs flex justify-between items-center">
28
- <span className="font-mono">Request Body Example</span>
29
- </SidecarBox.Head>
30
- <SidecarBox.Body className="p-0">
31
- <CollapsibleCode>
32
- <SyntaxHighlight
33
- language={example ? "json" : "plain"}
34
- noBackground
35
- copyable
36
- className="[--scrollbar-color:gray] text-xs max-h-[500px] p-2"
37
- code={example ? JSON.stringify(example, null, 2) : "No example"}
38
- />
39
- </CollapsibleCode>
40
- </SidecarBox.Body>
41
- </SidecarBox.Root>
42
- </>
10
+ <SidecarBox.Root>
11
+ <SidecarBox.Head className="text-xs flex justify-between items-center">
12
+ <span className="font-mono">Request Body Example</span>
13
+ </SidecarBox.Head>
14
+ <SidecarBody />
15
+ {hasContent && <SidebarFooter />}
16
+ </SidecarBox.Root>
43
17
  );
44
18
  };