+ {/* Deploy gate warnings */}
+ {deployWarning && (
+
+
+
+ {deployWarning}
+
+
+ )}
+
+ {/* Upload info note */}
+ {bytecodeSource === "upload" && bytecode && (
+
+
+
+ Uploaded bytecode will be deployed as-is via Revive. If this is EVM
+ bytecode (not PVM), the dry-run will fail.
+
+
+ )}
+
+ {/* Unverified ABI warning */}
+ {bytecodeSource === "upload" && abi && (
+
+
+
+ Uploaded ABI is not verified against the bytecode. Ensure they are
+ from the same contract.
+
+
+ )}
+
+ {/* Constructor */}
+
+
+ Constructor
+
+ {noAbiMode ? (
+
+
+
+ {hexDataError && (
+
{hexDataError}
+ )}
+
+ ) : (
+ <>
+
+ {needsHexFallback && (
+
+
+
+ {hexDataError && (
+
{hexDataError}
+ )}
+
+ )}
+ >
+ )}
+
+
+ {/* Config */}
+
+
+ Config
+
+
+
+ setValueInput(e.target.value)}
+ className="font-mono text-xs h-8"
+ placeholder="0"
+ />
+
+
+
+ setSaltInput(e.target.value)}
+ className="font-mono text-xs h-8"
+ placeholder="0x... or leave empty"
+ />
+
+
+
+ {/* Gas estimation */}
+
+
+
+ {gasEstimation.weightRequired && (
+
+
Weight: {formatWeight(gasEstimation.weightRequired)}
+ {gasEstimation.storageDeposit && (
+
+ Storage ({gasEstimation.storageDeposit.type}):{" "}
+ {formatFee(
+ gasEstimation.storageDeposit.value,
+ symbol,
+ decimals
+ )}
+
+ )}
+ {gasEstimation.gasConsumed !== null && (
+
Gas consumed: {gasEstimation.gasConsumed.toString()}
+ )}
+
+ )}
+
+ {gasEstimation.error && (
+
{gasEstimation.error}
+ )}
+
+
+ {/* Deploy */}
+
+
+ {/* Transaction log */}
+
+
+ );
+}
diff --git a/components/studio/editor-area.tsx b/components/studio/editor-area.tsx
new file mode 100644
index 0000000..d9b9d11
--- /dev/null
+++ b/components/studio/editor-area.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import React from "react";
+import dynamic from "next/dynamic";
+import { useTheme } from "next-themes";
+import { useStudio } from "@/context/studio-provider";
+import {
+ ResizablePanel,
+ ResizablePanelGroup,
+ ResizableHandle,
+} from "@/components/ui/resizable";
+import { EditorTabs } from "./editor-tabs";
+import { OutputPanel } from "./output-panel";
+
+const MonacoEditor = dynamic(() => import("@monaco-editor/react"), {
+ ssr: false,
+});
+
+export function EditorArea() {
+ const { resolvedTheme } = useTheme();
+ const { state, dispatch, activeFile } = useStudio();
+
+ const handleEditorChange = (value: string | undefined) => {
+ if (!activeFile) return;
+ dispatch({
+ type: "UPDATE_FILE_CONTENT",
+ fileId: activeFile.id,
+ content: value || "",
+ });
+ };
+
+ return (
+
+ {state.openTabs.map((tab) => {
+ const file = state.files[tab.fileId];
+ if (!file) return null;
+ const isActive = state.activeTabId === tab.fileId;
+
+ return (
+
dispatch({ type: "SET_ACTIVE_TAB", fileId: tab.fileId })}
+ >
+ {file.name}
+ {isDirtySinceCompile && (
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/components/studio/file-explorer.tsx b/components/studio/file-explorer.tsx
new file mode 100644
index 0000000..18ba964
--- /dev/null
+++ b/components/studio/file-explorer.tsx
@@ -0,0 +1,163 @@
+"use client";
+
+import React, { useState, useRef } from "react";
+import { useStudio } from "@/context/studio-provider";
+import { FileTreeItem } from "./file-tree-item";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Search, Plus, Upload, Info } from "lucide-react";
+
+export function FileExplorer() {
+ const { state, dispatch } = useStudio();
+ const [searchQuery, setSearchQuery] = useState("");
+ const fileInputRef = useRef