feat: initial commit — RangerHQ Tuner v0.1.0 (Tier 1 MVP)
Chrome MV3 extension, browser-resident sibling to rangerhq-radio (WP plugin). Plays SomaFM via the chrome.offscreen API + a source- adapter pattern at src/sources/. Architecture highlights: - Audio runs in offscreen document — SW would get killed. - Source-adapter pattern locks Tier 1 contract (RadioSource interface in src/sources/base-source.js). Adding a network = drop a file + register one line in src/sources/index.js. - Vanilla JS, no build step. Pure ES modules. - No telemetry, no third-party JS. Outbound only to somafm.com. - Narrow permissions: offscreen + storage + somafm.com host_perms. No tabs, no <all_urls>, no webRequest. 22 files, ~30 min build following the saved plan at ~/.ranger-memory/projects/rangerhq-tuner-plan.md. Tier 2 + Tier 3 (Web Store submission) not started.
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
/* RangerHQ Tuner popup — ~360x520, dark earthy palette echoing the
|
||||
RangerHQ family brand (forest green + warm cream). Vanilla CSS,
|
||||
no preprocessor, no build step. */
|
||||
|
||||
:root {
|
||||
--bg: #0f1411;
|
||||
--bg-soft: #1a221c;
|
||||
--bg-row: #1f2823;
|
||||
--bg-row-hi: #2a3530;
|
||||
--fg: #e8e4d4;
|
||||
--fg-muted: #97a094;
|
||||
--accent: #6dbf7a; /* RangerHQ green */
|
||||
--accent-dim: #2a7d3e;
|
||||
--cream: #f4e9b7;
|
||||
--danger: #c9685b;
|
||||
--radius: 6px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 360px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.tuner-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-soft);
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.tuner-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tuner-brand h1 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.tuner-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 8px var(--accent);
|
||||
}
|
||||
|
||||
.tuner-state {
|
||||
font-size: 11px;
|
||||
color: var(--fg-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-row);
|
||||
}
|
||||
|
||||
.tuner-state[data-state="playing"] {
|
||||
color: var(--bg);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.tuner-state[data-state="buffering"] {
|
||||
color: var(--bg);
|
||||
background: var(--cream);
|
||||
}
|
||||
|
||||
.tuner-state[data-state="error"] {
|
||||
color: var(--cream);
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
/* Now playing */
|
||||
.now-playing {
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-soft);
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.np-station {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.np-track {
|
||||
font-size: 12px;
|
||||
color: var(--fg-muted);
|
||||
margin-top: 2px;
|
||||
min-height: 1.4em;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--accent-dim);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-row);
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
background: var(--bg-row-hi);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.btn-primary[aria-pressed="true"] {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.vol-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vol-label {
|
||||
font-size: 11px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
#volume {
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.search-wrap {
|
||||
display: block;
|
||||
padding: 8px 12px 4px;
|
||||
}
|
||||
|
||||
#search {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
background: var(--bg-row);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--bg-row-hi);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
#search:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Station list */
|
||||
.station-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.station-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--bg-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.station-row:hover {
|
||||
background: var(--bg-row);
|
||||
}
|
||||
|
||||
.station-row.is-active {
|
||||
background: var(--bg-row-hi);
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
.station-art {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-soft);
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.station-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.station-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.station-genre {
|
||||
font-size: 10px;
|
||||
color: var(--fg-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.station-empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--fg-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.tuner-footer {
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-soft);
|
||||
border-top: 1px solid #000;
|
||||
font-size: 10px;
|
||||
color: var(--fg-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Scrollbar styling — subtle, on-brand */
|
||||
.station-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.station-list::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-row-hi);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.station-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
Reference in New Issue
Block a user