VolumeControl
A mute button and a level slider with an icon that shows how loud it is; a real range, so forms and keyboards work.
Demo
Level: 60%
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/volume_control.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/volume_control.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/volume_control.qwe
component VolumeControlDemoLive() { state level = 0.6 <div.demo-row> <VolumeControl level={0.6} on_change={(v) => { level = v }}/> <p.demo-note id="volume-control-demo-note">Level: {round(level * 100)}%</p> </div>}view VolumeControlDemo() = <div.demo-stack> <VolumeControlDemoLive/> <div.demo-row id="volume-control-demo-others" style="align-items: flex-end"> <VolumeControl name="alerts" label="Alert volume" level={1} size="sm"/> <VolumeControl level={0.3} muted={true}/> <VolumeControl level={0.8} vertical={true} label="Vertical"/> </div> </div>Source
ui/volume_control.qwe
// VolumeControl: a mute button and a slider for a level from 0 to 1, with an// icon that shows how loud it is. The slider is a real range input, so it works// with the keyboard and screen readers, and a form sends `name` when there is one.//// <VolumeControl level={0.6} on_change={(v) => { player.volume = v }}/>// <VolumeControl name="volume" label="Alert volume" level={1} size="sm"/>//// level: where it starts, 0 to 1 (1). muted: starts muted (false).// name: the slider's name in a form. label: what the slider is called// ("Volume"). size: "sm" or "md" (default). vertical: the slider stands up.// on_change(level): inside one of your components, called with the level as it// moves, 0 when muted, and the level kept when unmuted.import ui/theme.qwe use ui_clslet volume_control_css = css`.ui-volume-control { display: inline-flex; align-items: center; gap: 6px; color: var(--ui-fg); font: 13px/1 var(--ui-font); }.ui-volume-control-btn { display: grid; place-items: center; flex: none; width: 34px; height: 34px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: inherit; cursor: pointer; }.ui-volume-control-btn:hover { background: var(--ui-subtle); }.ui-volume-control-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-volume-control-btn svg { width: 20px; height: 20px; }.ui-volume-control-btn[aria-pressed="true"] { color: var(--ui-muted); }.ui-volume-control input { appearance: none; width: 100px; height: 20px; margin: 0; background: transparent; cursor: pointer; }.ui-volume-control input::-webkit-slider-runnable-track { height: 5px; border-radius: 999px; background: linear-gradient(to right, var(--ui-accent) var(--f, 0%), var(--ui-subtle) var(--f, 0%)); box-shadow: inset 0 0 0 1px var(--ui-border); }.ui-volume-control input::-moz-range-track { height: 5px; border-radius: 999px; background: var(--ui-subtle); }.ui-volume-control input::-moz-range-progress { height: 5px; border-radius: 999px; background: var(--ui-accent); }.ui-volume-control input::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; margin-top: -4.5px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); box-shadow: var(--ui-shadow); }.ui-volume-control input::-moz-range-thumb { width: 10px; height: 10px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); }.ui-volume-control input:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 999px; }.ui-volume-control-sm .ui-volume-control-btn { width: 28px; height: 28px; }.ui-volume-control-sm .ui-volume-control-btn svg { width: 16px; height: 16px; }.ui-volume-control-sm input { width: 72px; }.ui-volume-control-vertical { flex-direction: column-reverse; }.ui-volume-control-vertical input { writing-mode: vertical-lr; direction: rtl; width: 20px; height: 100px; }.ui-volume-control-vertical input::-webkit-slider-runnable-track { width: 5px; height: auto; background: linear-gradient(to top, var(--ui-accent) var(--f, 0%), var(--ui-subtle) var(--f, 0%)); }.ui-volume-control-vertical input::-webkit-slider-thumb { margin-top: 0; margin-left: -4.5px; }`component VolumeControl(level: num = 1, muted: bool = false, name: str? = null, label: str = "Volume", size: str = "md", vertical: bool = false, on_change = null, class: str? = null) { style global volume_control_css state v = level < 0 ? 0 : level > 1 ? 1 : level state quiet = muted let shown = quiet ? 0 : v let pct = round(shown * 100) let tell = () => { on_change && on_change(shown) } <div class={ui_cls(["ui-volume-control", size == "sm" && "ui-volume-control-sm", vertical && "ui-volume-control-vertical", class])}> <button.ui-volume-control-btn type="button" aria-label={quiet ? "Unmute" : "Mute"} aria-pressed={quiet ? "true" : "false"} @click={() => { quiet = !quiet if !quiet && v == 0 { v = 0.5 } tell() }}> {shown == 0 ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5ZM23 9l-6 6M17 9l6 6"/></svg> : shown < 0.5 ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5ZM15.54 8.46a5 5 0 0 1 0 7.07"/></svg> : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5ZM19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>} </button> <input type="range" min="0" max="1" step="0.01" value={shown} name={name} aria-label={label} aria-valuetext={`${pct}%`} style={`--f: ${pct}%`} aria-orientation={vertical ? "vertical" : null} @input={(e) => { let n = float(e.value) v = n quiet = false tell() }}/> </div>}Related
AudioPlayerAn audio clip with play, a seek bar that says the time, and a button cycling through playback speeds.6 views · 7 downloadsCameraCaptureTakes a photo with the camera into a hidden input as a data URL, with retake, and says so when there is no camera.6 views · 6 downloadsImageCropA crop box over a picture, dragged and resized by its corners or the keys, with the crop in hidden inputs a form sends.9 views · 6 downloadsPlaylistNumbered tracks with the one playing marked, each one's length and the total; with sources it plays them in turn.6 views · 5 downloadsFieldsetA <fieldset> with a legend and a line about it, its fields stacked, in a row or in a grid; disabled as one.6 views · 7 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloads
Thread
Thread 0 posts
On VolumeControl: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.