A social internet radio platform built on AT Protocol. atradio.fm
atproto radio
7

Configure Feed

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

refactor(web): use HeroUI Slider for all audio + connect sliders

Replace the plain <input type="range"> sliders with the HeroUI Slider
component already used by the player, for a consistent synthwave look.

- AudioSettingsModal: shared Range helper (tone/crossfeed/PBE/surround/
compressor/stereo) + a vertical EqBandSlider for the 10 EQ faders
- ConnectBanner: remote-device volume slider
- drop the obsolete .eq-slider CSS hack; add explicit aria-labels

Tsiry Sandratraina (Jul 17, 2026, 8:33 PM +0300) 345714de bc7e352b

+68 -34
+50 -16
apps/web/src/components/AudioSettingsModal.tsx
··· 1 1 import type { ReactNode } from "react"; 2 2 import { useAtom } from "jotai"; 3 3 import { useTranslation } from "react-i18next"; 4 - import { Modal, useOverlayState } from "@heroui/react"; 4 + import { Modal, Slider, useOverlayState } from "@heroui/react"; 5 5 import { 6 6 ChannelMode, 7 7 CrossfeedMode, ··· 88 88 step?: number; 89 89 value: number; 90 90 onChange: (v: number) => void; 91 + "aria-label"?: string; 91 92 }) { 92 93 return ( 93 - <input 94 - type="range" 95 - min={props.min} 96 - max={props.max} 94 + <Slider 95 + aria-label={props["aria-label"]} 96 + minValue={props.min} 97 + maxValue={props.max} 97 98 step={props.step ?? 1} 98 - value={props.value} 99 - onChange={(e) => props.onChange(Number(e.target.value))} 100 - className="accent-synth-pink" 101 - /> 99 + value={[props.value]} 100 + onChange={(v) => props.onChange(Array.isArray(v) ? v[0] : v)} 101 + > 102 + <Slider.Track className="h-1.5 rounded-full bg-white/10"> 103 + <Slider.Fill className="rounded-full bg-gradient-to-r from-synth-pink to-synth-cyan" /> 104 + <Slider.Thumb className="h-3.5 w-3.5 bg-synth-cyan shadow-neon-cyan" /> 105 + </Slider.Track> 106 + </Slider> 107 + ); 108 + } 109 + 110 + /** One vertical EQ band fader (HeroUI Slider, gain in dB). */ 111 + function EqBandSlider(props: { 112 + value: number; 113 + onChange: (v: number) => void; 114 + "aria-label": string; 115 + }) { 116 + return ( 117 + <Slider 118 + orientation="vertical" 119 + aria-label={props["aria-label"]} 120 + minValue={-24} 121 + maxValue={24} 122 + step={1} 123 + value={[props.value]} 124 + onChange={(v) => props.onChange(Array.isArray(v) ? v[0] : v)} 125 + className="flex h-28 justify-center" 126 + > 127 + <Slider.Track className="h-full w-1.5 rounded-full bg-white/10"> 128 + <Slider.Fill className="rounded-full bg-gradient-to-t from-synth-pink to-synth-cyan" /> 129 + <Slider.Thumb className="h-3.5 w-3.5 bg-synth-cyan shadow-neon-cyan" /> 130 + </Slider.Track> 131 + </Slider> 102 132 ); 103 133 } 104 134 ··· 235 265 <span className="font-mono text-[0.65rem] text-foreground/80"> 236 266 {eqGains[i] > 0 ? `+${eqGains[i]}` : eqGains[i]} 237 267 </span> 238 - <input 239 - type="range" 240 - min={-24} 241 - max={24} 242 - step={1} 268 + <EqBandSlider 243 269 value={eqGains[i]} 244 270 aria-label={t("equalizer.bandGain", { hz })} 245 - onChange={(e) => onEqBand(i, Number(e.target.value))} 246 - className="eq-slider accent-synth-pink" 271 + onChange={(v) => onEqBand(i, v)} 247 272 /> 248 273 <span className="font-mono text-[0.6rem] text-foreground/40"> 249 274 {hz >= 1000 ? `${hz / 1000}k` : hz} ··· 257 282 <Section title={t("tone.title")}> 258 283 <Field label={t("tone.bass")} value={`${bass} dB`}> 259 284 <Range 285 + aria-label={t("tone.bass")} 260 286 min={-24} 261 287 max={24} 262 288 value={bass} ··· 268 294 </Field> 269 295 <Field label={t("tone.treble")} value={`${treble} dB`}> 270 296 <Range 297 + aria-label={t("tone.treble")} 271 298 min={-24} 272 299 max={24} 273 300 value={treble} ··· 299 326 value={`${cfDirect.toFixed(1)} dB`} 300 327 > 301 328 <Range 329 + aria-label={t("crossfeed.directGain")} 302 330 min={-6} 303 331 max={0} 304 332 step={0.5} ··· 314 342 <Section title={t("pbe.title")}> 315 343 <Field label={t("pbe.strength")} value={`${pbe}%`}> 316 344 <Range 345 + aria-label={t("pbe.strength")} 317 346 min={0} 318 347 max={100} 319 348 value={pbe} ··· 325 354 </Field> 326 355 <Field label={t("pbe.precut")} value={`-${pbePrecut} dB`}> 327 356 <Range 357 + aria-label={t("pbe.precut")} 328 358 min={0} 329 359 max={24} 330 360 value={pbePrecut} ··· 339 369 <Section title={t("surround.title")}> 340 370 <Field label={t("surround.delay")} value={`${surDelay} ms`}> 341 371 <Range 372 + aria-label={t("surround.delay")} 342 373 min={0} 343 374 max={30} 344 375 value={surDelay} ··· 350 381 </Field> 351 382 <Field label={t("surround.balance")} value={`${surBalance}%`}> 352 383 <Range 384 + aria-label={t("surround.balance")} 353 385 min={0} 354 386 max={100} 355 387 value={surBalance} ··· 367 399 value={`${compThresh} dB`} 368 400 > 369 401 <Range 402 + aria-label={t("compressor.threshold")} 370 403 min={-30} 371 404 max={0} 372 405 value={compThresh} ··· 414 447 </Field> 415 448 <Field label={t("stereo.width")} value={`${width}%`}> 416 449 <Range 450 + aria-label={t("stereo.width")} 417 451 min={0} 418 452 max={255} 419 453 value={width}
+18 -10
apps/web/src/components/ConnectBanner.tsx
··· 1 1 import { useAtomValue, useSetAtom } from "jotai"; 2 2 import { useTranslation } from "react-i18next"; 3 + import { Slider } from "@heroui/react"; 3 4 import { 4 5 IconBroadcast, 5 6 IconPlayerPlayFilled, ··· 87 88 88 89 <div className="hidden items-center gap-2 md:flex"> 89 90 <IconVolume size={16} className="text-foreground/50" /> 90 - <input 91 - type="range" 92 - min={0} 93 - max={1} 91 + <Slider 92 + aria-label={t("remoteVolume")} 93 + minValue={0} 94 + maxValue={1} 94 95 step={0.01} 95 - value={state.volume} 96 - onChange={(e) => 97 - send({ action: "setVolume", value: Number(e.target.value) }) 96 + value={[state.volume]} 97 + onChange={(v) => 98 + send({ 99 + action: "setVolume", 100 + value: Array.isArray(v) ? v[0] : v, 101 + }) 98 102 } 99 - className="h-1 w-24 cursor-pointer accent-synth-cyan" 100 - aria-label={t("remoteVolume")} 101 - /> 103 + className="w-24" 104 + > 105 + <Slider.Track className="h-1.5 rounded-full bg-white/10"> 106 + <Slider.Fill className="rounded-full bg-gradient-to-r from-synth-pink to-synth-cyan" /> 107 + <Slider.Thumb className="h-3.5 w-3.5 bg-synth-cyan shadow-neon-cyan" /> 108 + </Slider.Track> 109 + </Slider> 102 110 </div> 103 111 104 112 <button
-8
apps/web/src/styles/index.css
··· 193 193 -webkit-background-clip: text; 194 194 color: transparent; 195 195 } 196 - 197 - /* Vertical EQ band sliders (advanced audio settings modal). */ 198 - .eq-slider { 199 - writing-mode: vertical-lr; 200 - direction: rtl; 201 - width: 8px; 202 - height: 7rem; 203 - } 204 196 }