zudoku 0.0.0-z25d5d85c → 0.0.0-z2f55b5ae

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 (244) hide show
  1. package/dist/app/main.js +1 -1
  2. package/dist/app/main.js.map +1 -1
  3. package/dist/config/create-plugin.d.ts +2 -0
  4. package/dist/config/create-plugin.js +55 -0
  5. package/dist/config/create-plugin.js.map +1 -0
  6. package/dist/config/loader.js +2 -2
  7. package/dist/config/loader.js.map +1 -1
  8. package/dist/config/validators/InputNavigationSchema.d.ts +118 -68
  9. package/dist/config/validators/InputNavigationSchema.js +17 -0
  10. package/dist/config/validators/InputNavigationSchema.js.map +1 -1
  11. package/dist/config/validators/NavigationSchema.d.ts +10 -2
  12. package/dist/config/validators/NavigationSchema.js +7 -0
  13. package/dist/config/validators/NavigationSchema.js.map +1 -1
  14. package/dist/config/validators/validate.d.ts +5 -4
  15. package/dist/config/validators/validate.js +2 -0
  16. package/dist/config/validators/validate.js.map +1 -1
  17. package/dist/flat-config.d.ts +36 -24
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/components/Bootstrap.js +1 -2
  22. package/dist/lib/components/Bootstrap.js.map +1 -1
  23. package/dist/lib/components/MobileTopNavigation.js +2 -1
  24. package/dist/lib/components/MobileTopNavigation.js.map +1 -1
  25. package/dist/lib/components/Slot.test.js +1 -1
  26. package/dist/lib/components/Slot.test.js.map +1 -1
  27. package/dist/lib/components/TopNavigation.d.ts +7 -1
  28. package/dist/lib/components/TopNavigation.js +7 -2
  29. package/dist/lib/components/TopNavigation.js.map +1 -1
  30. package/dist/lib/components/Zudoku.d.ts +4 -1
  31. package/dist/lib/components/Zudoku.js +4 -7
  32. package/dist/lib/components/Zudoku.js.map +1 -1
  33. package/dist/lib/components/context/ZudokuContext.d.ts +9 -4
  34. package/dist/lib/components/context/ZudokuContext.js +4 -2
  35. package/dist/lib/components/context/ZudokuContext.js.map +1 -1
  36. package/dist/lib/components/context/ZudokuProvider.js +1 -1
  37. package/dist/lib/components/context/ZudokuProvider.js.map +1 -1
  38. package/dist/lib/components/context/ZudokuReactContext.d.ts +11 -0
  39. package/dist/lib/components/context/ZudokuReactContext.js +4 -0
  40. package/dist/lib/components/context/ZudokuReactContext.js.map +1 -0
  41. package/dist/lib/components/navigation/Navigation.js +4 -3
  42. package/dist/lib/components/navigation/Navigation.js.map +1 -1
  43. package/dist/lib/components/navigation/NavigationCategory.js +8 -0
  44. package/dist/lib/components/navigation/NavigationCategory.js.map +1 -1
  45. package/dist/lib/components/navigation/NavigationFilterContext.d.ts +8 -0
  46. package/dist/lib/components/navigation/NavigationFilterContext.js +12 -0
  47. package/dist/lib/components/navigation/NavigationFilterContext.js.map +1 -0
  48. package/dist/lib/components/navigation/NavigationFilterInput.d.ts +3 -0
  49. package/dist/lib/components/navigation/NavigationFilterInput.js +9 -0
  50. package/dist/lib/components/navigation/NavigationFilterInput.js.map +1 -0
  51. package/dist/lib/components/navigation/NavigationItem.js +11 -1
  52. package/dist/lib/components/navigation/NavigationItem.js.map +1 -1
  53. package/dist/lib/components/navigation/utils.d.ts +2 -1
  54. package/dist/lib/components/navigation/utils.js +22 -1
  55. package/dist/lib/components/navigation/utils.js.map +1 -1
  56. package/dist/lib/core/ZudokuContext.d.ts +2 -1
  57. package/dist/lib/core/ZudokuContext.js +3 -1
  58. package/dist/lib/core/ZudokuContext.js.map +1 -1
  59. package/dist/lib/core/__internal.d.ts +1 -0
  60. package/dist/lib/core/__internal.js +2 -0
  61. package/dist/lib/core/__internal.js.map +1 -1
  62. package/dist/lib/core/plugins.d.ts +5 -1
  63. package/dist/lib/core/plugins.js.map +1 -1
  64. package/dist/lib/core/transform-config.d.ts +4 -2
  65. package/dist/lib/core/transform-config.js +33 -13
  66. package/dist/lib/core/transform-config.js.map +1 -1
  67. package/dist/lib/core/transform-config.test.d.ts +1 -0
  68. package/dist/lib/core/transform-config.test.js +83 -0
  69. package/dist/lib/core/transform-config.test.js.map +1 -0
  70. package/dist/lib/errors/ErrorAlert.js +1 -2
  71. package/dist/lib/errors/ErrorAlert.js.map +1 -1
  72. package/dist/lib/hooks/useEvent.test.js +1 -1
  73. package/dist/lib/hooks/useEvent.test.js.map +1 -1
  74. package/dist/lib/plugins/openapi/playground/fileUtils.d.ts +1 -0
  75. package/dist/lib/plugins/openapi/playground/fileUtils.js +3 -0
  76. package/dist/lib/plugins/openapi/playground/fileUtils.js.map +1 -1
  77. package/dist/lib/plugins/openapi/playground/result-panel/AudioPlayer.d.ts +6 -0
  78. package/dist/lib/plugins/openapi/playground/result-panel/AudioPlayer.js +20 -0
  79. package/dist/lib/plugins/openapi/playground/result-panel/AudioPlayer.js.map +1 -0
  80. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +7 -2
  81. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  82. package/dist/lib/ui/Alert.d.ts +3 -2
  83. package/dist/lib/ui/Alert.js +9 -5
  84. package/dist/lib/ui/Alert.js.map +1 -1
  85. package/dist/lib/ui/InputGroup.d.ts +16 -0
  86. package/dist/lib/ui/InputGroup.js +65 -0
  87. package/dist/lib/ui/InputGroup.js.map +1 -0
  88. package/dist/lib/ui/Secret.js +2 -2
  89. package/dist/lib/ui/Secret.js.map +1 -1
  90. package/dist/vite/config.js +5 -2
  91. package/dist/vite/config.js.map +1 -1
  92. package/dist/vite/plugin-config.js +16 -4
  93. package/dist/vite/plugin-config.js.map +1 -1
  94. package/dist/vite/plugin-theme.js +2 -1
  95. package/dist/vite/plugin-theme.js.map +1 -1
  96. package/dist/vite/prerender/prerender.js +3 -1
  97. package/dist/vite/prerender/prerender.js.map +1 -1
  98. package/dist/vite/prerender/worker.js +3 -1
  99. package/dist/vite/prerender/worker.js.map +1 -1
  100. package/lib/{ClaudeLogo-C6q-Xn_l.js → ClaudeLogo-Br8C_vTq.js} +3 -3
  101. package/lib/{ClaudeLogo-C6q-Xn_l.js.map → ClaudeLogo-Br8C_vTq.js.map} +1 -1
  102. package/lib/Drawer-Ch7927PF.js.map +1 -1
  103. package/lib/{HydrationBoundary-CNF2ZV3E.js → HydrationBoundary-CJu4vUlG.js} +6 -6
  104. package/lib/{HydrationBoundary-CNF2ZV3E.js.map → HydrationBoundary-CJu4vUlG.js.map} +1 -1
  105. package/lib/{MdxPage-B1G4W1TK.js → MdxPage-C0QFAsgv.js} +6 -6
  106. package/lib/{MdxPage-B1G4W1TK.js.map → MdxPage-C0QFAsgv.js.map} +1 -1
  107. package/lib/Mermaid-Chx5BPHn.js +104 -0
  108. package/lib/Mermaid-Chx5BPHn.js.map +1 -0
  109. package/lib/{OAuthErrorPage-01Ke086W.js → OAuthErrorPage-CFz_gBFx.js} +11 -10
  110. package/lib/{OAuthErrorPage-01Ke086W.js.map → OAuthErrorPage-CFz_gBFx.js.map} +1 -1
  111. package/lib/{OasProvider-oHPiMJZg.js → OasProvider-BnQ1_ehf.js} +3 -3
  112. package/lib/{OasProvider-oHPiMJZg.js.map → OasProvider-BnQ1_ehf.js.map} +1 -1
  113. package/lib/OperationList-Bw-3OS_8.js +5907 -0
  114. package/lib/OperationList-Bw-3OS_8.js.map +1 -0
  115. package/lib/{RouteGuard-B1lCR0C_.js → RouteGuard-CVs3yvEs.js} +3 -3
  116. package/lib/{RouteGuard-B1lCR0C_.js.map → RouteGuard-CVs3yvEs.js.map} +1 -1
  117. package/lib/{SchemaList-DoQFkJgM.js → SchemaList-IehIWcDV.js} +7 -7
  118. package/lib/{SchemaList-DoQFkJgM.js.map → SchemaList-IehIWcDV.js.map} +1 -1
  119. package/lib/{SchemaView-D2k6ZJck.js → SchemaView-BZLyoQRI.js} +3 -3
  120. package/lib/{SchemaView-D2k6ZJck.js.map → SchemaView-BZLyoQRI.js.map} +1 -1
  121. package/lib/{Secret-BDBqq4p3.js → Secret-DUpgv4V3.js} +92 -72
  122. package/lib/Secret-DUpgv4V3.js.map +1 -0
  123. package/lib/{SignUp-8kDBaLbO.js → SignUp-Dug1jAGC.js} +4 -4
  124. package/lib/{SignUp-8kDBaLbO.js.map → SignUp-Dug1jAGC.js.map} +1 -1
  125. package/lib/{SyntaxHighlight-hZOFnYl0.js → SyntaxHighlight-BMu0b_hF.js} +8 -8
  126. package/lib/{SyntaxHighlight-hZOFnYl0.js.map → SyntaxHighlight-BMu0b_hF.js.map} +1 -1
  127. package/lib/{Toc-qEIii_-W.js → Toc-BiJ2YL0O.js} +2 -2
  128. package/lib/{Toc-qEIii_-W.js.map → Toc-BiJ2YL0O.js.map} +1 -1
  129. package/lib/{Zudoku-DUsdmPME.js → Zudoku-iyiXgWFY.js} +2777 -2622
  130. package/lib/Zudoku-iyiXgWFY.js.map +1 -0
  131. package/lib/ZudokuContext-CYyb_PB_.js +175 -0
  132. package/lib/ZudokuContext-CYyb_PB_.js.map +1 -0
  133. package/lib/ZudokuReactContext-DGJAP1sN.js +222 -0
  134. package/lib/ZudokuReactContext-DGJAP1sN.js.map +1 -0
  135. package/lib/chunk-EPOLDU6W-C6C8jAwd.js.map +1 -1
  136. package/lib/{circular-D9tSKG2c.js → circular-CfBNz-Ot.js} +2 -2
  137. package/lib/{circular-D9tSKG2c.js.map → circular-CfBNz-Ot.js.map} +1 -1
  138. package/lib/{createServer-BprC4n85.js → createServer-BtZPTSEO.js} +4 -4
  139. package/lib/{createServer-BprC4n85.js.map → createServer-BtZPTSEO.js.map} +1 -1
  140. package/lib/{errors-7hgPDs1h.js → errors-B77S9iOc.js} +2 -2
  141. package/lib/{errors-7hgPDs1h.js.map → errors-B77S9iOc.js.map} +1 -1
  142. package/lib/{firebase-Dwn-2ju-.js → firebase-C7XKRGLf.js} +25 -24
  143. package/lib/{firebase-Dwn-2ju-.js.map → firebase-C7XKRGLf.js.map} +1 -1
  144. package/lib/{hook-ZEd1Es7D.js → hook-Dz_n9SoE.js} +16 -15
  145. package/lib/{hook-ZEd1Es7D.js.map → hook-Dz_n9SoE.js.map} +1 -1
  146. package/lib/{index-Dxdhrp-I.js → index-BDp2MTiq.js} +2 -2
  147. package/lib/{index-Dxdhrp-I.js.map → index-BDp2MTiq.js.map} +1 -1
  148. package/lib/{index-CyIW9rHv.js → index-CQ-p1wyT.js} +642 -606
  149. package/lib/index-CQ-p1wyT.js.map +1 -0
  150. package/lib/index-CrcNWbel.js.map +1 -1
  151. package/lib/index-DAWHN3cH.js.map +1 -1
  152. package/lib/index.esm-BYObtETB.js.map +1 -1
  153. package/lib/index.esm-Ca5zvoff.js.map +1 -1
  154. package/lib/{index.esm-DG4KaDKR.js → index.esm-Cth49JBv.js} +2 -2
  155. package/lib/index.esm-Cth49JBv.js.map +1 -0
  156. package/lib/jsx-runtime-BzflLqGi.js.map +1 -1
  157. package/lib/{mutation-BISOc7OM.js → mutation-B7eFBLZY.js} +2 -2
  158. package/lib/{mutation-BISOc7OM.js.map → mutation-B7eFBLZY.js.map} +1 -1
  159. package/lib/ui/Alert.js +32 -20
  160. package/lib/ui/Alert.js.map +1 -1
  161. package/lib/ui/Carousel.js.map +1 -1
  162. package/lib/ui/InputGroup.js +155 -0
  163. package/lib/ui/InputGroup.js.map +1 -0
  164. package/lib/ui/Secret.js +2 -2
  165. package/lib/ui/Secret.js.map +1 -1
  166. package/lib/ui/SyntaxHighlight.js +2 -2
  167. package/lib/useExposedProps-CzTDfXfq.js.map +1 -1
  168. package/lib/{useMutation-CFMGlAMW.js → useMutation-CErliDZ9.js} +5 -5
  169. package/lib/{useMutation-CFMGlAMW.js.map → useMutation-CErliDZ9.js.map} +1 -1
  170. package/lib/{useSuspenseQuery-CSB_rVek.js → useQuery-ht7aWJ3S.js} +432 -446
  171. package/lib/useQuery-ht7aWJ3S.js.map +1 -0
  172. package/lib/useSuspenseQuery-DQH4Bmc2.js +18 -0
  173. package/lib/useSuspenseQuery-DQH4Bmc2.js.map +1 -0
  174. package/lib/zudoku.__internal.js +1518 -1033
  175. package/lib/zudoku.__internal.js.map +1 -1
  176. package/lib/zudoku.auth-auth0.js +6 -5
  177. package/lib/zudoku.auth-auth0.js.map +1 -1
  178. package/lib/zudoku.auth-azureb2c.js +14 -13
  179. package/lib/zudoku.auth-azureb2c.js.map +1 -1
  180. package/lib/zudoku.auth-clerk.js +2 -2
  181. package/lib/zudoku.auth-firebase.js +4 -4
  182. package/lib/zudoku.auth-openid.js +7 -6
  183. package/lib/zudoku.auth-openid.js.map +1 -1
  184. package/lib/zudoku.auth-supabase.js +4 -4
  185. package/lib/zudoku.components.js +3 -3
  186. package/lib/zudoku.hooks.js +3 -3
  187. package/lib/zudoku.mermaid.js +3 -3
  188. package/lib/zudoku.plugin-api-catalog.js +28 -27
  189. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  190. package/lib/zudoku.plugin-api-keys.js +98 -96
  191. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  192. package/lib/zudoku.plugin-custom-pages.js +1 -1
  193. package/lib/zudoku.plugin-markdown.js +1 -1
  194. package/lib/zudoku.plugin-openapi.js +2 -2
  195. package/lib/zudoku.plugin-search-pagefind.js +19 -18
  196. package/lib/zudoku.plugin-search-pagefind.js.map +1 -1
  197. package/lib/zudoku.plugins.js.map +1 -1
  198. package/lib/zudoku.react-query.js +26 -25
  199. package/lib/zudoku.react-query.js.map +1 -1
  200. package/lib/zudoku.router.js.map +1 -1
  201. package/package.json +15 -9
  202. package/src/app/defaultTheme.css +4 -0
  203. package/src/app/main.css +2 -0
  204. package/src/app/main.tsx +1 -1
  205. package/src/lib/components/Bootstrap.tsx +1 -4
  206. package/src/lib/components/MobileTopNavigation.tsx +13 -8
  207. package/src/lib/components/Slot.test.tsx +1 -1
  208. package/src/lib/components/TopNavigation.tsx +25 -7
  209. package/src/lib/components/Zudoku.tsx +18 -14
  210. package/src/lib/components/context/ZudokuContext.ts +3 -6
  211. package/src/lib/components/context/ZudokuProvider.tsx +1 -1
  212. package/src/lib/components/context/ZudokuReactContext.tsx +17 -0
  213. package/src/lib/components/navigation/Navigation.tsx +4 -3
  214. package/src/lib/components/navigation/NavigationCategory.tsx +9 -0
  215. package/src/lib/components/navigation/NavigationFilterContext.tsx +28 -0
  216. package/src/lib/components/navigation/NavigationFilterInput.tsx +35 -0
  217. package/src/lib/components/navigation/NavigationItem.tsx +17 -1
  218. package/src/lib/components/navigation/utils.ts +32 -1
  219. package/src/lib/core/ZudokuContext.ts +7 -1
  220. package/src/lib/core/__internal.tsx +2 -0
  221. package/src/lib/core/plugins.ts +7 -3
  222. package/src/lib/core/transform-config.test.tsx +99 -0
  223. package/src/lib/core/transform-config.ts +57 -19
  224. package/src/lib/errors/ErrorAlert.tsx +1 -6
  225. package/src/lib/hooks/useEvent.test.tsx +1 -1
  226. package/src/lib/plugins/openapi/playground/fileUtils.ts +4 -0
  227. package/src/lib/plugins/openapi/playground/result-panel/AudioPlayer.tsx +50 -0
  228. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +33 -17
  229. package/src/lib/ui/Alert.tsx +17 -5
  230. package/src/lib/ui/InputGroup.tsx +168 -0
  231. package/src/lib/ui/Secret.tsx +2 -2
  232. package/lib/Mermaid-B1xNo-pf.js +0 -103
  233. package/lib/Mermaid-B1xNo-pf.js.map +0 -1
  234. package/lib/OperationList-CZ4OK8Pm.js +0 -5823
  235. package/lib/OperationList-CZ4OK8Pm.js.map +0 -1
  236. package/lib/Secret-BDBqq4p3.js.map +0 -1
  237. package/lib/Separator-BXt1LYnm.js +0 -27
  238. package/lib/Separator-BXt1LYnm.js.map +0 -1
  239. package/lib/Zudoku-DUsdmPME.js.map +0 -1
  240. package/lib/ZudokuContext-BBI06sOx.js +0 -387
  241. package/lib/ZudokuContext-BBI06sOx.js.map +0 -1
  242. package/lib/index-CyIW9rHv.js.map +0 -1
  243. package/lib/index.esm-DG4KaDKR.js.map +0 -1
  244. package/lib/useSuspenseQuery-CSB_rVek.js.map +0 -1
@@ -68,11 +68,15 @@ export interface ConfigHookContext {
68
68
  configPath: string;
69
69
  }
70
70
 
71
+ export interface TransformConfigContext {
72
+ config: ZudokuConfig;
73
+ merge: <T extends Partial<ZudokuConfig>>(partial: T) => ZudokuConfig & T;
74
+ }
75
+
71
76
  export interface TransformConfigPlugin {
72
77
  transformConfig?: (
73
- config: ZudokuConfig,
74
- ctx: ConfigHookContext,
75
- ) => Partial<ZudokuConfig> | void | Promise<Partial<ZudokuConfig> | void>;
78
+ context: TransformConfigContext,
79
+ ) => ZudokuConfig | void | Promise<ZudokuConfig | void>;
76
80
  }
77
81
 
78
82
  export interface CommonPlugin {
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { isPlainObject, mergeConfig } from "./transform-config.js";
3
+
4
+ describe("isPlainObject", () => {
5
+ test("returns true for plain objects", () => {
6
+ expect(isPlainObject({})).toBe(true);
7
+ expect(isPlainObject({ a: 1 })).toBe(true);
8
+ });
9
+
10
+ test("returns false for arrays", () => {
11
+ expect(isPlainObject([])).toBe(false);
12
+ expect(isPlainObject([1, 2, 3])).toBe(false);
13
+ });
14
+
15
+ test("returns false for null and undefined", () => {
16
+ expect(isPlainObject(null)).toBe(false);
17
+ expect(isPlainObject(undefined)).toBe(false);
18
+ });
19
+
20
+ test("returns false for class instances", () => {
21
+ expect(isPlainObject(new Date())).toBe(false);
22
+ expect(isPlainObject(new Map())).toBe(false);
23
+ expect(isPlainObject(/regex/)).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe("mergeConfig", () => {
28
+ test("merges flat objects", () => {
29
+ const target = { a: 1, b: 2 };
30
+ const source = { b: 3, c: 4 };
31
+ expect(mergeConfig(target, source)).toEqual({ a: 1, b: 3, c: 4 });
32
+ });
33
+
34
+ test("merges nested objects", () => {
35
+ const target = { nested: { a: 1, b: 2 } } as Record<string, unknown>;
36
+ const source = { nested: { b: 3, c: 4 } };
37
+ expect(mergeConfig(target, source)).toEqual({
38
+ nested: { a: 1, b: 3, c: 4 },
39
+ });
40
+ });
41
+
42
+ test("replaces arrays instead of merging", () => {
43
+ const target = { arr: [1, 2, 3] };
44
+ const source = { arr: [4, 5] };
45
+ expect(mergeConfig(target, source)).toEqual({ arr: [4, 5] });
46
+ });
47
+
48
+ test("preserves React elements without deep cloning", () => {
49
+ const element = <div className="test">Hello</div>;
50
+ const target = { banner: { message: "old" } };
51
+ const source = { banner: { message: element } };
52
+
53
+ const result = mergeConfig(target, source);
54
+
55
+ // Should be the exact same reference, not a clone
56
+ expect(result.banner.message).toBe(element);
57
+ });
58
+
59
+ test("does not clone React element children", () => {
60
+ const child = <strong>Bold</strong>;
61
+ const element = <div>{child} text</div>;
62
+ const target = { site: { banner: {} } };
63
+ const source = { site: { banner: { message: element } } };
64
+
65
+ const result = mergeConfig(target, source);
66
+
67
+ // The element should be identical (same reference)
68
+ expect(result.site.banner.message).toBe(element);
69
+ // Children should be preserved exactly
70
+ expect(result.site.banner.message.props.children).toBe(
71
+ element.props.children,
72
+ );
73
+ });
74
+
75
+ test("handles null and undefined values", () => {
76
+ const target = { a: 1, b: 2 };
77
+ const source = { a: null, c: undefined };
78
+ expect(mergeConfig(target, source)).toEqual({
79
+ a: null,
80
+ b: 2,
81
+ c: undefined,
82
+ });
83
+ });
84
+
85
+ test("replaces non-plain objects", () => {
86
+ const date = new Date("2024-01-01");
87
+ const target = { date: new Date("2020-01-01") };
88
+ const source = { date };
89
+ const result = mergeConfig(target, source);
90
+ expect(result.date).toBe(date);
91
+ });
92
+
93
+ test("does not mutate target", () => {
94
+ const target = { a: 1, nested: { b: 2 } };
95
+ const source = { a: 2, nested: { c: 3 } };
96
+ mergeConfig(target, source);
97
+ expect(target).toEqual({ a: 1, nested: { b: 2 } });
98
+ });
99
+ });
@@ -1,28 +1,66 @@
1
- import createDeepmerge from "@fastify/deepmerge";
2
- import type { ConfigWithMeta } from "../../config/loader.js";
3
- import { type ConfigHookContext, isTransformConfigPlugin } from "./plugins.js";
4
-
5
- const mergeConfig = createDeepmerge({
6
- mergeArray: (opt) => (_, source) => opt.clone(source),
7
- });
8
-
9
- export const runTransformConfigHooks = async (
10
- config: ConfigWithMeta,
11
- ): Promise<ConfigWithMeta> => {
12
- const ctx = {
13
- mode: config.__meta.mode,
14
- rootDir: config.__meta.rootDir,
15
- configPath: config.__meta.configPath,
16
- } satisfies ConfigHookContext;
1
+ import { isValidElement } from "react";
2
+ import type { ZudokuConfig } from "../../config/validators/validate.js";
3
+ import { isTransformConfigPlugin } from "./plugins.js";
4
+
5
+ export const isPlainObject = (
6
+ value: unknown,
7
+ ): value is Record<string, unknown> =>
8
+ typeof value === "object" &&
9
+ value !== null &&
10
+ !Array.isArray(value) &&
11
+ Object.getPrototypeOf(value) === Object.prototype;
12
+
13
+ export const mergeConfig = <
14
+ T extends Record<string, unknown>,
15
+ S extends Record<string, unknown>,
16
+ >(
17
+ target: T,
18
+ source: S,
19
+ ): T & S => {
20
+ const result = { ...target } as T & S;
21
+
22
+ for (const key of Object.keys(source) as (keyof S)[]) {
23
+ const sourceValue = source[key];
24
+ const targetValue = target[key as keyof T];
25
+
26
+ // Don't merge React elements, arrays, or non-plain objects - just replace
27
+ if (
28
+ isValidElement(sourceValue) ||
29
+ Array.isArray(sourceValue) ||
30
+ !isPlainObject(sourceValue)
31
+ ) {
32
+ (result as Record<string, unknown>)[key as string] = sourceValue;
33
+ } else if (isPlainObject(targetValue)) {
34
+ (result as Record<string, unknown>)[key as string] = mergeConfig(
35
+ targetValue,
36
+ sourceValue,
37
+ );
38
+ } else {
39
+ (result as Record<string, unknown>)[key as string] = sourceValue;
40
+ }
41
+ }
42
+
43
+ return result;
44
+ };
45
+
46
+ export const runPluginTransformConfig = async <T extends ZudokuConfig>(
47
+ config: T,
48
+ ): Promise<T> => {
17
49
  const plugins = config.plugins ?? [];
18
50
 
19
51
  let result = config;
20
52
 
21
53
  for (const plugin of plugins.filter(isTransformConfigPlugin)) {
22
- const partial = await plugin.transformConfig?.(result, ctx);
23
- if (!partial) continue;
54
+ const merge = <T extends Record<string, unknown>>(partial: T) =>
55
+ mergeConfig(result, partial);
56
+
57
+ const transformed = await plugin.transformConfig?.({
58
+ config: result,
59
+ merge,
60
+ });
61
+ if (!transformed) continue;
24
62
 
25
- result = mergeConfig(result, partial) as ConfigWithMeta;
63
+ result = transformed as T;
26
64
  }
27
65
 
28
66
  return result;
@@ -1,4 +1,3 @@
1
- import { SyntaxHighlight } from "zudoku/ui/SyntaxHighlight.js";
2
1
  import { DeveloperHint } from "../components/DeveloperHint.js";
3
2
  import { Heading } from "../components/Heading.js";
4
3
  import { Typography } from "../components/Typography.js";
@@ -22,11 +21,7 @@ export function ErrorAlert({ error }: { error: unknown }) {
22
21
  Error: {message}
23
22
  {hint && <DeveloperHint className="mb-4">{hint}</DeveloperHint>}
24
23
  {stringError && (
25
- <SyntaxHighlight
26
- className="max-h-[400px] [&>pre]:p-4"
27
- language="js"
28
- code={stringError}
29
- />
24
+ <pre className="max-h-[400px] [&>pre]:p-4">{stringError}</pre>
30
25
  )}
31
26
  </Typography>
32
27
  );
@@ -13,7 +13,7 @@ import { useEvent } from "./useEvent.js";
13
13
 
14
14
  const createTestContext = () => {
15
15
  const queryClient = new QueryClient();
16
- const context = new ZudokuContext({}, queryClient);
16
+ const context = new ZudokuContext({}, queryClient, {});
17
17
  const wrapper = ({ children }: PropsWithChildren) => (
18
18
  <QueryClientProvider client={queryClient}>
19
19
  <ZudokuProvider context={context}>{children}</ZudokuProvider>
@@ -4,6 +4,10 @@ export function isBinaryContentType(contentType: string) {
4
4
  );
5
5
  }
6
6
 
7
+ export function isAudioContentType(contentType: string) {
8
+ return /^audio\//i.test(contentType);
9
+ }
10
+
7
11
  export const extractFileName = (
8
12
  headers: Array<[string, string]>,
9
13
  url: string,
@@ -0,0 +1,50 @@
1
+ import { DownloadIcon } from "lucide-react";
2
+ import { useEffect, useState } from "react";
3
+ import { Button } from "zudoku/ui/Button.js";
4
+ import { humanFileSize } from "../../../../util/humanFileSize.js";
5
+
6
+ export const AudioPlayer = ({
7
+ blob,
8
+ fileName,
9
+ size,
10
+ onDownload,
11
+ }: {
12
+ blob: Blob;
13
+ fileName: string;
14
+ size: number;
15
+ onDownload: () => void;
16
+ }) => {
17
+ const [audioUrl, setAudioUrl] = useState<string | null>(null);
18
+
19
+ useEffect(() => {
20
+ const url = URL.createObjectURL(blob);
21
+ setAudioUrl(url);
22
+
23
+ return () => {
24
+ URL.revokeObjectURL(url);
25
+ };
26
+ }, [blob]);
27
+
28
+ if (!audioUrl) {
29
+ return (
30
+ <div className="p-4 text-center">
31
+ <div className="text-sm text-muted-foreground">Loading audio...</div>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <div className="p-4 text-center">
38
+ <div className="flex flex-col items-center gap-4">
39
+ {/* biome-ignore lint/a11y/useMediaCaption: API response audio cannot have predefined captions */}
40
+ <audio controls src={audioUrl} className="w-full max-w-md">
41
+ Your browser does not support the audio element.
42
+ </audio>
43
+ <Button onClick={onDownload} className="flex items-center gap-2">
44
+ <DownloadIcon className="h-4 w-4" />
45
+ Download {fileName} ({humanFileSize(size)})
46
+ </Button>
47
+ </div>
48
+ </div>
49
+ );
50
+ };
@@ -32,6 +32,8 @@ import {
32
32
  CollapsibleHeader,
33
33
  CollapsibleHeaderTrigger,
34
34
  } from "../CollapsibleHeader.js";
35
+ import { isAudioContentType } from "../fileUtils.js";
36
+ import { AudioPlayer } from "./AudioPlayer.js";
35
37
  import { convertToTypes } from "./convertToTypes.js";
36
38
 
37
39
  const mimeTypeToLanguage = (mimeType: string) => {
@@ -50,9 +52,14 @@ const mimeTypeToLanguage = (mimeType: string) => {
50
52
  )?.[1];
51
53
  };
52
54
 
55
+ const getContentType = (headers: Array<[string, string]>) => {
56
+ return (
57
+ headers.find(([key]) => key.toLowerCase() === "content-type")?.[1] || ""
58
+ );
59
+ };
60
+
53
61
  const detectLanguage = (headers: Array<[string, string]>) => {
54
- const contentType =
55
- headers.find(([key]) => key.toLowerCase() === "content-type")?.[1] || "";
62
+ const contentType = getContentType(headers);
56
63
  return mimeTypeToLanguage(contentType);
57
64
  };
58
65
 
@@ -293,23 +300,32 @@ export const ResponseTab = ({
293
300
  </div>
294
301
  <div className="flex-1">
295
302
  {isBinary ? (
296
- <div className="p-4 text-center">
297
- <div className="flex flex-col items-center gap-4">
298
- <div className="text-lg font-semibold">Binary Content</div>
299
- <div className="text-sm text-muted-foreground">
300
- This response contains binary data that cannot be displayed as
301
- text.
303
+ blob && isAudioContentType(getContentType(headers)) ? (
304
+ <AudioPlayer
305
+ blob={blob}
306
+ fileName={fileName ?? "audio"}
307
+ size={size}
308
+ onDownload={handleDownload}
309
+ />
310
+ ) : (
311
+ <div className="p-4 text-center">
312
+ <div className="flex flex-col items-center gap-4">
313
+ <div className="text-lg font-semibold">Binary Content</div>
314
+ <div className="text-sm text-muted-foreground">
315
+ This response contains binary data that cannot be displayed as
316
+ text.
317
+ </div>
318
+ <Button
319
+ onClick={handleDownload}
320
+ className="flex items-center gap-2"
321
+ disabled={!blob}
322
+ >
323
+ <DownloadIcon className="h-4 w-4" />
324
+ Download {fileName || "file"} ({humanFileSize(size)})
325
+ </Button>
302
326
  </div>
303
- <Button
304
- onClick={handleDownload}
305
- className="flex items-center gap-2"
306
- disabled={!blob}
307
- >
308
- <DownloadIcon className="h-4 w-4" />
309
- Download {fileName || "file"} ({humanFileSize(size)})
310
- </Button>
311
327
  </div>
312
- </div>
328
+ )
313
329
  ) : (
314
330
  <SyntaxHighlight
315
331
  className="text-xs flex-1"
@@ -3,13 +3,15 @@ import type * as React from "react";
3
3
  import { cn } from "../util/cn.js";
4
4
 
5
5
  const alertVariants = cva(
6
- "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
6
+ "grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert",
7
7
  {
8
8
  variants: {
9
9
  variant: {
10
10
  default: "bg-card text-card-foreground",
11
11
  destructive:
12
- "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
12
+ "text-destructive bg-card bg-destructive/5 border-destructive/20 *:data-[slot=alert-description]:text-destructive-foreground *:[svg]:text-current",
13
+ warning:
14
+ "text-warning-foreground bg-card bg-warning/10 border-warning/50 *:data-[slot=alert-description]:text-warning-foreground *:[svg]:text-current",
13
15
  },
14
16
  },
15
17
  defaultVariants: {
@@ -38,7 +40,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
40
  <div
39
41
  data-slot="alert-title"
40
42
  className={cn(
41
- "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
42
44
  className,
43
45
  )}
44
46
  {...props}
@@ -54,7 +56,7 @@ function AlertDescription({
54
56
  <div
55
57
  data-slot="alert-description"
56
58
  className={cn(
57
- "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ "text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
58
60
  className,
59
61
  )}
60
62
  {...props}
@@ -62,4 +64,14 @@ function AlertDescription({
62
64
  );
63
65
  }
64
66
 
65
- export { Alert, AlertDescription, AlertTitle };
67
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div
70
+ data-slot="alert-action"
71
+ className={cn("absolute top-2 right-2", className)}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ export { Alert, AlertTitle, AlertDescription, AlertAction };
@@ -0,0 +1,168 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import type * as React from "react";
3
+ import { cn } from "../util/cn.js";
4
+ import { Button } from "./Button.js";
5
+ import { Input } from "./Input.js";
6
+ import { Textarea } from "./Textarea.js";
7
+
8
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
9
+ return (
10
+ <div
11
+ data-slot="input-group"
12
+ role="group"
13
+ className={cn(
14
+ "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
15
+ "h-9 min-w-0 has-[>textarea]:h-auto",
16
+
17
+ // Variants based on alignment.
18
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
19
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
20
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
21
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
22
+
23
+ // Focus state.
24
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
25
+
26
+ // Error state.
27
+ "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
28
+
29
+ className,
30
+ )}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ const inputGroupAddonVariants = cva(
37
+ "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
38
+ {
39
+ variants: {
40
+ align: {
41
+ "inline-start":
42
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
43
+ "inline-end":
44
+ "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
45
+ "block-start":
46
+ "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
47
+ "block-end":
48
+ "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
49
+ },
50
+ },
51
+ defaultVariants: {
52
+ align: "inline-start",
53
+ },
54
+ },
55
+ );
56
+
57
+ function InputGroupAddon({
58
+ className,
59
+ align = "inline-start",
60
+ ...props
61
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
62
+ return (
63
+ // biome-ignore lint/a11y/useKeyWithClickEvents: Focus management
64
+ <div
65
+ role="group"
66
+ data-slot="input-group-addon"
67
+ data-align={align}
68
+ className={cn(inputGroupAddonVariants({ align }), className)}
69
+ onClick={(e) => {
70
+ if ((e.target as HTMLElement).closest("button")) {
71
+ return;
72
+ }
73
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
74
+ }}
75
+ {...props}
76
+ />
77
+ );
78
+ }
79
+
80
+ const inputGroupButtonVariants = cva(
81
+ "text-sm shadow-none flex gap-2 items-center",
82
+ {
83
+ variants: {
84
+ size: {
85
+ xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
86
+ sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
87
+ "icon-xs":
88
+ "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
89
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
90
+ },
91
+ },
92
+ defaultVariants: {
93
+ size: "xs",
94
+ },
95
+ },
96
+ );
97
+
98
+ function InputGroupButton({
99
+ className,
100
+ type = "button",
101
+ variant = "ghost",
102
+ size = "xs",
103
+ ...props
104
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
105
+ VariantProps<typeof inputGroupButtonVariants>) {
106
+ return (
107
+ <Button
108
+ type={type}
109
+ data-size={size}
110
+ variant={variant}
111
+ className={cn(inputGroupButtonVariants({ size }), className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
118
+ return (
119
+ <span
120
+ className={cn(
121
+ "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
122
+ className,
123
+ )}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ function InputGroupInput({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<"input">) {
133
+ return (
134
+ <Input
135
+ data-slot="input-group-control"
136
+ className={cn(
137
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
138
+ className,
139
+ )}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ function InputGroupTextarea({
146
+ className,
147
+ ...props
148
+ }: React.ComponentProps<"textarea">) {
149
+ return (
150
+ <Textarea
151
+ data-slot="input-group-control"
152
+ className={cn(
153
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
154
+ className,
155
+ )}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+
161
+ export {
162
+ InputGroup,
163
+ InputGroupAddon,
164
+ InputGroupButton,
165
+ InputGroupText,
166
+ InputGroupInput,
167
+ InputGroupTextarea,
168
+ };
@@ -103,7 +103,7 @@ export const Secret = ({
103
103
  setRevealed((prev) => !prev);
104
104
  onReveal?.(!revealed);
105
105
  }}
106
- size="icon"
106
+ size="icon-sm"
107
107
  >
108
108
  {revealed ? <EyeOffIcon size={16} /> : <EyeIcon size={16} />}
109
109
  </Button>
@@ -114,7 +114,7 @@ export const Secret = ({
114
114
  copyToClipboard(secret);
115
115
  onCopy?.(secret);
116
116
  }}
117
- size="icon"
117
+ size="icon-sm"
118
118
  >
119
119
  {isCopied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
120
120
  </Button>