@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 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
+ }
@@ -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] ?? ""));
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { RadioBar, type RadioBarProps } from "./radio-bar";
2
+ export {
3
+ CHANNELS,
4
+ DEFAULT_CHANNEL_ID,
5
+ type Channel,
6
+ } from "./channels";
7
+ export { DEFAULT_RADIO_COPY, type RadioCopy } from "./copy";