Yes — you’re connecting two ideas:
Browse files1. WebContainer API → run and preview code directly in the browser.
2. Spaces (like Hugging Face Spaces) → publish/share the project so others can use it.
That flow is totally logical:
---
🔄 Workflow: From Local Run → Published Space
<LinearProcessFlow steps={[ "⚡ Boot WebContainer runtime in the browser", "📂 Load project files into the container (FS API)", "📦 Install deps (npm / yarn / pnpm)", "▶️ Start dev/build server in-browser", "🌍 Save and publish to a Space on the Hub", "🖼️ Import and use Space directly in your UI" ]} />
---
✅ Why this works
WebContainer gives you a runtime (sandboxed Node.js) to build/preview your app.
Spaces give you persistence & hosting so your preview can be shared, reused, or embedded.
By publishing as a Space, you create a versioned, public (or private) endpoint. - Initial Deployment
- README.md +7 -5
- index.html +395 -18
- prompts.txt +378 -0
|
@@ -1,10 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: webcontainer-preview
|
| 3 |
+
emoji: 🐳
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
@@ -1,19 +1,396 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>WebContainer Live Preview</title>
|
| 7 |
+
<link rel="icon" type="image/x-icon" href="data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
| 10 |
+
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* Custom scrollbar */
|
| 14 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 15 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 16 |
+
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
|
| 17 |
+
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
| 18 |
+
/* Animated pulse for live dot */
|
| 19 |
+
@keyframes pulse-dot {
|
| 20 |
+
0%,100%{transform:scale(1);opacity:1;}
|
| 21 |
+
50%{transform:scale(1.4);opacity:.8;}
|
| 22 |
+
}
|
| 23 |
+
.pulse-dot{animation:pulse-dot 1.5s infinite;}
|
| 24 |
+
</style>
|
| 25 |
+
</head>
|
| 26 |
+
<body class="bg-gray-50 text-gray-900 font-sans antialiased">
|
| 27 |
+
|
| 28 |
+
<!-- Header -->
|
| 29 |
+
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
| 30 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 31 |
+
<div class="flex items-center justify-between h-14">
|
| 32 |
+
<div class="flex items-center gap-2">
|
| 33 |
+
<i data-feather="box" class="w-5 h-5 text-blue-600"></i>
|
| 34 |
+
<h1 class="text-lg font-semibold">WebContainer Preview</h1>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="flex items-center gap-3">
|
| 37 |
+
<button id="publishBtn" class="hidden text-sm bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded-md transition items-center gap-2">
|
| 38 |
+
<i data-feather="upload-cloud" class="w-4 h-4"></i><span>Publish to Space</span>
|
| 39 |
+
</button>
|
| 40 |
+
<div class="flex items-center gap-2 text-sm text-gray-500">
|
| 41 |
+
<span id="status">Offline</span>
|
| 42 |
+
<span class="w-2 h-2 bg-gray-300 rounded-full" id="status-dot"></span>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</header>
|
| 48 |
+
|
| 49 |
+
<!-- Main -->
|
| 50 |
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
| 51 |
+
<section class="grid grid-cols-1 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-9 gap-4">
|
| 52 |
+
|
| 53 |
+
<!-- Controls -->
|
| 54 |
+
<aside class="col-span-full md:col-span-6 lg:col-span-8 xl:col-span-3 flex flex-col gap-4" data-aos="fade-right">
|
| 55 |
+
<!-- Upload -->
|
| 56 |
+
<div class="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
|
| 57 |
+
<h2 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
| 58 |
+
<i data-feather="upload" class="w-4 h-4 text-indigo-600"></i>Upload Project ZIP
|
| 59 |
+
</h2>
|
| 60 |
+
<label class="group relative flex flex-col items-center justify-center w-full h-28 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-indigo-400 hover:bg-indigo-50 transition">
|
| 61 |
+
<i data-feather="folder-plus" class="w-8 h-8 text-gray-400 group-hover:text-indigo-600"></i>
|
| 62 |
+
<span class="mt-2 text-sm text-gray-500 group-hover:text-indigo-600">Click or drop ZIP here</span>
|
| 63 |
+
<input id="zipInput" type="file" accept=".zip" class="absolute opacity-0 w-full h-full">
|
| 64 |
+
</label>
|
| 65 |
+
<p id="fileName" class="mt-2 text-xs text-gray-400 truncate"></p>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<!-- Actions -->
|
| 69 |
+
<div class="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
|
| 70 |
+
<h2 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
| 71 |
+
<i data-feather="play" class="w-4 h-4 text-green-600"></i>Actions
|
| 72 |
+
</h2>
|
| 73 |
+
<div class="grid grid-cols-2 gap-2">
|
| 74 |
+
<button id="bootBtn" class="flex items-center justify-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition">
|
| 75 |
+
<i data-feather="zap"></i><span>Boot</span>
|
| 76 |
+
</button>
|
| 77 |
+
<button id="stopBtn" class="flex items-center justify-center gap-2 px-3 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 text-sm rounded-lg transition">
|
| 78 |
+
<i data-feather="square"></i><span>Stop</span>
|
| 79 |
+
</button>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<!-- Terminal -->
|
| 84 |
+
<div class="bg-gray-900 rounded-xl border border-gray-700 p-4 shadow-sm flex-1 flex flex-col">
|
| 85 |
+
<div class="flex items-center justify-between mb-2">
|
| 86 |
+
<h2 class="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
| 87 |
+
<i data-feather="terminal" class="w-4 h-4 text-green-400"></i>Terminal
|
| 88 |
+
</h2>
|
| 89 |
+
<button id="clearTerm" class="text-xs text-gray-400 hover:text-white transition">
|
| 90 |
+
Clear
|
| 91 |
+
</button>
|
| 92 |
+
</div>
|
| 93 |
+
<pre id="terminal" class="flex-1 text-xs text-green-400 overflow-auto whitespace-pre-wrap"></pre>
|
| 94 |
+
</div>
|
| 95 |
+
</aside>
|
| 96 |
+
|
| 97 |
+
<!-- Preview -->
|
| 98 |
+
<div id="previewWrapper" class="col-span-full md:col-span-6 lg:col-span-8 xl:col-span-6 h-[calc(70vh-53px)] lg:h-[calc(100vh-54px)] bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col" data-aos="fade-left">
|
| 99 |
+
<!-- Preview Header -->
|
| 100 |
+
<div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
| 101 |
+
<div class="flex items-center gap-2">
|
| 102 |
+
<i data-feather="eye" class="w-4 h-4 text-gray-500"></i>
|
| 103 |
+
<span class="text-sm font-medium text-gray-700">Preview</span>
|
| 104 |
+
<span id="previewHost" class="text-xs text-gray-500 font-mono hidden"></span>
|
| 105 |
+
</div>
|
| 106 |
+
<div class="flex items-center gap-1">
|
| 107 |
+
<button id="refreshBtn" class="p-1.5 hover:bg-gray-200 rounded-md transition disabled:opacity-50" title="Refresh">
|
| 108 |
+
<i data-feather="refresh-cw" class="w-4 h-4 text-gray-600"></i>
|
| 109 |
+
</button>
|
| 110 |
+
<button id="openBtn" class="p-1.5 hover:bg-gray-200 rounded-md transition disabled:opacity-50" title="Open in new tab">
|
| 111 |
+
<i data-feather="external-link" class="w-4 h-4 text-gray-600"></i>
|
| 112 |
+
</button>
|
| 113 |
+
<button id="restartBtn" class="p-1.5 hover:bg-gray-200 rounded-md transition disabled:opacity-50" title="Restart container">
|
| 114 |
+
<i data-feather="rotate-ccw" class="w-4 h-4 text-gray-600"></i>
|
| 115 |
+
</button>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- Preview Body -->
|
| 120 |
+
<div class="flex-1 relative">
|
| 121 |
+
<!-- Loading -->
|
| 122 |
+
<div id="loader" class="absolute inset-0 flex items-center justify-center bg-white z-10">
|
| 123 |
+
<div class="text-center">
|
| 124 |
+
<div class="w-8 h-8 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mx-auto mb-3"></div>
|
| 125 |
+
<div class="text-sm text-gray-600 font-medium">Booting WebContainer…</div>
|
| 126 |
+
<div class="text-xs text-gray-400 mt-1">This may take a moment</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<!-- Error -->
|
| 131 |
+
<div id="error" class="absolute inset-0 hidden items-center justify-center bg-white z-10">
|
| 132 |
+
<div class="text-center max-w-md">
|
| 133 |
+
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 134 |
+
<i data-feather="alert-triangle" class="w-6 h-6 text-red-600"></i>
|
| 135 |
+
</div>
|
| 136 |
+
<h3 class="text-lg font-medium text-gray-900 mb-2">Container Error</h3>
|
| 137 |
+
<p id="errorText" class="text-sm text-red-600 mb-4"></p>
|
| 138 |
+
<button id="retryBtn" class="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition">
|
| 139 |
+
<i data-feather="rotate-ccw" class="w-4 h-4 mr-2"></i>Restart Container
|
| 140 |
+
</button>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<!-- Iframe -->
|
| 145 |
+
<iframe id="preview" class="w-full h-full border-0" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>
|
| 146 |
+
|
| 147 |
+
<!-- Live indicator -->
|
| 148 |
+
<div id="liveIndicator" class="absolute bottom-2 left-2 hidden items-center gap-2 bg-green-50 border border-green-200 rounded-full px-2 py-1">
|
| 149 |
+
<span class="w-2 h-2 bg-green-500 rounded-full pulse-dot"></span>
|
| 150 |
+
<span class="text-xs text-green-700 font-medium">Live</span>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</section>
|
| 155 |
+
</main>
|
| 156 |
+
|
| 157 |
+
<script type="module">
|
| 158 |
+
/* ---------- Feather Icons ---------- */
|
| 159 |
+
feather.replace();
|
| 160 |
+
|
| 161 |
+
/* ---------- Elements ---------- */
|
| 162 |
+
const zipInput = document.getElementById('zipInput');
|
| 163 |
+
const fileName = document.getElementById('fileName');
|
| 164 |
+
const bootBtn = document.getElementById('bootBtn');
|
| 165 |
+
const stopBtn = document.getElementById('stopBtn');
|
| 166 |
+
const clearTerm = document.getElementById('clearTerm');
|
| 167 |
+
const terminal = document.getElementById('terminal');
|
| 168 |
+
const status = document.getElementById('status');
|
| 169 |
+
const statusDot = document.getElementById('status-dot');
|
| 170 |
+
const loader = document.getElementById('loader');
|
| 171 |
+
const error = document.getElementById('error');
|
| 172 |
+
const errorText = document.getElementById('errorText');
|
| 173 |
+
const retryBtn = document.getElementById('retryBtn');
|
| 174 |
+
const refreshBtn = document.getElementById('refreshBtn');
|
| 175 |
+
const openBtn = document.getElementById('openBtn');
|
| 176 |
+
const restartBtn = document.getElementById('restartBtn');
|
| 177 |
+
const preview = document.getElementById('preview');
|
| 178 |
+
const previewHost = document.getElementById('previewHost');
|
| 179 |
+
const liveIndicator = document.getElementById('liveIndicator');
|
| 180 |
+
const publishBtn = document.getElementById('publishBtn');
|
| 181 |
+
|
| 182 |
+
let webcontainer = null;
|
| 183 |
+
let url = '';
|
| 184 |
+
|
| 185 |
+
/* ---------- Util ---------- */
|
| 186 |
+
const log = (msg, type = 'info') => {
|
| 187 |
+
const stamp = `[${new Date().toLocaleTimeString()}]`;
|
| 188 |
+
const color = type === 'error' ? 'text-red-400' : type === 'warn' ? 'text-yellow-400' : 'text-green-400';
|
| 189 |
+
terminal.insertAdjacentHTML('beforeend', `<div><span class="${color}">${stamp}</span> ${msg}</div>`);
|
| 190 |
+
terminal.scrollTop = terminal.scrollHeight;
|
| 191 |
+
};
|
| 192 |
+
|
| 193 |
+
const setStatus = (state) => {
|
| 194 |
+
const labels = { offline: 'Offline', booting: 'Booting', online: 'Online', error: 'Error' };
|
| 195 |
+
const colors = {
|
| 196 |
+
offline: 'bg-gray-300',
|
| 197 |
+
booting: 'bg-yellow-400',
|
| 198 |
+
online: 'bg-green-500',
|
| 199 |
+
error: 'bg-red-500'
|
| 200 |
+
};
|
| 201 |
+
status.textContent = labels[state];
|
| 202 |
+
statusDot.className = `w-2 h-2 rounded-full ${colors[state]}`;
|
| 203 |
+
};
|
| 204 |
+
|
| 205 |
+
const showLoader = () => {
|
| 206 |
+
loader.classList.remove('hidden');
|
| 207 |
+
loader.classList.add('flex');
|
| 208 |
+
error.classList.add('hidden');
|
| 209 |
+
error.classList.remove('flex');
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
const hideLoader = () => {
|
| 213 |
+
loader.classList.add('hidden');
|
| 214 |
+
loader.classList.remove('flex');
|
| 215 |
+
};
|
| 216 |
+
|
| 217 |
+
const showError = (msg) => {
|
| 218 |
+
hideLoader();
|
| 219 |
+
errorText.textContent = msg;
|
| 220 |
+
error.classList.remove('hidden');
|
| 221 |
+
error.classList.add('flex');
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
const hideError = () => {
|
| 225 |
+
error.classList.add('hidden');
|
| 226 |
+
error.classList.remove('flex');
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
const setLive = (u) => {
|
| 230 |
+
url = u;
|
| 231 |
+
preview.src = url;
|
| 232 |
+
previewHost.textContent = new URL(url).host;
|
| 233 |
+
previewHost.classList.remove('hidden');
|
| 234 |
+
liveIndicator.classList.remove('hidden');
|
| 235 |
+
setStatus('online');
|
| 236 |
+
publishBtn.classList.remove('hidden');
|
| 237 |
+
publishBtn.classList.add('inline-flex');
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
/* ---------- File Input ---------- */
|
| 241 |
+
zipInput.addEventListener('change', (e) => {
|
| 242 |
+
const f = e.target.files[0];
|
| 243 |
+
if (f) fileName.textContent = f.name;
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
/* ---------- Terminal Clear ---------- */
|
| 247 |
+
clearTerm.addEventListener('click', () => terminal.innerHTML = '');
|
| 248 |
+
|
| 249 |
+
/* ---------- Boot ---------- */
|
| 250 |
+
bootBtn.addEventListener('click', async () => {
|
| 251 |
+
if (!zipInput.files.length) return alert('Please select a ZIP file');
|
| 252 |
+
showLoader();
|
| 253 |
+
hideError();
|
| 254 |
+
setStatus('booting');
|
| 255 |
+
log('Booting WebContainer…');
|
| 256 |
+
|
| 257 |
+
try {
|
| 258 |
+
const { WebContainer } = await import('@webcontainer/api');
|
| 259 |
+
webcontainer = await WebContainer.boot();
|
| 260 |
+
log('WebContainer booted');
|
| 261 |
+
|
| 262 |
+
const zip = zipInput.files[0];
|
| 263 |
+
const zipBuffer = await zip.arrayBuffer();
|
| 264 |
+
const zipArray = new Uint8Array(zipBuffer);
|
| 265 |
+
const unzip = (await import('unzipit')).unzip;
|
| 266 |
+
const { entries } = await unzip(zipArray);
|
| 267 |
+
|
| 268 |
+
const files = {};
|
| 269 |
+
for (const [path, entry] of Object.entries(entries)) {
|
| 270 |
+
if (!entry.isFile) continue;
|
| 271 |
+
const blob = await entry.blob();
|
| 272 |
+
files[path] = { file: { blob } };
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
await webcontainer.mount(files);
|
| 276 |
+
log('Files mounted');
|
| 277 |
+
|
| 278 |
+
// Default package.json
|
| 279 |
+
const defaultPackageJson = {
|
| 280 |
+
name: 'preview-app',
|
| 281 |
+
type: 'module',
|
| 282 |
+
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
|
| 283 |
+
devDependencies: { vite: '^5.0.0' }
|
| 284 |
+
};
|
| 285 |
+
if (!files['package.json']) {
|
| 286 |
+
await webcontainer.fs.writeFile('package.json', JSON.stringify(defaultPackageJson, null, 2));
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
log('Installing dependencies…');
|
| 290 |
+
const install = await webcontainer.spawn('npm', ['install']);
|
| 291 |
+
await install.exit;
|
| 292 |
+
log('Dependencies installed');
|
| 293 |
+
|
| 294 |
+
log('Starting dev server…');
|
| 295 |
+
await webcontainer.spawn('npm', ['run', 'dev']);
|
| 296 |
+
|
| 297 |
+
webcontainer.on('server-ready', (port, u) => {
|
| 298 |
+
hideLoader();
|
| 299 |
+
setLive(u);
|
| 300 |
+
log(`Server ready at ${u}`);
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
} catch (err) {
|
| 304 |
+
log(err.message, 'error');
|
| 305 |
+
setStatus('error');
|
| 306 |
+
showError(err.message);
|
| 307 |
+
}
|
| 308 |
+
});
|
| 309 |
+
|
| 310 |
+
/* ---------- Stop ---------- */
|
| 311 |
+
stopBtn.addEventListener('click', async () => {
|
| 312 |
+
if (!webcontainer) return;
|
| 313 |
+
try {
|
| 314 |
+
await webcontainer.spawn('pkill', ['-f', 'node']);
|
| 315 |
+
webcontainer = null;
|
| 316 |
+
url = '';
|
| 317 |
+
preview.src = '';
|
| 318 |
+
previewHost.textContent = '';
|
| 319 |
+
previewHost.classList.add('hidden');
|
| 320 |
+
liveIndicator.classList.add('hidden');
|
| 321 |
+
setStatus('offline');
|
| 322 |
+
log('Container stopped');
|
| 323 |
+
} catch (e) {
|
| 324 |
+
log(e.message, 'error');
|
| 325 |
+
}
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
+
/* ---------- Refresh ---------- */
|
| 329 |
+
refreshBtn.addEventListener('click', () => {
|
| 330 |
+
if (!url) return;
|
| 331 |
+
preview.src = '';
|
| 332 |
+
setTimeout(() => preview.src = url, 100);
|
| 333 |
+
log('Preview refreshed');
|
| 334 |
+
});
|
| 335 |
+
|
| 336 |
+
/* ---------- Open in new tab ---------- */
|
| 337 |
+
openBtn.addEventListener('click', () => {
|
| 338 |
+
if (url) window.open(url, '_blank');
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
/* ---------- Restart container ---------- */
|
| 342 |
+
restartBtn.addEventListener('click', async () => {
|
| 343 |
+
if (!webcontainer) return;
|
| 344 |
+
showLoader();
|
| 345 |
+
try {
|
| 346 |
+
await webcontainer.spawn('pkill', ['-f', 'node']);
|
| 347 |
+
await bootBtn.click();
|
| 348 |
+
} catch (e) {
|
| 349 |
+
log(e.message, 'error');
|
| 350 |
+
showError(e.message);
|
| 351 |
+
}
|
| 352 |
+
});
|
| 353 |
+
|
| 354 |
+
/* ---------- Retry ---------- */
|
| 355 |
+
retryBtn.addEventListener('click', () => bootBtn.click());
|
| 356 |
+
|
| 357 |
+
/* ---------- Publish to Space ---------- */
|
| 358 |
+
publishBtn.addEventListener('click', async () => {
|
| 359 |
+
if (!url) return;
|
| 360 |
+
publishBtn.disabled = true;
|
| 361 |
+
publishBtn.innerHTML = '<i data-feather="loader" class="w-4 h-4 animate-spin mr-2"></i>Publishing…';
|
| 362 |
+
feather.replace();
|
| 363 |
+
|
| 364 |
+
try {
|
| 365 |
+
// Create project snapshot
|
| 366 |
+
const snapshot = {
|
| 367 |
+
name: 'webcontainer-preview',
|
| 368 |
+
files: {}, // Populate from mounted files if needed
|
| 369 |
+
url: url,
|
| 370 |
+
timestamp: new Date().toISOString()
|
| 371 |
+
};
|
| 372 |
+
|
| 373 |
+
// Export snapshot as JSON file
|
| 374 |
+
const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: 'application/json' });
|
| 375 |
+
const a = document.createElement('a');
|
| 376 |
+
a.href = URL.createObjectURL(blob);
|
| 377 |
+
a.download = 'space-export.json';
|
| 378 |
+
a.click();
|
| 379 |
+
|
| 380 |
+
// Provide instructions for Spaces upload
|
| 381 |
+
alert('Snapshot downloaded! Upload the JSON file to Hugging Face Spaces to publish your preview.');
|
| 382 |
+
log('Published snapshot for Spaces');
|
| 383 |
+
} catch (e) {
|
| 384 |
+
log(e.message, 'error');
|
| 385 |
+
} finally {
|
| 386 |
+
publishBtn.disabled = false;
|
| 387 |
+
publishBtn.innerHTML = '<i data-feather="upload-cloud" class="w-4 h-4 mr-2"></i>Publish to Space';
|
| 388 |
+
feather.replace();
|
| 389 |
+
}
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
/* ---------- Init status ---------- */
|
| 393 |
+
setStatus('offline');
|
| 394 |
+
</script>
|
| 395 |
+
</body>
|
| 396 |
</html>
|
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import classNames from "classnames";
|
| 2 |
+
import { useRef, useEffect, useState, forwardRef } from "react";
|
| 3 |
+
import { TbReload, TbLoader, TbExternalLink } from "react-icons/tb";
|
| 4 |
+
import { WebContainer } from '@webcontainer/api';
|
| 5 |
+
|
| 6 |
+
// PreviewEye icon component
|
| 7 |
+
const PreviewEye = ({ className = "w-4 h-4" }: { className?: string }) => (
|
| 8 |
+
<svg
|
| 9 |
+
className={className}
|
| 10 |
+
viewBox="0 0 24 24"
|
| 11 |
+
fill="none"
|
| 12 |
+
stroke="currentColor"
|
| 13 |
+
strokeWidth={2}
|
| 14 |
+
strokeLinecap="round"
|
| 15 |
+
strokeLinejoin="round"
|
| 16 |
+
>
|
| 17 |
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
| 18 |
+
<circle cx="12" cy="12" r="3" />
|
| 19 |
+
</svg>
|
| 20 |
+
);
|
| 21 |
+
|
| 22 |
+
// Tooltip component
|
| 23 |
+
const Tooltip = ({
|
| 24 |
+
children,
|
| 25 |
+
content,
|
| 26 |
+
position = "top"
|
| 27 |
+
}: {
|
| 28 |
+
children: React.ReactNode;
|
| 29 |
+
content: string;
|
| 30 |
+
position?: "top" | "bottom" | "left" | "right"
|
| 31 |
+
}) => {
|
| 32 |
+
const [isVisible, setIsVisible] = useState(false);
|
| 33 |
+
|
| 34 |
+
const positionClasses = {
|
| 35 |
+
top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
|
| 36 |
+
bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
|
| 37 |
+
left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
|
| 38 |
+
right: "left-full top-1/2 transform -translate-y-1/2 ml-2"
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<div
|
| 43 |
+
className="relative inline-block"
|
| 44 |
+
onMouseEnter={() => setIsVisible(true)}
|
| 45 |
+
onMouseLeave={() => setIsVisible(false)}
|
| 46 |
+
>
|
| 47 |
+
{children}
|
| 48 |
+
{isVisible && (
|
| 49 |
+
<div className={classNames(
|
| 50 |
+
"absolute z-50 px-2 py-1 text-xs text-white bg-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none",
|
| 51 |
+
positionClasses[position]
|
| 52 |
+
)}>
|
| 53 |
+
{content}
|
| 54 |
+
<div className={classNames(
|
| 55 |
+
"absolute w-1 h-1 bg-gray-900 transform rotate-45",
|
| 56 |
+
position === "top" && "top-full left-1/2 -translate-x-1/2 -mt-0.5",
|
| 57 |
+
position === "bottom" && "bottom-full left-1/2 -translate-x-1/2 -mb-0.5",
|
| 58 |
+
position === "left" && "left-full top-1/2 -translate-y-1/2 -ml-0.5",
|
| 59 |
+
position === "right" && "right-full top-1/2 -translate-y-1/2 -mr-0.5"
|
| 60 |
+
)} />
|
| 61 |
+
</div>
|
| 62 |
+
)}
|
| 63 |
+
</div>
|
| 64 |
+
);
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
type PreviewProps = {
|
| 68 |
+
files: Record<string, any>;
|
| 69 |
+
isResizing: boolean;
|
| 70 |
+
isAiWorking: boolean;
|
| 71 |
+
packageJson?: any;
|
| 72 |
+
className?: string;
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
const Preview = forwardRef<HTMLDivElement, PreviewProps>(
|
| 76 |
+
({ files, isResizing, isAiWorking, packageJson, className }, ref) => {
|
| 77 |
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
| 78 |
+
const [webcontainer, setWebcontainer] = useState<WebContainer | null>(null);
|
| 79 |
+
const [url, setUrl] = useState<string>('');
|
| 80 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 81 |
+
const [error, setError] = useState<string>('');
|
| 82 |
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 83 |
+
|
| 84 |
+
// Initialize WebContainer
|
| 85 |
+
useEffect(() => {
|
| 86 |
+
const initWebContainer = async () => {
|
| 87 |
+
try {
|
| 88 |
+
const container = await WebContainer.boot();
|
| 89 |
+
setWebcontainer(container);
|
| 90 |
+
} catch (err) {
|
| 91 |
+
setError('Failed to initialize WebContainer');
|
| 92 |
+
console.error('WebContainer init error:', err);
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
initWebContainer();
|
| 97 |
+
}, []);
|
| 98 |
+
|
| 99 |
+
// Mount files and start dev server
|
| 100 |
+
useEffect(() => {
|
| 101 |
+
if (!webcontainer || !files) return;
|
| 102 |
+
|
| 103 |
+
const setupProject = async () => {
|
| 104 |
+
setIsLoading(true);
|
| 105 |
+
setError('');
|
| 106 |
+
|
| 107 |
+
try {
|
| 108 |
+
await webcontainer.mount(files);
|
| 109 |
+
|
| 110 |
+
const defaultPackageJson = {
|
| 111 |
+
name: 'preview-app',
|
| 112 |
+
type: 'module',
|
| 113 |
+
scripts: {
|
| 114 |
+
dev: 'vite',
|
| 115 |
+
build: 'vite build',
|
| 116 |
+
preview: 'vite preview'
|
| 117 |
+
},
|
| 118 |
+
devDependencies: {
|
| 119 |
+
vite: '^5.0.0'
|
| 120 |
+
},
|
| 121 |
+
...packageJson
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
if (!files['package.json']) {
|
| 125 |
+
await webcontainer.fs.writeFile(
|
| 126 |
+
'package.json',
|
| 127 |
+
JSON.stringify(defaultPackageJson, null, 2)
|
| 128 |
+
);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
const installProcess = await webcontainer.spawn('npm', ['install']);
|
| 132 |
+
const installExitCode = await installProcess.exit;
|
| 133 |
+
|
| 134 |
+
if (installExitCode !== 0) {
|
| 135 |
+
throw new Error('Failed to install dependencies');
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const serverProcess = await webcontainer.spawn('npm', ['run', 'dev']);
|
| 139 |
+
|
| 140 |
+
webcontainer.on('server-ready', (port, url) => {
|
| 141 |
+
setUrl(url);
|
| 142 |
+
setIsLoading(false);
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
serverProcess.output.pipeTo(
|
| 146 |
+
new WritableStream({
|
| 147 |
+
write(data) {
|
| 148 |
+
console.log('[Server]', data);
|
| 149 |
+
},
|
| 150 |
+
})
|
| 151 |
+
);
|
| 152 |
+
|
| 153 |
+
} catch (err) {
|
| 154 |
+
setError(err instanceof Error ? err.message : 'Setup failed');
|
| 155 |
+
setIsLoading(false);
|
| 156 |
+
console.error('Setup error:', err);
|
| 157 |
+
}
|
| 158 |
+
};
|
| 159 |
+
|
| 160 |
+
setupProject();
|
| 161 |
+
}, [webcontainer, files, packageJson]);
|
| 162 |
+
|
| 163 |
+
const handleRefresh = async () => {
|
| 164 |
+
if (!webcontainer || !url) return;
|
| 165 |
+
|
| 166 |
+
setIsLoading(true);
|
| 167 |
+
try {
|
| 168 |
+
await webcontainer.spawn('npm', ['run', 'dev']);
|
| 169 |
+
|
| 170 |
+
if (iframeRef.current) {
|
| 171 |
+
const iframe = iframeRef.current;
|
| 172 |
+
iframe.src = '';
|
| 173 |
+
setTimeout(() => {
|
| 174 |
+
iframe.src = url;
|
| 175 |
+
}, 100);
|
| 176 |
+
}
|
| 177 |
+
} catch (err) {
|
| 178 |
+
console.error('Refresh error:', err);
|
| 179 |
+
} finally {
|
| 180 |
+
setIsLoading(false);
|
| 181 |
+
}
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
const handleRestartContainer = async () => {
|
| 185 |
+
if (!webcontainer) return;
|
| 186 |
+
|
| 187 |
+
setIsLoading(true);
|
| 188 |
+
setUrl('');
|
| 189 |
+
|
| 190 |
+
try {
|
| 191 |
+
await webcontainer.spawn('pkill', ['-f', 'node']);
|
| 192 |
+
await webcontainer.mount(files);
|
| 193 |
+
const serverProcess = await webcontainer.spawn('npm', ['run', 'dev']);
|
| 194 |
+
|
| 195 |
+
webcontainer.on('server-ready', (port, url) => {
|
| 196 |
+
setUrl(url);
|
| 197 |
+
setIsLoading(false);
|
| 198 |
+
});
|
| 199 |
+
} catch (err) {
|
| 200 |
+
setError('Failed to restart container');
|
| 201 |
+
setIsLoading(false);
|
| 202 |
+
console.error('Restart error:', err);
|
| 203 |
+
}
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
const openInNewTab = () => {
|
| 207 |
+
if (url) {
|
| 208 |
+
window.open(url, '_blank');
|
| 209 |
+
}
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
return (
|
| 213 |
+
<div
|
| 214 |
+
ref={ref}
|
| 215 |
+
className={classNames(
|
| 216 |
+
// Vercel-style grid system
|
| 217 |
+
"w-full relative",
|
| 218 |
+
// Base responsive grid
|
| 219 |
+
"col-span-full", // Full width on mobile
|
| 220 |
+
"md:col-span-6", // Half width on medium screens
|
| 221 |
+
"lg:col-span-8", // Larger portion on desktop
|
| 222 |
+
"xl:col-span-9", // Even larger on xl screens
|
| 223 |
+
// Height system
|
| 224 |
+
"h-[calc(70dvh-53px)]",
|
| 225 |
+
"lg:h-[calc(100dvh-54px)]",
|
| 226 |
+
// Borders and styling
|
| 227 |
+
"border border-gray-200",
|
| 228 |
+
"rounded-lg overflow-hidden",
|
| 229 |
+
"bg-white shadow-sm",
|
| 230 |
+
className
|
| 231 |
+
)}
|
| 232 |
+
>
|
| 233 |
+
{/* Header bar - Vercel style */}
|
| 234 |
+
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
| 235 |
+
<div className="flex items-center gap-2">
|
| 236 |
+
<PreviewEye className="w-4 h-4 text-gray-500" />
|
| 237 |
+
<span className="text-sm font-medium text-gray-700">Preview</span>
|
| 238 |
+
{url && (
|
| 239 |
+
<span className="text-xs text-gray-500 font-mono">
|
| 240 |
+
{new URL(url).host}
|
| 241 |
+
</span>
|
| 242 |
+
)}
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div className="flex items-center gap-1">
|
| 246 |
+
<Tooltip content="Refresh preview">
|
| 247 |
+
<button
|
| 248 |
+
className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
|
| 249 |
+
onClick={handleRefresh}
|
| 250 |
+
disabled={!url || isLoading}
|
| 251 |
+
>
|
| 252 |
+
<TbReload
|
| 253 |
+
className={classNames("w-4 h-4 text-gray-600", {
|
| 254 |
+
"animate-spin": isLoading
|
| 255 |
+
})}
|
| 256 |
+
/>
|
| 257 |
+
</button>
|
| 258 |
+
</Tooltip>
|
| 259 |
+
|
| 260 |
+
<Tooltip content="Open in new tab">
|
| 261 |
+
<button
|
| 262 |
+
className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
|
| 263 |
+
onClick={openInNewTab}
|
| 264 |
+
disabled={!url}
|
| 265 |
+
>
|
| 266 |
+
<TbExternalLink className="w-4 h-4 text-gray-600" />
|
| 267 |
+
</button>
|
| 268 |
+
</Tooltip>
|
| 269 |
+
|
| 270 |
+
<Tooltip content="Restart container">
|
| 271 |
+
<button
|
| 272 |
+
className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
|
| 273 |
+
onClick={handleRestartContainer}
|
| 274 |
+
disabled={!webcontainer || isLoading}
|
| 275 |
+
>
|
| 276 |
+
<TbLoader
|
| 277 |
+
className={classNames("w-4 h-4 text-gray-600", {
|
| 278 |
+
"animate-spin": isLoading
|
| 279 |
+
})}
|
| 280 |
+
/>
|
| 281 |
+
</button>
|
| 282 |
+
</Tooltip>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
|
| 286 |
+
{/* Content area */}
|
| 287 |
+
<div className="flex-1 relative h-[calc(100%-49px)]">
|
| 288 |
+
{error ? (
|
| 289 |
+
<div className="flex items-center justify-center h-full p-8">
|
| 290 |
+
<div className="text-center max-w-md">
|
| 291 |
+
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 292 |
+
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 293 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
| 294 |
+
</svg>
|
| 295 |
+
</div>
|
| 296 |
+
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
| 297 |
+
Container Error
|
| 298 |
+
</h3>
|
| 299 |
+
<p className="text-sm text-red-600 mb-4">{error}</p>
|
| 300 |
+
<button
|
| 301 |
+
onClick={handleRestartContainer}
|
| 302 |
+
className="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition-colors"
|
| 303 |
+
>
|
| 304 |
+
<TbReload className="w-4 h-4 mr-2" />
|
| 305 |
+
Restart Container
|
| 306 |
+
</button>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
) : !url || isLoading ? (
|
| 310 |
+
<div className="flex items-center justify-center h-full">
|
| 311 |
+
<div className="text-center">
|
| 312 |
+
<div className="w-8 h-8 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mx-auto mb-4"></div>
|
| 313 |
+
<div className="text-sm text-gray-600 font-medium">
|
| 314 |
+
{!webcontainer
|
| 315 |
+
? 'Booting WebContainer...'
|
| 316 |
+
: 'Setting up project...'}
|
| 317 |
+
</div>
|
| 318 |
+
<div className="text-xs text-gray-400 mt-1">
|
| 319 |
+
This may take a moment
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
) : (
|
| 324 |
+
<iframe
|
| 325 |
+
ref={iframeRef}
|
| 326 |
+
title="WebContainer Preview"
|
| 327 |
+
src={url}
|
| 328 |
+
className={classNames("w-full h-full border-0", {
|
| 329 |
+
"pointer-events-none": isResizing || isAiWorking,
|
| 330 |
+
})}
|
| 331 |
+
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
| 332 |
+
/>
|
| 333 |
+
)}
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
+
{/* Status indicator */}
|
| 337 |
+
{url && (
|
| 338 |
+
<div className="absolute bottom-2 left-2 flex items-center gap-2 bg-green-50 border border-green-200 rounded-full px-2 py-1">
|
| 339 |
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
| 340 |
+
<span className="text-xs text-green-700 font-medium">Live</span>
|
| 341 |
+
</div>
|
| 342 |
+
)}
|
| 343 |
+
</div>
|
| 344 |
+
);
|
| 345 |
+
}
|
| 346 |
+
);
|
| 347 |
+
|
| 348 |
+
Preview.displayName = "Preview";
|
| 349 |
+
|
| 350 |
+
export default Preview;
|
| 351 |
+
Yes — you’re connecting two ideas:
|
| 352 |
+
|
| 353 |
+
1. WebContainer API → run and preview code directly in the browser.
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
2. Spaces (like Hugging Face Spaces) → publish/share the project so others can use it.
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
That flow is totally logical:
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
---
|
| 364 |
+
|
| 365 |
+
🔄 Workflow: From Local Run → Published Space
|
| 366 |
+
|
| 367 |
+
<LinearProcessFlow steps={[ "⚡ Boot WebContainer runtime in the browser", "📂 Load project files into the container (FS API)", "📦 Install deps (npm / yarn / pnpm)", "▶️ Start dev/build server in-browser", "🌍 Save and publish to a Space on the Hub", "🖼️ Import and use Space directly in your UI" ]} />
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
---
|
| 371 |
+
|
| 372 |
+
✅ Why this works
|
| 373 |
+
|
| 374 |
+
WebContainer gives you a runtime (sandboxed Node.js) to build/preview your app.
|
| 375 |
+
|
| 376 |
+
Spaces give you persistence & hosting so your preview can be shared, reused, or embedded.
|
| 377 |
+
|
| 378 |
+
By publishing as a Space, you create a versioned, public (or private) endpoint.
|