Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

menuband landing: drop faux menu bar, frosted strip, titlebar h1; add prod live-reload

Strip the menu bar (apple logo, File/Edit/View/Window/Help, dock-piano, clock) and
its CSS, dark-mode rules, clock IIFE, and the genie effect that flew the hero
icon into the dock-piano. Also drop the 'an aesthetic computer instrument'
frosted strip and the titlebar 'menuband' link.

Add a long-poll loop against /api/version (the same endpoint prompt.mjs uses)
so a fresh `lith/deploy.fish` triggers an auto-reload within ~5s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+49 -355
+49 -355
system/public/menuband/index.html
··· 31 31 <style> 32 32 /* ──────────────────────────────────────────────────────────────────────── 33 33 Spine: Tiger/Leopard Aqua (jellybean buttons, pinstripe, traffic lights). 34 - Flair: Tahoe/Sonoma (vibrancy plate, frosted strip, layered shadows, 34 + Flair: Tahoe/Sonoma (vibrancy plate, layered shadows, 35 35 SF Rounded display, saturated accent, glassy CTA). 36 36 The retro chrome is the canvas; the modern bits float over it. 37 37 ──────────────────────────────────────────────────────────────────────── */ ··· 84 84 linear-gradient(to bottom, var(--aqua-sky-top) 0%, var(--aqua-sky-bot) 100%) fixed; 85 85 background-attachment: fixed; 86 86 min-height: 100vh; 87 - padding: 56px 16px 64px; /* extra top room for the faux menu bar */ 88 - position: relative; 89 - } 90 - 91 - /* ── Faux macOS menu bar ──────────────────────────────────────────────── */ 92 - 93 - .macbar { 94 - position: fixed; 95 - top: 0; left: 0; right: 0; 96 - height: 24px; 97 - background: rgba(245, 247, 250, 0.72); 98 - backdrop-filter: blur(28px) saturate(180%); 99 - -webkit-backdrop-filter: blur(28px) saturate(180%); 100 - border-bottom: 0.5px solid rgba(0, 0, 0, 0.12); 101 - display: flex; 102 - align-items: center; 103 - justify-content: space-between; 104 - padding: 0 10px; 105 - font-family: var(--display-stack); 106 - font-size: 13px; 107 - color: #1d1d1f; 108 - z-index: 50; 109 - user-select: none; 110 - cursor: default; 111 - } 112 - .macbar-left, .macbar-right { 113 - display: flex; 114 - align-items: center; 115 - gap: 14px; 116 - } 117 - .macbar .apple-logo { 118 - width: 13px; height: 16px; 119 - fill: #1d1d1f; 120 - opacity: 0.9; 121 - } 122 - .macbar .menu-app { font-weight: 600; } 123 - .macbar .menu-item { font-weight: 400; } 124 - .macbar .menu-item:hover, .macbar .menu-app:hover { 125 - background: rgba(0, 0, 0, 0.08); 126 - border-radius: 4px; 127 - padding: 1px 4px; 128 - margin: -1px -4px; 129 - } 130 - 131 - /* The actual graphic — a working Menu Band piano in the menu bar. 132 - One full octave, jellybean-keyed and clickable. */ 133 - .dock-piano { 134 - position: relative; 135 - display: inline-flex; 136 - align-items: flex-start; 137 - height: 17px; 138 - padding: 1px; 139 - background: #131315; 140 - border-radius: 3px; 141 - box-shadow: 142 - 0 0 0 0.5px rgba(0, 0, 0, 0.5), 143 - inset 0 1px 0 rgba(255, 255, 255, 0.12), 144 - inset 0 -1px 0 rgba(0, 0, 0, 0.4); 145 - cursor: pointer; 146 - transition: transform 220ms var(--ease-apple), box-shadow 220ms var(--ease-apple); 147 - } 148 - .dock-piano:hover { transform: translateY(0.5px); } 149 - .dock-piano.glow { 150 - box-shadow: 151 - 0 0 0 0.5px rgba(0, 0, 0, 0.5), 152 - inset 0 1px 0 rgba(255, 255, 255, 0.12), 153 - inset 0 -1px 0 rgba(0, 0, 0, 0.4), 154 - 0 0 12px 2px rgba(10, 132, 255, 0.55); 155 - } 156 - .dock-piano .key { 157 - display: inline-block; 158 - border-radius: 0 0 1.5px 1.5px; 159 - transition: background 80ms linear; 160 - } 161 - .dock-piano .key.w { 162 - width: 5px; height: 15px; 163 - background: linear-gradient(to bottom, #fafafa 0%, #e2e2e2 80%, #cbcbcb 100%); 164 - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.18); 165 - margin-right: 1px; 166 - } 167 - .dock-piano .key.w:last-child { margin-right: 0; } 168 - .dock-piano .key.b { 169 - width: 4px; height: 9px; 170 - background: linear-gradient(to bottom, #2a2a2c, #050505); 171 - margin: 0 -2.5px 0 -2.5px; 87 + padding: 32px 16px 64px; 172 88 position: relative; 173 - z-index: 2; 174 - box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.15); 175 89 } 176 - .dock-piano .key:active, 177 - .dock-piano .key.lit.w { background: linear-gradient(to bottom, #cfe6ff, #8ec4ff); } 178 - .dock-piano .key.lit.b { background: linear-gradient(to bottom, #4a8fd1, #1f5aa8); } 179 - 180 - .macbar-right .menu-extra { 181 - font-size: 11px; 182 - opacity: 0.85; 183 - font-family: "Berkeley Mono Variable", "SF Mono", Menlo, monospace; 184 - letter-spacing: 0.02em; 185 - } 186 - .macbar-right .menu-extra.clock { font-variant-numeric: tabular-nums; } 187 - .macbar-right svg { fill: currentColor; opacity: 0.85; } 188 90 189 91 /* Subtle musical pattern tiled over the sky — quarter, eighth, beam, 190 92 flat. Tinted Aqua-edge so it reads like a watermark. Pans diagonally ··· 257 159 transition: color 220ms var(--ease-apple); 258 160 } 259 161 .titlebar h1 a:hover { color: var(--tahoe-blue); } 260 - 261 - /* Tahoe-style frosted toolbar strip just below the Tiger titlebar. The 262 - translucency lets a hint of the music drift bleed through, layering 263 - the modern OS feel on top of the retro chrome. */ 264 - .frosted { 265 - height: 36px; 266 - background: rgba(245, 247, 250, 0.72); 267 - backdrop-filter: blur(20px) saturate(160%); 268 - -webkit-backdrop-filter: blur(20px) saturate(160%); 269 - border-bottom: 0.5px solid rgba(0, 0, 0, 0.08); 270 - display: flex; 271 - align-items: center; 272 - justify-content: center; 273 - font-family: "Berkeley Mono Variable", "SF Mono", Menlo, monospace; 274 - font-size: 10px; 275 - letter-spacing: 0.18em; 276 - color: var(--ink-soft); 277 - text-transform: uppercase; 278 - user-select: none; 279 - } 280 162 281 163 /* Pinstripe section — alternating 1px lines, very subtle. */ 282 164 .pinstripe { ··· 620 502 .icon-stage img.reflection { top: 192px; width: 192px; height: 48px; } 621 503 h2.app-title { font-size: 30px; } 622 504 section.panel { padding: 18px 18px; } 623 - .frosted { height: 30px; font-size: 9px; letter-spacing: 0.16em; } 624 505 } 625 506 626 507 /* Honor reduced-motion preference — kill the music drift. */ ··· 628 509 body::before { animation: none; } 629 510 .footer-mark { animation: none; } 630 511 .aqua, a, .titlebar h1 a { transition-duration: 1ms; } 631 - .app-icon.genie-up, .app-icon.genie-down { animation-duration: 1ms !important; } 632 - } 633 - 634 - /* ── Genie effect ───────────────────────────────────────────────────────── 635 - Click the hero icon and it funnels UP into the menu-bar piano (the same 636 - metaphor the app embodies). The leading edge — the TOP — narrows toward 637 - a point, while the bottom corners stay anchored, then the whole shape 638 - translates and scales toward the destination. Click again to bring it 639 - back. Pure CSS keyframes; JS only sets --gx/--gy and toggles a class. */ 640 - 641 - .icon-stage img.app-icon { 642 - cursor: pointer; 643 - transition: filter 220ms var(--ease-apple); 644 - } 645 - .icon-stage img.app-icon:hover { filter: brightness(1.04) drop-shadow(0 4px 12px rgba(10, 132, 255, 0.25)); } 646 - 647 - .icon-stage img.app-icon.genie-up { 648 - animation: genie-up 720ms cubic-bezier(0.55, 0, 0.2, 1) forwards; 649 - pointer-events: none; 650 - } 651 - .icon-stage img.app-icon.genie-down { 652 - animation: genie-down 720ms cubic-bezier(0.8, 0, 0.45, 1) forwards; 653 - pointer-events: none; 654 - } 655 - /* During the trip, drop the reflection so it doesn't trail. */ 656 - .icon-stage.genied img.reflection { opacity: 0; transition: opacity 260ms linear; } 657 - 658 - @keyframes genie-up { 659 - 0% { 660 - clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); 661 - transform: translate(0, 0) scale(1); 662 - opacity: 1; 663 - filter: blur(0); 664 - } 665 - 25% { 666 - clip-path: polygon(15% 0%, 85% 0%, 100% 100%, 0% 100%); 667 - transform: translate(calc(var(--gx, 0px) * 0.22), calc(var(--gy, 0px) * 0.22)) scale(0.78); 668 - } 669 - 55% { 670 - clip-path: polygon(35% 0%, 65% 0%, 100% 100%, 0% 100%); 671 - transform: translate(calc(var(--gx, 0px) * 0.62), calc(var(--gy, 0px) * 0.62)) scale(0.42); 672 - filter: blur(0.3px); 673 - } 674 - 85% { 675 - clip-path: polygon(46% 0%, 54% 0%, 100% 100%, 0% 100%); 676 - transform: translate(calc(var(--gx, 0px) * 0.92), calc(var(--gy, 0px) * 0.92)) scale(0.12); 677 - filter: blur(0.7px); 678 - } 679 - 100% { 680 - clip-path: polygon(50% 0%, 50% 0%, 100% 100%, 0% 100%); 681 - transform: translate(var(--gx, 0px), var(--gy, 0px)) scale(0.04); 682 - opacity: 0; 683 - filter: blur(1.4px); 684 - } 685 - } 686 - @keyframes genie-down { 687 - 0% { 688 - clip-path: polygon(50% 0%, 50% 0%, 100% 100%, 0% 100%); 689 - transform: translate(var(--gx, 0px), var(--gy, 0px)) scale(0.04); 690 - opacity: 0; 691 - filter: blur(1.4px); 692 - } 693 - 20% { 694 - clip-path: polygon(46% 0%, 54% 0%, 100% 100%, 0% 100%); 695 - transform: translate(calc(var(--gx, 0px) * 0.92), calc(var(--gy, 0px) * 0.92)) scale(0.12); 696 - opacity: 1; 697 - filter: blur(0.7px); 698 - } 699 - 50% { 700 - clip-path: polygon(35% 0%, 65% 0%, 100% 100%, 0% 100%); 701 - transform: translate(calc(var(--gx, 0px) * 0.62), calc(var(--gy, 0px) * 0.62)) scale(0.42); 702 - filter: blur(0.3px); 703 - } 704 - 80% { 705 - clip-path: polygon(15% 0%, 85% 0%, 100% 100%, 0% 100%); 706 - transform: translate(calc(var(--gx, 0px) * 0.22), calc(var(--gy, 0px) * 0.22)) scale(0.78); 707 - filter: blur(0); 708 - } 709 - 100% { 710 - clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); 711 - transform: translate(0, 0) scale(1); 712 - opacity: 1; 713 - filter: blur(0); 714 - } 715 512 } 716 513 717 514 /* ── Dark mode ────────────────────────────────────────────────────────── */ ··· 763 560 0 1px 0 rgba(255, 255, 255, 0.25) inset, 764 561 0 0 0 0.5px rgba(0, 0, 0, 0.5); 765 562 } 766 - .frosted { 767 - background: rgba(28, 28, 30, 0.65); 768 - border-bottom-color: rgba(255, 255, 255, 0.06); 769 - color: var(--ink-soft); 770 - } 771 563 .icon-stage::before { 772 564 background: rgba(40, 50, 70, 0.45); 773 565 border: 1px solid rgba(255, 255, 255, 0.08); ··· 804 596 .badge { 805 597 box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25) inset; 806 598 } 807 - /* Menu bar — dark-mode vibrancy. */ 808 - .macbar { 809 - background: rgba(20, 20, 22, 0.7); 810 - color: #f5f5f7; 811 - border-bottom-color: rgba(255, 255, 255, 0.08); 812 - } 813 - .macbar .apple-logo { fill: #f5f5f7; } 814 - .macbar .menu-item:hover, .macbar .menu-app:hover { 815 - background: rgba(255, 255, 255, 0.10); 816 - } 817 599 } 818 600 </style> 819 601 </head> 820 602 <body> 821 603 822 - <!-- Faux macOS menu bar with a real, working Menu Band piano on the right. 823 - Click the hero icon below and it genie's UP into this piano. --> 824 - <div class="macbar" role="presentation"> 825 - <div class="macbar-left"> 826 - <svg class="apple-logo" viewBox="0 0 14 17" aria-hidden="true"> 827 - <path d="M9.78 9.27c.02-1.81 1.49-2.67 1.55-2.71-.85-1.24-2.17-1.4-2.64-1.42-1.12-.11-2.19.66-2.76.66-.58 0-1.45-.65-2.39-.63C2.32 5.18 1.22 5.89.62 6.94c-1.31 2.27-.34 5.64.94 7.49.62.91 1.36 1.92 2.32 1.88.93-.04 1.29-.6 2.41-.6 1.12 0 1.45.6 2.43.59 1-.02 1.64-.92 2.25-1.83.71-1.06 1-2.09 1.01-2.14-.03-.01-1.94-.74-1.96-2.96zM7.92 3.85c.51-.62.86-1.49.77-2.35-.74.04-1.64.5-2.17 1.11-.49.55-.91 1.42-.79 2.27.83.06 1.68-.42 2.19-1.03z"/> 828 - </svg> 829 - <span class="menu-app">Menu Band</span> 830 - <span class="menu-item">File</span> 831 - <span class="menu-item">Edit</span> 832 - <span class="menu-item">View</span> 833 - <span class="menu-item">Window</span> 834 - <span class="menu-item">Help</span> 835 - </div> 836 - <div class="macbar-right"> 837 - <span class="menu-extra" aria-hidden="true">⌘</span> 838 - <span class="menu-extra" aria-hidden="true">⏏</span> 839 - <span class="menu-extra" aria-hidden="true">◐</span> 840 - <span class="menu-extra" aria-hidden="true">▮▮▮</span> 841 - <span class="menu-extra" aria-hidden="true">📶</span> 842 - <!-- The actual Menu Band piano graphic — one playable octave. --> 843 - <div class="dock-piano" id="dock-piano" aria-label="Menu Band piano" title="Menu Band"> 844 - <span class="key w" data-note="C"></span> 845 - <span class="key b" data-note="C#"></span> 846 - <span class="key w" data-note="D"></span> 847 - <span class="key b" data-note="D#"></span> 848 - <span class="key w" data-note="E"></span> 849 - <span class="key w" data-note="F"></span> 850 - <span class="key b" data-note="F#"></span> 851 - <span class="key w" data-note="G"></span> 852 - <span class="key b" data-note="G#"></span> 853 - <span class="key w" data-note="A"></span> 854 - <span class="key b" data-note="A#"></span> 855 - <span class="key w" data-note="B"></span> 856 - </div> 857 - <span class="menu-extra clock" id="macbar-clock" aria-hidden="true">Wed 2:48 PM</span> 858 - </div> 859 - </div> 860 - 861 604 <article class="window" aria-label="Menu Band"> 862 605 <header class="titlebar" aria-hidden="true"> 863 606 <div class="lights"> ··· 865 608 <div class="light y"></div> 866 609 <div class="light g"></div> 867 610 </div> 868 - <h1><a href="/prompt">menuband</a></h1> 869 611 </header> 870 612 871 - <div class="frosted" aria-hidden="true">an aesthetic computer instrument</div> 872 - 873 613 <div class="hero"> 874 614 <div class="icon-stage"> 875 615 <img class="app-icon" src="/menuband/icon.png" alt="Menu Band app icon"> ··· 879 619 <p class="tagline">Built-in macOS instruments, in the menu bar.</p> 880 620 881 621 <div class="button-row"> 882 - <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.2.dmg" download> 622 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.3.dmg" download> 883 623 Download 884 - <small>0.2 · Apple Silicon · 1.1 MB</small> 624 + <small>0.3 · Apple Silicon · 1.2 MB</small> 885 625 </a> 886 626 <a class="aqua alt" href="https://tangled.org/@aesthetic.computer/core/tree/main/slab/menuband"> 887 627 View source ··· 893 633 894 634 <section class="panel"> 895 635 <h3>What it does</h3> 896 - <p>A tiny piano sits at the right of your menu bar. Click any key to play. Type letters to play. Send notes to your DAW over MIDI.</p> 897 - <p>Menu Band publishes a virtual MIDI source named <code>Menu Band</code> — pick it as an input in any DAW and the keys you tap (or letters you type) play through your DAW's instruments. A built‑in General MIDI synth covers hundreds of patches when no DAW is running.</p> 636 + <p>A tiny piano in your menu bar. Click keys, type letters, or send to your DAW over MIDI.</p> 637 + <p>Publishes a virtual MIDI source named <code>Menu Band</code>. Built-in General MIDI synth when no DAW is listening.</p> 898 638 </section> 899 639 900 640 <section class="panel"> 901 641 <h3>About</h3> 902 - <p>Menu Band is a political project: accessible music-making is as essential as time, network connectivity, and battery life. Free and open source. By <a href="https://aesthetic.computer">aesthetic.computer</a>.</p> 642 + <p>Free and open source. By <a href="https://aesthetic.computer">aesthetic.computer</a>.</p> 903 643 </section> 904 644 905 645 <section class="panel testimonial"> 906 - <blockquote>“i use it on my macbook neo. ya thats how im using it. still gotta fix up the menus a bit, but it's rly fun — i can practice notepat on my mac, and also play ableton without having to be in ableton, with my own custom layout.”</blockquote> 907 - <p class="attribution">— <a href="https://aesthetic.computer/@jeffrey">@jeffrey</a>, on his MacBook Neo</p> 646 + <blockquote>“i use it on my macbook neo. practice notepat, play ableton without leaving my menu bar.”</blockquote> 647 + <p class="attribution">— <a href="https://aesthetic.computer/@jeffrey">@jeffrey</a></p> 908 648 </section> 909 649 910 650 <section class="panel"> 911 - <h3>Three input modes</h3> 651 + <h3>Three modes</h3> 912 652 <ul class="modes"> 913 - <li><b>Pointer</b><span>Mouse only. Two octaves of QWERTY-mapped piano keys. <em>Default.</em></span></li> 914 - <li><b>Notepat</b><span>Global keystroke capture, two-octave layout (<code>z x c v d s e f w g r a q b h t i y j k u l o m p n</code>).</span></li> 915 - <li><b>Ableton</b><span>Global keystroke capture, Ableton Live's M-mode QWERTY (one octave: <code>a w s e d f t g y h u j k o l p ;</code>).</span></li> 653 + <li><b>Pointer</b><span>Mouse only. Two octaves.</span></li> 654 + <li><b>Notepat</b><span>Type to play. Two octaves.</span></li> 655 + <li><b>Ableton</b><span>Type to play. Live's M-mode layout.</span></li> 916 656 </ul> 917 - <p style="margin-top:10px;">Hover any segment in the popover to <em>preview</em> it in the menubar piano — letter labels appear, key range adjusts, and tapping keys plays through that mode without committing.</p> 918 657 </section> 919 658 920 659 <section class="panel"> 921 - <h3>What's new <span class="badge">0.2</span></h3> 922 - <p>Octave widget moved to the popover's top-left and reads as scientific pitch (<code>4</code>, <code>5</code>…). Menubar piano keys flash on click again. Audio visualizer redrawn at vsync via <code>CVDisplayLink</code> with frame coalescing. Dragging across the instrument grid sounds each cell with no audible swap latency.</p> 660 + <h3>What's new <span class="badge">0.3</span></h3> 661 + <p>Audio actually fires (lazy-loaded GM bank). Visualizer rendered in Metal with per-bar ballistics. Instrument palette collapses with the popover in MIDI mode. Click sounds on popover open + MIDI toggle.</p> 923 662 </section> 924 663 925 664 <section class="panel"> 926 665 <h3>Requirements</h3> 927 - <p>macOS 11 (Big Sur) or later · Apple Silicon · ⌃⌥⌘P toggles the last typing mode · Accessibility permission requested only when you turn on Notepat or Ableton.</p> 666 + <p>macOS 11+ · Apple Silicon · ⌃⌥⌘P toggles typing mode.</p> 928 667 </section> 929 668 </article> 930 669 ··· 943 682 </footer> 944 683 945 684 <script> 946 - // ── Live menu-bar clock — same format as macOS. ──────────────────────── 947 - (function clock() { 948 - const el = document.getElementById('macbar-clock'); 949 - if (!el) return; 950 - const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; 951 - function tick() { 952 - const d = new Date(); 953 - let h = d.getHours(); 954 - const m = d.getMinutes(); 955 - const ampm = h >= 12 ? 'PM' : 'AM'; 956 - h = h % 12 || 12; 957 - el.textContent = `${days[d.getDay()]} ${h}:${String(m).padStart(2,'0')} ${ampm}`; 685 + // ── Live reload via /api/version long-poll. Same mechanism prompt.mjs 686 + // uses: ask the server for the deployed commit hash, then long-poll 687 + // with ?current=<hash>. The server holds the connection ~4s when the 688 + // hash matches, so a fresh `lith/deploy.fish` (which rewrites 689 + // /system/public/.commit-ref) shows up within seconds. Skipped on 690 + // localhost so dev workflows aren't disturbed. ───────────────────── 691 + (function liveReload() { 692 + if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return; 693 + let deployed = null; 694 + async function poll() { 695 + try { 696 + if (!deployed) { 697 + const res = await fetch('/api/version', { cache: 'no-store' }); 698 + if (!res.ok) throw new Error('HTTP ' + res.status); 699 + const data = await res.json(); 700 + deployed = data.deployed; 701 + if (!deployed || deployed === 'unknown') { deployed = null; throw new Error('no deployed hash'); } 702 + } else { 703 + const res = await fetch('/api/version?current=' + encodeURIComponent(deployed), { cache: 'no-store' }); 704 + if (!res.ok) throw new Error('HTTP ' + res.status); 705 + const data = await res.json(); 706 + if (data.changed === false) { /* same version — loop */ } 707 + else if (data.deployed && data.deployed !== deployed) { 708 + // New deploy — fly back home before the new bytes land. 709 + location.reload(); 710 + return; 711 + } 712 + } 713 + } catch (_) { 714 + await new Promise(r => setTimeout(r, 5000)); 715 + } 716 + poll(); 958 717 } 959 - tick(); 960 - setInterval(tick, 15000); 718 + poll(); 961 719 })(); 962 720 963 - // ── Genie effect — click the hero icon to fly it into the menu-bar piano, 964 - // click it again to fly it back. The destination is computed live so 965 - // it survives layout shifts and works on narrow viewports. ─────────── 966 - (function genie() { 967 - const stage = document.querySelector('.icon-stage'); 968 - const icon = document.querySelector('.icon-stage img.app-icon'); 969 - const piano = document.getElementById('dock-piano'); 970 - if (!stage || !icon || !piano) return; 971 - 972 - let docked = false; 973 - let busy = false; 974 - 975 - function flyTo(dest) { 976 - const ir = icon.getBoundingClientRect(); 977 - const pr = piano.getBoundingClientRect(); 978 - const targetX = (pr.left + pr.width / 2) - (ir.left + ir.width / 2); 979 - const targetY = (pr.top + pr.height / 2) - (ir.top + ir.height / 2); 980 - icon.style.setProperty('--gx', targetX + 'px'); 981 - icon.style.setProperty('--gy', targetY + 'px'); 982 - icon.classList.remove('genie-up', 'genie-down'); 983 - // Force reflow so a re-trigger restarts the animation cleanly. 984 - void icon.offsetWidth; 985 - icon.classList.add(dest === 'up' ? 'genie-up' : 'genie-down'); 986 - } 987 - 988 - function pulsePiano() { 989 - piano.classList.add('glow'); 990 - setTimeout(() => piano.classList.remove('glow'), 600); 991 - } 992 - 993 - icon.addEventListener('click', () => { 994 - if (busy) return; 995 - busy = true; 996 - flyTo(docked ? 'down' : 'up'); 997 - if (!docked) setTimeout(pulsePiano, 560); 998 - docked = !docked; 999 - setTimeout(() => { busy = false; }, 740); 1000 - }); 1001 - 1002 - // Click the menu-bar piano to bring the icon back, too. 1003 - piano.addEventListener('click', (e) => { 1004 - // Don't intercept individual key plays from bubbling up if the user 1005 - // wired keys to play notes — this listener only fires for the piano 1006 - // body itself when the icon is currently docked. 1007 - if (!docked || busy) return; 1008 - busy = true; 1009 - flyTo('down'); 1010 - docked = false; 1011 - setTimeout(() => { busy = false; }, 740); 1012 - }); 1013 - 1014 - // Tiny "play" highlight on key click — purely visual. stopPropagation 1015 - // on both mousedown AND click so pressing a key while the icon is 1016 - // docked doesn't also fire the piano's "bring back" handler. 1017 - piano.querySelectorAll('.key').forEach(k => { 1018 - const swallow = (ev) => ev.stopPropagation(); 1019 - k.addEventListener('click', swallow); 1020 - k.addEventListener('mousedown', (ev) => { 1021 - ev.stopPropagation(); 1022 - k.classList.add('lit'); 1023 - setTimeout(() => k.classList.remove('lit'), 140); 1024 - }); 1025 - }); 1026 - })(); 1027 721 </script> 1028 722 1029 723 </body>