index copy.html (111970B)
1 <!DOCTYPE html> 2 <html lang="en" class="dark"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 6 <title>Veyrix IDE</title> 7 8 <link rel="manifest" href="assets/manifest.json"> 9 <link rel="icon" type="image/svg+xml" href="assets/veyrix.svg"> 10 <meta name="theme-color" content="#121b2a"> 11 12 <!-- Tailwind CSS --> 13 <script src="https://cdn.tailwindcss.com"></script> 14 <script> 15 tailwind.config = { 16 darkMode: 'class', 17 theme: { 18 extend: { 19 colors: { 20 veyrix: { 21 bg: '#0a0f1a', 22 surface: '#121b2a', 23 border: '#1e293b', 24 accent: '#3b82f6', 25 accentHover: '#2563eb', 26 text: '#e2e8f0', 27 muted: '#94a3b8' 28 } 29 }, 30 transitionTimingFunction: { 31 'material': 'cubic-bezier(0.2, 0.0, 0, 1.0)', 32 } 33 } 34 } 35 } 36 </script> 37 38 <style> 39 /* Global tap highlight & focus reset */ 40 * { 41 -webkit-tap-highlight-color: transparent; 42 } 43 44 :focus:not(:focus-visible) { 45 outline: none; 46 } 47 48 :focus-visible { 49 outline: 2px solid theme('colors.veyrix.accent'); 50 outline-offset: 1px; 51 border-radius: 2px; 52 } 53 54 /* Custom UI Scrollbar & Base Styles */ 55 ::-webkit-scrollbar { width: 8px; height: 8px; } 56 ::-webkit-scrollbar-track { background: transparent; } 57 ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; } 58 ::-webkit-scrollbar-thumb:hover { background: #475569; } 59 60 body { 61 background-color: theme('colors.veyrix.bg'); 62 color: theme('colors.veyrix.text'); 63 overflow: hidden; 64 touch-action: none; 65 } 66 67 .glass-panel { 68 background: theme('colors.veyrix.surface'); 69 border: 1px solid theme('colors.veyrix.border'); 70 } 71 72 /* --- SIDEBAR RESPONSIVE LOGIC --- */ 73 #sidebar { 74 width: 280px; 75 background-color: theme('colors.veyrix.surface'); 76 border-right: 1px solid theme('colors.veyrix.border'); 77 transition: width 0.25s ease, transform 0.25s ease; 78 overflow: hidden; 79 flex-shrink: 0; 80 z-index: 40; 81 display: flex; 82 flex-direction: column; 83 } 84 85 #sidebar.sidebar-collapsed { 86 width: 0; 87 border-right-color: transparent; 88 } 89 90 @media (max-width: 768px) { 91 #sidebar { 92 position: absolute; 93 top: 0; 94 bottom: 0; 95 left: 0; 96 transform: translateX(0); 97 box-shadow: 5px 0 25px rgba(0,0,0,0.7); 98 border-right: none; 99 } 100 #sidebar.sidebar-collapsed { 101 width: 280px; 102 transform: translateX(-100%); 103 } 104 } 105 106 #editor { 107 position: absolute; 108 top: 0; 109 right: 0; 110 bottom: 0; 111 left: 0; 112 border-radius: 0.5rem; 113 background: theme('colors.veyrix.surface'); 114 } 115 116 .ace_editor { font-family: 'Fira Code', 'Consolas', monospace !important; } 117 .ace-veyrix-dark { 118 background-color: theme('colors.veyrix.surface'); 119 color: #d4d4d4; 120 } 121 122 .modal-enter { animation: modalIn 0.25s cubic-bezier(0.2, 0, 0, 1) forwards; } 123 .modal-exit { animation: modalOut 0.2s cubic-bezier(0.2, 0, 0, 1) forwards; } 124 125 @keyframes modalIn { 126 from { opacity: 0; transform: scale(0.95) translateY(10px); } 127 to { opacity: 1; transform: scale(1) translateY(0); } 128 } 129 @keyframes modalOut { 130 from { opacity: 1; transform: scale(1) translateY(0); } 131 to { opacity: 0; transform: scale(0.95) translateY(10px); } 132 } 133 134 /* Jon Suh Hamburger - Spin */ 135 .hamburger { 136 display: inline-flex; 137 align-items: center; 138 justify-content: center; 139 cursor: pointer; 140 transition-property: opacity, filter; 141 transition-duration: 0.15s; 142 transition-timing-function: linear; 143 border: 0; 144 margin: 0; 145 overflow: visible; 146 background: transparent; 147 } 148 .hamburger:hover { opacity: 0.8; } 149 .hamburger-box { 150 width: 20px; 151 height: 14px; 152 display: inline-block; 153 position: relative; 154 } 155 .hamburger-inner { 156 display: block; 157 top: 50%; 158 margin-top: -1px; 159 } 160 .hamburger-inner, .hamburger-inner::before, .hamburger-inner::after { 161 width: 20px; 162 height: 2px; 163 background-color: white; 164 border-radius: 4px; 165 position: absolute; 166 transition-property: transform; 167 transition-duration: 0.15s; 168 transition-timing-function: ease; 169 } 170 .hamburger:hover .hamburger-inner, .hamburger:hover .hamburger-inner::before, .hamburger:hover .hamburger-inner::after { 171 background-color: white; 172 } 173 .hamburger.is-active .hamburger-inner, .hamburger.is-active .hamburger-inner::before, .hamburger.is-active .hamburger-inner::after { 174 background-color: white; 175 } 176 .hamburger-inner::before, .hamburger-inner::after { content: ""; display: block; } 177 .hamburger-inner::before { top: -6px; } 178 .hamburger-inner::after { bottom: -6px; } 179 180 .hamburger--spin .hamburger-inner { transition-duration: 0.22s; transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 181 .hamburger--spin .hamburger-inner::before { transition: top 0.1s 0.25s ease-in, opacity 0.1s ease-in; } 182 .hamburger--spin .hamburger-inner::after { transition: bottom 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19); } 183 184 .hamburger--spin.is-active .hamburger-inner { transform: rotate(225deg); transition-delay: 0.12s; transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 185 .hamburger--spin.is-active .hamburger-inner::before { top: 0; opacity: 0; transition: top 0.1s ease-out, opacity 0.1s 0.12s ease-out; } 186 .hamburger--spin.is-active .hamburger-inner::after { bottom: 0; transform: rotate(-90deg); transition: bottom 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1); } 187 </style> 188 189 <!-- Dependencies --> 190 <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/ace.js"></script> 191 <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/ext-beautify.js"></script> 192 <script src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js"></script> 193 </head> 194 <body class="flex flex-col text-sm antialiased selection:bg-veyrix-accent/30"> 195 196 <!-- Top Navigation Bar --> 197 <header class="h-12 border-b border-veyrix-border bg-veyrix-surface flex items-center justify-between px-2 md:px-3 flex-shrink-0 z-40 relative gap-2"> 198 199 <!-- Left Side: Menu + Logo --> 200 <div class="flex items-center gap-2 md:gap-3 shrink-0"> 201 <button class="hamburger hamburger--spin sidebar-toggle-btn p-1.5 rounded transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent" type="button" aria-expanded="false" aria-controls="sidebar" aria-label="Toggle menu" onclick="UI.toggleSidebar()"> 202 <span class="hamburger-box"> 203 <span class="hamburger-inner"></span> 204 </span> 205 </button> 206 207 <!-- Desktop Logo --> 208 <div class="hidden md:flex items-center gap-2"> 209 <img src="assets/veyrix.svg" alt="Veyrix Logo" class="w-6 h-6"> 210 <span class="font-semibold text-white tracking-wide">Veyrix IDE</span> 211 </div> 212 213 <!-- Mobile Logo (Stacked) --> 214 <div class="flex flex-col items-center justify-center md:hidden leading-[1.1] ml-0.5"> 215 <img src="assets/veyrix.svg" alt="Veyrix Logo" class="w-[18px] h-[18px]"> 216 <span class="font-bold text-white tracking-wider text-[8px] uppercase mt-[1px]">Veyrix</span> 217 </div> 218 219 <div class="h-5 w-px bg-veyrix-border mx-2 hidden md:block"></div> 220 221 </div> 222 223 <!-- Center: Active File Display (Dynamic Width) --> 224 <div class="flex-1 flex justify-start md:justify-center min-w-0 overflow-hidden"> 225 <div id="top-active-file" class="hidden items-center gap-1.5 bg-black/30 px-3 py-1 rounded-full border border-white/10 max-w-full overflow-hidden whitespace-nowrap text-veyrix-text font-mono text-xs"> 226 <span id="top-file-name" class="truncate">No file open</span> 227 <span id="top-file-unsaved" class="w-2 h-2 rounded-full bg-veyrix-accent hidden shrink-0"></span> 228 </div> 229 </div> 230 231 <!-- Right Side: Action Buttons --> 232 <div class="flex items-center gap-0.5 md:gap-1 shrink-0"> 233 <button onclick="document.getElementById('file-upload').click()" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Open File (Ctrl+O)"> 234 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"></path></svg> 235 </button> 236 <button onclick="App.saveCurrentFile()" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Save (Ctrl+S)"> 237 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg> 238 </button> 239 <button onclick="App.triggerDownload()" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Download File"> 240 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg> 241 </button> 242 <button onclick="App.triggerSnapshot()" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Save Snapshot"> 243 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg> 244 </button> 245 <button onclick="UI.showHistory()" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="View History"> 246 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> 247 </button> 248 <button onclick="UI.showModal('share-modal')" class="p-1 md:hidden rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Share"> 249 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg> 250 </button> 251 <button onclick="UI.showModal('share-modal')" class="hidden md:flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-veyrix-accent/10 text-veyrix-accent hover:bg-veyrix-accent/20 transition-colors border border-veyrix-accent/20 md:ml-1"> 252 <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg> 253 Share 254 </button> 255 <button onclick="UI.showModal('help-modal')" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger md:ml-1" title="Help & Shortcuts"> 256 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> 257 </button> 258 <button onclick="App.toggleFullscreen()" class="hidden md:block p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Fullscreen (F11)"> 259 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg> 260 </button> 261 <button onclick="UI.showModal('settings-modal')" class="p-1 md:p-1.5 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors tooltip-trigger" title="Settings"> 262 <svg class="w-4 h-4 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> 263 </button> 264 </div> 265 </header> 266 267 <!-- Main Workspace --> 268 <main class="flex-1 flex overflow-hidden relative z-10 w-full" id="main-workspace"> 269 270 <!-- Mobile Sidebar Backdrop --> 271 <div id="sidebar-backdrop" class="fixed inset-0 bg-black/60 z-30 hidden md:hidden transition-opacity duration-250 opacity-0"></div> 272 273 <!-- Sidebar / File Explorer --> 274 <aside id="sidebar" class="sidebar-collapsed bg-veyrix-surface"> 275 <div class="p-3 border-b border-veyrix-border flex items-center justify-between"> 276 <span class="text-xs font-bold text-veyrix-muted uppercase tracking-wider">Explorer</span> 277 <div class="flex gap-1"> 278 <button onclick="UI.showModal('new-file-modal')" class="p-1 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors" title="New File"> 279 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> 280 </button> 281 </div> 282 </div> 283 <div id="file-list" class="flex-1 overflow-y-auto p-2 space-y-0.5 relative"> 284 <!-- File items injected here --> 285 </div> 286 287 <!-- Mobile Sidebar Footer --> 288 <div class="md:hidden p-3 border-t border-veyrix-border flex justify-between"> 289 <button onclick="UI.toggleSidebar(true)" class="text-xs text-veyrix-muted hover:text-white flex items-center gap-1 w-full justify-center bg-white/5 hover:bg-white/10 py-2 rounded-lg transition-colors"> 290 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> 291 Close Sidebar 292 </button> 293 </div> 294 </aside> 295 296 <!-- Editor Area --> 297 <section id="editor-section" class="flex-1 relative p-2 md:p-4 bg-veyrix-bg flex flex-col min-w-0"> 298 <!-- Security/Share Warning Banner --> 299 <div id="banner-warning" class="hidden mb-2 bg-amber-500/10 border border-amber-500/20 text-amber-200 text-xs px-3 py-2 rounded-lg flex items-center justify-between"> 300 <div class="flex items-center gap-2"> 301 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg> 302 <span id="banner-text">Viewing shared file. Edits won't sync automatically.</span> 303 </div> 304 <button onclick="this.parentElement.classList.add('hidden')" class="hover:text-amber-100">×</button> 305 </div> 306 307 <div class="flex-1 relative rounded-lg overflow-hidden border border-veyrix-border shadow-2xl glass-panel"> 308 <div id="editor-placeholder" class="absolute inset-0 flex flex-col items-center justify-center text-veyrix-muted bg-veyrix-surface z-10"> 309 <img src="assets/veyrix.svg" alt="Veyrix" class="w-20 h-20 mb-6 opacity-20 grayscale"> 310 <p class="text-sm font-medium text-veyrix-text">Ready to build something?</p> 311 <div class="mt-6 flex gap-4 text-xs text-veyrix-muted"> 312 <button onclick="UI.showModal('new-file-modal')" class="px-3 py-1.5 rounded-md bg-white/5 hover:bg-white/10 border border-white/10 text-veyrix-text transition-colors">Create New File</button> 313 </div> 314 <div id="app-version-display" class="absolute bottom-4 text-[10px] font-mono text-veyrix-muted/30 select-none"></div> 315 </div> 316 <div id="editor"></div> 317 </div> 318 </section> 319 320 <!-- Floating Action Button for Mobile --> 321 <button onclick="UI.showModal('new-file-modal')" class="md:hidden fixed bottom-6 right-6 w-14 h-14 bg-veyrix-accent hover:bg-veyrix-accentHover rounded-full shadow-lg flex items-center justify-center text-white z-20 transition-transform active:scale-95"> 322 <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> 323 </button> 324 </main> 325 326 <!-- Global Dropdown Menu (Z-index top priority) --> 327 <div id="global-file-dropdown" class="hidden absolute w-40 bg-slate-800 border border-slate-700 rounded-lg shadow-xl z-[100] flex-col py-1 transition-opacity duration-150 opacity-0 scale-95 origin-top-right"> 328 <button id="dd-rename" class="flex items-center gap-2 w-full text-left px-3 py-2 text-xs text-slate-200 hover:bg-slate-700 hover:text-white transition-colors"> 329 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg> Rename 330 </button> 331 <button id="dd-download" class="flex items-center gap-2 w-full text-left px-3 py-2 text-xs text-slate-200 hover:bg-slate-700 hover:text-white transition-colors"> 332 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg> Download 333 </button> 334 <button id="dd-share" class="flex items-center gap-2 w-full text-left px-3 py-2 text-xs text-slate-200 hover:bg-slate-700 hover:text-white transition-colors"> 335 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg> Share 336 </button> 337 <div class="h-px bg-slate-700 my-1 mx-2"></div> 338 <button id="dd-delete" class="flex items-center gap-2 w-full text-left px-3 py-2 text-xs text-red-400 hover:bg-slate-700 hover:text-red-300 transition-colors"> 339 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg> Delete 340 </button> 341 </div> 342 343 <!-- Hidden File Input for Open --> 344 <input type="file" id="file-upload" class="hidden" accept=".txt,.js,.html,.css,.json,.md,.xml,.csv,.log,.sh,.py,.yml,.yaml,.ini,.env,text/*" onchange="App.handleFileUpload(event)"> 345 346 <!-- TOAST CONTAINER --> 347 <div id="toast-container" class="fixed bottom-16 md:bottom-6 right-4 md:right-6 z-50 flex flex-col gap-2 pointer-events-none"></div> 348 349 <!-- MODALS OVERLAY --> 350 <div id="modal-overlay" class="fixed inset-0 bg-black/60 z-50 hidden items-start pt-16 md:pt-0 md:items-center justify-center p-4 overflow-y-auto"> 351 352 <!-- Generic Confirm Modal --> 353 <div id="confirm-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-sm shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0"> 354 <div class="p-5 space-y-4 text-center"> 355 <div class="w-12 h-12 rounded-full bg-veyrix-accent/10 text-veyrix-accent flex items-center justify-center mx-auto mb-2" id="confirm-icon-bg"> 356 <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg> 357 </div> 358 <h3 class="font-semibold text-white" id="confirm-title">Confirm Action</h3> 359 <p class="text-xs text-veyrix-muted" id="confirm-message">Are you sure?</p> 360 <div class="flex gap-2 justify-center pt-2"> 361 <button onclick="UI.hideModal()" class="flex-1 py-2 bg-white/5 hover:bg-white/10 text-white rounded-lg text-sm transition-colors">Cancel</button> 362 <button id="confirm-action-btn" class="flex-1 py-2 bg-veyrix-accent hover:bg-veyrix-accentHover text-white rounded-lg text-sm font-medium transition-colors">Confirm</button> 363 </div> 364 </div> 365 </div> 366 367 <!-- Snapshot Limit Modal --> 368 <div id="snapshot-limit-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-sm shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0"> 369 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg"> 370 <h3 class="font-semibold text-white">Snapshot Limit Reached</h3> 371 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 372 </div> 373 <div class="p-5 space-y-4"> 374 <p class="text-sm text-veyrix-text">You have reached the maximum limit of 5 snapshots for this file.</p> 375 <div class="bg-white/5 border border-white/10 rounded-lg p-3"> 376 <p class="text-xs text-veyrix-muted mb-1">Oldest Snapshot:</p> 377 <p class="text-sm font-mono text-white" id="snapshot-limit-oldest-info"></p> 378 </div> 379 <p class="text-xs text-amber-400">Creating a new snapshot will permanently delete the oldest one.</p> 380 </div> 381 <div class="p-4 border-t border-veyrix-border bg-black/20 flex justify-end gap-2"> 382 <button onclick="UI.hideModal()" class="px-4 py-2 text-sm rounded-lg hover:bg-white/5 text-veyrix-text transition-colors">Cancel</button> 383 <button onclick="App.executeSnapshotSave(true)" class="px-4 py-2 text-sm rounded-lg bg-veyrix-accent text-white hover:bg-veyrix-accentHover transition-colors font-medium">Delete Oldest & Continue</button> 384 </div> 385 </div> 386 387 <!-- Help & Shortcuts Modal --> 388 <div id="help-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-md shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0 max-h-[90vh]"> 389 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg shrink-0"> 390 <h3 class="font-semibold text-white flex items-center gap-2"> 391 <svg class="w-5 h-5 text-veyrix-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> 392 Help & Shortcuts 393 </h3> 394 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 395 </div> 396 <div class="p-5 overflow-y-auto space-y-6"> 397 <div class="bg-veyrix-accent/10 border border-veyrix-accent/20 rounded-lg p-3 text-sm text-veyrix-accent"> 398 <div class="flex flex-wrap items-center gap-x-2 leading-relaxed"> 399 <svg class="w-4 h-4 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 400 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> 401 </svg> 402 403 <span class="font-medium">Recommendation:</span> 404 405 <span class="flex items-center gap-1.5"> 406 Use <strong>Fullscreen</strong> 407 <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 408 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path> 409 </svg> 410 </span> 411 412 <span>or press</span> 413 414 <kbd class="px-2 py-0.5 text-[11px] font-bold bg-veyrix-accent/25 border-b-2 border-veyrix-accent/40 rounded text-veyrix-accent"> 415 F11 416 </kbd> 417 418 <span class="opacity-90">for the best IDE experience.</span> 419 420 421 </div> 422 </div> 423 <div class="bg-veyrix-accent/10 border border-veyrix-accent/20 rounded-lg p-3 text-sm text-veyrix-accent leading-relaxed"> 424 <!-- Info Icon - inline-block keeps it in the flow --> 425 <svg class="w-4 h-4 inline-block mr-1.5 opacity-80 -mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 426 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> 427 </svg> 428 429 <span>Install Veyrix IDE on your device for a faster experience with offline capabilities.<br>Go to</span> 430 431 <!-- Action Group: Locked together so they never break apart --> 432 <span class="inline-flex items-center gap-1 ml-1"> 433 <svg class="w-3.5 h-3.5 opacity-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 434 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path> 435 <circle cx="12" cy="12" r="3" stroke-width="2"></circle> 436 </svg> 437 <span class="mx-0.5 opacity-80">→</span> 438 <strong class="font-bold">Install Web App</strong> 439 </span> 440 </div> 441 442 <section> 443 <h4 class="text-sm font-semibold text-white border-b border-veyrix-border pb-1 mb-3">Icon Guide & Usage</h4> 444 <ul class="space-y-4 text-xs text-veyrix-muted"> 445 <li class="flex items-start gap-3"> 446 <div class="p-1.5 bg-white/5 rounded text-white shrink-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"></path></svg></div> 447 <div><strong class="text-white block mb-0.5">Open File</strong> Load a text or code file from your device into the IDE.</div> 448 </li> 449 <li class="flex items-start gap-3"> 450 <div class="p-1.5 bg-white/5 rounded text-white shrink-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg></div> 451 <div><strong class="text-white block mb-0.5">Save</strong> Manually save the current file to your browser's local storage.</div> 452 </li> 453 <li class="flex items-start gap-3"> 454 <div class="p-1.5 bg-white/5 rounded text-white shrink-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg></div> 455 <div><strong class="text-white block mb-0.5">Download</strong> Download the file to your device.</div> 456 </li> 457 <li class="flex items-start gap-3"> 458 <div class="p-1.5 bg-white/5 rounded text-white shrink-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg></div> 459 <div><strong class="text-white block mb-0.5">Save Snapshot</strong> Create a versioned backup of your current file (maximum 5 snapshots per file).</div> 460 </li> 461 <li class="flex items-start gap-3"> 462 <div class="p-1.5 bg-white/5 rounded text-white shrink-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg></div> 463 <div><strong class="text-white block mb-0.5">View History</strong> View all your saved snapshots and safely restore older versions.</div> 464 </li> 465 <li class="flex items-start gap-3"> 466 <div class="p-1.5 bg-white/5 rounded text-white shrink-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg></div> 467 <div><strong class="text-white block mb-0.5">Share</strong> Share your code directly via device, fast local link, or cloud upload.</div> 468 </li> 469 </ul> 470 </section> 471 472 <section> 473 <h4 class="text-sm font-semibold text-white border-b border-veyrix-border pb-1 mb-2">Keyboard Shortcuts</h4> 474 <div class="grid grid-cols-2 gap-2 text-xs"> 475 <div class="text-veyrix-muted flex justify-between pr-2 border-r border-white/5"><span>Save File</span> <kbd class="px-1 py-0.5 rounded bg-white/10 text-white">Ctrl+S</kbd></div> 476 <div class="text-veyrix-muted flex justify-between pl-2"><span>Open File</span> <kbd class="px-1 py-0.5 rounded bg-white/10 text-white">Ctrl+O</kbd></div> 477 <div class="text-veyrix-muted flex justify-between pr-2 border-r border-white/5"><span>Format Code</span> <kbd class="px-1 py-0.5 rounded bg-white/10 text-white">Alt+Shift+F</kbd></div> 478 <div class="text-veyrix-muted flex justify-between pl-2"><span>Fullscreen</span> <kbd class="px-1 py-0.5 rounded bg-white/10 text-white">F11</kbd></div> 479 <div class="text-veyrix-muted flex justify-between pr-2 border-r border-white/5"><span>Close Modal</span> <kbd class="px-1 py-0.5 rounded bg-white/10 text-white">Esc</kbd></div> 480 </div> 481 </section> 482 </div> 483 </div> 484 485 <!-- Create New File Modal --> 486 <div id="new-file-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-sm shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0"> 487 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg"> 488 <h3 class="font-semibold text-white">Create New File</h3> 489 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 490 </div> 491 <div class="p-5 space-y-4"> 492 <div> 493 <label class="block text-xs font-medium text-veyrix-muted mb-1.5">Filename</label> 494 <input type="text" id="new-file-input" placeholder="e.g., app.js, Dockerfile, .env" 495 class="w-full bg-veyrix-bg border border-veyrix-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent transition-all"> 496 <p class="text-[10px] text-veyrix-muted mt-1.5">No extension required. Recognized files will be auto-formatted.</p> 497 </div> 498 </div> 499 <div class="p-4 border-t border-veyrix-border bg-black/20 flex justify-end gap-2"> 500 <button onclick="UI.hideModal()" class="px-4 py-2 text-sm rounded-lg hover:bg-white/5 text-veyrix-text transition-colors">Cancel</button> 501 <button onclick="App.createNewFile()" class="px-4 py-2 text-sm rounded-lg bg-veyrix-accent text-white hover:bg-veyrix-accentHover transition-colors font-medium">Create</button> 502 </div> 503 </div> 504 505 <!-- Rename File Modal --> 506 <div id="rename-file-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-sm shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0"> 507 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg"> 508 <h3 class="font-semibold text-white">Rename File</h3> 509 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 510 </div> 511 <div class="p-5 space-y-4"> 512 <div> 513 <label class="block text-xs font-medium text-veyrix-muted mb-1.5">New Filename</label> 514 <input type="text" id="rename-file-input" placeholder="e.g., app.js, Dockerfile, .env" 515 class="w-full bg-veyrix-bg border border-veyrix-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent transition-all"> 516 </div> 517 </div> 518 <div class="p-4 border-t border-veyrix-border bg-black/20 flex justify-end gap-2"> 519 <button onclick="UI.hideModal()" class="px-4 py-2 text-sm rounded-lg hover:bg-white/5 text-veyrix-text transition-colors">Cancel</button> 520 <button onclick="App.executeRename()" class="px-4 py-2 text-sm rounded-lg bg-veyrix-accent text-white hover:bg-veyrix-accentHover transition-colors font-medium">Rename</button> 521 </div> 522 </div> 523 524 <!-- Share Modal --> 525 <div id="share-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-md shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0 max-h-[90vh]"> 526 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg shrink-0"> 527 <h3 class="font-semibold text-white">Share File</h3> 528 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 529 </div> 530 531 <div class="flex border-b border-veyrix-border shrink-0"> 532 <button onclick="UI.switchShareTab('local')" id="tab-btn-local" class="flex-1 py-3 text-sm font-medium border-b-2 border-veyrix-accent text-veyrix-accent transition-colors">Fast Local Share</button> 533 <button onclick="UI.switchShareTab('cloud')" id="tab-btn-cloud" class="flex-1 py-3 text-sm font-medium border-b-2 border-transparent text-veyrix-muted hover:text-white transition-colors">Cloud Share</button> 534 </div> 535 536 <div class="overflow-y-auto flex-1"> 537 <div class="p-5"> 538 <!-- Local Share Tab --> 539 <div id="tab-local" class="space-y-4"> 540 <p class="text-xs text-veyrix-muted">Generates a URL containing the compressed code. No data is stored on servers. URL might be long.</p> 541 <button onclick="Cloud.generateLocalShare()" class="w-full py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-sm text-veyrix-text transition-colors flex items-center justify-center gap-2"> 542 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg> 543 Generate Local Link 544 </button> 545 <div id="local-share-result" class="hidden mt-3"> 546 <input type="text" id="local-share-url" readonly class="w-full bg-veyrix-bg border border-veyrix-border rounded-lg px-3 py-2 text-xs text-veyrix-muted focus:outline-none mb-2"> 547 <button onclick="UI.copyToClipboard('local-share-url')" class="w-full py-2 bg-veyrix-accent/20 text-veyrix-accent rounded-lg text-sm font-medium hover:bg-veyrix-accent/30 transition-colors">Copy Link</button> 548 </div> 549 </div> 550 551 <!-- Cloud Share Tab --> 552 <div id="tab-cloud" class="space-y-4 hidden"> 553 <div class="bg-blue-500/10 border border-blue-500/20 p-3 rounded-lg flex gap-3 text-sm"> 554 <svg class="w-5 h-5 text-blue-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg> 555 <p class="text-blue-200 text-xs">Saves to Firebase Firestore. Firebase initializes ONLY when you use this feature.</p> 556 </div> 557 558 <div> 559 <label class="block text-xs font-medium text-veyrix-muted mb-1.5">Optional Password Protection</label> 560 <input type="password" id="cloud-password" placeholder="Leave blank for public access" 561 class="w-full bg-veyrix-bg border border-veyrix-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent transition-all"> 562 </div> 563 564 <button onclick="Cloud.generateCloudShare()" id="btn-cloud-share" class="w-full py-2 bg-veyrix-accent hover:bg-veyrix-accentHover text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"> 565 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg> 566 Upload & Share 567 </button> 568 569 <div id="cloud-share-result" class="hidden mt-3"> 570 <input type="text" id="cloud-share-url" readonly class="w-full bg-veyrix-bg border border-veyrix-border rounded-lg px-3 py-2 text-xs text-veyrix-muted focus:outline-none mb-2"> 571 <button onclick="UI.copyToClipboard('cloud-share-url')" class="w-full py-2 bg-veyrix-accent/20 text-veyrix-accent rounded-lg text-sm font-medium hover:bg-veyrix-accent/30 transition-colors">Copy Link</button> 572 </div> 573 </div> 574 </div> 575 </div> 576 577 <!-- Native Web Share API Section --> 578 <div id="native-share-section" class="hidden border-t border-veyrix-border bg-black/20 p-5 mt-auto shrink-0"> 579 <h4 class="text-sm font-semibold text-white mb-3">Quick Share (Device Share)</h4> 580 <button onclick="Cloud.nativeShare()" class="w-full py-2 bg-veyrix-surface hover:bg-white/5 border border-white/10 rounded-lg text-sm text-veyrix-text transition-colors flex items-center justify-center gap-2"> 581 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg> 582 Share via Device 583 </button> 584 </div> 585 </div> 586 587 <!-- Password Prompt Modal --> 588 <div id="password-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-sm shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0"> 589 <div class="p-5 space-y-4 text-center"> 590 <div class="w-12 h-12 rounded-full bg-amber-500/10 text-amber-500 flex items-center justify-center mx-auto mb-2"> 591 <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg> 592 </div> 593 <h3 class="font-semibold text-white">Protected File</h3> 594 <p class="text-xs text-veyrix-muted">This shared file requires a password.</p> 595 <input type="password" id="access-password" placeholder="Enter password" 596 class="w-full bg-veyrix-bg border border-veyrix-border rounded-lg px-3 py-2 text-sm text-center text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent transition-all"> 597 <button onclick="Cloud.verifyAndLoadCloudFile()" class="w-full py-2 bg-veyrix-accent text-white rounded-lg text-sm font-medium hover:bg-veyrix-accentHover transition-colors">Unlock</button> 598 </div> 599 </div> 600 601 <!-- Settings & Data Security Modal --> 602 <div id="settings-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-lg shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden max-h-[90vh] mb-auto md:mb-0 mt-4 md:mt-0"> 603 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg sticky top-0 z-10"> 604 <h3 class="font-semibold text-white flex items-center gap-2"> 605 <svg class="w-5 h-5 text-veyrix-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg> 606 Settings & Data Panel 607 </h3> 608 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 609 </div> 610 <div class="p-5 overflow-y-auto space-y-6"> 611 612 <!-- App Installation --> 613 <section class="space-y-3"> 614 <h4 class="text-sm font-semibold text-white">App Installation</h4> 615 <div class="bg-white/5 border border-white/10 rounded-lg p-3 text-xs text-veyrix-muted space-y-2 flex flex-col sm:flex-row justify-between sm:items-center gap-3"> 616 <p>Install Veyrix IDE on your device for a faster, app-like experience with offline capabilities.</p> 617 <button id="install-app-btn" onclick="App.installApp()" disabled class="shrink-0 px-4 py-2 bg-veyrix-accent text-white rounded-lg text-sm font-medium hover:bg-veyrix-accentHover transition-colors opacity-50 cursor-not-allowed">Install Web App</button> 618 </div> 619 </section> 620 621 <!-- Frequently Asked Questions --> 622 <section class="space-y-3"> 623 <h4 class="text-sm font-semibold text-white">Frequently Asked Questions</h4> 624 <div class="space-y-2"> 625 626 <div class="border border-white/10 rounded-lg overflow-hidden"> 627 <button class="w-full text-left p-3 bg-white/5 text-sm font-medium text-white flex justify-between items-center hover:bg-white/10 transition-colors" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180');"> 628 Where are my files stored? 629 <svg class="w-4 h-4 text-veyrix-muted transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> 630 </button> 631 <div class="p-3 text-xs text-veyrix-muted hidden bg-black/20 border-t border-white/5"> 632 All your files are stored locally on your device inside your browser's secure IndexedDB. They are never uploaded to any server unless you specifically use the Cloud Share tool. 633 </div> 634 </div> 635 636 <div class="border border-white/10 rounded-lg overflow-hidden"> 637 <button class="w-full text-left p-3 bg-white/5 text-sm font-medium text-white flex justify-between items-center hover:bg-white/10 transition-colors" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180');"> 638 What is Fast Share vs Cloud Share? 639 <svg class="w-4 h-4 text-veyrix-muted transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> 640 </button> 641 <div class="p-3 text-xs text-veyrix-muted hidden bg-black/20 border-t border-white/5"> 642 <strong>Fast Share</strong> compresses your code using Lempel-Ziv and puts it entirely in the URL without using any server. <strong>Cloud Share</strong> uploads your code to an anonymous Firebase Firestore database, giving you a shorter, cleaner link. 643 </div> 644 </div> 645 646 <div class="border border-white/10 rounded-lg overflow-hidden"> 647 <button class="w-full text-left p-3 bg-white/5 text-sm font-medium text-white flex justify-between items-center hover:bg-white/10 transition-colors" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180');"> 648 Is my data secure? 649 <svg class="w-4 h-4 text-veyrix-muted transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> 650 </button> 651 <div class="p-3 text-xs text-veyrix-muted hidden bg-black/20 border-t border-white/5"> 652 Yes. Because files are local, no one can see them. If you use Cloud Share, you can add a password. Passwords are securely hashed using the Web Crypto API (SHA-256) locally before being checked against the database. 653 </div> 654 </div> 655 656 <div class="border border-white/10 rounded-lg overflow-hidden"> 657 <button class="w-full text-left p-3 bg-white/5 text-sm font-medium text-white flex justify-between items-center hover:bg-white/10 transition-colors" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180');"> 658 How many snapshots can I take? 659 <svg class="w-4 h-4 text-veyrix-muted transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> 660 </button> 661 <div class="p-3 text-xs text-veyrix-muted hidden bg-black/20 border-t border-white/5"> 662 You can save a maximum of 5 snapshots per file. If you try to save a 6th snapshot, you will be prompted to replace the oldest one. 663 </div> 664 </div> 665 666 <div class="border border-white/10 rounded-lg overflow-hidden"> 667 <button class="w-full text-left p-3 bg-white/5 text-sm font-medium text-white flex justify-between items-center hover:bg-white/10 transition-colors" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180');"> 668 Does Veyrix IDE work offline? 669 <svg class="w-4 h-4 text-veyrix-muted transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> 670 </button> 671 <div class="p-3 text-xs text-veyrix-muted hidden bg-black/20 border-t border-white/5"> 672 Yes! Once loaded for the first time, Veyrix operates entirely offline using PWA Service Workers. Note: Cloud Share requires an internet connection. 673 </div> 674 </div> 675 676 <div class="border border-white/10 rounded-lg overflow-hidden"> 677 <button class="w-full text-left p-3 bg-white/5 text-sm font-medium text-white flex justify-between items-center hover:bg-white/10 transition-colors" onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('svg').classList.toggle('rotate-180');"> 678 Can I create custom file extensions? 679 <svg class="w-4 h-4 text-veyrix-muted transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg> 680 </button> 681 <div class="p-3 text-xs text-veyrix-muted hidden bg-black/20 border-t border-white/5"> 682 Absolutely. When creating a file, you can type any extension (e.g., <code>project.custom</code>) or no extension at all (e.g., <code>Dockerfile</code>). Unrecognized extensions will default to plain text mode formatting. 683 </div> 684 </div> 685 686 </div> 687 </section> 688 689 <!-- Extensions --> 690 <section class="space-y-3"> 691 <h4 class="text-sm font-semibold text-white">Extensions</h4> 692 <div class="space-y-2"> 693 <label class="flex items-center justify-between p-3 bg-white/5 border border-white/10 rounded-lg cursor-pointer hover:bg-white/10 transition-colors"> 694 <div> 695 <span class="text-sm text-white block">Prettier Code Formatter</span> 696 <span class="text-[10px] text-veyrix-muted">Auto-formats on save (Alt+Shift+F)</span> 697 </div> 698 <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"> 699 <input type="checkbox" name="toggle" checked class="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer border-veyrix-accent" style="right:0; border-color: #3b82f6;"/> 700 <label class="toggle-label block overflow-hidden h-5 rounded-full bg-veyrix-accent cursor-pointer"></label> 701 </div> 702 </label> 703 </div> 704 </section> 705 706 <div class="border-t border-veyrix-border pt-4"> 707 <button onclick="App.factoryReset()" class="w-full py-2 bg-red-500/10 text-red-400 border border-red-500/20 rounded-lg text-sm font-medium hover:bg-red-500/20 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500">Factory Reset (Delete All Data)</button> 708 </div> 709 710 <!-- Footer & Branding Section --> 711 <div class="border-t border-veyrix-border pt-6 pb-2 space-y-6"> 712 <!-- Tagline & Version --> 713 <div class="text-center space-y-1"> 714 <center><img src="assets/veyrix.svg" alt="Veyrix Logo" class="w-10 h-10"></center> 715 <h4 class="text-lg font-bold text-white tracking-wide">Veyrix</h4> 716 <p class="text-xs text-veyrix-muted font-light">Code. Anywhere.</p> 717 <p class="text-[10px] text-veyrix-muted/60 font-mono pt-1" id="settings-version-display"></p> 718 </div> 719 720 <!-- Buttons --> 721 <div class="flex flex-col sm:flex-row gap-3"> 722 <a href="https://github.com/notamitgamer/veyrix" target="_blank" rel="noopener noreferrer" class="flex-1 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-sm text-veyrix-text transition-colors flex items-center justify-center gap-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent"> 723 <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z"/></svg> 724 View Source on GitHub 725 </a> 726 <a href="/docs" class="flex-1 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-sm text-veyrix-text transition-colors flex items-center justify-center gap-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent"> 727 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg> 728 Documentation 729 </a> 730 </div> 731 732 <!-- Creator & Disclaimer --> 733 <div class="text-center space-y-4"> 734 <div class="text-xs text-veyrix-muted"> 735 © <a href="https://amit.is-a.dev" target="_blank" rel="noopener noreferrer" class="hover:text-veyrix-accent hover:underline transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent rounded px-0.5">Amit Dutta</a> 736 <span class="mx-1 opacity-50">|</span> 737 Username: <a href="https://github.com/notamitgamer" target="_blank" rel="noopener noreferrer" class="hover:text-veyrix-accent hover:underline transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-veyrix-accent rounded px-0.5">notamitgamer</a> 738 </div> 739 740 <div class="text-[10px] text-veyrix-muted/60 leading-relaxed bg-black/20 p-3 rounded-lg border border-white/5 mx-auto text-center"> 741 Use of this tool is at your own risk.<br> 742 The creator is not responsible for any data loss, misuse, or damages caused by using this application. 743 The creator holds full rights to modify or permanently discontinue this project at any time without prior notice. 744 </div> 745 </div> 746 </div> 747 </div> 748 </div> 749 750 <!-- History Modal --> 751 <div id="history-modal" class="hidden bg-veyrix-surface border border-veyrix-border rounded-lg w-full max-w-sm shadow-2xl modal-content pointer-events-auto flex flex-col overflow-hidden mb-auto md:mb-0 mt-4 md:mt-0"> 752 <div class="p-4 border-b border-veyrix-border flex justify-between items-center bg-veyrix-bg"> 753 <h3 class="font-semibold text-white">Version History</h3> 754 <button onclick="UI.hideModal()" class="text-veyrix-muted hover:text-white transition-colors">✕</button> 755 </div> 756 <div id="history-list" class="overflow-y-auto max-h-64 p-2 space-y-1"> 757 <!-- Snapshots injected here --> 758 </div> 759 </div> 760 761 </div> 762 763 <script> 764 /** 765 * VEYRIX IDE - CORE APPLICATION LOGIC 766 */ 767 768 const APP_VERSION = '1.0.6'; 769 770 // ===== FIREBASE CONFIGURATION ===== 771 // Firebase initializes ONLY when Cloud Share is selected. 772 const firebaseConfig = { 773 774 }; 775 776 // Application State 777 const state = { 778 files: [], 779 activeFileId: null, 780 editor: null, 781 db: null, 782 renameTargetId: null 783 }; 784 785 // Utilities 786 const Utils = { 787 generateId: () => 'f_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36), 788 getDisplayName: (file) => file.ext ? `${file.name}.${file.ext}` : file.name, 789 getMobileShortName: (name, ext) => { 790 let sName = name || ''; 791 let sExt = ext || ''; 792 793 if (sName.length > 3) { 794 sName = sName.substring(0, 2) + '...'; 795 } 796 if (sExt.length > 3) { 797 sExt = sExt.substring(0, 2) + '...'; 798 } 799 800 return sExt ? `${sName}.${sExt}` : sName; 801 }, 802 getFileIcon: (ext, name = '') => { 803 const fullName = ext ? `${name}.${ext}` : name; 804 const lowerName = fullName.toLowerCase(); 805 const lowerExt = ext.toLowerCase(); 806 807 const baseClass = "w-4 h-4 shrink-0 "; 808 if(lowerExt === 'html') return `<svg class="${baseClass} text-orange-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l2 15 8 2 8-2 2-15L12 2zm0 2.5l8 4-1.5 11.5-6.5 1.5-6.5-1.5L4 8.5l8-4z"/></svg>`; 809 if(lowerExt === 'css') return `<svg class="${baseClass} text-blue-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l2 15 8 2 8-2 2-15L12 2zm0 2.5l8 4-1.5 11.5-6.5 1.5-6.5-1.5L4 8.5l8-4z"/></svg>`; 810 if(lowerExt === 'js' || lowerExt === 'jsx' || lowerExt === 'ts' || lowerExt === 'tsx') return `<svg class="${baseClass} text-yellow-400" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l2 15 8 2 8-2 2-15L12 2zm0 2.5l8 4-1.5 11.5-6.5 1.5-6.5-1.5L4 8.5l8-4z"/></svg>`; 811 if(lowerExt === 'json') return `<svg class="${baseClass} text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path></svg>`; 812 if(lowerExt === 'md') return `<svg class="${baseClass} text-blue-300" fill="currentColor" viewBox="0 0 24 24"><path d="M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6 3.44 6h17.12C21.35 6 22 6.63 22 7.41v9.18c0 .78-.65 1.41-1.44 1.41zM11 14l-2.5-3.5L6 14H4V9h2v2.5l2.5-3.5L11 11.5V9h2v5h-2zM20 12h-2V9h-2v3h-2l3 3 3-3z"/></svg>`; 813 814 // Config and special files 815 if(lowerName === 'dockerfile' || lowerName === 'makefile' || lowerName === '.env' || lowerExt === 'env' || lowerName.endsWith('gitignore') || lowerExt === 'yml' || lowerExt === 'yaml') { 816 return `<svg class="${baseClass} text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`; 817 } 818 819 return `<svg class="${baseClass} text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>`; 820 }, 821 getModeFromExt: (ext, name = '') => { 822 const fullName = ext ? `${name}.${ext}` : name; 823 const lowerFullName = fullName.toLowerCase(); 824 const lowerExt = ext.toLowerCase(); 825 826 const map = { html: 'html', css: 'css', js: 'javascript', jsx: 'javascript', ts: 'javascript', tsx: 'javascript', json: 'json', md: 'markdown', txt: 'text', py: 'python', yml: 'yaml', yaml: 'yaml', xml: 'xml', sh: 'sh', csv: 'text' }; 827 828 if (map[lowerExt]) return map[lowerExt]; 829 if (lowerFullName === 'dockerfile') return 'dockerfile'; 830 if (lowerFullName === 'makefile') return 'makefile'; 831 if (lowerFullName.endsWith('.env') || lowerExt === 'env') return 'ini'; 832 if (lowerFullName.endsWith('gitignore')) return 'gitignore'; 833 834 return 'text'; 835 }, 836 debounce: (func, wait) => { 837 let timeout; 838 return function executedFunction(...args) { 839 const later = () => { clearTimeout(timeout); func(...args); }; 840 clearTimeout(timeout); 841 timeout = setTimeout(later, wait); 842 }; 843 }, 844 async hashPassword(password) { 845 if (!password) return null; 846 const msgBuffer = new TextEncoder().encode(password); 847 const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); 848 const hashArray = Array.from(new Uint8Array(hashBuffer)); 849 return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 850 } 851 }; 852 853 // IndexedDB Wrapper 854 const DB = { 855 init: () => { 856 return new Promise((resolve, reject) => { 857 const request = indexedDB.open('VeyrixFS', 1); 858 request.onerror = () => reject(request.error); 859 request.onsuccess = () => { state.db = request.result; resolve(); }; 860 request.onupgradeneeded = (e) => { 861 const db = e.target.result; 862 if (!db.objectStoreNames.contains('files')) { 863 db.createObjectStore('files', { keyPath: 'id' }); 864 } 865 }; 866 }); 867 }, 868 getAll: () => { 869 return new Promise((resolve, reject) => { 870 const tx = state.db.transaction('files', 'readonly'); 871 const store = tx.objectStore('files'); 872 const request = store.getAll(); 873 request.onsuccess = () => resolve(request.result); 874 request.onerror = () => reject(request.error); 875 }); 876 }, 877 save: (file) => { 878 return new Promise((resolve, reject) => { 879 const tx = state.db.transaction('files', 'readwrite'); 880 const store = tx.objectStore('files'); 881 file.lastModified = Date.now(); 882 const request = store.put(file); 883 request.onsuccess = () => resolve(file); 884 request.onerror = () => reject(request.error); 885 }); 886 }, 887 delete: (id) => { 888 return new Promise((resolve, reject) => { 889 const tx = state.db.transaction('files', 'readwrite'); 890 const store = tx.objectStore('files'); 891 const request = store.delete(id); 892 request.onsuccess = () => resolve(); 893 request.onerror = () => reject(request.error); 894 }); 895 } 896 }; 897 898 // PWA Install Logic 899 let deferredPrompt; 900 window.addEventListener('beforeinstallprompt', (e) => { 901 e.preventDefault(); 902 deferredPrompt = e; 903 const btn = document.getElementById('install-app-btn'); 904 if(btn) { 905 btn.classList.remove('opacity-50', 'cursor-not-allowed'); 906 btn.disabled = false; 907 } 908 }); 909 910 // UI Manager 911 const UI = { 912 activeModal: null, 913 activeDropdownId: null, 914 915 toggleSidebar: (forceClose = false) => { 916 const sidebar = document.getElementById('sidebar'); 917 const backdrop = document.getElementById('sidebar-backdrop'); 918 const hamburger = document.querySelector('.hamburger'); 919 920 let isCollapsed = sidebar.classList.contains('sidebar-collapsed'); 921 if (forceClose) { 922 isCollapsed = false; // Intention: want to collapse it 923 } 924 925 if (isCollapsed) { 926 // Open Sidebar 927 sidebar.classList.remove('sidebar-collapsed'); 928 localStorage.setItem('veyrix_sidebar', 'open'); 929 if (hamburger) { 930 hamburger.classList.add('is-active'); 931 hamburger.setAttribute('aria-expanded', 'true'); 932 } 933 if (window.innerWidth <= 768) { 934 backdrop.classList.remove('hidden'); 935 requestAnimationFrame(() => backdrop.classList.remove('opacity-0')); 936 } 937 } else { 938 // Close Sidebar 939 sidebar.classList.add('sidebar-collapsed'); 940 localStorage.setItem('veyrix_sidebar', 'closed'); 941 if (hamburger) { 942 hamburger.classList.remove('is-active'); 943 hamburger.setAttribute('aria-expanded', 'false'); 944 } 945 backdrop.classList.add('opacity-0'); 946 setTimeout(() => backdrop.classList.add('hidden'), 250); 947 } 948 949 // Ensure Ace Editor resizes after DOM reflow transition 950 setTimeout(() => { if (state.editor) state.editor.resize(); }, 260); 951 }, 952 953 toggleFileDropdown: (id, btnElement, e) => { 954 e.stopPropagation(); 955 const dd = document.getElementById('global-file-dropdown'); 956 957 if (UI.activeDropdownId === id && !dd.classList.contains('hidden')) { 958 UI.closeAllDropdowns(); 959 return; 960 } 961 962 UI.activeDropdownId = id; 963 const rect = btnElement.getBoundingClientRect(); 964 965 dd.classList.remove('hidden'); 966 dd.style.top = (rect.bottom + 4) + 'px'; 967 dd.style.left = (rect.right - 160) + 'px'; // width 40 = 160px 968 969 // Map current actions to buttons 970 document.getElementById('dd-rename').onclick = (ev) => { App.triggerRename(id, ev); }; 971 document.getElementById('dd-download').onclick = (ev) => { App.triggerDownload(id, ev); }; 972 document.getElementById('dd-share').onclick = (ev) => { App.triggerShare(id, ev); }; 973 document.getElementById('dd-delete').onclick = (ev) => { App.triggerDelete(id, ev); }; 974 975 requestAnimationFrame(() => { 976 dd.classList.remove('opacity-0', 'scale-95'); 977 dd.classList.add('opacity-100', 'scale-100'); 978 }); 979 }, 980 981 closeAllDropdowns: () => { 982 const dd = document.getElementById('global-file-dropdown'); 983 if(dd && !dd.classList.contains('hidden')) { 984 dd.classList.remove('opacity-100', 'scale-100'); 985 dd.classList.add('opacity-0', 'scale-95'); 986 setTimeout(() => dd.classList.add('hidden'), 150); 987 UI.activeDropdownId = null; 988 } 989 }, 990 991 showConfirm: (title, message, confirmText, actionCallback) => { 992 document.getElementById('confirm-title').textContent = title; 993 document.getElementById('confirm-message').textContent = message; 994 995 const btn = document.getElementById('confirm-action-btn'); 996 btn.textContent = confirmText; 997 998 // Handle danger vs normal confirm look 999 const iconBg = document.getElementById('confirm-icon-bg'); 1000 if (confirmText.toLowerCase().includes('delete') || confirmText.toLowerCase().includes('restore')) { 1001 btn.className = "flex-1 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-medium transition-colors"; 1002 iconBg.className = "w-12 h-12 rounded-full bg-red-500/10 text-red-500 flex items-center justify-center mx-auto mb-2"; 1003 } else { 1004 btn.className = "flex-1 py-2 bg-veyrix-accent hover:bg-veyrix-accentHover text-white rounded-lg text-sm font-medium transition-colors"; 1005 iconBg.className = "w-12 h-12 rounded-full bg-veyrix-accent/10 text-veyrix-accent flex items-center justify-center mx-auto mb-2"; 1006 } 1007 1008 btn.onclick = () => { 1009 UI.hideModal(); 1010 actionCallback(); 1011 }; 1012 UI.showModal('confirm-modal'); 1013 }, 1014 1015 showHistory: () => { 1016 const file = state.files.find(f => f.id === state.activeFileId); 1017 if(!file) return UI.toast('No file open', 'error'); 1018 const list = document.getElementById('history-list'); 1019 list.innerHTML = ''; 1020 1021 if (!file.snapshots || file.snapshots.length === 0) { 1022 list.innerHTML = `<p class="text-xs text-veyrix-muted p-3 text-center">No snapshots saved.</p>`; 1023 } else { 1024 [...file.snapshots].reverse().forEach((snap, idx) => { 1025 const date = new Date(snap.ts).toLocaleString(); 1026 const realIndex = file.snapshots.length - 1 - idx; 1027 const versionNumber = snap.version || (realIndex + 1); 1028 1029 const el = document.createElement('div'); 1030 el.className = 'flex items-center justify-between p-3 hover:bg-white/5 rounded-md text-sm border-b border-veyrix-border/50'; 1031 el.innerHTML = ` 1032 <div class="flex flex-col"> 1033 <span class="text-white">Version ${versionNumber}</span> 1034 <span class="text-xs text-veyrix-muted">${date}</span> 1035 </div> 1036 <button onclick="App.triggerRestore(${realIndex})" class="text-veyrix-accent hover:text-veyrix-accentHover text-xs font-medium px-3 py-1.5 rounded bg-veyrix-accent/10 hover:bg-veyrix-accent/20 transition-colors">Restore</button> 1037 `; 1038 list.appendChild(el); 1039 }); 1040 } 1041 UI.showModal('history-modal'); 1042 }, 1043 1044 showModal: (id) => { 1045 if (UI.activeModal) UI.hideModal(); 1046 UI.closeAllDropdowns(); 1047 const overlay = document.getElementById('modal-overlay'); 1048 const modal = document.getElementById(id); 1049 overlay.classList.remove('hidden'); 1050 overlay.classList.add('flex'); 1051 modal.classList.remove('hidden'); 1052 modal.classList.add('modal-enter'); 1053 modal.classList.remove('modal-exit'); 1054 UI.activeModal = modal; 1055 1056 if(id === 'new-file-modal') { 1057 setTimeout(() => { 1058 const input = document.getElementById('new-file-input'); 1059 if (input) input.focus(); 1060 }, 100); 1061 } else if(id === 'rename-file-modal') { 1062 setTimeout(() => { 1063 const input = document.getElementById('rename-file-input'); 1064 if (input) { 1065 input.focus(); 1066 input.select(); 1067 } 1068 }, 100); 1069 } 1070 }, 1071 1072 hideModal: () => { 1073 if (!UI.activeModal) return; 1074 const overlay = document.getElementById('modal-overlay'); 1075 const modal = UI.activeModal; 1076 modal.classList.remove('modal-enter'); 1077 modal.classList.add('modal-exit'); 1078 setTimeout(() => { 1079 modal.classList.add('hidden'); 1080 overlay.classList.add('hidden'); 1081 overlay.classList.remove('flex'); 1082 UI.activeModal = null; 1083 }, 200); 1084 }, 1085 1086 toast: (msg, type = 'info') => { 1087 const container = document.getElementById('toast-container'); 1088 const toast = document.createElement('div'); 1089 const colors = type === 'error' ? 'bg-red-500/90 text-white border-red-400' : 'bg-veyrix-surface text-white border-veyrix-border'; 1090 toast.className = `px-4 py-2 rounded-lg border shadow-lg text-sm font-medium transform transition-all duration-300 translate-y-10 opacity-0 flex items-center gap-2 ${colors}`; 1091 toast.innerHTML = type === 'error' ? `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> ${msg}` 1092 : `<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> ${msg}`; 1093 container.appendChild(toast); 1094 1095 requestAnimationFrame(() => { 1096 toast.classList.remove('translate-y-10', 'opacity-0'); 1097 }); 1098 1099 setTimeout(() => { 1100 toast.classList.add('translate-y-10', 'opacity-0'); 1101 setTimeout(() => toast.remove(), 300); 1102 }, 3000); 1103 }, 1104 1105 renderSidebar: () => { 1106 const list = document.getElementById('file-list'); 1107 list.innerHTML = ''; 1108 state.files.sort((a,b) => b.lastModified - a.lastModified).forEach(file => { 1109 const isActive = file.id === state.activeFileId; 1110 const el = document.createElement('div'); 1111 el.className = `flex items-center justify-between p-1 px-1.5 rounded-lg cursor-pointer group transition-colors ${isActive ? 'bg-veyrix-accent/20 text-white' : 'text-veyrix-text hover:bg-white/5 hover:text-white'}`; 1112 el.innerHTML = ` 1113 <div class="flex items-center gap-2 overflow-hidden flex-1 py-1 px-1" onclick="App.openFile('${file.id}')" role="button" tabindex="0"> 1114 ${Utils.getFileIcon(file.ext, file.name)} 1115 <span class="text-sm truncate select-none">${Utils.getDisplayName(file)}</span> 1116 ${file.unsaved ? `<div class="w-1.5 h-1.5 rounded-full bg-veyrix-accent shrink-0 ml-1"></div>` : ''} 1117 </div> 1118 <div class="relative dd-trigger"> 1119 <button onclick="UI.toggleFileDropdown('${file.id}', this, event)" class="p-1 rounded hover:bg-white/10 text-veyrix-muted hover:text-white transition-colors" title="Options"> 1120 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path></svg> 1121 </button> 1122 </div> 1123 `; 1124 list.appendChild(el); 1125 }); 1126 }, 1127 1128 updateTopBar: () => { 1129 const file = state.files.find(f => f.id === state.activeFileId); 1130 const placeholder = document.getElementById('editor-placeholder'); 1131 const activeFileContainer = document.getElementById('top-active-file'); 1132 const nameEl = document.getElementById('top-file-name'); 1133 const unsavedEl = document.getElementById('top-file-unsaved'); 1134 1135 if (file) { 1136 placeholder.classList.add('hidden'); 1137 activeFileContainer.classList.remove('hidden'); 1138 activeFileContainer.classList.add('flex'); 1139 1140 let displayName = Utils.getDisplayName(file); 1141 if (window.innerWidth <= 768) { 1142 displayName = Utils.getMobileShortName(file.name, file.ext); 1143 } 1144 1145 nameEl.textContent = displayName; 1146 unsavedEl.classList.toggle('hidden', !file.unsaved); 1147 } else { 1148 placeholder.classList.remove('hidden'); 1149 activeFileContainer.classList.add('hidden'); 1150 activeFileContainer.classList.remove('flex'); 1151 nameEl.textContent = 'No file open'; 1152 unsavedEl.classList.add('hidden'); 1153 if(state.editor) { 1154 state.editor.setValue('', -1); 1155 state.editor.setReadOnly(true); 1156 } 1157 } 1158 }, 1159 1160 switchShareTab: (tab) => { 1161 document.getElementById('tab-local').classList.toggle('hidden', tab !== 'local'); 1162 document.getElementById('tab-cloud').classList.toggle('hidden', tab !== 'cloud'); 1163 1164 document.getElementById('tab-btn-local').className = `flex-1 py-3 text-sm font-medium border-b-2 transition-colors ${tab === 'local' ? 'border-veyrix-accent text-veyrix-accent' : 'border-transparent text-veyrix-muted hover:text-white'}`; 1165 document.getElementById('tab-btn-cloud').className = `flex-1 py-3 text-sm font-medium border-b-2 transition-colors ${tab === 'cloud' ? 'border-veyrix-accent text-veyrix-accent' : 'border-transparent text-veyrix-muted hover:text-white'}`; 1166 }, 1167 1168 copyToClipboard: (elementId) => { 1169 const el = document.getElementById(elementId); 1170 el.select(); 1171 document.execCommand('copy'); 1172 UI.toast('Link copied to clipboard'); 1173 } 1174 }; 1175 1176 // Cloud & Firebase Features 1177 const Cloud = { 1178 firebaseLoaded: false, 1179 loadFirebase: async () => { 1180 if (Cloud.firebaseLoaded) return true; 1181 try { 1182 const [app, auth, firestore] = await Promise.all([ 1183 import('https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js'), 1184 import('https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js'), 1185 import('https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js') 1186 ]); 1187 1188 const fbApp = app.initializeApp(firebaseConfig); 1189 const fbAuth = auth.getAuth(fbApp); 1190 const fbDb = firestore.getFirestore(fbApp); 1191 1192 await auth.signInAnonymously(fbAuth); 1193 1194 window.fbDb = fbDb; 1195 window.fbFirestore = firestore; 1196 Cloud.firebaseLoaded = true; 1197 return true; 1198 } catch (e) { 1199 console.error(e); 1200 UI.toast('Failed to initialize secure cloud share', 'error'); 1201 return false; 1202 } 1203 }, 1204 generateLocalShare: () => { 1205 const file = state.files.find(f => f.id === state.activeFileId); 1206 if(!file) return UI.toast('No file to share', 'error'); 1207 1208 const compressed = LZString.compressToEncodedURIComponent(file.content); 1209 const url = `${window.location.origin}${window.location.pathname}?ext=${file.ext}&code=${compressed}`; 1210 1211 const resDiv = document.getElementById('local-share-result'); 1212 const urlInput = document.getElementById('local-share-url'); 1213 urlInput.value = url; 1214 resDiv.classList.remove('hidden'); 1215 }, 1216 generateCloudShare: async () => { 1217 const file = state.files.find(f => f.id === state.activeFileId); 1218 if(!file) return UI.toast('No file to share', 'error'); 1219 1220 const btn = document.getElementById('btn-cloud-share'); 1221 btn.innerHTML = `<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Uploading...`; 1222 btn.disabled = true; 1223 1224 if(await Cloud.loadFirebase()) { 1225 try { 1226 const pass = document.getElementById('cloud-password').value; 1227 const passHash = await Utils.hashPassword(pass); 1228 1229 const data = { 1230 content: file.content, 1231 ext: file.ext, 1232 name: file.name, 1233 timestamp: window.fbFirestore.serverTimestamp(), 1234 protected: !!passHash, 1235 hash: passHash 1236 }; 1237 1238 const docRef = await window.fbFirestore.addDoc(window.fbFirestore.collection(window.fbDb, "shared_snippets"), data); 1239 1240 const url = `${window.location.origin}${window.location.pathname}?id=${docRef.id}`; 1241 const resDiv = document.getElementById('cloud-share-result'); 1242 const urlInput = document.getElementById('cloud-share-url'); 1243 urlInput.value = url; 1244 resDiv.classList.remove('hidden'); 1245 UI.toast('Uploaded to secure cloud'); 1246 } catch(e) { 1247 console.error(e); 1248 UI.toast('Upload failed', 'error'); 1249 } 1250 } 1251 1252 btn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg> Upload & Share`; 1253 btn.disabled = false; 1254 }, 1255 currentCloudDoc: null, 1256 handleCloudLoad: async (id) => { 1257 UI.toast('Loading from cloud...'); 1258 if(await Cloud.loadFirebase()) { 1259 try { 1260 const docRef = window.fbFirestore.doc(window.fbDb, "shared_snippets", id); 1261 const docSnap = await window.fbFirestore.getDoc(docRef); 1262 1263 if (docSnap.exists()) { 1264 const data = docSnap.data(); 1265 if(data.protected) { 1266 Cloud.currentCloudDoc = data; 1267 UI.showModal('password-modal'); 1268 } else { 1269 App.loadExternalContent(data.name || 'shared', data.ext, data.content); 1270 } 1271 } else { 1272 UI.toast('Cloud file not found', 'error'); 1273 } 1274 } catch(e) { 1275 console.error(e); 1276 UI.toast('Failed to load cloud file', 'error'); 1277 } 1278 } 1279 }, 1280 verifyAndLoadCloudFile: async () => { 1281 const pass = document.getElementById('access-password').value; 1282 const hash = await Utils.hashPassword(pass); 1283 if(hash === Cloud.currentCloudDoc.hash) { 1284 UI.hideModal(); 1285 App.loadExternalContent(Cloud.currentCloudDoc.name || 'shared', Cloud.currentCloudDoc.ext, Cloud.currentCloudDoc.content); 1286 Cloud.currentCloudDoc = null; 1287 document.getElementById('access-password').value = ''; 1288 } else { 1289 UI.toast('Incorrect password', 'error'); 1290 } 1291 }, 1292 nativeShare: async () => { 1293 const file = state.files.find(f => f.id === state.activeFileId); 1294 if(!file) return UI.toast('No file to share', 'error'); 1295 try { 1296 await navigator.share({ 1297 title: Utils.getDisplayName(file), 1298 text: file.content, 1299 url: window.location.href 1300 }); 1301 } catch (err) { 1302 console.log('Share canceled or failed'); 1303 } 1304 } 1305 }; 1306 1307 // Core Application Logic 1308 const App = { 1309 init: async () => { 1310 // Init Ace Editor 1311 state.editor = ace.edit("editor"); 1312 state.editor.setTheme("ace/theme/tomorrow_night_eighties"); 1313 state.editor.setOptions({ 1314 fontSize: "14px", 1315 showPrintMargin: false, 1316 wrap: true, 1317 enableBasicAutocompletion: true, 1318 enableLiveAutocompletion: true, 1319 fontFamily: "'Fira Code', 'Consolas', monospace" 1320 }); 1321 1322 // Exact viewport fix for mobile keyboards 1323 if (window.visualViewport) { 1324 const adjustViewport = () => { 1325 const vh = window.visualViewport.height; 1326 const headerHeight = document.querySelector('header').offsetHeight; 1327 1328 // Pin body to actual visual height 1329 document.body.style.height = vh + 'px'; 1330 1331 // Allow exact remainder height to the main workspace 1332 const mainArea = document.getElementById('main-workspace'); 1333 if (mainArea) { 1334 mainArea.style.height = (vh - headerHeight) + 'px'; 1335 } 1336 1337 if (state.editor) { 1338 state.editor.resize(); 1339 } 1340 }; 1341 window.visualViewport.addEventListener('resize', adjustViewport); 1342 adjustViewport(); 1343 } 1344 1345 // Set version displays 1346 const versionDisplay = document.getElementById('app-version-display'); 1347 if (versionDisplay) versionDisplay.textContent = `v${APP_VERSION}`; 1348 1349 const settingsVersionDisplay = document.getElementById('settings-version-display'); 1350 if (settingsVersionDisplay) settingsVersionDisplay.textContent = `Version: v${APP_VERSION}`; 1351 1352 // Check version for onboarding modal 1353 const storedVersion = localStorage.getItem('veyrix_version'); 1354 if (storedVersion !== APP_VERSION) { 1355 setTimeout(() => { 1356 UI.showModal('help-modal'); 1357 localStorage.setItem('veyrix_version', APP_VERSION); 1358 }, 500); // Small delay to let the UI finish rendering first 1359 } 1360 1361 // Feature Detect Native Share 1362 if (navigator.share) { 1363 const nativeShareSection = document.getElementById('native-share-section'); 1364 if(nativeShareSection) { 1365 nativeShareSection.classList.remove('hidden'); 1366 } 1367 } 1368 1369 // --- Dot-Space Auto Fix Implementation --- 1370 const handleDotSpace = (e) => { 1371 const el = e.target; 1372 const val = el.value; 1373 const selStart = el.selectionStart; 1374 1375 // Detect if ". " exists exactly right before the cursor 1376 if (selStart >= 2 && val.substring(selStart - 2, selStart) === '. ') { 1377 el.value = val.substring(0, selStart - 1) + val.substring(selStart); 1378 el.setSelectionRange(selStart - 1, selStart - 1); 1379 } 1380 }; 1381 1382 const newFileInput = document.getElementById('new-file-input'); 1383 if(newFileInput) newFileInput.addEventListener('input', handleDotSpace); 1384 1385 const renameFileInput = document.getElementById('rename-file-input'); 1386 if(renameFileInput) renameFileInput.addEventListener('input', handleDotSpace); 1387 1388 // --- Enter Key Listeners for Modals --- 1389 newFileInput.addEventListener('keydown', (e) => { 1390 if (e.key === 'Enter') { 1391 e.preventDefault(); 1392 App.createNewFile(); 1393 } 1394 }); 1395 1396 renameFileInput.addEventListener('keydown', (e) => { 1397 if (e.key === 'Enter') { 1398 e.preventDefault(); 1399 App.executeRename(); 1400 } 1401 }); 1402 1403 // Global Event Delegation for Clicks (Closes sidebars & dropdowns) 1404 document.addEventListener('click', (e) => { 1405 // Dropdowns 1406 if (!e.target.closest('#global-file-dropdown') && !e.target.closest('.dd-trigger')) { 1407 UI.closeAllDropdowns(); 1408 } 1409 1410 // Outside Sidebar Click 1411 const sidebar = document.getElementById('sidebar'); 1412 const toggleBtn = e.target.closest('.sidebar-toggle-btn'); 1413 const isSidebarClick = e.target.closest('#sidebar'); 1414 const isBackdropClick = e.target.id === 'sidebar-backdrop'; 1415 1416 if (!sidebar.classList.contains('sidebar-collapsed')) { 1417 // If it's a mobile backdrop click, OR an external desktop click 1418 if (isBackdropClick || (!isSidebarClick && !toggleBtn)) { 1419 UI.toggleSidebar(true); 1420 } 1421 } 1422 }); 1423 1424 // Window resize listeners 1425 window.addEventListener('resize', Utils.debounce(() => { 1426 UI.updateTopBar(); 1427 }, 200)); 1428 1429 // Swipe to open/close sidebar 1430 let touchStartX = 0; 1431 let touchStartY = 0; 1432 document.addEventListener('touchstart', e => { 1433 touchStartX = e.changedTouches[0].screenX; 1434 touchStartY = e.changedTouches[0].screenY; 1435 }, {passive: true}); 1436 1437 document.addEventListener('touchend', e => { 1438 let touchEndX = e.changedTouches[0].screenX; 1439 let touchEndY = e.changedTouches[0].screenY; 1440 if (Math.abs(touchEndX - touchStartX) > Math.abs(touchEndY - touchStartY)) { 1441 if (touchEndX - touchStartX > 80 && touchStartX < 50) { 1442 const sb = document.getElementById('sidebar'); 1443 if (sb.classList.contains('sidebar-collapsed')) UI.toggleSidebar(); // open 1444 } else if (touchStartX - touchEndX > 80) { 1445 const sb = document.getElementById('sidebar'); 1446 if (!sb.classList.contains('sidebar-collapsed')) UI.toggleSidebar(true); // close 1447 } 1448 } 1449 }, {passive: true}); 1450 1451 state.editor.on("change", Utils.debounce(() => { 1452 if(state.activeFileId) { 1453 const file = state.files.find(f => f.id === state.activeFileId); 1454 if(file && file.content !== state.editor.getValue()) { 1455 file.content = state.editor.getValue(); 1456 if(!file.unsaved) { 1457 file.unsaved = true; 1458 UI.renderSidebar(); 1459 UI.updateTopBar(); 1460 } 1461 } 1462 } 1463 }, 500)); 1464 1465 // Close modals on overlay click 1466 document.getElementById('modal-overlay').addEventListener('click', (e) => { 1467 if(e.target.id === 'modal-overlay') UI.hideModal(); 1468 }); 1469 1470 // Keyboard Shortcuts 1471 window.addEventListener('keydown', (e) => { 1472 if (e.ctrlKey && e.key === 's') { e.preventDefault(); App.saveCurrentFile(); } 1473 if (e.ctrlKey && e.key === 'o') { e.preventDefault(); document.getElementById('file-upload').click(); } 1474 if (e.altKey && e.shiftKey && e.key.toLowerCase() === 'f') { e.preventDefault(); App.formatCode(); } 1475 if (e.key === 'Escape') { 1476 UI.hideModal(); 1477 UI.closeAllDropdowns(); 1478 const sb = document.getElementById('sidebar'); 1479 if (sb && !sb.classList.contains('sidebar-collapsed')) { 1480 UI.toggleSidebar(true); 1481 } 1482 } 1483 }); 1484 1485 // Init DB & State Setup 1486 try { 1487 await DB.init(); 1488 state.files = await DB.getAll(); 1489 1490 // Apply Initial Sidebar State 1491 const savedState = localStorage.getItem('veyrix_sidebar'); 1492 const sb = document.getElementById('sidebar'); 1493 const hamburger = document.querySelector('.hamburger'); 1494 if (savedState === 'closed') { 1495 sb.classList.add('sidebar-collapsed'); 1496 if(hamburger) { hamburger.classList.remove('is-active'); hamburger.setAttribute('aria-expanded', 'false'); } 1497 } else if (savedState === 'open') { 1498 sb.classList.remove('sidebar-collapsed'); 1499 if(hamburger) { hamburger.classList.add('is-active'); hamburger.setAttribute('aria-expanded', 'true'); } 1500 } else { 1501 // Default fallback: closed on mobile, open on desktop 1502 if (window.innerWidth <= 768) { 1503 sb.classList.add('sidebar-collapsed'); 1504 if(hamburger) { hamburger.classList.remove('is-active'); hamburger.setAttribute('aria-expanded', 'false'); } 1505 } else { 1506 sb.classList.remove('sidebar-collapsed'); 1507 if(hamburger) { hamburger.classList.add('is-active'); hamburger.setAttribute('aria-expanded', 'true'); } 1508 } 1509 } 1510 1511 UI.renderSidebar(); 1512 1513 const lastActive = localStorage.getItem('veyrix_active'); 1514 if(lastActive && state.files.find(f => f.id === lastActive)) { 1515 App.openFile(lastActive); 1516 } else { 1517 UI.updateTopBar(); 1518 } 1519 } catch(e) { 1520 UI.toast('Failed to load local DB', 'error'); 1521 } 1522 1523 // Handle URL Parameters (Sharing) 1524 const params = new URLSearchParams(window.location.search); 1525 if(params.has('ext') && params.has('code')) { 1526 const ext = params.get('ext'); 1527 const code = LZString.decompressFromEncodedURIComponent(params.get('code')); 1528 if(code !== null) { 1529 App.loadExternalContent('shared_snippet', ext, code); 1530 } 1531 } else if(params.has('id')) { 1532 Cloud.handleCloudLoad(params.get('id')); 1533 } 1534 }, 1535 1536 installApp: async () => { 1537 if (deferredPrompt) { 1538 deferredPrompt.prompt(); 1539 const { outcome } = await deferredPrompt.userChoice; 1540 if (outcome === 'accepted') { 1541 deferredPrompt = null; 1542 const btn = document.getElementById('install-app-btn'); 1543 btn.classList.add('opacity-50', 'cursor-not-allowed'); 1544 btn.disabled = true; 1545 } 1546 } 1547 }, 1548 1549 createNewFile: async () => { 1550 const input = document.getElementById('new-file-input').value.trim(); 1551 if(!input) return UI.toast('Filename cannot be empty', 'error'); 1552 1553 let name = input; 1554 let ext = ''; 1555 1556 const lastDotIdx = input.lastIndexOf('.'); 1557 if (lastDotIdx > 0 && lastDotIdx < input.length - 1) { 1558 ext = input.substring(lastDotIdx + 1); 1559 name = input.substring(0, lastDotIdx); 1560 } else if (lastDotIdx === 0) { 1561 name = input; 1562 ext = ''; 1563 } else if (lastDotIdx === input.length - 1) { 1564 name = input.substring(0, lastDotIdx); 1565 ext = ''; 1566 } 1567 1568 const newFile = { 1569 id: Utils.generateId(), 1570 name, 1571 ext, 1572 content: '', 1573 unsaved: true 1574 }; 1575 1576 state.files.push(newFile); 1577 await DB.save(newFile); 1578 UI.hideModal(); 1579 document.getElementById('new-file-input').value = ''; 1580 App.openFile(newFile.id); 1581 UI.toast('File created'); 1582 }, 1583 1584 openFile: (id) => { 1585 const file = state.files.find(f => f.id === id); 1586 if(!file) return; 1587 1588 state.activeFileId = id; 1589 localStorage.setItem('veyrix_active', id); 1590 1591 state.editor.setReadOnly(false); 1592 state.editor.setValue(file.content, -1); 1593 1594 const mode = Utils.getModeFromExt(file.ext, file.name); 1595 state.editor.session.setMode(`ace/mode/${mode}`); 1596 1597 UI.renderSidebar(); 1598 UI.updateTopBar(); 1599 1600 if(window.innerWidth <= 768) { 1601 UI.toggleSidebar(true); // force close on mobile selection 1602 } 1603 }, 1604 1605 saveCurrentFile: async () => { 1606 if(!state.activeFileId) return; 1607 const file = state.files.find(f => f.id === state.activeFileId); 1608 if(!file) return; 1609 1610 file.content = state.editor.getValue(); 1611 file.unsaved = false; 1612 await DB.save(file); 1613 1614 UI.renderSidebar(); 1615 UI.updateTopBar(); 1616 UI.toast('File saved'); 1617 }, 1618 1619 triggerDownload: async (id = null, e = null) => { 1620 if(e) e.stopPropagation(); 1621 UI.closeAllDropdowns(); 1622 1623 const fileId = id || state.activeFileId; 1624 if(!fileId) return UI.toast('No file to download', 'error'); 1625 1626 const file = state.files.find(f => f.id === fileId); 1627 if(!file) return; 1628 1629 const content = file.id === state.activeFileId ? state.editor.getValue() : file.content; 1630 const fileName = Utils.getDisplayName(file); 1631 1632 if (navigator.canShare) { 1633 try { 1634 const fileObj = new File([content], fileName, { type: 'application/octet-stream' }); 1635 if (navigator.canShare({ files: [fileObj] })) { 1636 await navigator.share({ 1637 files: [fileObj], 1638 title: fileName 1639 }); 1640 return; 1641 } 1642 } catch (err) { 1643 console.log('Share canceled or failed, falling back to native download', err); 1644 } 1645 } 1646 1647 const blob = new Blob([content], { type: 'application/octet-stream' }); 1648 const url = URL.createObjectURL(blob); 1649 1650 const a = document.createElement('a'); 1651 a.href = url; 1652 a.download = fileName; 1653 document.body.appendChild(a); 1654 a.click(); 1655 document.body.removeChild(a); 1656 URL.revokeObjectURL(url); 1657 1658 UI.toast('Download started'); 1659 }, 1660 1661 triggerSnapshot: () => { 1662 if(!state.activeFileId) return UI.toast('No file open', 'error'); 1663 const file = state.files.find(f => f.id === state.activeFileId); 1664 if(!file) return; 1665 1666 file.snapshots = file.snapshots || []; 1667 if(file.snapshots.length >= 5) { 1668 const oldest = file.snapshots[0]; 1669 const date = new Date(oldest.ts).toLocaleString(); 1670 document.getElementById('snapshot-limit-oldest-info').textContent = `Version ${oldest.version || 1} - ${date}`; 1671 UI.showModal('snapshot-limit-modal'); 1672 } else { 1673 App.executeSnapshotSave(false); 1674 } 1675 }, 1676 1677 executeSnapshotSave: async (replaceOldest = false) => { 1678 const file = state.files.find(f => f.id === state.activeFileId); 1679 if(!file) return; 1680 1681 file.snapshots = file.snapshots || []; 1682 file.snapshotCounter = (file.snapshotCounter || file.snapshots.length) + 1; 1683 1684 if (replaceOldest) { 1685 file.snapshots.shift(); 1686 UI.hideModal(); 1687 } 1688 1689 file.snapshots.push({ 1690 ts: Date.now(), 1691 content: state.editor.getValue(), 1692 version: file.snapshotCounter 1693 }); 1694 1695 await DB.save(file); 1696 UI.toast('Snapshot saved to history'); 1697 }, 1698 1699 triggerRestore: (index) => { 1700 UI.showConfirm( 1701 "Restore Snapshot", 1702 "Are you sure you want to restore this snapshot? Current unsaved changes will be lost.", 1703 "Restore", 1704 () => App.restoreSnapshot(index) 1705 ); 1706 }, 1707 1708 restoreSnapshot: async (index) => { 1709 const file = state.files.find(f => f.id === state.activeFileId); 1710 if(!file || !file.snapshots || !file.snapshots[index]) return; 1711 1712 state.editor.setValue(file.snapshots[index].content, -1); 1713 UI.toast('Restored previous version'); 1714 App.saveCurrentFile(); // auto save after restore 1715 }, 1716 1717 triggerRename: async (id, e) => { 1718 if(e) e.stopPropagation(); 1719 UI.closeAllDropdowns(); 1720 const file = state.files.find(f => f.id === id); 1721 if(!file) return; 1722 1723 state.renameTargetId = id; 1724 const input = document.getElementById('rename-file-input'); 1725 input.value = Utils.getDisplayName(file); 1726 UI.showModal('rename-file-modal'); 1727 }, 1728 1729 executeRename: async () => { 1730 const id = state.renameTargetId; 1731 const file = state.files.find(f => f.id === id); 1732 if(!file) return; 1733 1734 const input = document.getElementById('rename-file-input').value.trim(); 1735 if(!input) return UI.toast('Filename cannot be empty', 'error'); 1736 1737 const lastDotIdx = input.lastIndexOf('.'); 1738 1739 if (lastDotIdx > 0 && lastDotIdx < input.length - 1) { 1740 file.ext = input.substring(lastDotIdx + 1); 1741 file.name = input.substring(0, lastDotIdx); 1742 } else if (lastDotIdx === 0) { 1743 file.name = input; 1744 file.ext = ''; 1745 } else if (lastDotIdx === input.length - 1) { 1746 file.name = input.substring(0, lastDotIdx); 1747 file.ext = ''; 1748 } else { 1749 file.name = input; 1750 file.ext = ''; 1751 } 1752 1753 await DB.save(file); 1754 UI.hideModal(); 1755 UI.renderSidebar(); 1756 UI.updateTopBar(); 1757 UI.toast('File renamed'); 1758 }, 1759 1760 triggerShare: async (id, e) => { 1761 if(e) e.stopPropagation(); 1762 UI.closeAllDropdowns(); 1763 App.openFile(id); 1764 UI.showModal('share-modal'); 1765 }, 1766 1767 triggerDelete: async (id, e) => { 1768 if(e) e.stopPropagation(); 1769 UI.closeAllDropdowns(); 1770 1771 UI.showConfirm( 1772 "Delete File", 1773 "Are you sure you want to delete this file? This action cannot be undone.", 1774 "Delete", 1775 async () => { 1776 await DB.delete(id); 1777 state.files = state.files.filter(f => f.id !== id); 1778 if(state.activeFileId === id) { 1779 state.activeFileId = null; 1780 localStorage.removeItem('veyrix_active'); 1781 UI.updateTopBar(); 1782 } 1783 UI.renderSidebar(); 1784 UI.toast('File deleted'); 1785 } 1786 ); 1787 }, 1788 1789 handleFileUpload: (e) => { 1790 const file = e.target.files[0]; 1791 if(!file) return; 1792 1793 // Validation for common non-text binaries (mobile native pickers might circumvent accept filter) 1794 if(file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/') || file.type === 'application/pdf' || file.name.match(/\.(exe|dll|bin|zip|rar|tar|gz|7z|apk|dmg|iso)$/i)) { 1795 e.target.value = ''; 1796 return UI.toast('Invalid file type (Text/Code only)', 'error'); 1797 } 1798 1799 const reader = new FileReader(); 1800 reader.onload = async (ev) => { 1801 const input = file.name; 1802 let name = input; 1803 let ext = ''; 1804 1805 const lastDotIdx = input.lastIndexOf('.'); 1806 if (lastDotIdx > 0 && lastDotIdx < input.length - 1) { 1807 ext = input.substring(lastDotIdx + 1); 1808 name = input.substring(0, lastDotIdx); 1809 } else if (lastDotIdx === 0) { 1810 name = input; 1811 ext = ''; 1812 } else if (lastDotIdx === input.length - 1) { 1813 name = input.substring(0, lastDotIdx); 1814 ext = ''; 1815 } 1816 1817 const newFile = { 1818 id: Utils.generateId(), 1819 name, 1820 ext, 1821 content: ev.target.result, 1822 unsaved: false 1823 }; 1824 1825 state.files.push(newFile); 1826 await DB.save(newFile); 1827 App.openFile(newFile.id); 1828 UI.toast('File imported'); 1829 }; 1830 reader.readAsText(file); 1831 e.target.value = ''; // reset 1832 }, 1833 1834 loadExternalContent: (name, ext, content) => { 1835 // Creates a temporary file that won't auto-save to DB unless specifically saved 1836 const file = { 1837 id: 'temp_' + Date.now(), 1838 name, 1839 ext, 1840 content, 1841 unsaved: true // implies it needs saving 1842 }; 1843 state.files.push(file); 1844 App.openFile(file.id); 1845 document.getElementById('banner-warning').classList.remove('hidden'); 1846 window.history.replaceState({}, document.title, window.location.pathname); 1847 }, 1848 1849 formatCode: () => { 1850 if(!state.activeFileId) return; 1851 const file = state.files.find(f => f.id === state.activeFileId); 1852 if(!file) return; 1853 1854 try { 1855 const beautify = ace.require("ace/ext/beautify"); 1856 beautify.beautify(state.editor.session); 1857 UI.toast('Document formatted'); 1858 } catch(e) { 1859 UI.toast('Formatting not supported for this language', 'error'); 1860 } 1861 }, 1862 1863 toggleFullscreen: () => { 1864 if (!document.fullscreenElement) { 1865 document.documentElement.requestFullscreen().catch(() => UI.toast('Fullscreen failed', 'error')); 1866 } else { 1867 if (document.exitFullscreen) document.exitFullscreen(); 1868 } 1869 }, 1870 1871 factoryReset: async () => { 1872 if(confirm('Are you sure? This will delete all local files and cannot be undone.')) { 1873 const request = indexedDB.deleteDatabase('VeyrixFS'); 1874 request.onsuccess = () => { 1875 localStorage.clear(); 1876 window.location.reload(); 1877 }; 1878 } 1879 } 1880 }; 1881 1882 // Start application 1883 window.onload = App.init; 1884 1885 // Register Service Worker for PWA 1886 if ('serviceWorker' in navigator) { 1887 window.addEventListener('load', () => { 1888 navigator.serviceWorker.register('assets/sw.js').catch(err => { 1889 console.log('SW registration failed: ', err); 1890 }); 1891 }); 1892 } 1893 </script> 1894 </body> 1895 1896 </html>