feat(v0.4.0-prep): first-run UX hint — pulse + bouncing arrow

The 'pick a station to begin' state was too subtle on first install
(David's own 30-second panic moment when he uninstalled the dev build
and reinstalled from the Web Store, 2026-06-09 evening).

Two layered cues, both pure CSS driven by a body.is-first-run class:

1. Subtle accent-green glow pulses around:
   - popup: the station list section
   - newtab: the Quick Stations chip row
   (rgba alpha 0.18-0.25, 2.4s ease-in-out infinite — visible but not noisy)

2. Bouncing ↓ arrow appended to the 'Pick a station to begin' text in
   both surfaces (after-pseudo with translateY animation).

The is-first-run class is toggled in popup.js + newtab.js via a new
reflectFirstRunHint() function called from:
  - init() once stations + currentStation are resolved
  - onPickStation() the moment a user picks
  - the chrome.storage.onChanged listener when another surface picks
    (so the hint disappears on both surfaces simultaneously)

Existing users with stored currentStationId never see either cue —
the class only attaches when currentStation is null.

Working on branch v0.4.0-prep so the live main (= what shipped to
Web Store v0.3.0) is unchanged. Merge to main when ready to bump the
manifest version + readme.txt + CHANGELOG.md for v0.4.0 release.
This commit is contained in:
2026-06-09 22:05:55 +01:00
parent 9695d9d341
commit b82f14ee7b
4 changed files with 92 additions and 0 deletions
+11
View File
@@ -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;