+
+
+
+ {
+ updateRequest({ ...request, name: event.target.value });
+ markTabDirty(activeTab.id, true);
+ }}
+ placeholder="Request name"
+ />
+
+ handleUrlChange(event.target.value)}
+ list="url-history"
+ placeholder="{{baseUrl}}/users"
+ />
+
+
+
+
+
+
+
+ Resolved URL: {resolvedUrl}
+
+ Web requests are subject to CORS. Use desktop/agent proxy for unrestricted access.
+
+
+
+
+ );
+};
diff --git a/src/components/request/auth/AuthEditor.tsx b/src/components/request/auth/AuthEditor.tsx
new file mode 100644
index 0000000..9e69266
--- /dev/null
+++ b/src/components/request/auth/AuthEditor.tsx
@@ -0,0 +1,87 @@
+import { RequestAuth } from "../../../types";
+import { Input } from "../../ui/input";
+import { Select } from "../../ui/select";
+
+export const AuthEditor = ({
+ auth,
+ onChange,
+}: {
+ auth: RequestAuth;
+ onChange: (auth: RequestAuth) => void;
+}) => {
+ return (
+
+
+
+
+ {auth.type === "bearer" && (
+
onChange({ ...auth, token: event.target.value })}
+ />
+ )}
+ {auth.type === "basic" && (
+
+ onChange({ ...auth, username: event.target.value })}
+ />
+ onChange({ ...auth, password: event.target.value })}
+ />
+
+ )}
+ {auth.type === "apiKey" && (
+
+ )}
+ {auth.type === "none" && (
+
No authentication will be attached.
+ )}
+
+ );
+};
diff --git a/src/components/request/body/BodyEditor.tsx b/src/components/request/body/BodyEditor.tsx
new file mode 100644
index 0000000..229d6c3
--- /dev/null
+++ b/src/components/request/body/BodyEditor.tsx
@@ -0,0 +1,108 @@
+import { FileText, FileUp, Brackets } from "lucide-react";
+import { RequestBody } from "../../../types";
+import { Tabs, Tab } from "../../ui/tabs";
+import { KeyValueTable } from "../KeyValueTable";
+import { Textarea } from "../../ui/textarea";
+import { Select } from "../../ui/select";
+import { Button } from "../../ui/button";
+
+const bodyTabs = ["none", "form-data", "x-www-form-urlencoded", "raw"] as const;
+
+export const BodyEditor = ({
+ body,
+ onChange,
+}: {
+ body: RequestBody;
+ onChange: (body: RequestBody) => void;
+}) => {
+ const currentTab = body.type;
+
+ const handleTabChange = (next: (typeof bodyTabs)[number]) => {
+ if (next === "none") onChange({ type: "none" });
+ if (next === "form-data") onChange({ type: "form-data", rows: [] });
+ if (next === "x-www-form-urlencoded") onChange({ type: "x-www-form-urlencoded", rows: [] });
+ if (next === "raw") onChange({ type: "raw", raw: { format: "json", value: "" } });
+ };
+
+ return (
+
+
+ {bodyTabs.map((tab) => (
+ handleTabChange(tab)}>
+ {tab}
+
+ ))}
+
+ {body.type === "none" && (
+
+ No request body will be sent.
+
+ )}
+ {body.type === "form-data" && (
+
+
Supports text + file placeholders.
+
onChange({ type: "form-data", rows })}
+ />
+
+ )}
+ {body.type === "x-www-form-urlencoded" && (
+
onChange({ type: "x-www-form-urlencoded", rows })}
+ />
+ )}
+ {body.type === "raw" && (
+
+
+
+
+
+ Raw body
+ Upload (placeholder)
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/request/scripts/ScriptEditor.tsx b/src/components/request/scripts/ScriptEditor.tsx
new file mode 100644
index 0000000..181d032
--- /dev/null
+++ b/src/components/request/scripts/ScriptEditor.tsx
@@ -0,0 +1,30 @@
+import { Textarea } from "../../ui/textarea";
+
+export const ScriptEditor = ({
+ label,
+ description,
+ value,
+ onChange,
+}: {
+ label: string;
+ description: string;
+ value: string;
+ onChange: (value: string) => void;
+}) => (
+
+
+
+
+
+
+
+ {mode === "preview" ? (
+
+
+
+ ) : (
+
+ {renderedChunks.join("")}
+
+ )}
+ {chunks.length > chunkCount && (
+
+ )}
+
+ );
+};
diff --git a/src/components/response/ResponseHeaders.tsx b/src/components/response/ResponseHeaders.tsx
new file mode 100644
index 0000000..2d278bb
--- /dev/null
+++ b/src/components/response/ResponseHeaders.tsx
@@ -0,0 +1,31 @@
+import { Copy } from "lucide-react";
+import { Button } from "../ui/button";
+import { useToast } from "../ui/toast";
+
+export const ResponseHeaders = ({ headers }: { headers: Record