@hustle-together/api-dev-tools 3.12.3 → 4.5.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 (159) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +7 -463
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. package/templates/ui-showcase/page.tsx +1 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useCallback } from "react";
4
4
 
5
5
  interface ParameterDoc {
6
6
  name: string;
@@ -9,6 +9,9 @@ interface ParameterDoc {
9
9
  required?: boolean;
10
10
  default?: string | number | boolean;
11
11
  enum?: string[];
12
+ example?: string;
13
+ min?: number;
14
+ max?: number;
12
15
  }
13
16
 
14
17
  interface SchemaDoc {
@@ -17,6 +20,19 @@ interface SchemaDoc {
17
20
  queryParams?: ParameterDoc[];
18
21
  }
19
22
 
23
+ /** Example request from registry.json */
24
+ interface EndpointExample {
25
+ description: string;
26
+ query: string;
27
+ curl: string;
28
+ }
29
+
30
+ /** State for each parameter in the interactive builder */
31
+ interface ParamState {
32
+ enabled: boolean;
33
+ value: string;
34
+ }
35
+
20
36
  interface APITesterProps {
21
37
  id: string;
22
38
  endpoint: string;
@@ -24,6 +40,16 @@ interface APITesterProps {
24
40
  selectedEndpoint?: string | null;
25
41
  schemaPath?: string;
26
42
  schema?: SchemaDoc;
43
+ /** Parameters from registry.json endpoints section */
44
+ endpointParams?: ParameterDoc[];
45
+ /** API route file path for reference */
46
+ apiRoute?: string;
47
+ /** Pre-built examples from registry.json */
48
+ examples?: Record<string, EndpointExample>;
49
+ /** Callback to expose submit function to parent */
50
+ onSubmitRef?: (submitFn: () => Promise<void>) => void;
51
+ /** Callback to notify parent of loading state */
52
+ onLoadingChange?: (isLoading: boolean) => void;
27
53
  }
28
54
 
29
55
  interface RequestState {
@@ -80,7 +106,7 @@ const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
80
106
  * - Response display with timing
81
107
  * - Audio playback for binary responses
82
108
  *
83
- * Created with Hustle API Dev Tools (v3.9.2)
109
+ * Created with Hustle API Dev Tools (v3.12.12)
84
110
  */
85
111
  export function APITester({
86
112
  id,
@@ -89,7 +115,63 @@ export function APITester({
89
115
  selectedEndpoint,
90
116
  schemaPath,
91
117
  schema,
118
+ endpointParams,
119
+ apiRoute,
120
+ examples,
121
+ onSubmitRef,
122
+ onLoadingChange,
92
123
  }: APITesterProps) {
124
+ const [selectedExample, setSelectedExample] = useState<string | null>(null);
125
+
126
+ // Interactive parameter builder state
127
+ const [paramStates, setParamStates] = useState<Record<string, ParamState>>({});
128
+
129
+ // Initialize param states from endpointParams
130
+ const initializeParamStates = useCallback((params: ParameterDoc[] | undefined) => {
131
+ if (!params) return {};
132
+ const states: Record<string, ParamState> = {};
133
+ for (const param of params) {
134
+ // Default value: example > default > empty
135
+ const defaultValue = param.example ||
136
+ (param.default !== undefined ? String(param.default) : "") ||
137
+ (param.enum?.[0] || "");
138
+
139
+ // Required params are enabled by default
140
+ states[param.name] = {
141
+ enabled: param.required || false,
142
+ value: defaultValue,
143
+ };
144
+ }
145
+ return states;
146
+ }, []);
147
+
148
+ // Build query string from param states
149
+ const buildQueryFromStates = useCallback((states: Record<string, ParamState>) => {
150
+ const parts: string[] = [];
151
+ for (const [name, state] of Object.entries(states)) {
152
+ if (state.enabled && state.value) {
153
+ parts.push(`${name}=${encodeURIComponent(state.value)}`);
154
+ }
155
+ }
156
+ return parts.join("&");
157
+ }, []);
158
+
159
+ // Update param states from query string (for example buttons)
160
+ const updateStatesFromQuery = useCallback((query: string, params: ParameterDoc[] | undefined) => {
161
+ if (!params) return;
162
+ const searchParams = new URLSearchParams(query);
163
+ const newStates: Record<string, ParamState> = {};
164
+
165
+ for (const param of params) {
166
+ const value = searchParams.get(param.name);
167
+ newStates[param.name] = {
168
+ enabled: value !== null,
169
+ value: value || param.example || (param.default !== undefined ? String(param.default) : "") || (param.enum?.[0] || ""),
170
+ };
171
+ }
172
+ setParamStates(newStates);
173
+ }, []);
174
+
93
175
  // Get default body for this API/endpoint
94
176
  const getDefaultBody = () => {
95
177
  const apiDefaults = DEFAULT_BODIES[id];
@@ -104,10 +186,30 @@ export function APITester({
104
186
 
105
187
  // Get default query params for GET requests
106
188
  const getDefaultQueryParams = () => {
189
+ // First check hardcoded defaults
107
190
  const apiParams = DEFAULT_QUERY_PARAMS[id];
108
191
  if (apiParams && selectedEndpoint) {
109
192
  return apiParams[selectedEndpoint] || "";
110
193
  }
194
+
195
+ // Build from endpointParams if available
196
+ if (endpointParams && endpointParams.length > 0) {
197
+ const queryParts: string[] = [];
198
+ for (const param of endpointParams) {
199
+ if (param.name === "action") {
200
+ // Add action with the selectedEndpoint or default value
201
+ queryParts.push(`action=${selectedEndpoint || param.default || ""}`);
202
+ } else if (param.required && param.example) {
203
+ // Add required params with example values
204
+ queryParts.push(`${param.name}=${encodeURIComponent(param.example)}`);
205
+ } else if (param.required && param.default !== undefined) {
206
+ // Add required params with defaults
207
+ queryParts.push(`${param.name}=${encodeURIComponent(String(param.default))}`);
208
+ }
209
+ }
210
+ return queryParts.join("&");
211
+ }
212
+
111
213
  return "";
112
214
  };
113
215
 
@@ -132,15 +234,34 @@ export function APITester({
132
234
  const [isLoading, setIsLoading] = useState(false);
133
235
  const [audioUrl, setAudioUrl] = useState<string | null>(null);
134
236
 
237
+ // Expose submit function to parent for footer button
238
+ useEffect(() => {
239
+ if (onSubmitRef) {
240
+ onSubmitRef(handleSubmit);
241
+ }
242
+ // eslint-disable-next-line react-hooks/exhaustive-deps
243
+ }, [onSubmitRef, request, endpoint]);
244
+
245
+ // Notify parent of loading state changes
246
+ useEffect(() => {
247
+ if (onLoadingChange) {
248
+ onLoadingChange(isLoading);
249
+ }
250
+ }, [isLoading, onLoadingChange]);
251
+
135
252
  // Update defaults when endpoint changes
136
253
  useEffect(() => {
254
+ const queryParams = endpointParams || schema?.queryParams;
255
+ const initialStates = initializeParamStates(queryParams);
256
+ setParamStates(initialStates);
257
+
137
258
  setRequest((prev) => ({
138
259
  ...prev,
139
260
  method: methods[0] || "POST",
140
261
  body: getDefaultBody(),
141
- queryParams: getDefaultQueryParams(),
262
+ queryParams: buildQueryFromStates(initialStates),
142
263
  }));
143
- // Clear previous response
264
+ // Clear previous response and example selection
144
265
  setResponse({
145
266
  status: null,
146
267
  statusText: "",
@@ -150,8 +271,17 @@ export function APITester({
150
271
  contentType: null,
151
272
  });
152
273
  setAudioUrl(null);
274
+ setSelectedExample(null);
153
275
  // eslint-disable-next-line react-hooks/exhaustive-deps
154
- }, [selectedEndpoint, id]);
276
+ }, [selectedEndpoint, id, endpointParams, examples, schema]);
277
+
278
+ // Auto-update query string when param states change
279
+ useEffect(() => {
280
+ if (Object.keys(paramStates).length > 0) {
281
+ const newQuery = buildQueryFromStates(paramStates);
282
+ setRequest((prev) => ({ ...prev, queryParams: newQuery }));
283
+ }
284
+ }, [paramStates, buildQueryFromStates]);
155
285
 
156
286
  const handleSubmit = async () => {
157
287
  setIsLoading(true);
@@ -298,6 +428,49 @@ export function APITester({
298
428
  </div>
299
429
  </div>
300
430
 
431
+ {/* Example Selector (for GET requests with examples) */}
432
+ {request.method === "GET" && examples && Object.keys(examples).length > 0 && (
433
+ <div>
434
+ <label className="mb-1 block text-sm font-bold text-black dark:text-white">
435
+ Example Requests
436
+ </label>
437
+ <div className="flex flex-wrap gap-2">
438
+ {Object.entries(examples).map(([key, example]) => (
439
+ <button
440
+ key={key}
441
+ onClick={() => {
442
+ setSelectedExample(key);
443
+ updateStatesFromQuery(example.query, endpointParams || schema?.queryParams);
444
+ }}
445
+ className={`border-2 px-3 py-1.5 text-xs font-medium transition-colors ${
446
+ selectedExample === key
447
+ ? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
448
+ : "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
449
+ }`}
450
+ title={example.description}
451
+ >
452
+ {key.replace(/_/g, " ")}
453
+ </button>
454
+ ))}
455
+ </div>
456
+ {selectedExample && examples[selectedExample] && (
457
+ <div className="mt-2 flex items-center gap-2">
458
+ <p className="text-xs text-gray-600 dark:text-gray-400">
459
+ {examples[selectedExample].description}
460
+ </p>
461
+ <button
462
+ onClick={() => {
463
+ navigator.clipboard.writeText(examples[selectedExample].curl);
464
+ }}
465
+ className="shrink-0 border border-gray-300 bg-gray-100 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
466
+ >
467
+ Copy curl
468
+ </button>
469
+ </div>
470
+ )}
471
+ </div>
472
+ )}
473
+
301
474
  {/* Query Parameters (for GET requests) */}
302
475
  {request.method === "GET" && (
303
476
  <div>
@@ -307,14 +480,15 @@ export function APITester({
307
480
  <input
308
481
  type="text"
309
482
  value={request.queryParams}
310
- onChange={(e) =>
311
- setRequest((prev) => ({ ...prev, queryParams: e.target.value }))
312
- }
483
+ onChange={(e) => {
484
+ setSelectedExample(null); // Clear example selection when manually editing
485
+ setRequest((prev) => ({ ...prev, queryParams: e.target.value }));
486
+ }}
313
487
  className="w-full border-2 border-black bg-white px-3 py-2 font-mono text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
314
488
  placeholder="key1=value1&key2=value2"
315
489
  />
316
490
  <p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
317
- Add query string parameters (without the ?)
491
+ Add query string parameters (without the ?) - or select an example above
318
492
  </p>
319
493
  </div>
320
494
  )}
@@ -346,14 +520,22 @@ export function APITester({
346
520
  </div>
347
521
  )}
348
522
 
349
- {/* Parameter Documentation */}
350
- {schema && (schema.request?.length || schema.queryParams?.length) ? (
351
- <ParameterDocs
352
- requestParams={schema.request}
353
- queryParams={schema.queryParams}
354
- isGetRequest={request.method === "GET"}
523
+ {/* Interactive Parameter Builder (for GET requests with params) */}
524
+ {request.method === "GET" && (endpointParams?.length || schema?.queryParams?.length) ? (
525
+ <InteractiveParamBuilder
526
+ params={endpointParams || schema?.queryParams || []}
527
+ paramStates={paramStates}
528
+ setParamStates={setParamStates}
355
529
  />
356
- ) : null}
530
+ ) : (
531
+ /* Static Parameter Documentation (for non-GET requests) */
532
+ (schema && schema.request?.length) ? (
533
+ <ParameterDocs
534
+ requestParams={schema?.request}
535
+ isGetRequest={false}
536
+ />
537
+ ) : null
538
+ )}
357
539
 
358
540
  {/* Headers */}
359
541
  <div>
@@ -376,41 +558,6 @@ export function APITester({
376
558
  API keys loaded from .env automatically
377
559
  </p>
378
560
  </div>
379
-
380
- {/* Submit Button */}
381
- <button
382
- onClick={handleSubmit}
383
- disabled={isLoading}
384
- className="w-full border-2 border-black bg-[#BA0C2F] py-3 font-bold text-white transition-colors hover:bg-[#8a0923] disabled:opacity-50"
385
- >
386
- {isLoading ? (
387
- <span className="flex items-center justify-center gap-2">
388
- <svg
389
- className="h-4 w-4 animate-spin"
390
- xmlns="http://www.w3.org/2000/svg"
391
- fill="none"
392
- viewBox="0 0 24 24"
393
- >
394
- <circle
395
- className="opacity-25"
396
- cx="12"
397
- cy="12"
398
- r="10"
399
- stroke="currentColor"
400
- strokeWidth="4"
401
- />
402
- <path
403
- className="opacity-75"
404
- fill="currentColor"
405
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
406
- />
407
- </svg>
408
- Sending...
409
- </span>
410
- ) : (
411
- `Send ${request.method} Request`
412
- )}
413
- </button>
414
561
  </div>
415
562
 
416
563
  {/* Response Panel */}
@@ -488,23 +635,170 @@ export function APITester({
488
635
  );
489
636
  }
490
637
 
638
+ /**
639
+ * Interactive Parameter Builder Component
640
+ * Allows users to check/uncheck params and edit values to build query strings.
641
+ */
642
+ function InteractiveParamBuilder({
643
+ params,
644
+ paramStates,
645
+ setParamStates,
646
+ }: {
647
+ params: ParameterDoc[];
648
+ paramStates: Record<string, ParamState>;
649
+ setParamStates: React.Dispatch<React.SetStateAction<Record<string, ParamState>>>;
650
+ }) {
651
+ const [isExpanded, setIsExpanded] = useState(true);
652
+
653
+ if (!params.length) return null;
654
+
655
+ const handleToggle = (name: string) => {
656
+ setParamStates((prev) => ({
657
+ ...prev,
658
+ [name]: {
659
+ ...prev[name],
660
+ enabled: !prev[name]?.enabled,
661
+ },
662
+ }));
663
+ };
664
+
665
+ const handleValueChange = (name: string, value: string) => {
666
+ setParamStates((prev) => ({
667
+ ...prev,
668
+ [name]: {
669
+ ...prev[name],
670
+ value,
671
+ enabled: true, // Auto-enable when value is changed
672
+ },
673
+ }));
674
+ };
675
+
676
+ return (
677
+ <div className="border-2 border-black dark:border-gray-600">
678
+ <button
679
+ onClick={() => setIsExpanded(!isExpanded)}
680
+ className="flex w-full items-center justify-between bg-gray-50 px-3 py-2 text-left dark:bg-gray-800"
681
+ >
682
+ <span className="text-sm font-bold text-black dark:text-white">
683
+ Query Parameters Builder
684
+ </span>
685
+ <svg
686
+ xmlns="http://www.w3.org/2000/svg"
687
+ width="16"
688
+ height="16"
689
+ viewBox="0 0 24 24"
690
+ fill="none"
691
+ stroke="currentColor"
692
+ strokeWidth="2"
693
+ strokeLinecap="round"
694
+ strokeLinejoin="round"
695
+ className={`text-gray-500 transition-transform ${isExpanded ? "rotate-180" : ""}`}
696
+ >
697
+ <polyline points="6 9 12 15 18 9" />
698
+ </svg>
699
+ </button>
700
+
701
+ {isExpanded && (
702
+ <div className="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
703
+ {params.map((param) => {
704
+ const state = paramStates[param.name] || { enabled: false, value: "" };
705
+
706
+ return (
707
+ <div key={param.name} className="flex items-start gap-3 px-3 py-2">
708
+ {/* Checkbox */}
709
+ <label className="flex h-8 cursor-pointer items-center">
710
+ <input
711
+ type="checkbox"
712
+ checked={state.enabled}
713
+ onChange={() => handleToggle(param.name)}
714
+ disabled={param.required}
715
+ className="h-4 w-4 cursor-pointer accent-[#BA0C2F] disabled:cursor-not-allowed disabled:opacity-50"
716
+ />
717
+ </label>
718
+
719
+ {/* Parameter Info */}
720
+ <div className="min-w-0 flex-1">
721
+ <div className="flex flex-wrap items-center gap-2">
722
+ <code className="text-sm font-bold text-[#BA0C2F]">
723
+ {param.name}
724
+ </code>
725
+ <span className="border border-gray-300 bg-gray-100 px-1.5 py-0.5 text-xs text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
726
+ {param.type}
727
+ </span>
728
+ {param.required && (
729
+ <span className="border border-red-300 bg-red-50 px-1.5 py-0.5 text-xs text-red-600 dark:border-red-800 dark:bg-red-900/30 dark:text-red-400">
730
+ required
731
+ </span>
732
+ )}
733
+ </div>
734
+ {param.description && (
735
+ <p className="mt-0.5 text-xs text-gray-600 dark:text-gray-400">
736
+ {param.description}
737
+ </p>
738
+ )}
739
+ </div>
740
+
741
+ {/* Input Control */}
742
+ <div className="w-36 shrink-0">
743
+ {param.enum ? (
744
+ /* Dropdown for enum types */
745
+ <select
746
+ value={state.value}
747
+ onChange={(e) => handleValueChange(param.name, e.target.value)}
748
+ className="w-full border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
749
+ >
750
+ <option value="">-- none --</option>
751
+ {param.enum.map((val) => (
752
+ <option key={val} value={val}>
753
+ {val}
754
+ </option>
755
+ ))}
756
+ </select>
757
+ ) : param.type === "number" || param.type === "integer" ? (
758
+ /* Number input for numeric types */
759
+ <input
760
+ type="number"
761
+ value={state.value}
762
+ onChange={(e) => handleValueChange(param.name, e.target.value)}
763
+ min={param.min}
764
+ max={param.max}
765
+ placeholder={param.example || (param.default !== undefined ? String(param.default) : "")}
766
+ className="w-full border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
767
+ />
768
+ ) : (
769
+ /* Text input for string types */
770
+ <input
771
+ type="text"
772
+ value={state.value}
773
+ onChange={(e) => handleValueChange(param.name, e.target.value)}
774
+ placeholder={param.example || (param.default !== undefined ? String(param.default) : "")}
775
+ className="w-full border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
776
+ />
777
+ )}
778
+ </div>
779
+ </div>
780
+ );
781
+ })}
782
+ </div>
783
+ )}
784
+ </div>
785
+ );
786
+ }
787
+
491
788
  /**
492
789
  * Parameter Documentation Component
493
- * Displays request body and query parameter documentation in a collapsible panel.
790
+ * Displays request body documentation in a collapsible panel (for non-GET requests).
494
791
  */
495
792
  function ParameterDocs({
496
793
  requestParams,
497
- queryParams,
498
794
  isGetRequest,
499
795
  }: {
500
796
  requestParams?: ParameterDoc[];
501
- queryParams?: ParameterDoc[];
502
797
  isGetRequest: boolean;
503
798
  }) {
504
799
  const [isExpanded, setIsExpanded] = useState(true);
505
800
 
506
- const paramsToShow = isGetRequest ? queryParams : requestParams;
507
- if (!paramsToShow?.length) return null;
801
+ if (!requestParams?.length) return null;
508
802
 
509
803
  return (
510
804
  <div className="border-2 border-black dark:border-gray-600">
@@ -513,7 +807,7 @@ function ParameterDocs({
513
807
  className="flex w-full items-center justify-between bg-gray-50 px-3 py-2 text-left dark:bg-gray-800"
514
808
  >
515
809
  <span className="text-sm font-bold text-black dark:text-white">
516
- {isGetRequest ? "Query Parameters" : "Request Body"} Documentation
810
+ Request Body Documentation
517
811
  </span>
518
812
  <svg
519
813
  xmlns="http://www.w3.org/2000/svg"
@@ -533,7 +827,7 @@ function ParameterDocs({
533
827
 
534
828
  {isExpanded && (
535
829
  <div className="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
536
- {paramsToShow.map((param) => (
830
+ {requestParams.map((param) => (
537
831
  <div key={param.name} className="px-3 py-2">
538
832
  <div className="flex items-center gap-2">
539
833
  <code className="text-sm font-bold text-[#BA0C2F]">
@@ -574,6 +868,21 @@ function ParameterDocs({
574
868
  </code>
575
869
  </p>
576
870
  )}
871
+ {param.example && (
872
+ <p className="mt-1 text-xs text-gray-500">
873
+ Example:{" "}
874
+ <code className="text-gray-700 dark:text-gray-300">
875
+ {param.example}
876
+ </code>
877
+ </p>
878
+ )}
879
+ {(param.min !== undefined || param.max !== undefined) && (
880
+ <p className="mt-1 text-xs text-gray-500">
881
+ {param.min !== undefined && `Min: ${param.min}`}
882
+ {param.min !== undefined && param.max !== undefined && " · "}
883
+ {param.max !== undefined && `Max: ${param.max}`}
884
+ </p>
885
+ )}
577
886
  </div>
578
887
  ))}
579
888
  </div>