@bufinance/radio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/src/channels.ts +112 -0
- package/src/cn.ts +20 -0
- package/src/copy.ts +44 -0
- package/src/index.ts +7 -0
- package/src/radio-bar.tsx +749 -0
- package/src/use-channel-availability.ts +129 -0
- package/src/use-channel-discovery.ts +64 -0
- package/src/use-yt-player.ts +113 -0
- package/tsconfig.json +5 -0
- package/tsconfig.tsbuildinfo +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bufinance/radio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "Publishable, self-contained morphing dynamic-island music player (BUFI Radio) ported from fx's radio component. Renders a YouTube-backed live-radio dynamic island: an idle pill with a marquee + play/pause that morphs into an expanded card with a persistent iframe, channel scroller, and prev/next transport. Fully prop-driven โ no app/data coupling. Audio-enabled state, copy/i18n, the channel-discovery URL, the channel list, and any in-bar extras (e.g. a TVL pill / community links) are all injected. External deps are framer-motion, lucide-react, and react-hotkeys-hook (peers); the consuming app provides Tailwind + the desk color palette (purpleDanis, violetDanis) and the `animate-marquee` / `font-knick` utilities.",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=18",
|
|
20
|
+
"react-dom": ">=18",
|
|
21
|
+
"framer-motion": ">=11",
|
|
22
|
+
"lucide-react": ">=0.4",
|
|
23
|
+
"react-hotkeys-hook": ">=4"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^19",
|
|
27
|
+
"bun-types": "1.3.10",
|
|
28
|
+
"framer-motion": "^11",
|
|
29
|
+
"lucide-react": "^0.460",
|
|
30
|
+
"react": "^19",
|
|
31
|
+
"react-hotkeys-hook": "^5",
|
|
32
|
+
"typescript": "5"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/channels.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export type Channel = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
mood: string;
|
|
5
|
+
emoji: string;
|
|
6
|
+
accent: string;
|
|
7
|
+
/** Hardcoded live videoId โ preferred when we've verified the stream. */
|
|
8
|
+
videoId?: string;
|
|
9
|
+
/** YouTube search query โ resolved at runtime by the discovery endpoint. */
|
|
10
|
+
query?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BUFX Radio โ 50+ channels, hybrid resolution.
|
|
15
|
+
*
|
|
16
|
+
* - Channels with `videoId` are verified long-running 24/7 streams.
|
|
17
|
+
* - Channels with `query` are dynamically resolved via the YouTube
|
|
18
|
+
* Data API v3 (search.list, eventType=live) at the `discoverUrl`
|
|
19
|
+
* endpoint the consuming app injects into `RadioBar`. Requires a
|
|
20
|
+
* `YOUTUBE_API_KEY` on that endpoint. Without the key, query-based
|
|
21
|
+
* channels are filtered out at runtime (won't appear in the scroller).
|
|
22
|
+
* - Both kinds are double-checked via oEmbed + iframe onError, so
|
|
23
|
+
* anything that goes dark is silently skipped.
|
|
24
|
+
*
|
|
25
|
+
* To add a channel: drop another entry below. Prefer `query` over
|
|
26
|
+
* `videoId` unless you've verified a specific live URL.
|
|
27
|
+
*/
|
|
28
|
+
export const CHANNELS: Channel[] = [
|
|
29
|
+
// === Verified static streams ===
|
|
30
|
+
{ id: "lofi-girl", name: "Lofi Girl", mood: "Beats to relax/study", videoId: "jfKfPfyJRdk", emoji: "๐", accent: "#FF6B9D" },
|
|
31
|
+
{ id: "lofi-girl-synth", name: "Lofi Synthwave", mood: "Synthwave to chill", videoId: "4xDzrJKXOOY", emoji: "๐", accent: "#A78BFA" },
|
|
32
|
+
{ id: "claude-fm", name: "Claude FM", mood: "Music for thinking", videoId: "YmQ7jRgf4f0", emoji: "๐ง ", accent: "#D8C2FF" },
|
|
33
|
+
|
|
34
|
+
// === Lofi / study ===
|
|
35
|
+
{ id: "lofi-sleep", name: "Lofi Sleep", mood: "Sleepy beats", emoji: "๐", accent: "#7C6BBF", query: "lofi hip hop sleep 24/7 live radio" },
|
|
36
|
+
{ id: "chillhop", name: "Chillhop", mood: "Jazzy hip-hop", emoji: "๐ต", accent: "#22D3EE", query: "chillhop music 24/7 live radio" },
|
|
37
|
+
{ id: "bootleg-boy", name: "Bootleg Boy", mood: "Sad lofi", emoji: "๐ชฆ", accent: "#94A3B8", query: "sad lofi hip hop 24/7 live" },
|
|
38
|
+
{ id: "lofi-radio-24", name: "Lofi 24/7", mood: "Endless beats", emoji: "๐ง", accent: "#F472B6", query: "lofi hip hop radio beats to relax 24/7 live" },
|
|
39
|
+
{ id: "anime-lofi", name: "Anime Lofi", mood: "Studio Ghibli mood", emoji: "๐ก", accent: "#F0ABFC", query: "anime lofi hip hop 24/7 live" },
|
|
40
|
+
|
|
41
|
+
// === Focus / coding ===
|
|
42
|
+
{ id: "coffee-shop", name: "Coffee Shop", mood: "Cafe ambience", emoji: "โ", accent: "#A8855E", query: "coffee shop ambience cafe music live 24/7" },
|
|
43
|
+
{ id: "deep-focus", name: "Deep Focus", mood: "Ambient productivity", emoji: "๐ฏ", accent: "#6366F1", query: "deep focus music for studying 24/7 live" },
|
|
44
|
+
{ id: "binaural-focus", name: "Binaural Focus", mood: "Brainwave entrainment", emoji: "๐งฌ", accent: "#10B981", query: "binaural beats focus concentration 24/7 live" },
|
|
45
|
+
|
|
46
|
+
// === Synthwave / cyberpunk / retro ===
|
|
47
|
+
{ id: "synthwave", name: "Synthwave Radio", mood: "Neon dreams", emoji: "๐", accent: "#A78BFA", query: "synthwave radio 24/7 live" },
|
|
48
|
+
{ id: "cyberpunk", name: "Cyberpunk Radio", mood: "Night city vibes", emoji: "๐ค", accent: "#F472B6", query: "cyberpunk 2077 ambient radio 24/7 live" },
|
|
49
|
+
{ id: "darksynth", name: "Darksynth", mood: "Heavy synthwave", emoji: "๐ฉธ", accent: "#EF4444", query: "darksynth dark synthwave 24/7 live" },
|
|
50
|
+
{ id: "outrun", name: "Outrun", mood: "80s drive", emoji: "๐", accent: "#FB7185", query: "outrun retrowave 80s music 24/7 live" },
|
|
51
|
+
{ id: "vaporwave", name: "Vaporwave", mood: "Mall plaza dreams", emoji: "๐", accent: "#22D3EE", query: "vaporwave aesthetic music 24/7 live" },
|
|
52
|
+
|
|
53
|
+
// === Jazz / cafe / acoustic ===
|
|
54
|
+
{ id: "jazz-hop-cafe", name: "Jazz Hop Cafe", mood: "Smooth jazz beats", emoji: "๐ท", accent: "#F59E0B", query: "jazz hop cafe music 24/7 live" },
|
|
55
|
+
{ id: "smooth-jazz", name: "Smooth Jazz", mood: "Late-night sax", emoji: "๐ถ", accent: "#D97706", query: "smooth jazz music 24/7 live radio" },
|
|
56
|
+
{ id: "bossa-nova", name: "Bossa Nova", mood: "Brazilian breeze", emoji: "๐ง๐ท", accent: "#16A34A", query: "bossa nova brazilian jazz 24/7 live" },
|
|
57
|
+
{ id: "piano-cafe", name: "Piano Cafe", mood: "Cinematic piano", emoji: "๐น", accent: "#0891B2", query: "piano music cafe 24/7 live" },
|
|
58
|
+
{ id: "acoustic-cafe", name: "Acoustic Cafe", mood: "Indie folk", emoji: "๐ช", accent: "#CA8A04", query: "acoustic indie folk music 24/7 live" },
|
|
59
|
+
|
|
60
|
+
// === Chillout / ambient / nature ===
|
|
61
|
+
{ id: "cafe-del-mar", name: "Cafe del Mar", mood: "Sunset chillout", emoji: "๐
", accent: "#FB923C", query: "cafe del mar chillout ibiza 24/7 live" },
|
|
62
|
+
{ id: "ambient-space", name: "Ambient Space", mood: "Drifting cosmos", emoji: "๐", accent: "#312E81", query: "ambient space music 24/7 live" },
|
|
63
|
+
{ id: "drone-ambient", name: "Drone Ambient", mood: "Endless drones", emoji: "๐", accent: "#475569", query: "drone ambient dark music 24/7 live" },
|
|
64
|
+
{ id: "rain-cafe", name: "Rain Cafe", mood: "Rain + coffee", emoji: "๐ง๏ธ", accent: "#64748B", query: "rain coffee shop sounds 24/7 live" },
|
|
65
|
+
{ id: "fireplace", name: "Fireplace", mood: "Crackling warmth", emoji: "๐ฅ", accent: "#DC2626", query: "fireplace crackling sounds 24/7 live" },
|
|
66
|
+
{ id: "forest-sounds", name: "Forest Sounds", mood: "Birds + leaves", emoji: "๐ฒ", accent: "#15803D", query: "forest birds nature sounds 24/7 live" },
|
|
67
|
+
{ id: "ocean-waves", name: "Ocean Waves", mood: "Tidal calm", emoji: "๐", accent: "#0284C7", query: "ocean waves sounds 24/7 live" },
|
|
68
|
+
|
|
69
|
+
// === Electronic / EDM ===
|
|
70
|
+
{ id: "monstercat", name: "Monstercat", mood: "Uplifting electronic", emoji: "๐พ", accent: "#10B981", query: "monstercat 24/7 live radio" },
|
|
71
|
+
{ id: "trap-nation", name: "Trap Nation", mood: "Trap & bass", emoji: "๐ฅ", accent: "#DC2626", query: "trap nation 24/7 live" },
|
|
72
|
+
{ id: "house-radio", name: "House Radio", mood: "Deep house", emoji: "๐บ", accent: "#0EA5E9", query: "deep house radio 24/7 live" },
|
|
73
|
+
{ id: "drum-n-bass", name: "Drum & Bass", mood: "Liquid DnB", emoji: "๐ง", accent: "#06B6D4", query: "liquid drum and bass 24/7 live" },
|
|
74
|
+
{ id: "techno-radio", name: "Techno", mood: "Berlin warehouse", emoji: "โ๏ธ", accent: "#1E293B", query: "techno music live mix 24/7" },
|
|
75
|
+
{ id: "trance", name: "Trance", mood: "Uplifting trance", emoji: "โจ", accent: "#A855F7", query: "uplifting trance music 24/7 live" },
|
|
76
|
+
|
|
77
|
+
// === Phonk ===
|
|
78
|
+
{ id: "phonk", name: "Phonk Radio", mood: "Drift mode", emoji: "๐", accent: "#7C3AED", query: "phonk music 24/7 live radio" },
|
|
79
|
+
{ id: "drift-phonk", name: "Drift Phonk", mood: "Tokyo midnight", emoji: "๐๏ธ", accent: "#9333EA", query: "drift phonk music 24/7 live" },
|
|
80
|
+
|
|
81
|
+
// === World ===
|
|
82
|
+
{ id: "kpop-radio", name: "K-Pop Radio", mood: "Idol energy", emoji: "๐ฐ๐ท", accent: "#F472B6", query: "kpop music 24/7 live radio" },
|
|
83
|
+
{ id: "city-pop", name: "Japanese City Pop", mood: "80s Tokyo", emoji: "๐ฏ๐ต", accent: "#FB7185", query: "japanese city pop 24/7 live" },
|
|
84
|
+
{ id: "latin-vibes", name: "Latin Vibes", mood: "Reggaeton + cumbia", emoji: "๐", accent: "#F97316", query: "latin reggaeton music 24/7 live" },
|
|
85
|
+
{ id: "reggae", name: "Reggae", mood: "Island rhythms", emoji: "๐ด", accent: "#16A34A", query: "reggae music 24/7 live radio" },
|
|
86
|
+
{ id: "afrobeat", name: "Afrobeat", mood: "African drums", emoji: "๐ฅ", accent: "#EA580C", query: "afrobeats music 24/7 live" },
|
|
87
|
+
|
|
88
|
+
// === Latin / reggaeton (user-requested) ===
|
|
89
|
+
{ id: "lofi-reggaeton", name: "Lofi Reggaeton", mood: "Perreo chill", emoji: "๐ต๐ท", accent: "#F97316", query: "lofi reggaeton 24/7 live" },
|
|
90
|
+
{ id: "latin-lofi", name: "Latin Lofi", mood: "Spanish chillhop", emoji: "๐ถ๏ธ", accent: "#DC2626", query: "lofi en espaรฑol 24/7 live" },
|
|
91
|
+
|
|
92
|
+
// === Rock / punk / metal (user-requested) ===
|
|
93
|
+
{ id: "indie-rock", name: "Indie Rock", mood: "Arctic Monkeys mood", emoji: "๐ธ", accent: "#06B6D4", query: "indie rock 24/7 live radio" },
|
|
94
|
+
{ id: "post-punk", name: "Post-Punk", mood: "Joy Division & co.", emoji: "๐ค", accent: "#1F2937", query: "post punk gothic rock 24/7 live" },
|
|
95
|
+
{ id: "lofi-post-punk", name: "Lofi Post-Punk", mood: "Slow gloom", emoji: "๐", accent: "#475569", query: "lofi post punk slowed 24/7 live" },
|
|
96
|
+
{ id: "doomerwave", name: "ะะพะพะผะตัwave", mood: "Molchat Doma vibes", emoji: "๐ฅ", accent: "#6B7280", query: "doomerwave russian post punk 24/7 live" },
|
|
97
|
+
{ id: "lofi-metal", name: "Lofi Metal", mood: "Slowed riffs", emoji: "๐ค", accent: "#18181B", query: "lofi metal slowed metal 24/7 live" },
|
|
98
|
+
|
|
99
|
+
// === Cinematic / game ===
|
|
100
|
+
{ id: "cinematic", name: "Cinematic", mood: "Epic & classical", emoji: "๐ป", accent: "#FBBF24", query: "epic cinematic music 24/7 live" },
|
|
101
|
+
{ id: "game-ost", name: "Game OST", mood: "RPG soundtracks", emoji: "๐ฎ", accent: "#7C3AED", query: "video game ost music 24/7 live" },
|
|
102
|
+
{ id: "ghibli-piano", name: "Ghibli Piano", mood: "Studio Ghibli themes", emoji: "๐", accent: "#22C55E", query: "ghibli piano music relax 24/7 live" },
|
|
103
|
+
|
|
104
|
+
// === Sleep / noise ===
|
|
105
|
+
{ id: "brown-noise", name: "Brown Noise", mood: "Deep static", emoji: "๐ค", accent: "#78350F", query: "brown noise 24/7 live" },
|
|
106
|
+
{ id: "white-noise", name: "White Noise", mood: "Pure hum", emoji: "โช", accent: "#E5E7EB", query: "white noise 24/7 live" },
|
|
107
|
+
|
|
108
|
+
// === John Wick Mode (user's favorite โ last on purpose) ===
|
|
109
|
+
{ id: "john-wick", name: "John Wick Mode", mood: "Neo-noir scenewave", emoji: "๐ด๏ธ", accent: "#0A0A0A", query: "darkwave neo noir scenewave music 24/7 live" },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
export const DEFAULT_CHANNEL_ID = CHANNELS[0].id;
|
package/src/cn.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// cn โ clsx-lite: join truthy class names (self-contained; no app import).
|
|
3
|
+
// Accepts strings, falsy values, and nested arrays of the same.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
type ClassValue = string | false | null | undefined | ClassValue[];
|
|
7
|
+
|
|
8
|
+
export function cn(...parts: ClassValue[]): string {
|
|
9
|
+
const out: string[] = [];
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (!part) continue;
|
|
12
|
+
if (Array.isArray(part)) {
|
|
13
|
+
const nested = cn(...part);
|
|
14
|
+
if (nested) out.push(nested);
|
|
15
|
+
} else {
|
|
16
|
+
out.push(part);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out.join(" ");
|
|
20
|
+
}
|
package/src/copy.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localizable copy for the radio. Defaults to English (`DEFAULT_RADIO_COPY`);
|
|
3
|
+
* the consuming app passes its own translations via the `copy` prop so the
|
|
4
|
+
* package carries no i18n framework dependency.
|
|
5
|
+
*
|
|
6
|
+
* `nowPlaying` and `switchTo` are interpolation templates โ they carry a
|
|
7
|
+
* `{name}` placeholder filled at render via the local `interp()` helper
|
|
8
|
+
* (mirrors the old `useScopedI18n("Radio")` interpolation behavior).
|
|
9
|
+
*/
|
|
10
|
+
export type RadioCopy = {
|
|
11
|
+
previousChannel: string;
|
|
12
|
+
nextChannel: string;
|
|
13
|
+
play: string;
|
|
14
|
+
pause: string;
|
|
15
|
+
openRadio: string;
|
|
16
|
+
collapseRadio: string;
|
|
17
|
+
/** Interpolation template with `{name}`. */
|
|
18
|
+
nowPlaying: string;
|
|
19
|
+
/** Interpolation template with `{name}`. */
|
|
20
|
+
switchTo: string;
|
|
21
|
+
onAir: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_RADIO_COPY: RadioCopy = {
|
|
25
|
+
previousChannel: "Previous channel",
|
|
26
|
+
nextChannel: "Next channel",
|
|
27
|
+
play: "Play",
|
|
28
|
+
pause: "Pause",
|
|
29
|
+
openRadio: "Open radio",
|
|
30
|
+
collapseRadio: "Collapse radio",
|
|
31
|
+
nowPlaying: "Now playing: {name}",
|
|
32
|
+
switchTo: "Switch to {name}",
|
|
33
|
+
onAir: "On Air",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fills `{placeholder}` tokens in a copy template. Kept local so the package
|
|
38
|
+
* doesn't depend on next-international (which couldn't param-type the appended
|
|
39
|
+
* Radio keys โ the original called `t()` with one arg and interpolated in JS).
|
|
40
|
+
*/
|
|
41
|
+
export const interp = (
|
|
42
|
+
str: string,
|
|
43
|
+
vars: Record<string, string | number>,
|
|
44
|
+
): string => str.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ""));
|