Merge v0.4.0-prep — first-run UX hint
This commit is contained in:
@@ -585,3 +585,39 @@ body::before {
|
|||||||
.nt-station { font-size: 22px; }
|
.nt-station { font-size: 22px; }
|
||||||
.nt-station-list { max-height: 220px; }
|
.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];
|
const currentId = stored[STORAGE_KEYS.currentId];
|
||||||
currentStation = stations.find(s => s.id === currentId) || null;
|
currentStation = stations.find(s => s.id === currentId) || null;
|
||||||
|
reflectFirstRunHint();
|
||||||
renderNow();
|
renderNow();
|
||||||
renderQuick();
|
renderQuick();
|
||||||
renderList();
|
renderList();
|
||||||
@@ -184,6 +185,7 @@ async function init() {
|
|||||||
const newId = changes[STORAGE_KEYS.currentId].newValue;
|
const newId = changes[STORAGE_KEYS.currentId].newValue;
|
||||||
currentStation = stations.find(s => s.id === newId) || null;
|
currentStation = stations.find(s => s.id === newId) || null;
|
||||||
els.play.disabled = !currentStation;
|
els.play.disabled = !currentStation;
|
||||||
|
reflectFirstRunHint();
|
||||||
renderNow();
|
renderNow();
|
||||||
renderQuick();
|
renderQuick();
|
||||||
renderList();
|
renderList();
|
||||||
@@ -317,6 +319,7 @@ function renderList() {
|
|||||||
async function onPickStation(station) {
|
async function onPickStation(station) {
|
||||||
currentStation = station;
|
currentStation = station;
|
||||||
els.play.disabled = false;
|
els.play.disabled = false;
|
||||||
|
reflectFirstRunHint();
|
||||||
await chrome.storage.local.set({ [STORAGE_KEYS.currentId]: station.id });
|
await chrome.storage.local.set({ [STORAGE_KEYS.currentId]: station.id });
|
||||||
renderNow();
|
renderNow();
|
||||||
renderQuick();
|
renderQuick();
|
||||||
@@ -364,6 +367,13 @@ async function onVolume() {
|
|||||||
|
|
||||||
/* ---------- Helpers ---------- */
|
/* ---------- 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() {
|
function reflectPlayButton() {
|
||||||
els.play.setAttribute('aria-pressed', playing ? 'true' : 'false');
|
els.play.setAttribute('aria-pressed', playing ? 'true' : 'false');
|
||||||
els.play.textContent = playing ? '❚❚ Pause' : '▶ Play';
|
els.play.textContent = playing ? '❚❚ Pause' : '▶ Play';
|
||||||
|
|||||||
@@ -310,3 +310,38 @@ html, body {
|
|||||||
.station-list::-webkit-scrollbar-track {
|
.station-list::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ async function init() {
|
|||||||
// 3. Restore current station selection.
|
// 3. Restore current station selection.
|
||||||
const currentId = stored[STORAGE_KEYS.currentId];
|
const currentId = stored[STORAGE_KEYS.currentId];
|
||||||
currentStation = stations.find(s => s.id === currentId) || null;
|
currentStation = stations.find(s => s.id === currentId) || null;
|
||||||
|
reflectFirstRunHint();
|
||||||
renderNowPlaying();
|
renderNowPlaying();
|
||||||
|
|
||||||
// 4. Render station list.
|
// 4. Render station list.
|
||||||
@@ -121,6 +122,7 @@ async function init() {
|
|||||||
const newId = changes[STORAGE_KEYS.currentId].newValue;
|
const newId = changes[STORAGE_KEYS.currentId].newValue;
|
||||||
currentStation = stations.find(s => s.id === newId) || null;
|
currentStation = stations.find(s => s.id === newId) || null;
|
||||||
els.btnPlay.disabled = !currentStation;
|
els.btnPlay.disabled = !currentStation;
|
||||||
|
reflectFirstRunHint();
|
||||||
renderNowPlaying();
|
renderNowPlaying();
|
||||||
renderStations();
|
renderStations();
|
||||||
}
|
}
|
||||||
@@ -202,6 +204,7 @@ function renderNowPlaying() {
|
|||||||
async function onPickStation(station) {
|
async function onPickStation(station) {
|
||||||
currentStation = station;
|
currentStation = station;
|
||||||
els.btnPlay.disabled = false;
|
els.btnPlay.disabled = false;
|
||||||
|
reflectFirstRunHint();
|
||||||
await chrome.storage.local.set({ [STORAGE_KEYS.currentId]: station.id });
|
await chrome.storage.local.set({ [STORAGE_KEYS.currentId]: station.id });
|
||||||
renderNowPlaying();
|
renderNowPlaying();
|
||||||
renderStations();
|
renderStations();
|
||||||
@@ -256,6 +259,14 @@ function reflectPlayButton() {
|
|||||||
els.btnPlay.textContent = playing ? '❚❚ Pause' : '▶ Play';
|
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) {
|
function setState(state) {
|
||||||
els.statePill.dataset.state = state;
|
els.statePill.dataset.state = state;
|
||||||
els.statePill.textContent = state;
|
els.statePill.textContent = state;
|
||||||
|
|||||||
Reference in New Issue
Block a user