Upload 20 files
Browse files- static/app.js +9 -2
- static/index.html +30 -21
- static/style.css +58 -14
static/app.js
CHANGED
|
@@ -17,7 +17,9 @@ function hugpanel() {
|
|
| 17 |
currentZone: localStorage.getItem('hugpanel_zone') || null,
|
| 18 |
activeTab: localStorage.getItem('hugpanel_tab') || 'files',
|
| 19 |
maxZones: 0,
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
tabs: [
|
| 22 |
{ id: 'files', label: 'Files', icon: 'folder' },
|
| 23 |
{ id: 'editor', label: 'Editor', icon: 'file-code' },
|
|
@@ -81,6 +83,9 @@ function hugpanel() {
|
|
| 81 |
await this.loadBackupStatus();
|
| 82 |
this.adminApiUrl = this.backupStatus.admin_url || '';
|
| 83 |
|
|
|
|
|
|
|
|
|
|
| 84 |
// Try to restore session from stored token
|
| 85 |
if (this.token && this.adminApiUrl) {
|
| 86 |
try {
|
|
@@ -140,7 +145,7 @@ function hugpanel() {
|
|
| 140 |
});
|
| 141 |
|
| 142 |
// Track desktop breakpoint
|
| 143 |
-
const mql = window.matchMedia('(min-width:
|
| 144 |
mql.addEventListener('change', (e) => { this.isDesktop = e.matches; });
|
| 145 |
|
| 146 |
// Persist session state
|
|
@@ -206,6 +211,8 @@ function hugpanel() {
|
|
| 206 |
if (resp.ok) {
|
| 207 |
const data = await resp.json();
|
| 208 |
this.maxZones = data.max_zones || 0;
|
|
|
|
|
|
|
| 209 |
}
|
| 210 |
} catch {}
|
| 211 |
},
|
|
|
|
| 17 |
currentZone: localStorage.getItem('hugpanel_zone') || null,
|
| 18 |
activeTab: localStorage.getItem('hugpanel_tab') || 'files',
|
| 19 |
maxZones: 0,
|
| 20 |
+
motd: '',
|
| 21 |
+
registrationDisabled: false,
|
| 22 |
+
isDesktop: window.innerWidth >= 1024,
|
| 23 |
tabs: [
|
| 24 |
{ id: 'files', label: 'Files', icon: 'folder' },
|
| 25 |
{ id: 'editor', label: 'Editor', icon: 'file-code' },
|
|
|
|
| 83 |
await this.loadBackupStatus();
|
| 84 |
this.adminApiUrl = this.backupStatus.admin_url || '';
|
| 85 |
|
| 86 |
+
// Load config (MOTD, registration state, zone limit) early
|
| 87 |
+
await this._loadZoneLimit();
|
| 88 |
+
|
| 89 |
// Try to restore session from stored token
|
| 90 |
if (this.token && this.adminApiUrl) {
|
| 91 |
try {
|
|
|
|
| 145 |
});
|
| 146 |
|
| 147 |
// Track desktop breakpoint
|
| 148 |
+
const mql = window.matchMedia('(min-width: 1024px)');
|
| 149 |
mql.addEventListener('change', (e) => { this.isDesktop = e.matches; });
|
| 150 |
|
| 151 |
// Persist session state
|
|
|
|
| 211 |
if (resp.ok) {
|
| 212 |
const data = await resp.json();
|
| 213 |
this.maxZones = data.max_zones || 0;
|
| 214 |
+
this.motd = data.motd || '';
|
| 215 |
+
this.registrationDisabled = !!data.disable_registration;
|
| 216 |
}
|
| 217 |
} catch {}
|
| 218 |
},
|
static/index.html
CHANGED
|
@@ -56,9 +56,12 @@
|
|
| 56 |
<!-- Tab switch -->
|
| 57 |
<div class="flex bg-gray-800 rounded-lg p-0.5">
|
| 58 |
<button @click="authMode = 'login'" :class="authMode === 'login' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng nhập</button>
|
| 59 |
-
<button @click="authMode = 'register'" :class="authMode === 'register' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng ký</button>
|
| 60 |
</div>
|
| 61 |
|
|
|
|
|
|
|
|
|
|
| 62 |
<!-- Error -->
|
| 63 |
<div x-show="authError" class="text-xs text-red-400 bg-red-400/10 rounded-lg px-3 py-2" x-text="authError"></div>
|
| 64 |
|
|
@@ -99,6 +102,12 @@
|
|
| 99 |
<!-- ═══ MAIN PANEL (shown when logged in) ═══ -->
|
| 100 |
<div x-show="user" x-cloak>
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
<!-- ═══ Mobile Top Bar ═══ -->
|
| 103 |
<header class="lg:hidden fixed top-0 inset-x-0 z-50 bg-gray-900/95 backdrop-blur border-b border-gray-800 px-4 py-3 flex items-center justify-between">
|
| 104 |
<button @click="sidebarOpen = !sidebarOpen" class="p-1.5 rounded-lg hover:bg-gray-800 transition">
|
|
@@ -117,7 +126,7 @@
|
|
| 117 |
@click="sidebarOpen = false"
|
| 118 |
class="lg:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"></div>
|
| 119 |
|
| 120 |
-
<div class="flex h-
|
| 121 |
|
| 122 |
<!-- ═══ Sidebar ═══ -->
|
| 123 |
<aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
|
@@ -186,20 +195,20 @@
|
|
| 186 |
</aside>
|
| 187 |
|
| 188 |
<!-- ═══ Main Content ═══ -->
|
| 189 |
-
<main class="flex-1 flex flex-col min-w-0 pt-14 lg:pt-0">
|
| 190 |
|
| 191 |
<!-- No zone selected -->
|
| 192 |
-
<div x-show="!currentZone" class="flex-1 overflow-y-auto p-4
|
| 193 |
-
<div class="text-center max-w-sm mx-auto mb-6 pt-4
|
| 194 |
-
<div class="w-16 h-16
|
| 195 |
-
<i data-lucide="layout-dashboard" class="w-8 h-8
|
| 196 |
</div>
|
| 197 |
-
<h2 class="text-lg
|
| 198 |
<p class="text-sm text-gray-600">Chọn zone từ sidebar hoặc tạo zone mới để bắt đầu.</p>
|
| 199 |
</div>
|
| 200 |
|
| 201 |
<!-- Cloud Backups (available even without selecting a zone) -->
|
| 202 |
-
<div x-show="backupStatus.configured" class="max-w-
|
| 203 |
<!-- Restore All button -->
|
| 204 |
<button @click="restoreAll()" :disabled="backupStatus.running"
|
| 205 |
:class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-emerald-500'"
|
|
@@ -276,22 +285,22 @@
|
|
| 276 |
</template>
|
| 277 |
|
| 278 |
<!-- Zone Actions (right side) -->
|
| 279 |
-
<div class="ml-auto flex items-center gap-1
|
| 280 |
-
<div class="hidden
|
| 281 |
<div class="w-2 h-2 rounded-full bg-green-500"></div>
|
| 282 |
<span x-text="currentZone" class="text-sm text-gray-300 font-medium"></span>
|
| 283 |
</div>
|
| 284 |
-
<span x-text="currentZone" class="text-xs text-gray-500 font-mono mr-2 hidden sm:inline
|
| 285 |
<button @click="confirmDeleteZone()" class="p-1.5 rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition flex items-center gap-1.5" title="Xoá zone">
|
| 286 |
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
| 287 |
-
<span class="hidden
|
| 288 |
</button>
|
| 289 |
</div>
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
|
| 293 |
<!-- ═══ TAB: Files + Editor (split on desktop) ═══ -->
|
| 294 |
-
<div x-show="activeTab === 'files' || activeTab === 'editor'" class="desktop-split flex-1 flex flex-col
|
| 295 |
|
| 296 |
<!-- Files Panel (always visible on desktop when in files/editor tab) -->
|
| 297 |
<div x-show="activeTab === 'files' || (isDesktop && activeTab === 'editor')" class="split-panel split-files flex flex-col min-h-0" :class="isDesktop ? '' : 'flex-1'">
|
|
@@ -413,10 +422,10 @@
|
|
| 413 |
</button>
|
| 414 |
</div>
|
| 415 |
</div>
|
| 416 |
-
<div class="flex-1 min-h-0">
|
| 417 |
<textarea x-model="editorContent" @input="editorDirty = true"
|
| 418 |
@keydown.ctrl.s.prevent="saveFile()"
|
| 419 |
-
class="w-full h-full p-4 bg-gray-950 text-gray-200 text-sm font-mono resize-none outline-none leading-relaxed"
|
| 420 |
spellcheck="false"></textarea>
|
| 421 |
</div>
|
| 422 |
</div>
|
|
@@ -431,7 +440,7 @@
|
|
| 431 |
|
| 432 |
<!-- ═══ TAB: Ports ═══ -->
|
| 433 |
<div x-show="activeTab === 'ports'" class="flex-1 overflow-y-auto">
|
| 434 |
-
<div class="p-4
|
| 435 |
<!-- Add Port -->
|
| 436 |
<div class="bg-gray-900 rounded-xl border border-gray-800 p-4">
|
| 437 |
<h3 class="text-sm font-medium text-gray-300 mb-3">Thêm Port</h3>
|
|
@@ -447,7 +456,7 @@
|
|
| 447 |
</div>
|
| 448 |
|
| 449 |
<!-- Port List -->
|
| 450 |
-
<div class="space-y-2
|
| 451 |
<template x-for="port in ports" :key="port.port">
|
| 452 |
<div class="bg-gray-900 rounded-xl border border-gray-800 p-4 flex items-center gap-3">
|
| 453 |
<div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center flex-shrink-0">
|
|
@@ -471,7 +480,7 @@
|
|
| 471 |
</div>
|
| 472 |
</template>
|
| 473 |
|
| 474 |
-
<div x-show="ports.length === 0" class="text-center py-8 text-gray-600 text-sm
|
| 475 |
Chưa có port nào
|
| 476 |
</div>
|
| 477 |
</div>
|
|
@@ -480,7 +489,7 @@
|
|
| 480 |
|
| 481 |
<!-- ═══ TAB: Backup ═══ -->
|
| 482 |
<div x-show="activeTab === 'backup'" x-effect="if(activeTab==='backup' && backupStatus.configured) loadBackupList()" class="flex-1 overflow-y-auto">
|
| 483 |
-
<div class="p-4
|
| 484 |
|
| 485 |
<!-- Not configured -->
|
| 486 |
<div x-show="!backupStatus.configured" class="bg-gray-900 rounded-xl border border-gray-800 p-6 text-center">
|
|
@@ -528,7 +537,7 @@
|
|
| 528 |
</div>
|
| 529 |
|
| 530 |
<!-- Action Buttons -->
|
| 531 |
-
<div class="grid grid-cols-2
|
| 532 |
<button @click="backupZone(currentZone)" :disabled="backupStatus.running"
|
| 533 |
:class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-brand-500'"
|
| 534 |
class="flex items-center justify-center gap-2 px-4 py-3 bg-brand-600 rounded-xl text-sm font-medium transition">
|
|
|
|
| 56 |
<!-- Tab switch -->
|
| 57 |
<div class="flex bg-gray-800 rounded-lg p-0.5">
|
| 58 |
<button @click="authMode = 'login'" :class="authMode === 'login' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng nhập</button>
|
| 59 |
+
<button x-show="!registrationDisabled" @click="authMode = 'register'" :class="authMode === 'register' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng ký</button>
|
| 60 |
</div>
|
| 61 |
|
| 62 |
+
<!-- Registration disabled notice -->
|
| 63 |
+
<div x-show="registrationDisabled && authMode === 'register'" x-init="if(registrationDisabled) authMode='login'" class="text-xs text-yellow-400 bg-yellow-400/10 rounded-lg px-3 py-2">Đăng ký đã bị tắt bởi admin</div>
|
| 64 |
+
|
| 65 |
<!-- Error -->
|
| 66 |
<div x-show="authError" class="text-xs text-red-400 bg-red-400/10 rounded-lg px-3 py-2" x-text="authError"></div>
|
| 67 |
|
|
|
|
| 102 |
<!-- ═══ MAIN PANEL (shown when logged in) ═══ -->
|
| 103 |
<div x-show="user" x-cloak>
|
| 104 |
|
| 105 |
+
<!-- MOTD Banner -->
|
| 106 |
+
<div x-show="motd" class="fixed top-0 inset-x-0 z-[60] bg-brand-600/95 backdrop-blur text-white text-sm px-4 py-2.5 flex items-center justify-between lg:relative lg:z-auto">
|
| 107 |
+
<span x-text="motd" class="flex-1 text-center"></span>
|
| 108 |
+
<button @click="motd=''" class="ml-3 p-1 hover:bg-white/20 rounded transition text-xs">✕</button>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
<!-- ═══ Mobile Top Bar ═══ -->
|
| 112 |
<header class="lg:hidden fixed top-0 inset-x-0 z-50 bg-gray-900/95 backdrop-blur border-b border-gray-800 px-4 py-3 flex items-center justify-between">
|
| 113 |
<button @click="sidebarOpen = !sidebarOpen" class="p-1.5 rounded-lg hover:bg-gray-800 transition">
|
|
|
|
| 126 |
@click="sidebarOpen = false"
|
| 127 |
class="lg:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"></div>
|
| 128 |
|
| 129 |
+
<div class="flex h-screen overflow-hidden">
|
| 130 |
|
| 131 |
<!-- ═══ Sidebar ═══ -->
|
| 132 |
<aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
|
|
|
| 195 |
</aside>
|
| 196 |
|
| 197 |
<!-- ═══ Main Content ═══ -->
|
| 198 |
+
<main class="flex-1 flex flex-col min-w-0 min-h-0 pt-14 lg:pt-0 h-full">
|
| 199 |
|
| 200 |
<!-- No zone selected -->
|
| 201 |
+
<div x-show="!currentZone" class="flex-1 overflow-y-auto p-4 lg:p-8" x-effect="if(!currentZone && backupStatus.configured) loadBackupList()">
|
| 202 |
+
<div class="text-center max-w-sm mx-auto mb-6 pt-4 lg:pt-8">
|
| 203 |
+
<div class="w-16 h-16 lg:w-20 lg:h-20 mx-auto mb-4 rounded-2xl bg-gray-800 flex items-center justify-center">
|
| 204 |
+
<i data-lucide="layout-dashboard" class="w-8 h-8 lg:w-10 lg:h-10 text-gray-600"></i>
|
| 205 |
</div>
|
| 206 |
+
<h2 class="text-lg lg:text-xl font-semibold text-gray-400 mb-2">Chọn hoặc tạo Zone</h2>
|
| 207 |
<p class="text-sm text-gray-600">Chọn zone từ sidebar hoặc tạo zone mới để bắt đầu.</p>
|
| 208 |
</div>
|
| 209 |
|
| 210 |
<!-- Cloud Backups (available even without selecting a zone) -->
|
| 211 |
+
<div x-show="backupStatus.configured" class="max-w-3xl mx-auto space-y-4">
|
| 212 |
<!-- Restore All button -->
|
| 213 |
<button @click="restoreAll()" :disabled="backupStatus.running"
|
| 214 |
:class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-emerald-500'"
|
|
|
|
| 285 |
</template>
|
| 286 |
|
| 287 |
<!-- Zone Actions (right side) -->
|
| 288 |
+
<div class="ml-auto flex items-center gap-1 lg:gap-2">
|
| 289 |
+
<div class="hidden lg:flex items-center gap-2 mr-3">
|
| 290 |
<div class="w-2 h-2 rounded-full bg-green-500"></div>
|
| 291 |
<span x-text="currentZone" class="text-sm text-gray-300 font-medium"></span>
|
| 292 |
</div>
|
| 293 |
+
<span x-text="currentZone" class="text-xs text-gray-500 font-mono mr-2 hidden sm:inline lg:hidden"></span>
|
| 294 |
<button @click="confirmDeleteZone()" class="p-1.5 rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition flex items-center gap-1.5" title="Xoá zone">
|
| 295 |
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
| 296 |
+
<span class="hidden lg:inline text-xs">Xoá zone</span>
|
| 297 |
</button>
|
| 298 |
</div>
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
|
| 302 |
<!-- ═══ TAB: Files + Editor (split on desktop) ═══ -->
|
| 303 |
+
<div x-show="activeTab === 'files' || activeTab === 'editor'" class="desktop-split flex-1 flex flex-col lg:flex-row min-h-0">
|
| 304 |
|
| 305 |
<!-- Files Panel (always visible on desktop when in files/editor tab) -->
|
| 306 |
<div x-show="activeTab === 'files' || (isDesktop && activeTab === 'editor')" class="split-panel split-files flex flex-col min-h-0" :class="isDesktop ? '' : 'flex-1'">
|
|
|
|
| 422 |
</button>
|
| 423 |
</div>
|
| 424 |
</div>
|
| 425 |
+
<div class="flex-1 min-h-0 overflow-hidden">
|
| 426 |
<textarea x-model="editorContent" @input="editorDirty = true"
|
| 427 |
@keydown.ctrl.s.prevent="saveFile()"
|
| 428 |
+
class="w-full h-full p-4 bg-gray-950 text-gray-200 text-sm font-mono resize-none outline-none leading-relaxed block"
|
| 429 |
spellcheck="false"></textarea>
|
| 430 |
</div>
|
| 431 |
</div>
|
|
|
|
| 440 |
|
| 441 |
<!-- ═══ TAB: Ports ═══ -->
|
| 442 |
<div x-show="activeTab === 'ports'" class="flex-1 overflow-y-auto">
|
| 443 |
+
<div class="p-4 lg:p-6 space-y-4 w-full">
|
| 444 |
<!-- Add Port -->
|
| 445 |
<div class="bg-gray-900 rounded-xl border border-gray-800 p-4">
|
| 446 |
<h3 class="text-sm font-medium text-gray-300 mb-3">Thêm Port</h3>
|
|
|
|
| 456 |
</div>
|
| 457 |
|
| 458 |
<!-- Port List -->
|
| 459 |
+
<div class="space-y-2 lg:grid lg:grid-cols-2 2xl:grid-cols-3 lg:gap-3 lg:space-y-0">
|
| 460 |
<template x-for="port in ports" :key="port.port">
|
| 461 |
<div class="bg-gray-900 rounded-xl border border-gray-800 p-4 flex items-center gap-3">
|
| 462 |
<div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center flex-shrink-0">
|
|
|
|
| 480 |
</div>
|
| 481 |
</template>
|
| 482 |
|
| 483 |
+
<div x-show="ports.length === 0" class="text-center py-8 text-gray-600 text-sm lg:col-span-2 2xl:col-span-3">
|
| 484 |
Chưa có port nào
|
| 485 |
</div>
|
| 486 |
</div>
|
|
|
|
| 489 |
|
| 490 |
<!-- ═══ TAB: Backup ═══ -->
|
| 491 |
<div x-show="activeTab === 'backup'" x-effect="if(activeTab==='backup' && backupStatus.configured) loadBackupList()" class="flex-1 overflow-y-auto">
|
| 492 |
+
<div class="p-4 lg:p-6 space-y-4 w-full">
|
| 493 |
|
| 494 |
<!-- Not configured -->
|
| 495 |
<div x-show="!backupStatus.configured" class="bg-gray-900 rounded-xl border border-gray-800 p-6 text-center">
|
|
|
|
| 537 |
</div>
|
| 538 |
|
| 539 |
<!-- Action Buttons -->
|
| 540 |
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
| 541 |
<button @click="backupZone(currentZone)" :disabled="backupStatus.running"
|
| 542 |
:class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-brand-500'"
|
| 543 |
class="flex items-center justify-center gap-2 px-4 py-3 bg-brand-600 rounded-xl text-sm font-medium transition">
|
static/style.css
CHANGED
|
@@ -25,6 +25,13 @@
|
|
| 25 |
display: none;
|
| 26 |
}
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
/* Terminal container */
|
| 29 |
#terminal-container {
|
| 30 |
min-height: 200px;
|
|
@@ -51,6 +58,18 @@ textarea {
|
|
| 51 |
-moz-tab-size: 4;
|
| 52 |
}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
/* Smooth transitions */
|
| 55 |
* {
|
| 56 |
-webkit-tap-highlight-color: transparent;
|
|
@@ -68,6 +87,20 @@ textarea {
|
|
| 68 |
}
|
| 69 |
}
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
/* Focus ring */
|
| 72 |
input:focus, textarea:focus, button:focus-visible {
|
| 73 |
outline: none;
|
|
@@ -94,24 +127,41 @@ input:focus, textarea:focus, button:focus-visible {
|
|
| 94 |
|
| 95 |
/* ═══ Desktop Enhancements ═══ */
|
| 96 |
|
| 97 |
-
/* Desktop split: files + editor side by side on
|
| 98 |
-
@media (min-width:
|
| 99 |
.desktop-split {
|
| 100 |
flex-direction: row !important;
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
.desktop-split > .split-files {
|
| 103 |
-
width:
|
|
|
|
|
|
|
| 104 |
flex-shrink: 0;
|
| 105 |
border-right: 1px solid #1f2937;
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
.desktop-split > .split-editor {
|
| 108 |
flex: 1;
|
| 109 |
min-width: 0;
|
|
|
|
|
|
|
|
|
|
| 110 |
}
|
| 111 |
}
|
| 112 |
|
| 113 |
-
/*
|
| 114 |
@media (min-width: 1280px) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
.file-item .file-actions {
|
| 116 |
opacity: 0;
|
| 117 |
transition: opacity 0.15s;
|
|
@@ -121,14 +171,8 @@ input:focus, textarea:focus, button:focus-visible {
|
|
| 121 |
}
|
| 122 |
}
|
| 123 |
|
| 124 |
-
/* Desktop:
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
}
|
| 129 |
-
}
|
| 130 |
-
@media (min-width: 1536px) {
|
| 131 |
-
.desktop-content {
|
| 132 |
-
max-width: 1100px;
|
| 133 |
-
}
|
| 134 |
}
|
|
|
|
| 25 |
display: none;
|
| 26 |
}
|
| 27 |
|
| 28 |
+
/* ═══ Full-height layout ═══ */
|
| 29 |
+
html, body {
|
| 30 |
+
height: 100%;
|
| 31 |
+
margin: 0;
|
| 32 |
+
overflow: hidden;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
/* Terminal container */
|
| 36 |
#terminal-container {
|
| 37 |
min-height: 200px;
|
|
|
|
| 58 |
-moz-tab-size: 4;
|
| 59 |
}
|
| 60 |
|
| 61 |
+
/* Editor area fills all available space */
|
| 62 |
+
.split-editor {
|
| 63 |
+
display: flex;
|
| 64 |
+
flex-direction: column;
|
| 65 |
+
min-height: 0;
|
| 66 |
+
flex: 1;
|
| 67 |
+
}
|
| 68 |
+
.split-editor textarea {
|
| 69 |
+
flex: 1;
|
| 70 |
+
min-height: 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
/* Smooth transitions */
|
| 74 |
* {
|
| 75 |
-webkit-tap-highlight-color: transparent;
|
|
|
|
| 87 |
}
|
| 88 |
}
|
| 89 |
|
| 90 |
+
/* ═══ Mobile: full-height panels ═══ */
|
| 91 |
+
@media (max-width: 1279px) {
|
| 92 |
+
/* Editor tab: fill remaining viewport height */
|
| 93 |
+
.split-editor {
|
| 94 |
+
height: calc(100vh - 7rem);
|
| 95 |
+
min-height: 300px;
|
| 96 |
+
}
|
| 97 |
+
/* Files tab: fill remaining height */
|
| 98 |
+
.split-files {
|
| 99 |
+
height: calc(100vh - 7rem);
|
| 100 |
+
min-height: 300px;
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
/* Focus ring */
|
| 105 |
input:focus, textarea:focus, button:focus-visible {
|
| 106 |
outline: none;
|
|
|
|
| 127 |
|
| 128 |
/* ═══ Desktop Enhancements ═══ */
|
| 129 |
|
| 130 |
+
/* Desktop split: files + editor side by side on lg+ (1024px) */
|
| 131 |
+
@media (min-width: 1024px) {
|
| 132 |
.desktop-split {
|
| 133 |
flex-direction: row !important;
|
| 134 |
+
flex: 1;
|
| 135 |
+
min-height: 0;
|
| 136 |
}
|
| 137 |
.desktop-split > .split-files {
|
| 138 |
+
width: 340px;
|
| 139 |
+
min-width: 280px;
|
| 140 |
+
max-width: 400px;
|
| 141 |
flex-shrink: 0;
|
| 142 |
border-right: 1px solid #1f2937;
|
| 143 |
+
display: flex;
|
| 144 |
+
flex-direction: column;
|
| 145 |
+
min-height: 0;
|
| 146 |
}
|
| 147 |
.desktop-split > .split-editor {
|
| 148 |
flex: 1;
|
| 149 |
min-width: 0;
|
| 150 |
+
display: flex;
|
| 151 |
+
flex-direction: column;
|
| 152 |
+
min-height: 0;
|
| 153 |
}
|
| 154 |
}
|
| 155 |
|
| 156 |
+
/* xl+ wider file panel */
|
| 157 |
@media (min-width: 1280px) {
|
| 158 |
+
.desktop-split > .split-files {
|
| 159 |
+
width: 380px;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* Desktop: better file list items */
|
| 164 |
+
@media (min-width: 1024px) {
|
| 165 |
.file-item .file-actions {
|
| 166 |
opacity: 0;
|
| 167 |
transition: opacity 0.15s;
|
|
|
|
| 171 |
}
|
| 172 |
}
|
| 173 |
|
| 174 |
+
/* Desktop: content areas fill width (no max-width constraint) */
|
| 175 |
+
.desktop-content {
|
| 176 |
+
width: 100%;
|
| 177 |
+
max-width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|