LiteBlackEagle/OpenCoding
1
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>LBE OpenCode</title>7 <script src="https://cdn.tailwindcss.com"></script>8 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />9 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>10 <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>11 <link rel="preconnect" href="https://fonts.googleapis.com">12 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>13 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Inconsolata:wght@400;600&display=swap" rel="stylesheet">14 <style>15 :root {16 --bg-primary: #18181b;17 --bg-secondary: #27272a;18 --bg-tertiary: #3f3f46;19 --border-color: rgba(255,255,255,0.15);20 --text-primary: #f4f4f5;21 --text-secondary: #a1a1aa;22 --accent-green: #2dd4bf;23 --accent-green-light: rgba(45,212,191,0.1);24 --error-color: #f87171;25 }26 body {27 font-family: 'Inter', sans-serif;28 background-color: var(--bg-primary);29 color: var(--text-primary);30 overflow: hidden;31 -webkit-font-smoothing: antialiased;32 -moz-osx-font-smoothing: grayscale;33 }34 body.is-resizing {35 cursor: ew-resize !important;36 user-select: none !important;37 }38 body.is-resizing * {39 transition: none !important;40 pointer-events: none !important;41 }42 ::-webkit-scrollbar {43 width: 8px;44 height: 8px;45 }46 ::-webkit-scrollbar-track {47 background: var(--bg-primary);48 }49 ::-webkit-scrollbar-thumb {50 background: var(--bg-tertiary);51 border-radius: 4px;52 }53 ::-webkit-scrollbar-thumb:hover {54 background: var(--accent-green);55 }56 .ace_editor {57 font-family: 'Inconsolata', monospace !important;58 font-size: 15px !important;59 line-height: 1.6 !important;60 }61 .ace-tomorrow_night {62 background-color: var(--bg-secondary) !important;63 color: var(--text-primary) !important;64 }65 .ace_gutter {66 background: var(--bg-primary) !important;67 color: var(--text-secondary) !important;68 }69 .ace-tomorrow_night .ace_cursor {70 border-left: 2px solid var(--accent-green) !important;71 }72 .ace-tomorrow_night .ace_marker-layer .ace_selection {73 background: rgba(45,212,191,0.25);74 }75 .ace-tomorrow_night .ace_active-line {76 background-color: rgba(255,255,255,0.04);77 }78 .ace-tomorrow_night .ace_gutter-active-line {79 background-color: var(--accent-green-light);80 }81 .ace_print-margin {82 display: none;83 }84 .ui-panel {85 background-color: rgba(39,39,42,0.8);86 border: 1px solid var(--border-color);87 backdrop-filter: blur(8px);88 }89 .tab-button {90 background-color: rgba(255,255,255,0.05);91 border: 1px solid transparent;92 transition: all .2s ease-in-out;93 }94 .tab-button:hover {95 background-color: rgba(255,255,255,0.1);96 border-color: var(--border-color);97 }98 .tab-button.active,99 .tab-button.glow {100 color: var(--accent-green);101 border-color: var(--accent-green);102 background-color: var(--accent-green-light);103 box-shadow: 0 0 8px rgba(45,212,191,0.3);104 }105 .list-item.active {106 border-color: var(--accent-green) !important;107 background-color: var(--accent-green-light);108 }109 .list-item:hover {110 border-color: rgba(45,212,191,0.5);111 }112 .history-entry.active {113 background-color: rgba(45,212,191,0.15);114 border-color: var(--accent-green) !important;115 }116 [x-cloak] {117 display: none !important;118 }119 #divider {120 z-index: 20;121 cursor: ew-resize;122 transition: all 0.2s ease;123 }124 #divider::before {125 content: '';126 position: absolute;127 top: 50%;128 left: 50%;129 transform: translate(-50%, -50%);130 width: 20px;131 height: 20px;132 background: radial-gradient(circle, rgba(45,212,191,0.15) 0%, transparent 70%);133 border-radius: 50%;134 opacity: 0;135 pointer-events: none;136 }137 #divider:hover::before,138 body.is-resizing #divider::before {139 opacity: 1;140 }141 #divider:hover .divider-line,142 body.is-resizing .divider-line {143 background-color: var(--accent-green) !important;144 }145 </style>146</head>147<body class="h-screen w-screen text-sm p-2.5" x-data="app()" x-init="init()" x-cloak>148 <div class="h-full w-full flex flex-col gap-2.5">149 <main class="flex-1 flex overflow-hidden">150 <aside class="ui-panel flex-shrink-0 flex flex-col rounded-lg transition-all duration-300 ease-in-out" :class="{'w-64 md:w-72 mr-2.5': sbVis, 'w-0': !sbVis, 'p-0 border-0': !sbVis}">151 <div class="flex-1 p-2 space-y-2 overflow-y-auto" :class="{'hidden': !sbVis}">152 <template x-if="loading">153 <div class="flex flex-col items-center justify-center h-full text-text-secondary">154 <div class="w-8 h-8 border-2 border-dashed rounded-full animate-spin border-accent-green"></div>155 <p class="mt-4 text-xs">Loading projects...</p>156 </div>157 </template>158 <template x-if="!loading && p.length === 0">159 <div class="text-center text-text-secondary p-4">Start typing in the editor to create a new project.</div>160 </template>161 <template x-for="item in p" :key="item.id">162 <div class="list-item flex flex-col rounded-md border border-transparent transition-colors duration-200" :class="{'active': item.id === sId}">163 <div class="flex items-center p-3 cursor-pointer gap-3" @click="selP(item.id)">164 <div class="flex-1 overflow-hidden">165 <template x-if="eId === item.id">166 <input class="w-full bg-bg-primary border border-accent-green rounded px-2 py-1 text-base" :value="item.name" @click.stop @blur="endEdit(item.id, $event.target.value)" @keydown.enter="$event.target.blur()" x-init="$nextTick(() => $el.focus())">167 </template>168 <template x-if="eId !== item.id">169 <div>170 <h3 class="font-bold truncate" @dblclick.stop="startEdit(item.id)" x-text="item.name"></h3>171 <p class="text-xs text-text-secondary truncate opacity-75" x-text="`${new Date(item.createdAt).toLocaleString()}`"></p>172 <p class="text-xs text-text-secondary truncate mt-1" x-text="getPrev(item.id)"></p>173 </div>174 </template>175 </div>176 <div class="flex items-center gap-2 text-text-secondary flex-shrink-0">177 <button title="History" @click.stop="togH(item.id)" class="w-7 h-7 hover:text-accent-green"><i class="fas fa-history"></i></button>178 <button title="Delete" @click.stop="delP(item.id)" class="w-7 h-7 hover:text-error-color"><i class="fas fa-trash-alt"></i></button>179 </div>180 </div>181 <div x-show="expHId === item.id" class="max-h-48 overflow-y-auto border-t border-border-color bg-black/20" x-transition>182 <template x-if="!h[item.id] || h[item.id].history.length === 0">183 <div class="p-3 text-center text-xs text-text-secondary">No history found.</div>184 </template>185 <template x-for="(entry, index) in [...(h[item.id]?.history || [])].reverse()" :key="entry.ts">186 <div class="history-entry flex justify-between items-center p-2 border-b border-border-color/50 last:border-b-0 hover:bg-black/20" :title="new Date(entry.ts).toLocaleString()" :class="{'active': h[item.id].selIdx === (h[item.id].history.length - 1 - index)}">187 <div class="text-xs overflow-hidden">188 <div class="font-semibold" x-text="new Date(entry.ts).toLocaleString()"></div>189 <div class="text-text-secondary truncate" x-text="entry.c.substring(0,30) + '...'"></div>190 </div>191 <button @click.stop="loadH(item.id, h[item.id].history.length - 1 - index)" class="text-xs px-2 py-1 rounded bg-bg-tertiary hover:bg-accent-green flex-shrink-0 ml-2">Load</button>192 </div>193 </template>194 </div>195 </div>196 </template>197 </div>198 <div class="flex-shrink-0 p-3 border-t border-border-color" :class="{'hidden': !sbVis}">199 <button @click="newP()" title="New Project" class="w-full flex items-center justify-center gap-2 h-10 rounded-md border text-accent-green border-accent-green bg-accent-green-light hover:bg-opacity-20 transition-all duration-200">200 <i class="fas fa-plus"></i>201 <span class="font-semibold">New Project</span>202 </button>203 </div>204 </aside>205 <div x-ref="edWrap" class="flex-1 relative flex rounded-lg">206 <div id="ed-cont" class="relative h-full bg-bg-secondary transition-all duration-300 ease-in-out" :style="{ width: codeVis ? `${edW * 100}%` : '0%', display: codeVis ? 'block' : 'none' }">207 <div id="code-ed" class="h-full w-full rounded-l-lg"></div>208 </div>209 <div id="divider"210 class="absolute top-0 h-full w-2.5 cursor-ew-resize"211 :style="{ left: `calc(${edW * 100}% - 5px)` }"212 x-show="codeVis"213 @pointerdown="startDrag($event)">214 <div class="divider-line h-full w-0.5 bg-border-color mx-auto transition-colors duration-200"></div>215 </div>216 <div id="prev-cont" class="h-full bg-white transition-all duration-300 ease-in-out" :class="{'rounded-r-lg': codeVis, 'rounded-lg': !codeVis}" :style="{ width: codeVis ? `${(1 - edW) * 100}%` : '100%' }">217 <iframe id="prev-frame" class="w-full h-full border-none" :class="{'rounded-r-lg': codeVis, 'rounded-lg': !codeVis}"></iframe>218 </div>219 </div>220 </main>221 <footer class="ui-panel h-12 flex-shrink-0 flex items-center justify-between rounded-lg px-3">222 <div class="flex items-center gap-1">223 <button @click="togSb()" class="tab-button p-2 rounded-md" :class="{'glow': sbVis}" title="Toggle Menu"><i class="fas fa-bars"></i></button>224 <button @click="togCode()" class="tab-button p-2 rounded-md" :class="{'active': codeVis}" title="Toggle Code Editor"><i class="fas fa-code"></i></button>225 </div>226 <div class="flex items-center gap-2">227 <button @click="logVis = !logVis" class="tab-button relative px-2.5 py-1.5 rounded-md" :class="{'glow': logVis}" title="View Console Logs">228 <i class="fas fa-bug mr-1.5"></i>229 <span x-show="errCt > 0" x-transition.scale class="absolute -top-1 -right-1 bg-error-color text-white text-[9px] w-4 h-4 rounded-full flex items-center justify-center font-bold border-1 border-bg-tertiary" x-text="errCt > 9 ? '9+' : errCt"></span>230 </button>231 </div>232 </footer>233 </div>234 <div x-show="logVis"235 x-transition:enter="transition ease-out duration-200"236 x-transition:enter-start="opacity-0 translate-y-4"237 x-transition:enter-end="opacity-100 translate-y-0"238 x-transition:leave="transition ease-in duration-150"239 x-transition:leave-start="opacity-100 translate-y-0"240 x-transition:leave-end="opacity-0 translate-y-4"241 class="fixed bottom-20 right-4 z-[9999] w-full max-w-lg ui-panel rounded-lg shadow-2xl">242 <div class="flex items-center justify-between p-3 border-b border-border-color">243 <h3 class="font-bold text-text-primary">Console Logs</h3>244 <button @click="logVis = false" class="text-text-secondary hover:text-white transition-all"><i class="fas fa-times"></i></button>245 </div>246 <div class="p-3 max-h-80 overflow-y-auto text-xs space-y-3">247 <template x-for="lg in logs.slice().reverse()" :key="lg.ts">248 <div class="border-l-4 pl-3" :class="lg.t === 'error' ? 'border-error-color' : 'border-accent-green'">249 <div class="flex justify-between items-start">250 <div class="font-bold break-words" :class="lg.t === 'error' ? 'text-error-color' : 'text-accent-green'" x-text="lg.m"></div>251 <div class="text-xs text-text-secondary whitespace-nowrap ml-4" x-text="new Date(lg.ts).toLocaleTimeString()"></div>252 </div>253 <div class="text-xs text-text-secondary mt-1" x-text="`Source: ${lg.s}`"></div>254 <template x-if="lg.stk">255 <pre class="mt-2 p-2 bg-black/30 rounded-md text-text-secondary text-[11px] leading-relaxed whitespace-pre-wrap break-all" x-text="lg.stk"></pre>256 </template>257 </div>258 </template>259 <template x-if="logs.length === 0">260 <div class="text-text-secondary text-center py-4">No logs yet. Errors from your code will appear here.</div>261 </template>262 </div>263 </div>264 <script>265 function app() {266 return {267 p: [],268 h: {},269 sId: null,270 eId: null,271 expHId: null,272 loading: true,273 sbVis: true,274 codeVis: true,275 logVis: false,276 edW: 0.5,277 logs: [],278 errCt: 0,279 ed: null,280 prevT: null,281 saveT: null,282 K: {283 PREV_D: 300,284 HIST_D: 1500,285 MAX_H: 50,286 ITEMS: 'LBE_OC_p',287 HISTS: 'LBE_OC_h',288 LAST: 'LBE_OC_last',289 EDW: 'LBE_OC_edW',290 SBV: 'LBE_OC_sbv',291 CDV: 'LBE_OC_cdv'292 },293 init() {294 this.loadUi();295 this.setupErr();296 this.initEd();297 this.load();298 this.$watch('logVis', v => { if (v) this.errCt = 0 });299 this.$watch('sbVis', v => {300 localStorage.setItem(this.K.SBV, JSON.stringify(v));301 this.$nextTick(() => this.ed?.resize());302 });303 this.$watch('codeVis', v => {304 localStorage.setItem(this.K.CDV, JSON.stringify(v));305 this.$nextTick(() => this.ed?.resize());306 });307 },308 loadUi() {309 this.edW = parseFloat(localStorage.getItem(this.K.EDW) || 0.5);310 this.sbVis = JSON.parse(localStorage.getItem(this.K.SBV) ?? (window.innerWidth > 768));311 this.codeVis = JSON.parse(localStorage.getItem(this.K.CDV) ?? true);312 },313 setupErr() {314 window.onerror = (m, s, l, c, e) => {315 this.log(m, `${s.split('/').pop()}:${l}:${c}`, 'error', e);316 return true;317 };318 window.addEventListener('unhandledrejection', e => {319 const r = e.reason || 'Unhandled promise rejection';320 this.log(r.message || r, 'Promise', 'error', r);321 e.preventDefault();322 });323 window.addEventListener('beforeunload', () => this.saveOnExit());324 window.addEventListener('message', e => {325 if (e.data?.type === 'iframeError') {326 this.log(e.data.payload.message, 'iframe', 'error', e.data.payload);327 }328 });329 },330 initEd() {331 this.ed = ace.edit("code-ed");332 this.ed.setTheme("ace/theme/tomorrow_night");333 this.ed.session.setMode("ace/mode/html");334 this.ed.setShowPrintMargin(false);335 this.ed.setOptions({336 enableBasicAutocompletion: true,337 enableLiveAutocompletion: true,338 tabSize: 2,339 useSoftTabs: true,340 wrap: true341 });342 this.ed.on('change', () => this.onEdChg());343 },344 onEdChg() {345 const c = this.ed.getValue();346 if (!this.sId && c.trim()) {347 const i = `p_${Date.now()}`;348 this.p.unshift({349 id: i,350 name: `Project ${this.p.length + 1}`,351 createdAt: new Date().toISOString()352 });353 this.h[i] = { c: '', history: [] };354 this.sId = i;355 localStorage.setItem(this.K.LAST, this.sId);356 }357 clearTimeout(this.prevT);358 this.prevT = setTimeout(() => this.updPrev(c), this.K.PREV_D);359 clearTimeout(this.saveT);360 this.saveT = setTimeout(() => this.saveH(c), this.K.HIST_D);361 },362 load() {363 try {364 this.p = JSON.parse(localStorage.getItem(this.K.ITEMS) || "[]");365 this.h = JSON.parse(localStorage.getItem(this.K.HISTS) || "{}");366 this.sId = localStorage.getItem(this.K.LAST);367 this.p.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));368 if (!this.p.find(i => i.id === this.sId)) {369 this.sId = this.p[0]?.id || null;370 }371 this.sId ? this.loadCont() : this.clearEd();372 } catch (e) {373 this.log('Failed to load data from storage. Resetting.', 'App Init', 'error', e);374 localStorage.clear();375 this.p = [];376 this.h = {};377 this.sId = null;378 this.clearEd();379 } finally {380 this.loading = false;381 }382 },383 save() {384 try {385 localStorage.setItem(this.K.ITEMS, JSON.stringify(this.p));386 localStorage.setItem(this.K.HISTS, JSON.stringify(this.h));387 if (this.sId) localStorage.setItem(this.K.LAST, this.sId);388 } catch (e) {389 this.log('Storage quota exceeded or other save error.', 'save', 'error', e);390 }391 },392 saveOnExit() {393 this.save();394 },395 newP(s = true) {396 const i = `p_${Date.now()}`;397 this.p.unshift({398 id: i,399 name: `Project ${this.p.length + 1}`,400 createdAt: new Date().toISOString()401 });402 this.h[i] = { c: '', history: [] };403 this.selP(i);404 s && this.save();405 },406 delP(i) {407 if (!confirm(`Delete "${this.p.find(p => p.id === i)?.name || 'this item'}"? This is irreversible.`)) return;408 this.p = this.p.filter(p => p.id !== i);409 delete this.h[i];410 if (this.sId === i) {411 this.sId = this.p[0]?.id || null;412 this.selP(this.sId);413 }414 this.save();415 },416 selP(i) {417 if (this.sId === i) return;418 this.sId = i;419 this.eId = null;420 this.expHId = null;421 if (this.sId) {422 this.loadCont();423 localStorage.setItem(this.K.LAST, this.sId);424 } else {425 this.clearEd();426 localStorage.removeItem(this.K.LAST);427 }428 },429 loadCont() {430 if (!this.sId || !this.h[this.sId]) return this.clearEd();431 const { c = '' } = this.h[this.sId];432 this.ed.session.off('change');433 this.ed.setValue(c, -1);434 this.ed.session.getUndoManager().reset();435 this.updPrev(c);436 this.ed.session.on('change', () => this.onEdChg());437 },438 clearEd() {439 this.ed?.setValue('', -1);440 document.getElementById('prev-frame').srcdoc = `441 <body style="background-color:#fff;color:#18181b;font-family:sans-serif;padding:2rem;display:grid;place-content:center;height:100vh;margin:0;text-align:center;">442 <h1>Preview Area</h1>443 <p style="color:#555;">Start typing to create your first project.</p>444 </body>445 `;446 },447 saveH(c) {448 if (!this.sId || !this.h[this.sId]) return;449 const d = this.h[this.sId];450 d.c = c;451 const last = d.history[d.history.length - 1];452 if (!last || last.c !== c) {453 d.history.push({ ts: Date.now(), c });454 if (d.history.length > this.K.MAX_H) d.history.shift();455 delete d.selIdx;456 }457 this.save();458 },459 loadH(i, x) {460 const e = this.h[i]?.history[x];461 if (!e || !this.ed) return;462 this.ed.session.off('change');463 this.ed.setValue(e.c, -1);464 this.updPrev(e.c);465 this.h[i].selIdx = x;466 this.h[i].c = e.c;467 this.ed.session.on('change', () => this.onEdChg());468 this.save();469 },470 startEdit(i) {471 this.eId = i;472 },473 endEdit(i, n) {474 const t = this.p.find(p => p.id === i);475 if (t && n.trim()) t.name = n.trim();476 this.eId = null;477 this.save();478 },479 togH(i) {480 this.expHId = this.expHId === i ? null : i;481 },482 togSb() {483 this.sbVis = !this.sbVis;484 },485 togCode() {486 this.codeVis = !this.codeVis;487 },488 getPrev(i) {489 return this.h[i]?.c?.trim().split('\n')[0].replace(/<[^>]+>/g, '') || 'No content yet';490 },491 startDrag(e) {492 e.preventDefault();493 const iframe = document.getElementById('prev-frame');494 iframe.style.pointerEvents = 'none';495 document.body.classList.add('is-resizing');496 const wrap = this.$refs.edWrap;497 const rect = wrap.getBoundingClientRect();498 const initialX = e.clientX || e.touches[0].clientX;499 const initialW = this.edW;500 const moveHandler = (e) => {501 e.preventDefault();502 const clientX = e.clientX || e.touches[0].clientX;503 const dx = (clientX - initialX) / rect.width;504 let newW = initialW + dx;505 newW = Math.max(0, Math.min(newW, 1));506 this.edW = newW;507 this.ed.resize();508 };509 const upHandler = () => {510 document.body.classList.remove('is-resizing');511 iframe.style.pointerEvents = 'auto';512 localStorage.setItem(this.K.EDW, this.edW);513 document.removeEventListener('pointermove', moveHandler);514 document.removeEventListener('touchmove', moveHandler);515 document.removeEventListener('pointerup', upHandler);516 document.removeEventListener('touchend', upHandler);517 };518 document.addEventListener('pointermove', moveHandler, { passive: false });519 document.addEventListener('touchmove', moveHandler, { passive: false });520 document.addEventListener('pointerup', upHandler, { once: true });521 document.addEventListener('touchend', upHandler, { once: true });522 },523 updPrev(c) {524 const frame = document.getElementById('prev-frame');525 const script = `<script>526 window.onerror = (m,s,l,c,e)=>{parent.postMessage({type:'iframeError',payload:{message:m,source:s,lineno:l,colno:c,stack:e?.stack}},'*');return true;};527 window.addEventListener('unhandledrejection', e=>{528 parent.postMessage({type:'iframeError',payload:{message:e.reason?.message||'Unhandled',source:'Promise',lineno:0,colno:0,stack:e.reason?.stack}},'*');529 e.preventDefault();530 });531 <\/script>`;532 const html = c.includes('<html') || c.includes('<body') ? c : 533 `534 <!DOCTYPE html>535 <html lang="en">536 <head> 537 <meta charset="UTF-8"> 538 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 539 <title>Hello</title> 540 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">541 <script src="https://cdn.tailwindcss.com"><\/script>542 </head>543 <body class="bg-gray-100 flex items-center justify-center min-h-screen"> 544 <div class="text-center"> 545 <h1 class="text-5xl font-bold text-blue-600 animate-bounce">Hello!🤗</h1> 546 <p class="mt-4 text-lg text-gray-700">Welcome to this simple website.</p> 547 </div>548 </body>549 </html> 550 `;551 frame.srcdoc = html + script;552 },553 log(m, s = 'unknown', t = 'info', e = {}) {554 const entry = {555 m: m.toString(),556 s: s,557 t: t,558 ts: Date.now(),559 stk: e?.stack || (e.payload ? e.payload.stack : null)560 };561 this.logs.push(entry);562 if (this.logs.length > 100) this.logs.shift();563 if (t === 'error' && !this.logVis) this.errCt++;564 }565 };566 }567 </script>568</body>569</html>