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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user