@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.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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, '&')
|
|
31
|
+
.replace(/</g, '<')
|
|
32
|
+
.replace(/>/g, '>');
|
|
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 };
|