diff --git a/src/newtab/newtab.css b/src/newtab/newtab.css index 42f597a..6174fb7 100644 --- a/src/newtab/newtab.css +++ b/src/newtab/newtab.css @@ -585,3 +585,39 @@ body::before { .nt-station { font-size: 22px; } .nt-station-list { max-height: 220px; } } + +/* ────────────────────────────────────────────────────────────────────── + First-run UX hint (v0.4.0) + Subtle accent-green pulse around the Quick Stations row + a bouncing + ↓ arrow on the now-playing area. Both disappear the moment a station + is picked. Existing users never see them. + ────────────────────────────────────────────────────────────────────── */ + +@keyframes nt-pulse-glow { + 0%, 100% { box-shadow: 0 0 0 0 rgba(109, 191, 122, 0); } + 50% { box-shadow: 0 0 0 6px rgba(109, 191, 122, 0.18); } +} + +@keyframes nt-bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(4px); } +} + +body.is-first-run .nt-quick { + animation: nt-pulse-glow 2.4s ease-in-out infinite; + border-radius: var(--radius); + padding: 14px; + margin: 0 -14px; +} + +body.is-first-run .nt-track { + color: var(--accent); + font-weight: 500; +} + +body.is-first-run .nt-track::after { + content: " ↓"; + display: inline-block; + animation: nt-bounce 1.8s ease-in-out infinite; + margin-left: 6px; +} diff --git a/src/newtab/newtab.js b/src/newtab/newtab.js index 95a8a73..af983d7 100644 --- a/src/newtab/newtab.js +++ b/src/newtab/newtab.js @@ -108,6 +108,7 @@ async function init() { const currentId = stored[STORAGE_KEYS.currentId]; currentStation = stations.find(s => s.id === currentId) || null; + reflectFirstRunHint(); renderNow(); renderQuick(); renderList(); @@ -184,6 +185,7 @@ async function init() { const newId = changes[STORAGE_KEYS.currentId].newValue; currentStation = stations.find(s => s.id === newId) || null; els.play.disabled = !currentStation; + reflectFirstRunHint(); renderNow(); renderQuick(); renderList(); @@ -317,6 +319,7 @@ function renderList() { async function onPickStation(station) { currentStation = station; els.play.disabled = false; + reflectFirstRunHint(); await chrome.storage.local.set({ [STORAGE_KEYS.currentId]: station.id }); renderNow(); renderQuick(); @@ -364,6 +367,13 @@ async function onVolume() { /* ---------- Helpers ---------- */ +// v0.4.0 first-run UX hint — body class drives the pulse + arrow CSS in +// newtab.css. Active when no station has been picked yet; cleared the +// moment one is selected (here or via cross-surface storage sync). +function reflectFirstRunHint() { + document.body.classList.toggle('is-first-run', !currentStation); +} + function reflectPlayButton() { els.play.setAttribute('aria-pressed', playing ? 'true' : 'false'); els.play.textContent = playing ? '❚❚ Pause' : '▶ Play'; diff --git a/src/popup/popup.css b/src/popup/popup.css index 2b4cb3e..93fc4a8 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -310,3 +310,38 @@ html, body { .station-list::-webkit-scrollbar-track { background: transparent; } + +/* ────────────────────────────────────────────────────────────────────── + First-run UX hint (v0.4.0) + When body.is-first-run is set (no station picked), the station list + subtly pulses with an accent-green glow and the now-playing area gets + a bouncing ↓ arrow pointing the user at the list. Both removed the + moment a station is picked, so existing users never see them again. + ────────────────────────────────────────────────────────────────────── */ + +@keyframes tuner-pulse-glow { + 0%, 100% { box-shadow: 0 0 0 0 rgba(109, 191, 122, 0); } + 50% { box-shadow: 0 0 0 4px rgba(109, 191, 122, 0.25); } +} + +@keyframes tuner-bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(3px); } +} + +body.is-first-run .stations { + animation: tuner-pulse-glow 2.4s ease-in-out infinite; + border-radius: 6px; +} + +body.is-first-run .np-track { + color: var(--accent); + font-weight: 500; +} + +body.is-first-run .np-track::after { + content: " ↓"; + display: inline-block; + animation: tuner-bounce 1.8s ease-in-out infinite; + margin-left: 4px; +} diff --git a/src/popup/popup.js b/src/popup/popup.js index 4360760..5afe67b 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -69,6 +69,7 @@ async function init() { // 3. Restore current station selection. const currentId = stored[STORAGE_KEYS.currentId]; currentStation = stations.find(s => s.id === currentId) || null; + reflectFirstRunHint(); renderNowPlaying(); // 4. Render station list. @@ -121,6 +122,7 @@ async function init() { const newId = changes[STORAGE_KEYS.currentId].newValue; currentStation = stations.find(s => s.id === newId) || null; els.btnPlay.disabled = !currentStation; + reflectFirstRunHint(); renderNowPlaying(); renderStations(); } @@ -202,6 +204,7 @@ function renderNowPlaying() { async function onPickStation(station) { currentStation = station; els.btnPlay.disabled = false; + reflectFirstRunHint(); await chrome.storage.local.set({ [STORAGE_KEYS.currentId]: station.id }); renderNowPlaying(); renderStations(); @@ -256,6 +259,14 @@ function reflectPlayButton() { els.btnPlay.textContent = playing ? '❚❚ Pause' : '▶ Play'; } +// v0.4.0 first-run UX hint — body class drives the pulse + arrow CSS in +// popup.css. Active when no station has been picked yet; cleared the +// moment a station is selected (here or in another surface via storage +// sync). +function reflectFirstRunHint() { + document.body.classList.toggle('is-first-run', !currentStation); +} + function setState(state) { els.statePill.dataset.state = state; els.statePill.textContent = state;