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