UI Components Medium
Podcast Player
A specialized player optimized for longer spoken-word content. Includes skip forward/back buttons and variable playback speed.
Open in Lab
MCP
vanilla-js html5 css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--pod-bg: rgba(255, 255, 255, 0.04);
--pod-border: rgba(255, 255, 255, 0.08);
--pod-accent: #818cf8;
--pod-accent-glow: rgba(129, 140, 248, 0.25);
--pod-text: #f8fafc;
--pod-muted: #94a3b8;
--pod-surface: rgba(255, 255, 255, 0.04);
--pod-select-bg: rgba(255, 255, 255, 0.08);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: #0b1221;
padding: 1rem;
}
.podcast-widget {
background: var(--pod-bg);
border: 1px solid var(--pod-border);
border-radius: 20px;
padding: 1.5rem;
max-width: 480px;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
backdrop-filter: blur(16px);
}
.podcast-header {
display: flex;
gap: 1.25rem;
margin-bottom: 1.75rem;
}
.podcast-cover {
width: 96px;
height: 96px;
border-radius: 12px;
overflow: hidden;
flex-shrink: 0;
border: 1px solid var(--pod-border);
background: linear-gradient(135deg, #4f46e5, #818cf8);
}
.podcast-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.podcast-meta h3 {
font-size: 1.0625rem;
margin: 0 0 4px;
color: var(--pod-text);
font-weight: 700;
}
.podcast-meta p {
font-size: 0.875rem;
color: var(--pod-muted);
margin: 0 0 0.625rem;
}
.category-tag {
background: rgba(129, 140, 248, 0.12);
border: 1px solid rgba(129, 140, 248, 0.25);
color: var(--pod-accent);
font-size: 0.688rem;
font-weight: 700;
padding: 0.25rem 0.625rem;
border-radius: 20px;
text-transform: uppercase;
letter-spacing: 0.07em;
display: inline-block;
}
.time-slider-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
#pod-current,
#pod-total {
font-size: 0.75rem;
color: var(--pod-muted);
font-family: "JetBrains Mono", monospace;
width: 44px;
}
#pod-progress {
flex: 1;
height: 5px;
accent-color: var(--pod-accent);
cursor: pointer;
}
.pod-controls {
display: flex;
align-items: center;
justify-content: space-between;
}
.pod-btn {
background: none;
border: none;
cursor: pointer;
color: var(--pod-muted);
font-size: 1.25rem;
padding: 0.5rem;
border-radius: 50%;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
}
.pod-btn:hover {
background: rgba(255, 255, 255, 0.07);
color: var(--pod-text);
}
.play-btn {
width: 54px;
height: 54px;
background: linear-gradient(135deg, var(--pod-accent), #6366f1);
color: white;
font-size: 1.35rem;
box-shadow: 0 0 24px var(--pod-accent-glow);
}
.play-btn:hover {
background: linear-gradient(135deg, #a5b4fc, #818cf8);
transform: scale(1.05);
color: white;
box-shadow: 0 4px 24px var(--pod-accent-glow);
}
.speed-control select {
border: 1px solid var(--pod-border);
border-radius: 8px;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
background: var(--pod-select-bg);
color: var(--pod-text);
outline: none;
}
.speed-control select option {
background: #1e293b;
}
.primary-pod-btns {
display: flex;
align-items: center;
gap: 0.75rem;
}
.vol-control {
display: flex;
align-items: center;
}const podAudio = document.getElementById("pod-audio");
const podPlayBtn = document.getElementById("pod-play-pause");
const podProgress = document.getElementById("pod-progress");
const podCurrentTime = document.getElementById("pod-current");
const podTotalTime = document.getElementById("pod-total");
const podSpeed = document.getElementById("pod-speed");
const skipBackBtn = document.getElementById("skip-back");
const skipForwardBtn = document.getElementById("skip-forward");
const podMuteBtn = document.getElementById("pod-mute");
function togglePodPlay() {
if (podAudio.paused) {
podAudio.play();
podPlayBtn.innerText = "⏸";
} else {
podAudio.pause();
podPlayBtn.innerText = "▶";
}
}
function updatePodProgress() {
const percent = (podAudio.currentTime / podAudio.duration) * 100;
podProgress.value = percent || 0;
podCurrentTime.innerText = formatDuration(podAudio.currentTime);
}
function formatDuration(time) {
const h = Math.floor(time / 3600);
const m = Math.floor((time % 3600) / 60);
const s = Math.floor(time % 60);
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
}
function seekPod() {
podAudio.currentTime = (podProgress.value / 100) * podAudio.duration;
}
skipBackBtn.addEventListener("click", () => {
podAudio.currentTime = Math.max(0, podAudio.currentTime - 15);
});
skipForwardBtn.addEventListener("click", () => {
podAudio.currentTime = Math.min(podAudio.duration, podAudio.currentTime + 30);
});
podSpeed.addEventListener("change", () => {
podAudio.playbackRate = parseFloat(podSpeed.value);
});
podMuteBtn.addEventListener("click", () => {
podAudio.muted = !podAudio.muted;
podMuteBtn.innerText = podAudio.muted ? "🔇" : "🔊";
});
podPlayBtn.addEventListener("click", togglePodPlay);
podAudio.addEventListener("timeupdate", updatePodProgress);
podAudio.addEventListener("loadedmetadata", () => {
podTotalTime.innerText = formatDuration(podAudio.duration);
});
podProgress.addEventListener("input", seekPod);<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Podcast Player</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="podcast-widget">
<div class="podcast-header">
<div class="podcast-cover">
<div style="width:100%;height:100%;background:linear-gradient(135deg,#4f46e5,#818cf8);display:flex;align-items:center;justify-content:center;font-size:1.5rem;">🎙</div>
</div>
<div class="podcast-meta">
<h3>The Tech Narrative</h3>
<p>Episode 42: The Rise of AI Agents</p>
<span class="category-tag">Technology</span>
</div>
</div>
<div class="podcast-interface">
<div class="time-slider-row">
<span id="pod-current">00:00:00</span>
<input type="range" id="pod-progress" min="0" max="100" value="0" />
<span id="pod-total">00:45:00</span>
</div>
<div class="pod-controls">
<div class="speed-control">
<select id="pod-speed">
<option value="0.8">0.8x</option>
<option value="1" selected>1.0x</option>
<option value="1.2">1.2x</option>
<option value="1.5">1.5x</option>
<option value="2">2.0x</option>
</select>
</div>
<div class="primary-pod-btns">
<button id="skip-back" class="pod-btn" title="Back 15s">↺ 15</button>
<button id="pod-play-pause" class="pod-btn play-btn">▶</button>
<button id="skip-forward" class="pod-btn" title="Forward 30s">30 ↻</button>
</div>
<div class="vol-control">
<button id="pod-mute" class="pod-btn">🔊</button>
</div>
</div>
</div>
<audio id="pod-audio" src="https://www.bensound.com/bensound-music/bensound-creativeminds.mp3"></audio>
</div>
<script src="script.js"></script>
</body>
</html>import { useRef, useState } from "react";
function formatTime(s: number) {
if (!isFinite(s)) return "0:00";
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = Math.floor(s % 60);
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
return `${m}:${String(sec).padStart(2, "0")}`;
}
const EPISODES = [
{
id: 1,
title: "Building Scalable APIs",
episode: "EP 42",
duration: "58:22",
date: "Mar 4, 2026",
},
{
id: 2,
title: "The Future of AI Development",
episode: "EP 41",
duration: "1:12:14",
date: "Feb 25, 2026",
},
{
id: 3,
title: "CSS Architecture at Scale",
episode: "EP 40",
duration: "45:51",
date: "Feb 18, 2026",
},
];
const SRC = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3";
const SPEEDS = [0.75, 1, 1.25, 1.5, 2];
export default function PodcastPlayerRC() {
const audioRef = useRef<HTMLAudioElement>(null);
const [epIdx, setEpIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [speed, setSpeed] = useState(1);
const [speedIdx, setSpeedIdx] = useState(1);
const ep = EPISODES[epIdx];
function togglePlay() {
const a = audioRef.current;
if (!a) return;
if (a.paused) {
a.play();
setPlaying(true);
} else {
a.pause();
setPlaying(false);
}
}
function skip(secs: number) {
const a = audioRef.current;
if (!a) return;
a.currentTime = Math.max(0, Math.min(a.duration, a.currentTime + secs));
}
function cycleSpeed() {
const next = (speedIdx + 1) % SPEEDS.length;
setSpeedIdx(next);
setSpeed(SPEEDS[next]);
if (audioRef.current) audioRef.current.playbackRate = SPEEDS[next];
}
function playEp(idx: number) {
setEpIdx(idx);
setCurrentTime(0);
setPlaying(false);
}
const pct = duration ? (currentTime / duration) * 100 : 0;
return (
<div className="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div className="w-full max-w-sm">
{/* Current episode player */}
<div className="bg-[#161b22] border border-[#30363d] rounded-2xl p-5 mb-4">
<div className="flex items-start gap-4 mb-5">
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-[#bc8cff] to-[#58a6ff] flex items-center justify-center text-2xl flex-shrink-0">
🎙️
</div>
<div className="flex-1 min-w-0">
<p className="text-[10px] text-[#bc8cff] font-semibold uppercase tracking-wider mb-0.5">
{ep.episode}
</p>
<p className="text-[#e6edf3] font-bold text-sm leading-snug">{ep.title}</p>
<p className="text-[#484f58] text-xs mt-0.5">Dev.Talks · {ep.date}</p>
</div>
</div>
<audio
ref={audioRef}
src={SRC}
onTimeUpdate={() => setCurrentTime(audioRef.current?.currentTime ?? 0)}
onLoadedMetadata={() => setDuration(audioRef.current?.duration ?? 0)}
onEnded={() => setPlaying(false)}
/>
{/* Seek */}
<input
type="range"
min={0}
max={duration || 100}
step={1}
value={currentTime}
onChange={(e) => {
if (audioRef.current) audioRef.current.currentTime = Number(e.target.value);
}}
className="w-full h-1 accent-[#bc8cff] cursor-pointer mb-1"
/>
<div className="flex justify-between text-[11px] text-[#484f58] tabular-nums mb-4">
<span>{formatTime(currentTime)}</span>
<span>-{formatTime(Math.max(0, duration - currentTime))}</span>
</div>
{/* Controls */}
<div className="flex items-center justify-center gap-4">
<button
onClick={cycleSpeed}
className="text-xs font-bold text-[#8b949e] hover:text-[#e6edf3] bg-[#21262d] rounded-lg px-2 py-1 min-w-[40px] text-center transition-colors"
>
{speed}×
</button>
<button
onClick={() => skip(-15)}
className="text-[#8b949e] hover:text-[#e6edf3] transition-colors text-sm font-bold"
>
-15
</button>
<button
onClick={togglePlay}
className="w-12 h-12 rounded-full bg-gradient-to-br from-[#bc8cff] to-[#58a6ff] flex items-center justify-center shadow-lg"
>
{playing ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
)}
</button>
<button
onClick={() => skip(30)}
className="text-[#8b949e] hover:text-[#e6edf3] transition-colors text-sm font-bold"
>
+30
</button>
<button className="text-[#8b949e] hover:text-[#e6edf3] text-xs transition-colors">
🔖
</button>
</div>
</div>
{/* Episode list */}
<div className="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden divide-y divide-[#21262d]">
{EPISODES.map((e, i) => (
<button
key={e.id}
onClick={() => playEp(i)}
className={`w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-white/[0.03] transition-colors ${i === epIdx ? "bg-white/[0.04]" : ""}`}
>
<div
className={`w-6 h-6 rounded-full border flex items-center justify-center text-[10px] font-bold flex-shrink-0 ${i === epIdx ? "border-[#bc8cff] text-[#bc8cff]" : "border-[#484f58] text-[#484f58]"}`}
>
{i === epIdx && playing ? "▶" : i + 1}
</div>
<div className="flex-1 min-w-0">
<p
className={`text-sm truncate ${i === epIdx ? "text-[#e6edf3] font-semibold" : "text-[#8b949e]"}`}
>
{e.title}
</p>
<p className="text-xs text-[#484f58]">{e.date}</p>
</div>
<span className="text-xs text-[#484f58] tabular-nums">{e.duration}</span>
</button>
))}
</div>
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const EPISODES = [
{
id: 1,
title: "Building Scalable APIs",
episode: "EP 42",
duration: "58:22",
date: "Mar 4, 2026",
},
{
id: 2,
title: "The Future of AI Development",
episode: "EP 41",
duration: "1:12:14",
date: "Feb 25, 2026",
},
{
id: 3,
title: "CSS Architecture at Scale",
episode: "EP 40",
duration: "45:51",
date: "Feb 18, 2026",
},
];
const SRC = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3";
const SPEEDS = [0.75, 1, 1.25, 1.5, 2];
const audioRef = ref(null);
const epIdx = ref(0);
const playing = ref(false);
const currentTime = ref(0);
const duration = ref(0);
const speed = ref(1);
const speedIdx = ref(1);
const ep = computed(() => EPISODES[epIdx.value]);
const pct = computed(() => (duration.value ? (currentTime.value / duration.value) * 100 : 0));
function formatTime(s) {
if (!isFinite(s)) return "0:00";
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = Math.floor(s % 60);
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
return `${m}:${String(sec).padStart(2, "0")}`;
}
function togglePlay() {
const a = audioRef.value;
if (!a) return;
if (a.paused) {
a.play();
playing.value = true;
} else {
a.pause();
playing.value = false;
}
}
function skip(secs) {
const a = audioRef.value;
if (!a) return;
a.currentTime = Math.max(0, Math.min(a.duration, a.currentTime + secs));
}
function cycleSpeed() {
const next = (speedIdx.value + 1) % SPEEDS.length;
speedIdx.value = next;
speed.value = SPEEDS[next];
if (audioRef.value) audioRef.value.playbackRate = SPEEDS[next];
}
function playEp(idx) {
epIdx.value = idx;
currentTime.value = 0;
playing.value = false;
}
function onTimeUpdate() {
currentTime.value = audioRef.value?.currentTime ?? 0;
}
function onLoadedMetadata() {
duration.value = audioRef.value?.duration ?? 0;
}
function onEnded() {
playing.value = false;
}
function onSeek(e) {
if (audioRef.value) audioRef.value.currentTime = Number(e.target.value);
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div class="w-full max-w-sm">
<!-- Current episode player -->
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl p-5 mb-4">
<div class="flex items-start gap-4 mb-5">
<div class="w-16 h-16 rounded-xl bg-gradient-to-br from-[#bc8cff] to-[#58a6ff] flex items-center justify-center text-2xl flex-shrink-0">
🎙️
</div>
<div class="flex-1 min-w-0">
<p class="text-[10px] text-[#bc8cff] font-semibold uppercase tracking-wider mb-0.5">{{ ep.episode }}</p>
<p class="text-[#e6edf3] font-bold text-sm leading-snug">{{ ep.title }}</p>
<p class="text-[#484f58] text-xs mt-0.5">Dev.Talks · {{ ep.date }}</p>
</div>
</div>
<audio
ref="audioRef"
:src="SRC"
@timeupdate="onTimeUpdate"
@loadedmetadata="onLoadedMetadata"
@ended="onEnded"
/>
<!-- Seek -->
<input
type="range" :min="0" :max="duration || 100" :step="1" :value="currentTime"
@input="onSeek"
class="w-full h-1 accent-[#bc8cff] cursor-pointer mb-1"
/>
<div class="flex justify-between text-[11px] text-[#484f58] tabular-nums mb-4">
<span>{{ formatTime(currentTime) }}</span>
<span>-{{ formatTime(Math.max(0, duration - currentTime)) }}</span>
</div>
<!-- Controls -->
<div class="flex items-center justify-center gap-4">
<button @click="cycleSpeed" class="text-xs font-bold text-[#8b949e] hover:text-[#e6edf3] bg-[#21262d] rounded-lg px-2 py-1 min-w-[40px] text-center transition-colors">
{{ speed }}×
</button>
<button @click="skip(-15)" class="text-[#8b949e] hover:text-[#e6edf3] transition-colors text-sm font-bold">-15</button>
<button @click="togglePlay" class="w-12 h-12 rounded-full bg-gradient-to-br from-[#bc8cff] to-[#58a6ff] flex items-center justify-center shadow-lg">
<svg v-if="playing" width="16" height="16" viewBox="0 0 24 24" fill="white"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="white"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button @click="skip(30)" class="text-[#8b949e] hover:text-[#e6edf3] transition-colors text-sm font-bold">+30</button>
<button class="text-[#8b949e] hover:text-[#e6edf3] text-xs transition-colors">🔖</button>
</div>
</div>
<!-- Episode list -->
<div class="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden divide-y divide-[#21262d]">
<button
v-for="(e, i) in EPISODES"
:key="e.id"
@click="playEp(i)"
:class="['w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-white/[0.03] transition-colors', i === epIdx ? 'bg-white/[0.04]' : '']"
>
<div :class="['w-6 h-6 rounded-full border flex items-center justify-center text-[10px] font-bold flex-shrink-0', i === epIdx ? 'border-[#bc8cff] text-[#bc8cff]' : 'border-[#484f58] text-[#484f58]']">
{{ i === epIdx && playing ? "▶" : i + 1 }}
</div>
<div class="flex-1 min-w-0">
<p :class="['text-sm truncate', i === epIdx ? 'text-[#e6edf3] font-semibold' : 'text-[#8b949e]']">{{ e.title }}</p>
<p class="text-xs text-[#484f58]">{{ e.date }}</p>
</div>
<span class="text-xs text-[#484f58] tabular-nums">{{ e.duration }}</span>
</button>
</div>
</div>
</div>
</template>
<style scoped>
</style><script>
let audioEl;
let epIdx = 0;
let playing = false;
let currentTime = 0;
let duration = 0;
let speed = 1;
let speedIdx = 1;
const EPISODES = [
{
id: 1,
title: "Building Scalable APIs",
episode: "EP 42",
duration: "58:22",
date: "Mar 4, 2026",
},
{
id: 2,
title: "The Future of AI Development",
episode: "EP 41",
duration: "1:12:14",
date: "Feb 25, 2026",
},
{
id: 3,
title: "CSS Architecture at Scale",
episode: "EP 40",
duration: "45:51",
date: "Feb 18, 2026",
},
];
const SRC = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3";
const SPEEDS = [0.75, 1, 1.25, 1.5, 2];
$: ep = EPISODES[epIdx];
$: pct = duration ? (currentTime / duration) * 100 : 0;
function formatTime(s) {
if (!isFinite(s)) return "0:00";
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = Math.floor(s % 60);
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
return `${m}:${String(sec).padStart(2, "0")}`;
}
function togglePlay() {
if (!audioEl) return;
if (audioEl.paused) {
audioEl.play();
playing = true;
} else {
audioEl.pause();
playing = false;
}
}
function skip(secs) {
if (!audioEl) return;
audioEl.currentTime = Math.max(0, Math.min(audioEl.duration, audioEl.currentTime + secs));
}
function cycleSpeed() {
const next = (speedIdx + 1) % SPEEDS.length;
speedIdx = next;
speed = SPEEDS[next];
if (audioEl) audioEl.playbackRate = SPEEDS[next];
}
function playEp(idx) {
epIdx = idx;
currentTime = 0;
playing = false;
}
function onTimeUpdate() {
currentTime = audioEl?.currentTime ?? 0;
}
function onLoadedMetadata() {
duration = audioEl?.duration ?? 0;
}
function onEnded() {
playing = false;
}
function onSeek(e) {
if (audioEl) audioEl.currentTime = Number(e.target.value);
}
</script>
<div class="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div class="w-full max-w-sm">
<!-- Current episode player -->
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl p-5 mb-4">
<div class="flex items-start gap-4 mb-5">
<div class="w-16 h-16 rounded-xl bg-gradient-to-br from-[#bc8cff] to-[#58a6ff] flex items-center justify-center text-2xl flex-shrink-0">
🎙️
</div>
<div class="flex-1 min-w-0">
<p class="text-[10px] text-[#bc8cff] font-semibold uppercase tracking-wider mb-0.5">{ep.episode}</p>
<p class="text-[#e6edf3] font-bold text-sm leading-snug">{ep.title}</p>
<p class="text-[#484f58] text-xs mt-0.5">Dev.Talks · {ep.date}</p>
</div>
</div>
<audio
bind:this={audioEl}
src={SRC}
on:timeupdate={onTimeUpdate}
on:loadedmetadata={onLoadedMetadata}
on:ended={onEnded}
/>
<!-- Seek -->
<input
type="range" min={0} max={duration || 100} step={1} value={currentTime}
on:input={onSeek}
class="w-full h-1 accent-[#bc8cff] cursor-pointer mb-1"
/>
<div class="flex justify-between text-[11px] text-[#484f58] tabular-nums mb-4">
<span>{formatTime(currentTime)}</span>
<span>-{formatTime(Math.max(0, duration - currentTime))}</span>
</div>
<!-- Controls -->
<div class="flex items-center justify-center gap-4">
<button on:click={cycleSpeed} class="text-xs font-bold text-[#8b949e] hover:text-[#e6edf3] bg-[#21262d] rounded-lg px-2 py-1 min-w-[40px] text-center transition-colors">
{speed}×
</button>
<button on:click={() => skip(-15)} class="text-[#8b949e] hover:text-[#e6edf3] transition-colors text-sm font-bold">-15</button>
<button on:click={togglePlay} class="w-12 h-12 rounded-full bg-gradient-to-br from-[#bc8cff] to-[#58a6ff] flex items-center justify-center shadow-lg">
{#if playing}
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><polygon points="5 3 19 12 5 21 5 3"/></svg>
{/if}
</button>
<button on:click={() => skip(30)} class="text-[#8b949e] hover:text-[#e6edf3] transition-colors text-sm font-bold">+30</button>
<button class="text-[#8b949e] hover:text-[#e6edf3] text-xs transition-colors">🔖</button>
</div>
</div>
<!-- Episode list -->
<div class="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden divide-y divide-[#21262d]">
{#each EPISODES as e, i}
<button on:click={() => playEp(i)} class="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-white/[0.03] transition-colors {i === epIdx ? 'bg-white/[0.04]' : ''}">
<div class="w-6 h-6 rounded-full border flex items-center justify-center text-[10px] font-bold flex-shrink-0 {i === epIdx ? 'border-[#bc8cff] text-[#bc8cff]' : 'border-[#484f58] text-[#484f58]'}">
{i === epIdx && playing ? "▶" : i + 1}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm truncate {i === epIdx ? 'text-[#e6edf3] font-semibold' : 'text-[#8b949e]'}">{e.title}</p>
<p class="text-xs text-[#484f58]">{e.date}</p>
</div>
<span class="text-xs text-[#484f58] tabular-nums">{e.duration}</span>
</button>
{/each}
</div>
</div>
</div>Podcast Player
Designed specifically for podcast enthusiasts, this player emphasizes navigation and focus. It includes dedicated skip buttons for commercial breaks and granular speed control.
Features
- Skip Forward (30s) / Back (15s)
- Variable playback speed (0.5x to 2.5x)
- Episode description toggle
- Progress tracking with timestamps
- Persistent playback position (simulated)