eloigil6 Claude Opus 4.8 commited on
Commit
8e4f3a3
·
1 Parent(s): 0d2e5fe

Add a top-right mute button that silences all audio (persisted)

Browse files

A fixed glass button in the top-right toggles mute for every audio element (tapes + lobby) via querySelectorAll('audio').muted - independent of the volume/fade logic, so unmuting restores it. The choice persists in localStorage ('lofinity:muted') and is restored on load. z-index 50 keeps it above the modals so it works while a tape plays. Verified in preview: renders, mutes all 3 audio elements, persists across reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (3) hide show
  1. frontend/index.html +17 -0
  2. frontend/style.css +44 -0
  3. frontend/ui.js +29 -0
frontend/index.html CHANGED
@@ -72,6 +72,23 @@
72
  </div>
73
  <button id="start-btn" type="button">▶ click to start</button>
74
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  <div id="vending-label" class="hover-label"><span>♪ vend a vibe</span></div>
76
  <div id="collection-label" class="hover-label">
77
  <span>♪ cassette collection</span>
 
72
  </div>
73
  <button id="start-btn" type="button">▶ click to start</button>
74
  </div>
75
+ <button
76
+ id="mute-btn"
77
+ class="mute-btn"
78
+ type="button"
79
+ aria-label="mute all sound"
80
+ aria-pressed="false"
81
+ title="mute"
82
+ >
83
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
84
+ <path d="M4 9 L7.5 9 L13 5 L13 19 L7.5 15 L4 15 Z" fill="currentColor" stroke="none" />
85
+ <g class="mute-waves">
86
+ <path d="M16 9.5a4 4 0 0 1 0 5" />
87
+ <path d="M18.6 7a8 8 0 0 1 0 10" />
88
+ </g>
89
+ <line class="mute-slash" x1="3.5" y1="3.5" x2="20.5" y2="20.5" />
90
+ </svg>
91
+ </button>
92
  <div id="vending-label" class="hover-label"><span>♪ vend a vibe</span></div>
93
  <div id="collection-label" class="hover-label">
94
  <span>♪ cassette collection</span>
frontend/style.css CHANGED
@@ -159,6 +159,50 @@ body {
159
  }
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  @keyframes title-bob {
163
  from {
164
  transform: translateY(0);
 
159
  }
160
  }
161
 
162
+ /* Global mute toggle, top-right. z-index above the modals (max 20) so it stays
163
+ clickable while a tape plays. No transform on this backdrop-filter element. */
164
+ .mute-btn {
165
+ position: fixed;
166
+ top: 1rem;
167
+ right: 1rem;
168
+ z-index: 50;
169
+ width: 2.6rem;
170
+ height: 2.6rem;
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ border-radius: 999px;
175
+ color: #ffffff;
176
+ background: rgba(18, 28, 58, 0.34);
177
+ border: 1px solid rgba(255, 255, 255, 0.32);
178
+ backdrop-filter: blur(6px);
179
+ -webkit-backdrop-filter: blur(6px);
180
+ cursor: pointer;
181
+ transition:
182
+ background 0.2s ease,
183
+ border-color 0.2s ease,
184
+ color 0.2s ease;
185
+ }
186
+
187
+ .mute-btn:hover {
188
+ background: rgba(18, 28, 58, 0.55);
189
+ border-color: rgba(255, 255, 255, 0.6);
190
+ }
191
+
192
+ .mute-btn.muted {
193
+ color: rgba(255, 255, 255, 0.55);
194
+ }
195
+
196
+ .mute-btn .mute-slash {
197
+ display: none;
198
+ }
199
+ .mute-btn.muted .mute-waves {
200
+ display: none;
201
+ }
202
+ .mute-btn.muted .mute-slash {
203
+ display: inline;
204
+ }
205
+
206
  @keyframes title-bob {
207
  from {
208
  transform: translateY(0);
frontend/ui.js CHANGED
@@ -157,6 +157,35 @@ export function initUI({
157
  window.addEventListener("pointerdown", primeLobby);
158
  window.addEventListener("keydown", primeLobby);
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  function setStage(stage) {
161
  controlsRow.classList.toggle("hidden", stage !== "prompt");
162
  generating.classList.toggle("hidden", stage !== "generating");
 
157
  window.addEventListener("pointerdown", primeLobby);
158
  window.addEventListener("keydown", primeLobby);
159
 
160
+ // --- mute -------------------------------------------------------------------
161
+ // Top-right toggle that silences ALL audio (tapes + lobby). Persisted in
162
+ // localStorage so the choice survives reloads. Uses each element's `muted`
163
+ // flag, which is independent of the volume/fade logic — unmuting restores it.
164
+ const muteBtn = $("mute-btn");
165
+ const MUTE_KEY = "lofinity:muted";
166
+ let muted = false;
167
+ try {
168
+ muted = localStorage.getItem(MUTE_KEY) === "1";
169
+ } catch {
170
+ /* private mode — default to unmuted */
171
+ }
172
+ function applyMute() {
173
+ document.querySelectorAll("audio").forEach((el) => (el.muted = muted));
174
+ muteBtn.classList.toggle("muted", muted);
175
+ muteBtn.setAttribute("aria-pressed", String(muted));
176
+ muteBtn.title = muted ? "unmute" : "mute";
177
+ }
178
+ applyMute(); // restore the persisted choice on load
179
+ muteBtn.addEventListener("click", () => {
180
+ muted = !muted;
181
+ try {
182
+ localStorage.setItem(MUTE_KEY, muted ? "1" : "0");
183
+ } catch {
184
+ /* ignore */
185
+ }
186
+ applyMute();
187
+ });
188
+
189
  function setStage(stage) {
190
  controlsRow.classList.toggle("hidden", stage !== "prompt");
191
  generating.classList.toggle("hidden", stage !== "generating");