@theihtisham/devtools-with-cloud 1.0.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 (150) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -0
  4. package/docker-compose.yml +23 -0
  5. package/jest.config.js +7 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +22 -0
  8. package/package.json +82 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +105 -0
  11. package/prisma/seed.ts +211 -0
  12. package/src/app/(app)/ai/page.tsx +122 -0
  13. package/src/app/(app)/collections/page.tsx +155 -0
  14. package/src/app/(app)/environments/page.tsx +96 -0
  15. package/src/app/(app)/history/page.tsx +107 -0
  16. package/src/app/(app)/import/page.tsx +102 -0
  17. package/src/app/(app)/layout.tsx +60 -0
  18. package/src/app/(app)/settings/page.tsx +79 -0
  19. package/src/app/(app)/workspace/page.tsx +284 -0
  20. package/src/app/api/ai/discover/route.ts +17 -0
  21. package/src/app/api/ai/explain/route.ts +29 -0
  22. package/src/app/api/ai/generate-tests/route.ts +37 -0
  23. package/src/app/api/ai/suggest/route.ts +29 -0
  24. package/src/app/api/collections/[id]/route.ts +66 -0
  25. package/src/app/api/collections/route.ts +48 -0
  26. package/src/app/api/environments/route.ts +40 -0
  27. package/src/app/api/export/openapi/route.ts +17 -0
  28. package/src/app/api/export/postman/route.ts +18 -0
  29. package/src/app/api/import/curl/route.ts +18 -0
  30. package/src/app/api/import/har/route.ts +20 -0
  31. package/src/app/api/import/openapi/route.ts +21 -0
  32. package/src/app/api/import/postman/route.ts +21 -0
  33. package/src/app/api/proxy/route.ts +35 -0
  34. package/src/app/api/requests/[id]/execute/route.ts +85 -0
  35. package/src/app/api/requests/[id]/history/route.ts +23 -0
  36. package/src/app/api/requests/[id]/route.ts +66 -0
  37. package/src/app/api/requests/route.ts +49 -0
  38. package/src/app/api/workspaces/route.ts +38 -0
  39. package/src/app/globals.css +99 -0
  40. package/src/app/layout.tsx +24 -0
  41. package/src/app/page.tsx +182 -0
  42. package/src/components/ai/ai-panel.tsx +65 -0
  43. package/src/components/ai/code-explainer.tsx +51 -0
  44. package/src/components/ai/endpoint-discovery.tsx +62 -0
  45. package/src/components/ai/test-generator.tsx +49 -0
  46. package/src/components/collections/collection-actions.tsx +36 -0
  47. package/src/components/collections/collection-tree.tsx +55 -0
  48. package/src/components/collections/folder-creator.tsx +54 -0
  49. package/src/components/landing/comparison.tsx +43 -0
  50. package/src/components/landing/cta.tsx +16 -0
  51. package/src/components/landing/features.tsx +24 -0
  52. package/src/components/landing/hero.tsx +23 -0
  53. package/src/components/response/body-viewer.tsx +33 -0
  54. package/src/components/response/headers-viewer.tsx +23 -0
  55. package/src/components/response/status-badge.tsx +25 -0
  56. package/src/components/response/test-results.tsx +50 -0
  57. package/src/components/response/timing-chart.tsx +39 -0
  58. package/src/components/ui/badge.tsx +24 -0
  59. package/src/components/ui/button.tsx +32 -0
  60. package/src/components/ui/code-editor.tsx +51 -0
  61. package/src/components/ui/dialog.tsx +56 -0
  62. package/src/components/ui/dropdown.tsx +63 -0
  63. package/src/components/ui/input.tsx +22 -0
  64. package/src/components/ui/key-value-editor.tsx +75 -0
  65. package/src/components/ui/select.tsx +24 -0
  66. package/src/components/ui/tabs.tsx +85 -0
  67. package/src/components/ui/textarea.tsx +22 -0
  68. package/src/components/ui/toast.tsx +54 -0
  69. package/src/components/workspace/request-panel.tsx +38 -0
  70. package/src/components/workspace/response-panel.tsx +81 -0
  71. package/src/components/workspace/sidebar.tsx +52 -0
  72. package/src/components/workspace/split-pane.tsx +49 -0
  73. package/src/components/workspace/tabs/auth-tab.tsx +94 -0
  74. package/src/components/workspace/tabs/body-tab.tsx +41 -0
  75. package/src/components/workspace/tabs/headers-tab.tsx +23 -0
  76. package/src/components/workspace/tabs/params-tab.tsx +23 -0
  77. package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
  78. package/src/components/workspace/url-bar.tsx +53 -0
  79. package/src/hooks/use-ai.ts +115 -0
  80. package/src/hooks/use-collection.ts +71 -0
  81. package/src/hooks/use-environment.ts +73 -0
  82. package/src/hooks/use-request.ts +111 -0
  83. package/src/lib/ai/endpoint-discovery.ts +158 -0
  84. package/src/lib/ai/explainer.ts +127 -0
  85. package/src/lib/ai/suggester.ts +164 -0
  86. package/src/lib/ai/test-generator.ts +161 -0
  87. package/src/lib/auth/api-key.ts +28 -0
  88. package/src/lib/auth/aws-sig.ts +131 -0
  89. package/src/lib/auth/basic.ts +17 -0
  90. package/src/lib/auth/bearer.ts +15 -0
  91. package/src/lib/auth/oauth2.ts +155 -0
  92. package/src/lib/auth/types.ts +16 -0
  93. package/src/lib/db/client.ts +15 -0
  94. package/src/lib/env/manager.ts +32 -0
  95. package/src/lib/env/resolver.ts +30 -0
  96. package/src/lib/exporters/openapi.ts +193 -0
  97. package/src/lib/exporters/postman.ts +140 -0
  98. package/src/lib/graphql/builder.ts +249 -0
  99. package/src/lib/graphql/formatter.ts +147 -0
  100. package/src/lib/graphql/index.ts +43 -0
  101. package/src/lib/graphql/introspection.ts +175 -0
  102. package/src/lib/graphql/types.ts +99 -0
  103. package/src/lib/graphql/validator.ts +216 -0
  104. package/src/lib/http/client.ts +112 -0
  105. package/src/lib/http/proxy.ts +83 -0
  106. package/src/lib/http/request-builder.ts +214 -0
  107. package/src/lib/http/response-parser.ts +106 -0
  108. package/src/lib/http/timing.ts +63 -0
  109. package/src/lib/importers/curl-parser.ts +346 -0
  110. package/src/lib/importers/har-parser.ts +128 -0
  111. package/src/lib/importers/openapi.ts +324 -0
  112. package/src/lib/importers/postman.ts +312 -0
  113. package/src/lib/test-runner/assertions.ts +163 -0
  114. package/src/lib/test-runner/reporter.ts +90 -0
  115. package/src/lib/test-runner/runner.ts +69 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/cn.ts +6 -0
  118. package/src/lib/utils/content-type.ts +123 -0
  119. package/src/lib/utils/download.ts +53 -0
  120. package/src/lib/utils/errors.ts +92 -0
  121. package/src/lib/utils/format.ts +142 -0
  122. package/src/lib/utils/syntax-highlight.ts +108 -0
  123. package/src/lib/utils/validation.ts +231 -0
  124. package/src/lib/websocket/client.ts +182 -0
  125. package/src/lib/websocket/frames.ts +96 -0
  126. package/src/lib/websocket/history.ts +121 -0
  127. package/src/lib/websocket/index.ts +25 -0
  128. package/src/lib/websocket/types.ts +57 -0
  129. package/src/types/ai.ts +28 -0
  130. package/src/types/collection.ts +24 -0
  131. package/src/types/environment.ts +16 -0
  132. package/src/types/request.ts +54 -0
  133. package/src/types/response.ts +37 -0
  134. package/tailwind.config.ts +82 -0
  135. package/tests/lib/env/resolver.test.ts +108 -0
  136. package/tests/lib/graphql/builder.test.ts +349 -0
  137. package/tests/lib/graphql/formatter.test.ts +99 -0
  138. package/tests/lib/http/request-builder.test.ts +160 -0
  139. package/tests/lib/http/response-parser.test.ts +150 -0
  140. package/tests/lib/http/timing.test.ts +188 -0
  141. package/tests/lib/importers/curl-parser.test.ts +245 -0
  142. package/tests/lib/test-runner/assertions.test.ts +342 -0
  143. package/tests/lib/utils/cn.test.ts +46 -0
  144. package/tests/lib/utils/content-type.test.ts +175 -0
  145. package/tests/lib/utils/format.test.ts +188 -0
  146. package/tests/lib/utils/validation.test.ts +237 -0
  147. package/tests/lib/websocket/history.test.ts +186 -0
  148. package/tsconfig.json +29 -0
  149. package/tsconfig.tsbuildinfo +1 -0
  150. package/vitest.config.ts +21 -0
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { highlightJson, highlightXml, highlightHeaders } from '@/lib/utils/syntax-highlight';
4
+
5
+ interface BodyViewerProps {
6
+ body: string;
7
+ contentType: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function BodyViewer({ body, contentType, className }: BodyViewerProps) {
12
+ const highlighted = contentType.includes('json')
13
+ ? highlightJson(body)
14
+ : contentType.includes('xml')
15
+ ? highlightXml(body)
16
+ : escapeHtml(body);
17
+
18
+ return (
19
+ <div className={className}>
20
+ <pre
21
+ className="text-sm font-mono whitespace-pre-wrap break-all"
22
+ dangerouslySetInnerHTML={{ __html: highlighted }}
23
+ />
24
+ </div>
25
+ );
26
+ }
27
+
28
+ function escapeHtml(str: string): string {
29
+ return str
30
+ .replace(/&/g, '&amp;')
31
+ .replace(/</g, '&lt;')
32
+ .replace(/>/g, '&gt;');
33
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { highlightHeaders } from '@/lib/utils/syntax-highlight';
4
+
5
+ interface HeadersViewerProps {
6
+ headers: Record<string, string>;
7
+ className?: string;
8
+ }
9
+
10
+ export function HeadersViewer({ headers, className }: HeadersViewerProps) {
11
+ const headerStr = Object.entries(headers)
12
+ .map(([key, value]) => `${key}: ${value}`)
13
+ .join('\n');
14
+
15
+ return (
16
+ <div className={className}>
17
+ <pre
18
+ className="text-sm font-mono whitespace-pre-wrap"
19
+ dangerouslySetInnerHTML={{ __html: highlightHeaders(headerStr) }}
20
+ />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,25 @@
1
+ import { cn } from '@/lib/utils/cn';
2
+
3
+ interface StatusBadgeProps {
4
+ status: number;
5
+ statusText?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function StatusBadge({ status, statusText, className }: StatusBadgeProps) {
10
+ return (
11
+ <span
12
+ className={cn(
13
+ 'inline-flex items-center px-2 py-0.5 rounded text-sm font-mono font-medium',
14
+ status < 200 && 'bg-gray-500/10 text-gray-400',
15
+ status >= 200 && status < 300 && 'bg-green-500/10 text-green-400',
16
+ status >= 300 && status < 400 && 'bg-blue-500/10 text-blue-400',
17
+ status >= 400 && status < 500 && 'bg-orange-500/10 text-orange-400',
18
+ status >= 500 && 'bg-red-500/10 text-red-400',
19
+ className,
20
+ )}
21
+ >
22
+ {status} {statusText}
23
+ </span>
24
+ );
25
+ }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils/cn';
4
+
5
+ interface TestResult {
6
+ name: string;
7
+ passed: boolean;
8
+ error?: string;
9
+ }
10
+
11
+ interface TestResultsProps {
12
+ results: TestResult[];
13
+ className?: string;
14
+ }
15
+
16
+ export function TestResults({ results, className }: TestResultsProps) {
17
+ const passed = results.filter((r) => r.passed).length;
18
+ const total = results.length;
19
+
20
+ return (
21
+ <div className={className}>
22
+ <div className="flex items-center gap-2 mb-3">
23
+ <span className={cn(
24
+ 'text-sm font-medium',
25
+ passed === total ? 'text-green-400' : 'text-orange-400',
26
+ )}>
27
+ {passed}/{total} passed
28
+ </span>
29
+ </div>
30
+ <div className="space-y-1">
31
+ {results.map((result, i) => (
32
+ <div key={i} className="flex items-start gap-2 text-sm">
33
+ <span className={cn(
34
+ 'mt-0.5',
35
+ result.passed ? 'text-green-400' : 'text-red-400',
36
+ )}>
37
+ {result.passed ? 'PASS' : 'FAIL'}
38
+ </span>
39
+ <div>
40
+ <span className="text-foreground">{result.name}</span>
41
+ {result.error && (
42
+ <p className="text-xs text-destructive mt-0.5">{result.error}</p>
43
+ )}
44
+ </div>
45
+ </div>
46
+ ))}
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { getTimingBreakdown, type RequestTiming } from '@/lib/http/timing';
4
+
5
+ interface TimingChartProps {
6
+ timing: RequestTiming;
7
+ className?: string;
8
+ }
9
+
10
+ export function TimingChart({ timing, className }: TimingChartProps) {
11
+ const breakdown = getTimingBreakdown(timing);
12
+
13
+ return (
14
+ <div className={className}>
15
+ <div className="flex h-4 rounded overflow-hidden">
16
+ {breakdown.map((phase) => (
17
+ <div
18
+ key={phase.label}
19
+ style={{
20
+ width: `${Math.max(phase.percentage, 2)}%`,
21
+ backgroundColor: phase.color,
22
+ }}
23
+ className="transition-all"
24
+ title={`${phase.label}: ${phase.value}ms`}
25
+ />
26
+ ))}
27
+ </div>
28
+ <div className="flex flex-wrap gap-3 mt-2">
29
+ {breakdown.map((phase) => (
30
+ <div key={phase.label} className="flex items-center gap-1 text-xs">
31
+ <div className="w-2 h-2 rounded-full" style={{ backgroundColor: phase.color }} />
32
+ <span className="text-muted-foreground">{phase.label}:</span>
33
+ <span className="text-foreground">{phase.value}ms</span>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,24 @@
1
+ import { cn } from '@/lib/utils/cn';
2
+
3
+ interface BadgeProps {
4
+ children: React.ReactNode;
5
+ variant?: 'default' | 'secondary' | 'destructive' | 'outline';
6
+ className?: string;
7
+ }
8
+
9
+ export function Badge({ children, variant = 'default', className }: BadgeProps) {
10
+ return (
11
+ <span
12
+ className={cn(
13
+ 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
14
+ variant === 'default' && 'bg-primary/10 text-primary',
15
+ variant === 'secondary' && 'bg-secondary text-secondary-foreground',
16
+ variant === 'destructive' && 'bg-destructive/10 text-destructive',
17
+ variant === 'outline' && 'border border-border text-foreground',
18
+ className,
19
+ )}
20
+ >
21
+ {children}
22
+ </span>
23
+ );
24
+ }
@@ -0,0 +1,32 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '@/lib/utils/cn';
3
+
4
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: 'default' | 'destructive' | 'outline' | 'ghost';
6
+ size?: 'sm' | 'md' | 'lg';
7
+ }
8
+
9
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
10
+ ({ className, variant = 'default', size = 'md', ...props }, ref) => {
11
+ return (
12
+ <button
13
+ ref={ref}
14
+ className={cn(
15
+ 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none',
16
+ variant === 'default' && 'bg-primary text-primary-foreground hover:bg-primary/90',
17
+ variant === 'destructive' && 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
18
+ variant === 'outline' && 'border border-border bg-background text-foreground hover:bg-accent',
19
+ variant === 'ghost' && 'text-foreground hover:bg-accent',
20
+ size === 'sm' && 'h-8 px-3 text-xs',
21
+ size === 'md' && 'h-9 px-4 text-sm',
22
+ size === 'lg' && 'h-10 px-6 text-base',
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ );
28
+ },
29
+ );
30
+
31
+ Button.displayName = 'Button';
32
+ export { Button };
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface CodeEditorProps {
7
+ value: string;
8
+ onChange?: (value: string) => void;
9
+ language?: string;
10
+ readOnly?: boolean;
11
+ className?: string;
12
+ placeholder?: string;
13
+ }
14
+
15
+ /**
16
+ * Simple code editor component.
17
+ * Falls back to a plain textarea since Monaco Editor requires dynamic imports
18
+ * and complex setup that may not work during build.
19
+ */
20
+ export function CodeEditor({
21
+ value,
22
+ onChange,
23
+ language = 'json',
24
+ readOnly = false,
25
+ className,
26
+ placeholder,
27
+ }: CodeEditorProps) {
28
+ const handleChange = useCallback(
29
+ (e: React.ChangeEvent<HTMLTextAreaElement>) => {
30
+ onChange?.(e.target.value);
31
+ },
32
+ [onChange],
33
+ );
34
+
35
+ return (
36
+ <textarea
37
+ value={value}
38
+ onChange={handleChange}
39
+ readOnly={readOnly}
40
+ placeholder={placeholder}
41
+ className={cn(
42
+ 'w-full h-full min-h-[200px] px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-none',
43
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
44
+ readOnly && 'cursor-default bg-muted/30',
45
+ className,
46
+ )}
47
+ spellCheck={false}
48
+ data-language={language}
49
+ />
50
+ );
51
+ }
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface DialogProps {
7
+ open: boolean;
8
+ onClose: () => void;
9
+ children: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function Dialog({ open, onClose, children, className }: DialogProps) {
14
+ if (!open) return null;
15
+
16
+ return (
17
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
18
+ <div className="fixed inset-0 bg-black/50" onClick={onClose} />
19
+ <div className={cn('relative z-50 w-full max-w-lg rounded-lg border border-border bg-card p-6 shadow-lg', className)}>
20
+ {children}
21
+ </div>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ interface DialogHeaderProps {
27
+ children: ReactNode;
28
+ }
29
+
30
+ export function DialogHeader({ children }: DialogHeaderProps) {
31
+ return <div className="mb-4">{children}</div>;
32
+ }
33
+
34
+ interface DialogTitleProps {
35
+ children: ReactNode;
36
+ }
37
+
38
+ export function DialogTitle({ children }: DialogTitleProps) {
39
+ return <h2 className="text-lg font-semibold text-foreground">{children}</h2>;
40
+ }
41
+
42
+ interface DialogContentProps {
43
+ children: ReactNode;
44
+ }
45
+
46
+ export function DialogContent({ children }: DialogContentProps) {
47
+ return <div className="mb-4">{children}</div>;
48
+ }
49
+
50
+ interface DialogFooterProps {
51
+ children: ReactNode;
52
+ }
53
+
54
+ export function DialogFooter({ children }: DialogFooterProps) {
55
+ return <div className="flex justify-end gap-2">{children}</div>;
56
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, type ReactNode } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface DropdownProps {
7
+ trigger: ReactNode;
8
+ children: ReactNode;
9
+ className?: string;
10
+ align?: 'left' | 'right';
11
+ }
12
+
13
+ export function Dropdown({ trigger, children, className, align = 'left' }: DropdownProps) {
14
+ const [open, setOpen] = useState(false);
15
+ const ref = useRef<HTMLDivElement>(null);
16
+
17
+ useEffect(() => {
18
+ function handleClickOutside(event: MouseEvent) {
19
+ if (ref.current && !ref.current.contains(event.target as Node)) {
20
+ setOpen(false);
21
+ }
22
+ }
23
+ document.addEventListener('mousedown', handleClickOutside);
24
+ return () => document.removeEventListener('mousedown', handleClickOutside);
25
+ }, []);
26
+
27
+ return (
28
+ <div ref={ref} className="relative inline-block">
29
+ <div onClick={() => setOpen(!open)}>{trigger}</div>
30
+ {open && (
31
+ <div
32
+ className={cn(
33
+ 'absolute z-50 mt-1 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md',
34
+ align === 'right' ? 'right-0' : 'left-0',
35
+ className,
36
+ )}
37
+ >
38
+ <div onClick={() => setOpen(false)}>{children}</div>
39
+ </div>
40
+ )}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ interface DropdownItemProps {
46
+ children: ReactNode;
47
+ onClick?: () => void;
48
+ className?: string;
49
+ }
50
+
51
+ export function DropdownItem({ children, onClick, className }: DropdownItemProps) {
52
+ return (
53
+ <button
54
+ onClick={onClick}
55
+ className={cn(
56
+ 'flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-foreground hover:bg-accent transition-colors text-left',
57
+ className,
58
+ )}
59
+ >
60
+ {children}
61
+ </button>
62
+ );
63
+ }
@@ -0,0 +1,22 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '@/lib/utils/cn';
3
+
4
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
5
+
6
+ const Input = forwardRef<HTMLInputElement, InputProps>(
7
+ ({ className, ...props }, ref) => {
8
+ return (
9
+ <input
10
+ ref={ref}
11
+ className={cn(
12
+ 'flex h-9 w-full rounded-md border border-border bg-background px-3 py-1 text-sm text-foreground shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ },
19
+ );
20
+
21
+ Input.displayName = 'Input';
22
+ export { Input };
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils/cn';
4
+ import type { KeyValue } from '@/lib/utils/validation';
5
+
6
+ interface KeyValueEditorProps {
7
+ pairs: KeyValue[];
8
+ onChange: (pairs: KeyValue[]) => void;
9
+ keyPlaceholder?: string;
10
+ valuePlaceholder?: string;
11
+ className?: string;
12
+ }
13
+
14
+ export function KeyValueEditor({
15
+ pairs,
16
+ onChange,
17
+ keyPlaceholder = 'Key',
18
+ valuePlaceholder = 'Value',
19
+ className,
20
+ }: KeyValueEditorProps) {
21
+ const addPair = () => {
22
+ onChange([...pairs, { key: '', value: '', enabled: true }]);
23
+ };
24
+
25
+ const removePair = (index: number) => {
26
+ onChange(pairs.filter((_, i) => i !== index));
27
+ };
28
+
29
+ const updatePair = (index: number, field: keyof KeyValue, value: string | boolean) => {
30
+ const updated = [...pairs];
31
+ updated[index] = { ...updated[index]!, [field]: value };
32
+ onChange(updated);
33
+ };
34
+
35
+ return (
36
+ <div className={cn('space-y-2', className)}>
37
+ {pairs.map((pair, i) => (
38
+ <div key={i} className="flex items-center gap-2">
39
+ <input
40
+ type="checkbox"
41
+ checked={pair.enabled}
42
+ onChange={(e) => updatePair(i, 'enabled', e.target.checked)}
43
+ className="h-4 w-4 rounded border-border"
44
+ />
45
+ <input
46
+ type="text"
47
+ value={pair.key}
48
+ onChange={(e) => updatePair(i, 'key', e.target.value)}
49
+ placeholder={keyPlaceholder}
50
+ className="flex-1 px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm font-mono"
51
+ />
52
+ <input
53
+ type="text"
54
+ value={pair.value}
55
+ onChange={(e) => updatePair(i, 'value', e.target.value)}
56
+ placeholder={valuePlaceholder}
57
+ className="flex-1 px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm font-mono"
58
+ />
59
+ <button
60
+ onClick={() => removePair(i)}
61
+ className="text-muted-foreground hover:text-destructive transition-colors text-sm"
62
+ >
63
+ x
64
+ </button>
65
+ </div>
66
+ ))}
67
+ <button
68
+ onClick={addPair}
69
+ className="text-sm text-primary hover:text-primary/80 transition-colors"
70
+ >
71
+ + Add
72
+ </button>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,24 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '@/lib/utils/cn';
3
+
4
+ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
5
+
6
+ const Select = forwardRef<HTMLSelectElement, SelectProps>(
7
+ ({ className, children, ...props }, ref) => {
8
+ return (
9
+ <select
10
+ ref={ref}
11
+ className={cn(
12
+ 'flex h-9 w-full rounded-md border border-border bg-background px-3 py-1 text-sm text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
13
+ className,
14
+ )}
15
+ {...props}
16
+ >
17
+ {children}
18
+ </select>
19
+ );
20
+ },
21
+ );
22
+
23
+ Select.displayName = 'Select';
24
+ export { Select };
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useState, type ReactNode } from 'react';
4
+ import { cn } from '@/lib/utils/cn';
5
+
6
+ interface TabsContextValue {
7
+ activeTab: string;
8
+ setActiveTab: (tab: string) => void;
9
+ }
10
+
11
+ const TabsContext = createContext<TabsContextValue | null>(null);
12
+
13
+ function useTabs(): TabsContextValue {
14
+ const ctx = useContext(TabsContext);
15
+ if (!ctx) throw new Error('Tabs components must be used within a Tabs provider');
16
+ return ctx;
17
+ }
18
+
19
+ interface TabsProps {
20
+ defaultValue: string;
21
+ children: ReactNode;
22
+ className?: string;
23
+ }
24
+
25
+ export function Tabs({ defaultValue, children, className }: TabsProps) {
26
+ const [activeTab, setActiveTab] = useState(defaultValue);
27
+
28
+ return (
29
+ <TabsContext.Provider value={{ activeTab, setActiveTab }}>
30
+ <div className={cn('', className)}>{children}</div>
31
+ </TabsContext.Provider>
32
+ );
33
+ }
34
+
35
+ interface TabsListProps {
36
+ children: ReactNode;
37
+ className?: string;
38
+ }
39
+
40
+ export function TabsList({ children, className }: TabsListProps) {
41
+ return (
42
+ <div className={cn('flex border-b border-border', className)}>
43
+ {children}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ interface TabsTriggerProps {
49
+ value: string;
50
+ children: ReactNode;
51
+ className?: string;
52
+ }
53
+
54
+ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
55
+ const { activeTab, setActiveTab } = useTabs();
56
+
57
+ return (
58
+ <button
59
+ onClick={() => setActiveTab(value)}
60
+ className={cn(
61
+ 'px-4 py-2 text-sm transition-colors',
62
+ activeTab === value
63
+ ? 'text-primary border-b-2 border-primary'
64
+ : 'text-muted-foreground hover:text-foreground',
65
+ className,
66
+ )}
67
+ >
68
+ {children}
69
+ </button>
70
+ );
71
+ }
72
+
73
+ interface TabsContentProps {
74
+ value: string;
75
+ children: ReactNode;
76
+ className?: string;
77
+ }
78
+
79
+ export function TabsContent({ value, children, className }: TabsContentProps) {
80
+ const { activeTab } = useTabs();
81
+
82
+ if (activeTab !== value) return null;
83
+
84
+ return <div className={cn('p-4', className)}>{children}</div>;
85
+ }
@@ -0,0 +1,22 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '@/lib/utils/cn';
3
+
4
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
5
+
6
+ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
7
+ ({ className, ...props }, ref) => {
8
+ return (
9
+ <textarea
10
+ ref={ref}
11
+ className={cn(
12
+ 'flex min-h-[60px] w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ },
19
+ );
20
+
21
+ Textarea.displayName = 'Textarea';
22
+ export { Textarea };