music video renderer web app deftoku.digi.rip
audio visualizer webapp
1

Configure Feed

Select the types of activity you want to include in your feed.

ui: bandaid fix

- not a fan of how the menus overlay the preview canvas, will fix later

digi.rip (Jun 23, 2026, 9:15 AM EDT) 8b771059 6215f543

+385 -6
+4
index.html
··· 27 27 <noscript> you need to enable javascript to run this app. </noscript> 28 28 29 29 <div class="menubar"> 30 + <button class="hamburger" aria-label="menu">☰</button> 30 31 <div class="menu-group"> 31 32 <button class="menu-trigger" data-menu="file">file</button> 32 33 <div class="menu-panel hidden" id="panel-file"> ··· 91 92 <div id="vis-params"></div> 92 93 </div> 93 94 </div> 95 + 96 + <div class="mobile-backdrop hidden"></div> 97 + <div class="mobile-drawer hidden" id="mobile-drawer"></div> 94 98 95 99 <div class="main-content"> 96 100 <canvas id="canvas" width="1920" height="1080"></canvas>
+129 -6
src/ui/menu-bar.ts
··· 1 1 function initMenubar() { 2 + const menubar = document.querySelector<HTMLDivElement>(".menubar")!; 3 + const hamburger = menubar.querySelector<HTMLButtonElement>(".hamburger")!; 4 + const backdrop = document.querySelector<HTMLDivElement>(".mobile-backdrop")!; 5 + const drawer = document.querySelector<HTMLDivElement>("#mobile-drawer")!; 2 6 const triggers = document.querySelectorAll<HTMLButtonElement>( 3 7 ".menu-trigger", 4 8 ); 5 9 const panels = document.querySelectorAll<HTMLDivElement>(".menu-panel"); 6 10 11 + // mobile check 12 + const mq = globalThis.matchMedia("(max-width: 768px)"); 13 + 14 + function moveToDrawer() { 15 + if (drawer.children.length > 0) return; // already moved 16 + 17 + let fileGroup: HTMLDivElement | null = null; 18 + let settingsGroup: HTMLDivElement | null = null; 19 + for ( 20 + const g of menubar.querySelectorAll<HTMLDivElement>( 21 + ":scope > .menu-group", 22 + ) 23 + ) { 24 + if (g.querySelector("[data-menu=file]")) fileGroup = g; 25 + if (g.querySelector("[data-menu=output]")) settingsGroup = g; 26 + } 27 + const visParams = document.querySelector<HTMLDivElement>("#vis-params")!; 28 + const imgGroup = document.querySelector<HTMLDivElement>("#img-group")!; 29 + 30 + // section label helper 31 + const label = (text: string) => { 32 + const el = document.createElement("div"); 33 + el.className = "drawer-section-label"; 34 + el.textContent = text; 35 + return el; 36 + }; 37 + 38 + if (visParams) { 39 + drawer.appendChild(label("visualizer params")); 40 + drawer.appendChild(visParams); 41 + } 42 + if (imgGroup) { 43 + drawer.appendChild(imgGroup); 44 + } 45 + if (fileGroup) { 46 + drawer.appendChild(label("file")); 47 + drawer.appendChild(fileGroup); 48 + } 49 + if (settingsGroup) { 50 + drawer.appendChild(label("settings")); 51 + drawer.appendChild(settingsGroup); 52 + } 53 + } 54 + 55 + function moveBack() { 56 + while (drawer.firstChild) { 57 + const child = drawer.firstChild; 58 + if (child instanceof HTMLElement && child.id === "vis-params") { 59 + const visGroup = document.querySelector<HTMLDivElement>("#vis-group")!; 60 + visGroup.appendChild(child); 61 + } else if ( 62 + child instanceof HTMLElement && child.id === "img-group" 63 + ) { 64 + const visGroup = document.querySelector<HTMLDivElement>("#vis-group")!; 65 + visGroup.appendChild(child); 66 + } else if ( 67 + child instanceof HTMLElement && child.classList.contains("menu-group") 68 + ) { 69 + menubar.appendChild(child); 70 + } else { 71 + // section labels etc — just remove 72 + child.remove(); 73 + } 74 + } 75 + } 76 + 77 + function applyLayout() { 78 + if (mq.matches) { 79 + moveToDrawer(); 80 + } else { 81 + moveBack(); 82 + } 83 + } 84 + 85 + applyLayout(); 86 + mq.addEventListener("change", applyLayout); 87 + 88 + function isOpen() { 89 + return !drawer.classList.contains("hidden"); 90 + } 91 + 92 + function openDrawer() { 93 + drawer.classList.remove("hidden"); 94 + backdrop.classList.remove("hidden"); 95 + hamburger.textContent = "✕"; 96 + document.body.classList.add("mobile-open"); 97 + } 98 + 99 + function closeDrawer() { 100 + drawer.classList.add("hidden"); 101 + backdrop.classList.add("hidden"); 102 + hamburger.textContent = "☰"; 103 + document.body.classList.remove("mobile-open"); 104 + closeAll(); 105 + } 106 + 107 + hamburger.addEventListener("click", (e) => { 108 + e.stopPropagation(); 109 + if (isOpen()) { 110 + closeDrawer(); 111 + } else { 112 + openDrawer(); 113 + } 114 + }); 115 + 116 + backdrop.addEventListener("click", () => { 117 + closeDrawer(); 118 + }); 119 + 7 120 function panelFor(trigger: HTMLElement): HTMLDivElement | null { 8 121 const menu = trigger.dataset.menu; 9 122 if (!menu) return null; ··· 36 149 }); 37 150 }); 38 151 39 - // hover to switch (macOS style) 152 + // hover to switch (macOS style) — desktop only 40 153 triggers.forEach((trigger) => { 41 154 trigger.addEventListener("mouseenter", () => { 155 + if (mq.matches) return; // no hover switching on mobile 42 156 const anyOpen = document.querySelector(".menu-trigger.open"); 43 157 if (anyOpen) { 44 158 open(trigger); ··· 48 162 49 163 // click outside → close 50 164 document.addEventListener("click", (e) => { 51 - const clickedInside = (e.target as HTMLElement).closest(".menubar"); 52 - if (!clickedInside) { 53 - closeAll(); 165 + const target = e.target as HTMLElement; 166 + const clickedInside = target.closest(".menubar"); 167 + const clickedDrawer = target.closest("#mobile-drawer"); 168 + if (!clickedInside && !clickedDrawer) { 169 + if (mq.matches) { 170 + closeDrawer(); 171 + } else { 172 + closeAll(); 173 + } 54 174 } 55 175 }); 56 176 57 - // escape → close 58 177 document.addEventListener("keydown", (e) => { 59 178 if (e.key === "Escape") { 60 - closeAll(); 179 + if (mq.matches) { 180 + closeDrawer(); 181 + } else { 182 + closeAll(); 183 + } 61 184 } 62 185 }); 63 186
+252
style.css
··· 316 316 padding: 0; 317 317 line-height: 1; 318 318 } 319 + 320 + .hamburger { 321 + display: none; 322 + background: none; 323 + border: 1px solid var(--border-light); 324 + color: var(--color-text-dim); 325 + font-size: 1.1rem; 326 + padding: var(--space-xs) var(--space-md); 327 + border-radius: var(--radius-sm); 328 + cursor: pointer; 329 + line-height: 1; 330 + } 331 + 332 + .hamburger:hover { 333 + background: rgba(255, 255, 255, 0.1); 334 + border-color: var(--border-medium); 335 + } 336 + 337 + /* mobile */ 338 + @media (max-width: 768px) { 339 + body { 340 + padding-top: 3rem; 341 + } 342 + 343 + /* top bar */ 344 + .hamburger { 345 + display: block; 346 + flex-shrink: 0; 347 + font-size: 1.25rem; 348 + padding: 6px 12px; 349 + } 350 + 351 + .menubar { 352 + flex-wrap: nowrap; 353 + gap: var(--space-sm); 354 + padding: var(--space-sm) var(--space-md); 355 + min-height: 3rem; 356 + } 357 + 358 + .menubar > .menu-group:not(#vis-group) { 359 + display: none; 360 + } 361 + 362 + #vis-params { 363 + display: none; 364 + } 365 + 366 + #vis-group { 367 + border-left: none; 368 + padding: 0; 369 + gap: 0; 370 + margin-left: auto; 371 + } 372 + 373 + #vis-select { 374 + font-size: 1rem; 375 + padding: 6px 12px; 376 + } 377 + 378 + .mobile-backdrop { 379 + position: fixed; 380 + inset: 0; 381 + background: rgba(0, 0, 0, 0.5); 382 + backdrop-filter: blur(4px); 383 + -webkit-backdrop-filter: blur(4px); 384 + z-index: 997; 385 + } 386 + 387 + .mobile-backdrop.hidden { 388 + display: none; 389 + } 390 + 391 + .mobile-drawer { 392 + position: fixed; 393 + top: 3rem; 394 + left: 0; 395 + right: 0; 396 + bottom: 0; 397 + z-index: 998; 398 + 399 + display: flex; 400 + flex-direction: column; 401 + gap: var(--space-md); 402 + 403 + background: var(--color-panel-bg); 404 + border-top: 1px solid var(--border-medium); 405 + overflow-y: auto; 406 + padding: var(--space-lg); 407 + } 408 + 409 + .mobile-drawer.hidden { 410 + display: none; 411 + } 412 + 413 + .drawer-section-label { 414 + display: block; 415 + padding: var(--space-sm) 0 var(--space-xs); 416 + margin-top: var(--space-sm); 417 + color: var(--color-muted); 418 + font-size: 0.7rem; 419 + text-transform: uppercase; 420 + letter-spacing: 0.1em; 421 + border-bottom: 1px solid var(--border-subtle); 422 + } 423 + 424 + .drawer-section-label:first-child { 425 + margin-top: 0; 426 + } 427 + 428 + .mobile-drawer .menu-group { 429 + display: flex; 430 + flex-direction: column; 431 + gap: 0; 432 + } 433 + 434 + .mobile-drawer .menu-trigger { 435 + width: 100%; 436 + text-align: left; 437 + font-size: 0.95rem; 438 + padding: 12px var(--space-md); 439 + border: 1px solid var(--border-light); 440 + border-radius: var(--radius-sm); 441 + background: rgba(255, 255, 255, 0.04); 442 + color: var(--color-text-dim); 443 + } 444 + 445 + .mobile-drawer .menu-trigger.open { 446 + background: rgba(255, 255, 255, 0.1); 447 + border-color: var(--border-medium); 448 + border-bottom-left-radius: 0; 449 + border-bottom-right-radius: 0; 450 + border-bottom-color: transparent; 451 + } 452 + 453 + .mobile-drawer .menu-panel { 454 + position: static; 455 + border: 1px solid var(--border-light); 456 + border-top: none; 457 + border-radius: 0 0 var(--radius-sm) var(--radius-sm); 458 + box-shadow: none; 459 + background: rgba(255, 255, 255, 0.03); 460 + padding: var(--space-sm) 0; 461 + margin: 0; 462 + min-width: unset; 463 + width: 100%; 464 + } 465 + 466 + .mobile-drawer .menu-panel.hidden { 467 + display: none; 468 + } 469 + 470 + /* panel internals */ 471 + .mobile-drawer .menu-label { 472 + font-size: 0.7rem; 473 + margin: 0 var(--space-md); 474 + padding: 8px var(--space-md) 2px; 475 + } 476 + 477 + .mobile-drawer .menu-item { 478 + font-size: 0.95rem; 479 + padding: 10px var(--space-md); 480 + width: calc(100% - 16px); 481 + margin: 0 var(--space-md); 482 + } 483 + 484 + .mobile-drawer select { 485 + font-size: 0.95rem; 486 + padding: 8px var(--space-md); 487 + width: calc(100% - 16px); 488 + margin: 0 var(--space-md); 489 + min-height: 40px; 490 + } 491 + 492 + .mobile-drawer .import-row { 493 + margin: 0 var(--space-md); 494 + } 495 + 496 + .mobile-drawer .clear-btn { 497 + font-size: 0.9rem !important; 498 + padding: 10px 14px !important; 499 + } 500 + 501 + .mobile-drawer #vis-params { 502 + display: flex; 503 + flex-wrap: wrap; 504 + gap: var(--space-sm); 505 + align-items: center; 506 + } 507 + 508 + .mobile-drawer #vis-params .menu-label { 509 + font-size: 0.7rem; 510 + padding: 0; 511 + margin: 0; 512 + min-width: 100%; 513 + margin-top: var(--space-xs); 514 + } 515 + 516 + .mobile-drawer #vis-params input[type="range"] { 517 + width: 100px; 518 + height: 36px; 519 + accent-color: #aaa; 520 + } 521 + 522 + .mobile-drawer #vis-params input[type="number"] { 523 + width: 56px; 524 + height: 36px; 525 + font-size: 0.9rem; 526 + padding: var(--space-xs) var(--space-sm); 527 + background: rgba(255, 255, 255, 0.06); 528 + border: 1px solid var(--border-light); 529 + border-radius: var(--radius-sm); 530 + color: var(--color-text-dim); 531 + font-family: inherit; 532 + text-align: center; 533 + appearance: textfield; 534 + } 535 + 536 + .mobile-drawer #vis-params input[type="color"] { 537 + width: 36px; 538 + height: 36px; 539 + padding: 2px; 540 + background: var(--color-surface); 541 + border: 1px solid var(--border-medium); 542 + border-radius: var(--radius-sm); 543 + cursor: pointer; 544 + } 545 + 546 + .mobile-drawer #vis-params select { 547 + font-size: 0.9rem; 548 + padding: 6px 10px; 549 + min-height: 36px; 550 + width: auto; 551 + margin: 0; 552 + } 553 + 554 + .mobile-drawer #vis-params input[type="text"] { 555 + font-size: 0.9rem; 556 + padding: 6px 8px; 557 + height: 36px; 558 + width: 52px; 559 + background: rgba(255, 255, 255, 0.06); 560 + border: 1px solid var(--border-light); 561 + border-radius: var(--radius-sm); 562 + color: var(--color-text-dim); 563 + font-family: inherit; 564 + text-align: center; 565 + } 566 + 567 + .main-content { 568 + padding: var(--space-sm); 569 + } 570 + }