@dialtribe/react-sdk 0.1.0-alpha.5 → 0.1.0-alpha.7

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.
@@ -14,6 +14,8 @@ interface DialTribeContextValue {
14
14
  isExpired: boolean;
15
15
  /** Mark token as expired */
16
16
  markExpired: () => void;
17
+ /** Optional API base URL override */
18
+ apiBaseUrl?: string;
17
19
  }
18
20
  interface DialTribeProviderProps {
19
21
  /** Session token from your backend (required) */
@@ -22,6 +24,12 @@ interface DialTribeProviderProps {
22
24
  onTokenRefresh?: (newToken: string, expiresAt: string) => void;
23
25
  /** Called when the session token expires or becomes invalid */
24
26
  onTokenExpired?: () => void;
27
+ /**
28
+ * Optional: Override API base URL (for local development)
29
+ * If not provided, uses NEXT_PUBLIC_DIALTRIBE_API_URL environment variable or production default
30
+ * @example "http://localhost:3001/api/public/v1"
31
+ */
32
+ apiBaseUrl?: string;
25
33
  /** Your React app */
26
34
  children: React$1.ReactNode;
27
35
  }
@@ -32,22 +40,32 @@ interface DialTribeProviderProps {
32
40
  *
33
41
  * @example
34
42
  * ```tsx
43
+ * // Production (default)
44
+ * <DialTribeProvider
45
+ * sessionToken="sess_xxx..."
46
+ * onTokenRefresh={(newToken) => setToken(newToken)}
47
+ * onTokenExpired={() => router.push('/login')}
48
+ * >
49
+ * <App />
50
+ * </DialTribeProvider>
51
+ *
52
+ * // Local development (explicit)
35
53
  * <DialTribeProvider
36
54
  * sessionToken="sess_xxx..."
37
- * onTokenRefresh={(newToken) => {
38
- * // Save to state/localStorage
39
- * setToken(newToken);
40
- * }}
41
- * onTokenExpired={() => {
42
- * // Redirect to login or refresh
43
- * router.push('/login');
44
- * }}
55
+ * apiBaseUrl="http://localhost:3001/api/public/v1"
56
+ * onTokenRefresh={(newToken) => setToken(newToken)}
45
57
  * >
46
58
  * <App />
47
59
  * </DialTribeProvider>
60
+ *
61
+ * // Local development (via env var - recommended)
62
+ * // Set NEXT_PUBLIC_DIALTRIBE_API_URL=http://localhost:3001/api/public/v1 in .env
63
+ * <DialTribeProvider sessionToken="sess_xxx...">
64
+ * <App />
65
+ * </DialTribeProvider>
48
66
  * ```
49
67
  */
50
- declare function DialTribeProvider({ sessionToken: initialToken, onTokenRefresh, onTokenExpired, children, }: DialTribeProviderProps): react_jsx_runtime.JSX.Element;
68
+ declare function DialTribeProvider({ sessionToken: initialToken, onTokenRefresh, onTokenExpired, apiBaseUrl, children, }: DialTribeProviderProps): react_jsx_runtime.JSX.Element;
51
69
  /**
52
70
  * Hook to access DialTribe context
53
71
  *
@@ -63,19 +81,25 @@ declare function useDialTribe(): DialTribeContextValue;
63
81
  /**
64
82
  * DialTribe API Client
65
83
  *
66
- * Handles all API communication with fixed DialTribe endpoints.
84
+ * Handles all API communication with DialTribe endpoints.
67
85
  * Automatically manages session token refresh via X-Session-Token headers.
86
+ *
87
+ * Supports configurable API base URL for development:
88
+ * 1. Via environment variable: NEXT_PUBLIC_DIALTRIBE_API_URL
89
+ * 2. Via apiBaseUrl prop on DialTribeProvider (takes precedence)
90
+ *
91
+ * Defaults to production URL if neither is specified.
68
92
  */
69
- /** DialTribe API base URL (production only) */
70
- declare const DIALTRIBE_API_BASE = "https://dialtribe.com/api/public/v1";
71
- /** API endpoints (fixed, not configurable) */
93
+ /** Default API base URL (production or from environment) */
94
+ declare const DIALTRIBE_API_BASE: string;
95
+ /** Default API endpoints (uses default base URL) */
72
96
  declare const ENDPOINTS: {
73
- readonly broadcasts: "https://dialtribe.com/api/public/v1/broadcasts";
97
+ readonly broadcasts: `${string}/broadcasts`;
74
98
  readonly broadcast: (id: number) => string;
75
- readonly contentPlay: "https://dialtribe.com/api/public/v1/content/play";
76
- readonly presignedUrl: "https://dialtribe.com/api/public/v1/media/presigned-url";
77
- readonly sessionStart: "https://dialtribe.com/api/public/v1/session/start";
78
- readonly sessionPing: "https://dialtribe.com/api/public/v1/session/ping";
99
+ readonly contentPlay: `${string}/content/play`;
100
+ readonly presignedUrl: `${string}/media/presigned-url`;
101
+ readonly sessionStart: `${string}/session/start`;
102
+ readonly sessionPing: `${string}/session/ping`;
79
103
  };
80
104
  interface ApiClientConfig {
81
105
  /** Session token for authentication */
@@ -84,6 +108,12 @@ interface ApiClientConfig {
84
108
  onTokenRefresh?: (newToken: string, expiresAt: string) => void;
85
109
  /** Called when token expires or becomes invalid */
86
110
  onTokenExpired?: () => void;
111
+ /**
112
+ * Optional: Override API base URL (e.g., for local development)
113
+ * If not provided, uses environment variable or production default
114
+ * @example "http://localhost:3001/api/public/v1"
115
+ */
116
+ apiBaseUrl?: string;
87
117
  }
88
118
  /**
89
119
  * DialTribe API Client
@@ -93,6 +123,7 @@ interface ApiClientConfig {
93
123
  */
94
124
  declare class DialTribeClient {
95
125
  private config;
126
+ private endpoints;
96
127
  constructor(config: ApiClientConfig);
97
128
  /**
98
129
  * Make an authenticated request to DialTribe API
@@ -14,6 +14,8 @@ interface DialTribeContextValue {
14
14
  isExpired: boolean;
15
15
  /** Mark token as expired */
16
16
  markExpired: () => void;
17
+ /** Optional API base URL override */
18
+ apiBaseUrl?: string;
17
19
  }
18
20
  interface DialTribeProviderProps {
19
21
  /** Session token from your backend (required) */
@@ -22,6 +24,12 @@ interface DialTribeProviderProps {
22
24
  onTokenRefresh?: (newToken: string, expiresAt: string) => void;
23
25
  /** Called when the session token expires or becomes invalid */
24
26
  onTokenExpired?: () => void;
27
+ /**
28
+ * Optional: Override API base URL (for local development)
29
+ * If not provided, uses NEXT_PUBLIC_DIALTRIBE_API_URL environment variable or production default
30
+ * @example "http://localhost:3001/api/public/v1"
31
+ */
32
+ apiBaseUrl?: string;
25
33
  /** Your React app */
26
34
  children: React$1.ReactNode;
27
35
  }
@@ -32,22 +40,32 @@ interface DialTribeProviderProps {
32
40
  *
33
41
  * @example
34
42
  * ```tsx
43
+ * // Production (default)
44
+ * <DialTribeProvider
45
+ * sessionToken="sess_xxx..."
46
+ * onTokenRefresh={(newToken) => setToken(newToken)}
47
+ * onTokenExpired={() => router.push('/login')}
48
+ * >
49
+ * <App />
50
+ * </DialTribeProvider>
51
+ *
52
+ * // Local development (explicit)
35
53
  * <DialTribeProvider
36
54
  * sessionToken="sess_xxx..."
37
- * onTokenRefresh={(newToken) => {
38
- * // Save to state/localStorage
39
- * setToken(newToken);
40
- * }}
41
- * onTokenExpired={() => {
42
- * // Redirect to login or refresh
43
- * router.push('/login');
44
- * }}
55
+ * apiBaseUrl="http://localhost:3001/api/public/v1"
56
+ * onTokenRefresh={(newToken) => setToken(newToken)}
45
57
  * >
46
58
  * <App />
47
59
  * </DialTribeProvider>
60
+ *
61
+ * // Local development (via env var - recommended)
62
+ * // Set NEXT_PUBLIC_DIALTRIBE_API_URL=http://localhost:3001/api/public/v1 in .env
63
+ * <DialTribeProvider sessionToken="sess_xxx...">
64
+ * <App />
65
+ * </DialTribeProvider>
48
66
  * ```
49
67
  */
50
- declare function DialTribeProvider({ sessionToken: initialToken, onTokenRefresh, onTokenExpired, children, }: DialTribeProviderProps): react_jsx_runtime.JSX.Element;
68
+ declare function DialTribeProvider({ sessionToken: initialToken, onTokenRefresh, onTokenExpired, apiBaseUrl, children, }: DialTribeProviderProps): react_jsx_runtime.JSX.Element;
51
69
  /**
52
70
  * Hook to access DialTribe context
53
71
  *
@@ -63,19 +81,25 @@ declare function useDialTribe(): DialTribeContextValue;
63
81
  /**
64
82
  * DialTribe API Client
65
83
  *
66
- * Handles all API communication with fixed DialTribe endpoints.
84
+ * Handles all API communication with DialTribe endpoints.
67
85
  * Automatically manages session token refresh via X-Session-Token headers.
86
+ *
87
+ * Supports configurable API base URL for development:
88
+ * 1. Via environment variable: NEXT_PUBLIC_DIALTRIBE_API_URL
89
+ * 2. Via apiBaseUrl prop on DialTribeProvider (takes precedence)
90
+ *
91
+ * Defaults to production URL if neither is specified.
68
92
  */
69
- /** DialTribe API base URL (production only) */
70
- declare const DIALTRIBE_API_BASE = "https://dialtribe.com/api/public/v1";
71
- /** API endpoints (fixed, not configurable) */
93
+ /** Default API base URL (production or from environment) */
94
+ declare const DIALTRIBE_API_BASE: string;
95
+ /** Default API endpoints (uses default base URL) */
72
96
  declare const ENDPOINTS: {
73
- readonly broadcasts: "https://dialtribe.com/api/public/v1/broadcasts";
97
+ readonly broadcasts: `${string}/broadcasts`;
74
98
  readonly broadcast: (id: number) => string;
75
- readonly contentPlay: "https://dialtribe.com/api/public/v1/content/play";
76
- readonly presignedUrl: "https://dialtribe.com/api/public/v1/media/presigned-url";
77
- readonly sessionStart: "https://dialtribe.com/api/public/v1/session/start";
78
- readonly sessionPing: "https://dialtribe.com/api/public/v1/session/ping";
99
+ readonly contentPlay: `${string}/content/play`;
100
+ readonly presignedUrl: `${string}/media/presigned-url`;
101
+ readonly sessionStart: `${string}/session/start`;
102
+ readonly sessionPing: `${string}/session/ping`;
79
103
  };
80
104
  interface ApiClientConfig {
81
105
  /** Session token for authentication */
@@ -84,6 +108,12 @@ interface ApiClientConfig {
84
108
  onTokenRefresh?: (newToken: string, expiresAt: string) => void;
85
109
  /** Called when token expires or becomes invalid */
86
110
  onTokenExpired?: () => void;
111
+ /**
112
+ * Optional: Override API base URL (e.g., for local development)
113
+ * If not provided, uses environment variable or production default
114
+ * @example "http://localhost:3001/api/public/v1"
115
+ */
116
+ apiBaseUrl?: string;
87
117
  }
88
118
  /**
89
119
  * DialTribe API Client
@@ -93,6 +123,7 @@ interface ApiClientConfig {
93
123
  */
94
124
  declare class DialTribeClient {
95
125
  private config;
126
+ private endpoints;
96
127
  constructor(config: ApiClientConfig);
97
128
  /**
98
129
  * Make an authenticated request to DialTribe API
@@ -14,6 +14,7 @@ function DialTribeProvider({
14
14
  sessionToken: initialToken,
15
15
  onTokenRefresh,
16
16
  onTokenExpired,
17
+ apiBaseUrl,
17
18
  children
18
19
  }) {
19
20
  const [sessionToken, setSessionTokenState] = react.useState(initialToken);
@@ -42,7 +43,8 @@ function DialTribeProvider({
42
43
  sessionToken,
43
44
  setSessionToken,
44
45
  isExpired,
45
- markExpired
46
+ markExpired,
47
+ apiBaseUrl
46
48
  };
47
49
  return /* @__PURE__ */ jsxRuntime.jsx(DialTribeContext.Provider, { value, children });
48
50
  }
@@ -57,18 +59,28 @@ function useDialTribe() {
57
59
  }
58
60
 
59
61
  // src/client/DialTribeClient.ts
60
- var DIALTRIBE_API_BASE = "https://dialtribe.com/api/public/v1";
61
- var ENDPOINTS = {
62
- broadcasts: `${DIALTRIBE_API_BASE}/broadcasts`,
63
- broadcast: (id) => `${DIALTRIBE_API_BASE}/broadcasts/${id}`,
64
- contentPlay: `${DIALTRIBE_API_BASE}/content/play`,
65
- presignedUrl: `${DIALTRIBE_API_BASE}/media/presigned-url`,
66
- sessionStart: `${DIALTRIBE_API_BASE}/session/start`,
67
- sessionPing: `${DIALTRIBE_API_BASE}/session/ping`
68
- };
62
+ function getDefaultApiBaseUrl() {
63
+ if (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_DIALTRIBE_API_URL) {
64
+ return process.env.NEXT_PUBLIC_DIALTRIBE_API_URL;
65
+ }
66
+ return "https://dialtribe.com/api/public/v1";
67
+ }
68
+ var DIALTRIBE_API_BASE = getDefaultApiBaseUrl();
69
+ function getEndpoints(baseUrl = DIALTRIBE_API_BASE) {
70
+ return {
71
+ broadcasts: `${baseUrl}/broadcasts`,
72
+ broadcast: (id) => `${baseUrl}/broadcasts/${id}`,
73
+ contentPlay: `${baseUrl}/content/play`,
74
+ presignedUrl: `${baseUrl}/media/presigned-url`,
75
+ sessionStart: `${baseUrl}/session/start`,
76
+ sessionPing: `${baseUrl}/session/ping`
77
+ };
78
+ }
79
+ var ENDPOINTS = getEndpoints();
69
80
  var DialTribeClient = class {
70
81
  constructor(config) {
71
82
  this.config = config;
83
+ this.endpoints = config.apiBaseUrl ? getEndpoints(config.apiBaseUrl) : ENDPOINTS;
72
84
  }
73
85
  /**
74
86
  * Make an authenticated request to DialTribe API
@@ -115,7 +127,7 @@ var DialTribeClient = class {
115
127
  if (params?.broadcastStatus) searchParams.set("broadcastStatus", params.broadcastStatus.toString());
116
128
  if (params?.search) searchParams.set("search", params.search);
117
129
  if (params?.includeDeleted) searchParams.set("includeDeleted", "true");
118
- const url = `${ENDPOINTS.broadcasts}${searchParams.toString() ? `?${searchParams}` : ""}`;
130
+ const url = `${this.endpoints.broadcasts}${searchParams.toString() ? `?${searchParams}` : ""}`;
119
131
  const response = await this.fetch(url);
120
132
  if (!response.ok) {
121
133
  throw new Error(`Failed to fetch broadcasts: ${response.status} ${response.statusText}`);
@@ -126,7 +138,7 @@ var DialTribeClient = class {
126
138
  * Get a single broadcast by ID
127
139
  */
128
140
  async getBroadcast(id) {
129
- const response = await this.fetch(ENDPOINTS.broadcast(id));
141
+ const response = await this.fetch(this.endpoints.broadcast(id));
130
142
  if (!response.ok) {
131
143
  if (response.status === 404) {
132
144
  throw new Error("Broadcast not found");
@@ -148,7 +160,7 @@ var DialTribeClient = class {
148
160
  });
149
161
  if (params.hash) searchParams.set("hash", params.hash);
150
162
  if (params.action) searchParams.set("action", params.action);
151
- const url = `${ENDPOINTS.contentPlay}?${searchParams}`;
163
+ const url = `${this.endpoints.contentPlay}?${searchParams}`;
152
164
  const response = await this.fetch(url, {
153
165
  redirect: "manual"
154
166
  // Don't follow redirect, we want the URL
@@ -172,7 +184,7 @@ var DialTribeClient = class {
172
184
  hash: params.hash,
173
185
  fileType: params.fileType
174
186
  });
175
- const url = `${ENDPOINTS.presignedUrl}?${searchParams}`;
187
+ const url = `${this.endpoints.presignedUrl}?${searchParams}`;
176
188
  const response = await this.fetch(url);
177
189
  if (!response.ok) {
178
190
  throw new Error(`Failed to refresh URL: ${response.status} ${response.statusText}`);
@@ -185,7 +197,7 @@ var DialTribeClient = class {
185
197
  * @returns audienceId and optional resumePosition
186
198
  */
187
199
  async startSession(params) {
188
- const response = await this.fetch(ENDPOINTS.sessionStart, {
200
+ const response = await this.fetch(this.endpoints.sessionStart, {
189
201
  method: "POST",
190
202
  body: JSON.stringify(params)
191
203
  });
@@ -204,7 +216,7 @@ var DialTribeClient = class {
204
216
  * - 3: UNMOUNT
205
217
  */
206
218
  async sendSessionPing(params) {
207
- const response = await this.fetch(ENDPOINTS.sessionPing, {
219
+ const response = await this.fetch(this.endpoints.sessionPing, {
208
220
  method: "POST",
209
221
  body: JSON.stringify(params)
210
222
  });
@@ -747,11 +759,12 @@ function BroadcastPlayer({
747
759
  className = "",
748
760
  enableKeyboardShortcuts = false
749
761
  }) {
750
- const { sessionToken, setSessionToken, markExpired } = useDialTribe();
762
+ const { sessionToken, setSessionToken, markExpired, apiBaseUrl } = useDialTribe();
751
763
  const clientRef = react.useRef(null);
752
764
  if (!clientRef.current && sessionToken) {
753
765
  clientRef.current = new DialTribeClient({
754
766
  sessionToken,
767
+ apiBaseUrl,
755
768
  onTokenRefresh: (newToken, expiresAt) => {
756
769
  debug.log(`[DialTribeClient] Token refreshed, expires at ${expiresAt}`);
757
770
  setSessionToken(newToken, expiresAt);
@@ -1410,8 +1423,8 @@ function BroadcastPlayer({
1410
1423
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center p-8", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, { variant: "white", text: "Loading..." }) });
1411
1424
  }
1412
1425
  const hasTranscript = broadcast.transcriptStatus === 2 && transcriptData && (transcriptData.segments && transcriptData.segments.some((s) => s.words && s.words.length > 0) || transcriptData.words && transcriptData.words.length > 0);
1413
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-black rounded-lg shadow-2xl w-full h-full flex flex-col ${className}`, children: [
1414
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm border-b border-zinc-800 px-4 md:px-6 py-3 md:py-4 flex justify-between items-center rounded-t-lg shrink-0", children: [
1426
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-black rounded-lg shadow-2xl w-full max-h-full flex flex-col overflow-hidden ${className}`, children: [
1427
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm border-b border-zinc-800 px-3 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 flex justify-between items-center rounded-t-lg shrink-0", children: [
1415
1428
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1416
1429
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-white", children: broadcast.streamKeyRecord?.foreignName || "Broadcast" }),
1417
1430
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-400", children: [
@@ -1439,8 +1452,8 @@ function BroadcastPlayer({
1439
1452
  }
1440
1453
  ) })
1441
1454
  ] }),
1442
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
1443
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
1455
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden", children: [
1456
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 md:shrink md:flex-1 flex flex-col overflow-hidden", children: [
1444
1457
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${isAudioOnly ? "bg-linear-to-br from-zinc-900 via-zinc-800 to-zinc-900 flex items-stretch" : "bg-black"}`, children: [
1445
1458
  isAudioOnly ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative cursor-pointer w-full flex flex-col", onClick: handleVideoClick, children: !hasError ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full h-full relative", children: [
1446
1459
  /* @__PURE__ */ jsxRuntime.jsx(AudioWaveform, { audioElement, isPlaying: isLiveStream ? true : playing, isLive: isLiveStream }),
@@ -1722,7 +1735,7 @@ function BroadcastPlayer({
1722
1735
  ] })
1723
1736
  ] }))
1724
1737
  ] }),
1725
- showTranscript && hasTranscript && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full md:w-96 bg-zinc-900 border-l border-zinc-800 flex flex-col overflow-hidden", children: [
1738
+ showTranscript && hasTranscript && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 md:flex-none min-h-0 w-full md:w-96 bg-zinc-900 border-t md:border-t-0 border-l border-zinc-800 flex flex-col overflow-hidden", children: [
1726
1739
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 border-b border-zinc-800 bg-zinc-900/50 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1727
1740
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1728
1741
  /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-green-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) }),
@@ -1751,7 +1764,7 @@ function BroadcastPlayer({
1751
1764
  ]
1752
1765
  }
1753
1766
  ) }),
1754
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: transcriptContainerRef, className: "flex-1 overflow-y-auto px-4 py-4 text-gray-300 leading-relaxed", children: isLoadingTranscript ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-6 border-2 border-gray-600 border-t-blue-500 rounded-full animate-spin" }) }) : transcriptData?.segments && transcriptData.segments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: (() => {
1767
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: transcriptContainerRef, className: "flex-1 min-h-0 overflow-y-auto px-4 py-4 text-gray-300 leading-relaxed", children: isLoadingTranscript ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-6 border-2 border-gray-600 border-t-blue-500 rounded-full animate-spin" }) }) : transcriptData?.segments && transcriptData.segments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: (() => {
1755
1768
  const filteredSegments = transcriptData.segments.filter((s) => s.words && s.words.length > 0);
1756
1769
  let globalWordIndex = 0;
1757
1770
  const wordMap = /* @__PURE__ */ new Map();
@@ -1911,14 +1924,12 @@ function BroadcastPlayerModal({
1911
1924
  }) {
1912
1925
  const closeButtonRef = react.useRef(null);
1913
1926
  const previousActiveElement = react.useRef(null);
1914
- if (!isOpen) return null;
1915
1927
  react.useEffect(() => {
1916
- if (isOpen) {
1917
- previousActiveElement.current = document.activeElement;
1918
- setTimeout(() => {
1919
- closeButtonRef.current?.focus();
1920
- }, 100);
1921
- }
1928
+ if (!isOpen) return;
1929
+ previousActiveElement.current = document.activeElement;
1930
+ setTimeout(() => {
1931
+ closeButtonRef.current?.focus();
1932
+ }, 100);
1922
1933
  return () => {
1923
1934
  if (previousActiveElement.current) {
1924
1935
  previousActiveElement.current.focus();
@@ -1926,6 +1937,7 @@ function BroadcastPlayerModal({
1926
1937
  };
1927
1938
  }, [isOpen]);
1928
1939
  react.useEffect(() => {
1940
+ if (!isOpen) return;
1929
1941
  const handleKeyDown = (e) => {
1930
1942
  if (e.key === "Escape") {
1931
1943
  onClose();
@@ -1933,7 +1945,8 @@ function BroadcastPlayerModal({
1933
1945
  };
1934
1946
  document.addEventListener("keydown", handleKeyDown);
1935
1947
  return () => document.removeEventListener("keydown", handleKeyDown);
1936
- }, [onClose]);
1948
+ }, [isOpen, onClose]);
1949
+ if (!isOpen) return null;
1937
1950
  const handleBackdropClick = (e) => {
1938
1951
  if (e.target === e.currentTarget) {
1939
1952
  onClose();
@@ -1942,18 +1955,18 @@ function BroadcastPlayerModal({
1942
1955
  return /* @__PURE__ */ jsxRuntime.jsx(
1943
1956
  "div",
1944
1957
  {
1945
- className: "fixed inset-0 bg-black/70 backdrop-blur-xl flex items-center justify-center z-50 p-4",
1958
+ className: "fixed inset-0 bg-black/70 backdrop-blur-xl flex items-center justify-center z-50 p-2 sm:p-4",
1946
1959
  onClick: handleBackdropClick,
1947
1960
  role: "dialog",
1948
1961
  "aria-modal": "true",
1949
1962
  "aria-label": "Broadcast player",
1950
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-full max-w-7xl max-h-[90vh]", children: [
1963
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full max-w-7xl max-h-[95vh] sm:max-h-[90vh] overflow-hidden", children: [
1951
1964
  /* @__PURE__ */ jsxRuntime.jsx(
1952
1965
  "button",
1953
1966
  {
1954
1967
  ref: closeButtonRef,
1955
1968
  onClick: onClose,
1956
- className: "absolute top-4 right-4 z-10 text-gray-400 hover:text-white text-2xl leading-none transition-colors w-8 h-8 flex items-center justify-center bg-black/50 rounded-full",
1969
+ className: "absolute top-2 right-2 sm:top-4 sm:right-4 z-10 text-gray-400 hover:text-white text-2xl leading-none transition-colors w-8 h-8 flex items-center justify-center bg-black/50 rounded-full",
1957
1970
  title: "Close (ESC)",
1958
1971
  "aria-label": "Close player",
1959
1972
  children: "\xD7"