@syra.fm/sdk 0.5.0 → 0.7.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.
Files changed (55) hide show
  1. package/README.md +33 -4
  2. package/lib/commonjs/live/colors.js +54 -0
  3. package/lib/commonjs/live/colors.js.map +1 -0
  4. package/lib/commonjs/live/components/LiveRoomSheet.js +9 -8
  5. package/lib/commonjs/live/components/LiveRoomSheet.js.map +1 -1
  6. package/lib/commonjs/live/components/MiniRoomBar.js +3 -2
  7. package/lib/commonjs/live/components/MiniRoomBar.js.map +1 -1
  8. package/lib/commonjs/live/components/PodcastStreamPicker.js +1 -1
  9. package/lib/commonjs/live/components/RecordingsPanel.js +3 -2
  10. package/lib/commonjs/live/components/RecordingsPanel.js.map +1 -1
  11. package/lib/commonjs/live/components/RoomCard.js +283 -404
  12. package/lib/commonjs/live/components/RoomCard.js.map +1 -1
  13. package/lib/commonjs/live/index.js +31 -0
  14. package/lib/commonjs/live/index.js.map +1 -1
  15. package/lib/module/live/colors.js +49 -0
  16. package/lib/module/live/colors.js.map +1 -0
  17. package/lib/module/live/components/LiveRoomSheet.js +9 -8
  18. package/lib/module/live/components/LiveRoomSheet.js.map +1 -1
  19. package/lib/module/live/components/MiniRoomBar.js +3 -2
  20. package/lib/module/live/components/MiniRoomBar.js.map +1 -1
  21. package/lib/module/live/components/PodcastStreamPicker.js +1 -1
  22. package/lib/module/live/components/RecordingsPanel.js +3 -2
  23. package/lib/module/live/components/RecordingsPanel.js.map +1 -1
  24. package/lib/module/live/components/RoomCard.js +284 -407
  25. package/lib/module/live/components/RoomCard.js.map +1 -1
  26. package/lib/module/live/index.js +4 -0
  27. package/lib/module/live/index.js.map +1 -1
  28. package/lib/typescript/commonjs/live/colors.d.ts +35 -0
  29. package/lib/typescript/commonjs/live/colors.d.ts.map +1 -0
  30. package/lib/typescript/commonjs/live/components/LiveRoomSheet.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/live/components/MiniRoomBar.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/live/components/PodcastStreamPicker.d.ts +1 -1
  33. package/lib/typescript/commonjs/live/components/RecordingsPanel.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/live/components/RoomCard.d.ts +18 -0
  35. package/lib/typescript/commonjs/live/components/RoomCard.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/live/index.d.ts +1 -0
  37. package/lib/typescript/commonjs/live/index.d.ts.map +1 -1
  38. package/lib/typescript/module/live/colors.d.ts +35 -0
  39. package/lib/typescript/module/live/colors.d.ts.map +1 -0
  40. package/lib/typescript/module/live/components/LiveRoomSheet.d.ts.map +1 -1
  41. package/lib/typescript/module/live/components/MiniRoomBar.d.ts.map +1 -1
  42. package/lib/typescript/module/live/components/PodcastStreamPicker.d.ts +1 -1
  43. package/lib/typescript/module/live/components/RecordingsPanel.d.ts.map +1 -1
  44. package/lib/typescript/module/live/components/RoomCard.d.ts +18 -0
  45. package/lib/typescript/module/live/components/RoomCard.d.ts.map +1 -1
  46. package/lib/typescript/module/live/index.d.ts +1 -0
  47. package/lib/typescript/module/live/index.d.ts.map +1 -1
  48. package/package.json +3 -13
  49. package/src/live/colors.ts +57 -0
  50. package/src/live/components/LiveRoomSheet.tsx +9 -8
  51. package/src/live/components/MiniRoomBar.tsx +3 -2
  52. package/src/live/components/PodcastStreamPicker.tsx +1 -1
  53. package/src/live/components/RecordingsPanel.tsx +3 -2
  54. package/src/live/components/RoomCard.tsx +245 -323
  55. package/src/live/index.ts +12 -0
@@ -1,15 +1,16 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useMemo } from 'react';
4
- import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
4
+ import { View, Text, TouchableOpacity } from 'react-native';
5
5
  import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
6
6
  import { useAuth } from '@oxyhq/services';
7
7
  import { useLiveConfig } from "../context/LiveConfigContext.js";
8
8
  import { AnimatedPulse } from "./AnimatedPulse.js";
9
9
  import { useRoomUsers, getAvatarUrl } from "../hooks/useRoomUsers.js";
10
-
11
- // --- Utility helpers ---
10
+ import { LIVE_COLOR, LIVE_FOREGROUND_COLOR } from "../colors.js";
12
11
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
12
+ // --- Utility helpers ---
13
+
13
14
  function formatCompact(num) {
14
15
  if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
15
16
  if (num >= 1_000) return `${(num / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
@@ -23,48 +24,86 @@ function formatDuration(startedAt, endedAt) {
23
24
  const m = totalMin % 60;
24
25
  return m > 0 ? `${h}h ${m}m` : `${h}h`;
25
26
  }
26
- const MONTH_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
27
- function formatDate(dateStr) {
28
- const d = new Date(dateStr);
29
- return `${d.getDate()} ${MONTH_SHORT[d.getMonth()]} ${d.getFullYear()}`;
27
+ const DAY_MS = 86_400_000;
28
+ function startOfDay(date) {
29
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
30
30
  }
31
- function getTimeLabel(room) {
32
- if (room.status === 'ended' && room.startedAt && room.endedAt) {
33
- return `${formatDuration(room.startedAt, room.endedAt)} · ${formatDate(room.endedAt)}`;
34
- }
35
- if (room.status === 'live' && room.startedAt) {
36
- return `Live · Started ${formatDate(room.startedAt)}`;
37
- }
31
+
32
+ /** `Today` / `Tomorrow` / `Yesterday`, else a locale short date (year only when it differs). */
33
+ function formatDay(date, now) {
34
+ const dayDelta = Math.round((startOfDay(date) - startOfDay(now)) / DAY_MS);
35
+ if (dayDelta === 0) return 'Today';
36
+ if (dayDelta === 1) return 'Tomorrow';
37
+ if (dayDelta === -1) return 'Yesterday';
38
+ const options = {
39
+ day: 'numeric',
40
+ month: 'short'
41
+ };
42
+ if (date.getFullYear() !== now.getFullYear()) options.year = 'numeric';
43
+ return date.toLocaleDateString(undefined, options);
44
+ }
45
+ function formatTime(date) {
46
+ return date.toLocaleTimeString(undefined, {
47
+ hour: 'numeric',
48
+ minute: '2-digit'
49
+ });
50
+ }
51
+
52
+ /**
53
+ * The when-label for a room that is NOT live: the start time for a scheduled room
54
+ * (`Tomorrow 18:00`), the day + duration for an ended one (`Yesterday · 45 min`).
55
+ * A live room never uses this — its lead metric is the listener count.
56
+ */
57
+ function getScheduleLabel(room, now) {
38
58
  if (room.status === 'scheduled' && room.scheduledStart) {
39
- return formatDate(room.scheduledStart);
59
+ const start = new Date(room.scheduledStart);
60
+ return `${formatDay(start, now)} ${formatTime(start)}`;
61
+ }
62
+ if (room.status === 'ended' && room.endedAt) {
63
+ const day = formatDay(new Date(room.endedAt), now);
64
+ return room.startedAt ? `${day} · ${formatDuration(room.startedAt, room.endedAt)}` : day;
40
65
  }
41
66
  if (room.createdAt) {
42
- return formatDate(room.createdAt);
67
+ return formatDay(new Date(room.createdAt), now);
43
68
  }
44
69
  return '';
45
70
  }
46
71
 
47
72
  // --- Constants ---
48
73
 
49
- const ROOM_TYPE_META = {
50
- stage: {
51
- icon: 'account-voice',
52
- label: 'Stage',
53
- color: '#3B82F6'
54
- },
55
- broadcast: {
56
- icon: 'broadcast',
57
- label: 'Broadcast',
58
- color: '#FF6B35'
59
- }
60
- };
61
- const MAX_SPEAKER_AVATARS = 4;
74
+ const MAX_SPEAKER_AVATARS = 3;
62
75
  const SPEAKER_AVATAR_SIZE = 44;
76
+ const ROW_AVATAR_SIZE = 40;
77
+ const BYLINE_AVATAR_SIZE = 20;
78
+ const ACTION_HIT_SLOP = {
79
+ top: 8,
80
+ bottom: 8,
81
+ left: 8,
82
+ right: 8
83
+ };
63
84
 
64
85
  // --- Types ---
65
86
 
66
87
  // --- Component ---
67
88
 
89
+ /**
90
+ * A room in a list. The `default` variant renders ONE of two presentations,
91
+ * selected by `room.status`, because a live room and a listing of one are
92
+ * different objects:
93
+ *
94
+ * - **live** → a featured card: the LIVE chip + the listener count (the only
95
+ * number that matters while a room is on air), the speaker faces, the host,
96
+ * and an explicit Join CTA.
97
+ * - **scheduled / ended** → a flush full-width row: host avatar, title + when,
98
+ * byline, and one subtle right-side action. No card chrome, no CTA — there is
99
+ * nothing to join yet (or ever again).
100
+ *
101
+ * At most ONE status chip is ever shown (a live room is not a scheduled one, and
102
+ * the date already says "scheduled"); the room type carries no badge at all.
103
+ *
104
+ * The `compact` variant is the small fixed-width card used by post attachments
105
+ * and the composer preview, and follows the same rules in miniature.
106
+ */
68
107
  export const RoomCard = ({
69
108
  room,
70
109
  onPress,
@@ -91,6 +130,7 @@ export const RoomCard = ({
91
130
  const isLive = room.status === 'live';
92
131
  const isScheduled = room.status === 'scheduled';
93
132
  const isCompact = variant === 'compact';
133
+ const isFeatured = isLive && !isCompact;
94
134
 
95
135
  // Resolve host display: an explicit prop wins, then the @handle, then the
96
136
  // canonical API-owned `name.displayName` from the Oxy user DTO (never a
@@ -99,264 +139,260 @@ export const RoomCard = ({
99
139
  const hostName = hostNameProp ?? (hostProfile?.username ? `@${hostProfile.username}` : (typeof hostProfileName === 'object' ? hostProfileName?.displayName : null) || room.host?.slice(0, 10) || 'Unknown');
100
140
  const hostAvatarUri = hostAvatarUriProp ?? getAvatarUrl(hostProfile, oxyServices, getCachedFileDownloadUrlSync);
101
141
 
102
- // Resolve speaker avatars (default variant only)
142
+ // Speaker faces only earn their space on the featured (live) card.
103
143
  const speakerIds = useMemo(() => {
104
- if (isCompact) return [];
144
+ if (!isFeatured) return [];
105
145
  const ids = room.speakers?.length ? room.speakers : [room.host];
106
146
  return ids.slice(0, MAX_SPEAKER_AVATARS);
107
- }, [room.speakers, room.host, isCompact]);
147
+ }, [isFeatured, room.speakers, room.host]);
148
+ const hiddenSpeakerCount = Math.max(0, (room.speakers?.length ?? 0) - speakerIds.length);
108
149
  useRoomUsers(speakerIds);
109
- const typeMeta = room.type && room.type !== 'talk' ? ROOM_TYPE_META[room.type] : null;
110
150
  const listenerCount = room.participants?.length || room.stats?.totalJoined || 0;
111
- const timeLabel = getTimeLabel(room);
151
+ const scheduleLabel = isLive ? '' : getScheduleLabel(room, new Date());
152
+ const byline = house ? `by ${hostName} · ${house.name}` : `by ${hostName}`;
112
153
 
113
- // --- Compact variant (kept simple) ---
154
+ // --- Compact variant (post attachments, composer preview) ---
155
+ // It lives inside HORIZONTAL carousels, where a percentage width has no parent
156
+ // to resolve against — hence the intrinsic width, capped at the parent so a
157
+ // constrained host box (the composer preview) can still shrink it. Hosts
158
+ // override any of this through `style`, which wins over the className.
114
159
  if (isCompact) {
115
160
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
116
- style: [styles.compactCard, {
117
- backgroundColor: theme.colors.card,
118
- borderColor: theme.colors.border
119
- }, style],
161
+ className: "w-[200px] max-w-full min-h-[140px] justify-between rounded-2xl border border-border bg-surface p-3",
162
+ style: style,
120
163
  onPress: onPress,
121
164
  activeOpacity: onPress ? 0.7 : 1,
122
165
  disabled: !onPress,
123
166
  children: [/*#__PURE__*/_jsxs(View, {
124
- style: styles.compactHeader,
125
- children: [house && /*#__PURE__*/_jsx(Text, {
126
- style: [styles.compactHouseName, {
127
- color: theme.colors.textSecondary
128
- }],
129
- numberOfLines: 1,
130
- children: house.name.toUpperCase()
167
+ className: "flex-1",
168
+ children: [isLive && /*#__PURE__*/_jsx(View, {
169
+ className: "mb-1.5 flex-row",
170
+ children: /*#__PURE__*/_jsx(LiveBadge, {})
131
171
  }), /*#__PURE__*/_jsx(Text, {
132
- style: [styles.compactTitle, {
133
- color: theme.colors.text
134
- }],
172
+ className: "text-sm font-semibold leading-[18px] text-foreground",
135
173
  numberOfLines: 2,
136
174
  children: room.title
137
- }), (isLive || isScheduled || typeMeta) && /*#__PURE__*/_jsxs(View, {
138
- style: styles.compactBadgeRow,
139
- children: [isLive && /*#__PURE__*/_jsxs(View, {
140
- style: [styles.liveBadge, {
141
- backgroundColor: '#FF4458'
142
- }],
143
- children: [/*#__PURE__*/_jsx(AnimatedPulse, {
144
- size: 6
145
- }), /*#__PURE__*/_jsx(Text, {
146
- style: styles.liveText,
147
- children: "LIVE"
148
- })]
149
- }), isScheduled && /*#__PURE__*/_jsxs(View, {
150
- style: [styles.scheduledBadge, {
151
- backgroundColor: theme.colors.backgroundSecondary
152
- }],
153
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
154
- name: "calendar",
155
- size: 10,
156
- color: theme.colors.textSecondary
157
- }), /*#__PURE__*/_jsx(Text, {
158
- style: [styles.scheduledText, {
159
- color: theme.colors.textSecondary
160
- }],
161
- children: "SCHEDULED"
162
- })]
163
- }), typeMeta && /*#__PURE__*/_jsxs(View, {
164
- style: [styles.typeBadge, {
165
- backgroundColor: typeMeta.color + '20'
166
- }],
167
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
168
- name: typeMeta.icon,
169
- size: 10,
170
- color: typeMeta.color
171
- }), /*#__PURE__*/_jsx(Text, {
172
- style: [styles.typeText, {
173
- color: typeMeta.color
174
- }],
175
- children: typeMeta.label
176
- })]
177
- })]
178
175
  })]
179
176
  }), /*#__PURE__*/_jsxs(View, {
180
- style: styles.compactFooter,
181
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
182
- name: "account-group",
183
- size: 14,
184
- color: theme.colors.textSecondary
185
- }), /*#__PURE__*/_jsxs(Text, {
186
- style: [styles.compactCount, {
187
- color: theme.colors.textSecondary
188
- }],
189
- children: [listenerCount, " listening"]
177
+ className: "mt-2 gap-1",
178
+ children: [/*#__PURE__*/_jsxs(View, {
179
+ className: "flex-row items-center gap-1",
180
+ children: [/*#__PURE__*/_jsx(AvatarComponent, {
181
+ size: 14,
182
+ source: hostAvatarUri,
183
+ shape: "squircle"
184
+ }), /*#__PURE__*/_jsx(Text, {
185
+ className: "flex-1 text-[11px] text-muted-foreground",
186
+ numberOfLines: 1,
187
+ children: byline
188
+ })]
190
189
  }), /*#__PURE__*/_jsx(Text, {
191
- style: {
192
- color: theme.colors.textSecondary,
193
- fontSize: 10
194
- },
195
- children: "\u2022"
196
- }), hostAvatarUri && /*#__PURE__*/_jsx(AvatarComponent, {
197
- size: 14,
190
+ className: "text-[11px] text-muted-foreground",
191
+ numberOfLines: 1,
192
+ children: isLive ? `${formatCompact(listenerCount)} listening` : scheduleLabel
193
+ })]
194
+ })]
195
+ });
196
+ }
197
+
198
+ // --- Featured card: a LIVE room, with presence and an explicit CTA ---
199
+ if (isFeatured) {
200
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
201
+ className: "mb-3 w-full gap-2.5 rounded-2xl border border-border bg-surface p-3",
202
+ style: style,
203
+ onPress: onPress,
204
+ activeOpacity: onPress ? 0.7 : 1,
205
+ disabled: !onPress,
206
+ children: [/*#__PURE__*/_jsxs(View, {
207
+ className: "flex-row items-center gap-1.5",
208
+ children: [/*#__PURE__*/_jsx(LiveBadge, {}), listenerCount > 0 && /*#__PURE__*/_jsxs(_Fragment, {
209
+ children: [/*#__PURE__*/_jsx(Text, {
210
+ className: "text-[13px] text-muted-foreground",
211
+ children: "\xB7"
212
+ }), /*#__PURE__*/_jsxs(Text, {
213
+ className: "text-[13px] font-semibold text-muted-foreground",
214
+ numberOfLines: 1,
215
+ children: [formatCompact(listenerCount), " listening"]
216
+ })]
217
+ }), /*#__PURE__*/_jsx(View, {
218
+ className: "flex-1"
219
+ }), onSave && /*#__PURE__*/_jsx(SaveAction, {
220
+ onSave: onSave,
221
+ isSaved: isSaved,
222
+ isScheduled: false,
223
+ theme: theme
224
+ }), onMenuPress && /*#__PURE__*/_jsx(MenuAction, {
225
+ onMenuPress: onMenuPress,
226
+ theme: theme
227
+ })]
228
+ }), /*#__PURE__*/_jsx(Text, {
229
+ className: "text-[17px] font-bold leading-[22px] text-foreground",
230
+ numberOfLines: 2,
231
+ children: room.title
232
+ }), /*#__PURE__*/_jsx(SpeakerStack, {
233
+ speakerIds: speakerIds,
234
+ hiddenCount: hiddenSpeakerCount
235
+ }), /*#__PURE__*/_jsxs(View, {
236
+ className: "flex-row items-center gap-2",
237
+ children: [/*#__PURE__*/_jsx(AvatarComponent, {
238
+ size: BYLINE_AVATAR_SIZE,
198
239
  source: hostAvatarUri,
199
- shape: "squircle",
200
- style: {
201
- marginRight: 2
202
- }
240
+ shape: "squircle"
203
241
  }), /*#__PURE__*/_jsx(Text, {
204
- style: [styles.compactHost, {
205
- color: theme.colors.textSecondary
206
- }],
242
+ className: "flex-1 text-[13px] text-muted-foreground",
207
243
  numberOfLines: 1,
208
- children: hostName
244
+ children: byline
245
+ }), onPress && /*#__PURE__*/_jsx(TouchableOpacity, {
246
+ className: "rounded-full px-4 py-1.5",
247
+ style: {
248
+ backgroundColor: LIVE_COLOR
249
+ },
250
+ onPress: onPress,
251
+ accessibilityRole: "button",
252
+ accessibilityLabel: "Join room",
253
+ children: /*#__PURE__*/_jsx(Text, {
254
+ className: "text-[13px] font-bold",
255
+ style: {
256
+ color: LIVE_FOREGROUND_COLOR
257
+ },
258
+ children: "Join"
259
+ })
209
260
  })]
210
261
  })]
211
262
  });
212
263
  }
213
264
 
214
- // --- Default variant ---
265
+ // --- Compact row: a SCHEDULED or ENDED room, flush in the feed ---
215
266
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
216
- style: [styles.card, {
217
- backgroundColor: theme.colors.card,
218
- borderColor: theme.colors.border
219
- }, style],
267
+ className: "w-full flex-row items-center gap-3 border-b border-border px-3 py-3",
268
+ style: style,
220
269
  onPress: onPress,
221
270
  activeOpacity: onPress ? 0.7 : 1,
222
271
  disabled: !onPress,
223
- children: [(house || onMenuPress) && /*#__PURE__*/_jsxs(View, {
224
- style: styles.houseRow,
225
- children: [house && /*#__PURE__*/_jsxs(_Fragment, {
226
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
227
- name: "home",
228
- size: 16,
229
- color: theme.colors.primary
230
- }), /*#__PURE__*/_jsx(Text, {
231
- style: [styles.houseName, {
232
- color: theme.colors.textSecondary
233
- }],
234
- numberOfLines: 1,
235
- children: house.name.toUpperCase()
236
- })]
237
- }), /*#__PURE__*/_jsx(View, {
238
- style: {
239
- flex: 1
240
- }
241
- }), onMenuPress && /*#__PURE__*/_jsx(TouchableOpacity, {
242
- onPress: onMenuPress,
243
- hitSlop: {
244
- top: 8,
245
- bottom: 8,
246
- left: 8,
247
- right: 8
248
- },
249
- children: /*#__PURE__*/_jsx(MaterialCommunityIcons, {
250
- name: "dots-horizontal",
251
- size: 20,
252
- color: theme.colors.textSecondary
253
- })
254
- })]
272
+ children: [/*#__PURE__*/_jsx(AvatarComponent, {
273
+ size: ROW_AVATAR_SIZE,
274
+ source: hostAvatarUri,
275
+ shape: "squircle"
255
276
  }), /*#__PURE__*/_jsxs(View, {
256
- children: [/*#__PURE__*/_jsx(Text, {
257
- style: [styles.title, {
258
- color: theme.colors.text
259
- }],
260
- numberOfLines: 2,
261
- children: room.title
262
- }), (isLive || isScheduled || typeMeta) && /*#__PURE__*/_jsxs(View, {
263
- style: styles.badgeRow,
264
- children: [isLive && /*#__PURE__*/_jsxs(View, {
265
- style: [styles.liveBadge, {
266
- backgroundColor: '#FF4458'
267
- }],
268
- children: [/*#__PURE__*/_jsx(AnimatedPulse, {
269
- size: 6
270
- }), /*#__PURE__*/_jsx(Text, {
271
- style: styles.liveText,
272
- children: "LIVE"
273
- })]
274
- }), isScheduled && /*#__PURE__*/_jsxs(View, {
275
- style: [styles.scheduledBadge, {
276
- backgroundColor: theme.colors.backgroundSecondary
277
- }],
278
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
279
- name: "calendar",
280
- size: 12,
281
- color: theme.colors.textSecondary
282
- }), /*#__PURE__*/_jsx(Text, {
283
- style: [styles.scheduledText, {
284
- color: theme.colors.textSecondary
285
- }],
286
- children: "SCHEDULED"
287
- })]
288
- }), typeMeta && /*#__PURE__*/_jsxs(View, {
289
- style: [styles.typeBadge, {
290
- backgroundColor: typeMeta.color + '20'
291
- }],
292
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
293
- name: typeMeta.icon,
294
- size: 10,
295
- color: typeMeta.color
296
- }), /*#__PURE__*/_jsx(Text, {
297
- style: [styles.typeText, {
298
- color: typeMeta.color
299
- }],
300
- children: typeMeta.label
301
- })]
277
+ className: "flex-1 gap-0.5",
278
+ children: [/*#__PURE__*/_jsxs(View, {
279
+ className: "flex-row items-baseline gap-1.5",
280
+ children: [/*#__PURE__*/_jsx(Text, {
281
+ className: "shrink text-[15px] font-semibold text-foreground",
282
+ numberOfLines: 1,
283
+ children: room.title
284
+ }), scheduleLabel !== '' && /*#__PURE__*/_jsxs(Text, {
285
+ className: "shrink-0 text-[13px] text-muted-foreground",
286
+ numberOfLines: 1,
287
+ children: ["\xB7 ", scheduleLabel]
302
288
  })]
289
+ }), /*#__PURE__*/_jsx(Text, {
290
+ className: "text-[13px] text-muted-foreground",
291
+ numberOfLines: 1,
292
+ children: byline
303
293
  })]
304
- }), /*#__PURE__*/_jsx(SpeakerRow, {
305
- speakerIds: speakerIds,
306
- listenerCount: listenerCount,
294
+ }), onSave && /*#__PURE__*/_jsx(SaveAction, {
295
+ onSave: onSave,
296
+ isSaved: isSaved,
297
+ isScheduled: isScheduled,
298
+ theme: theme
299
+ }), onMenuPress && /*#__PURE__*/_jsx(MenuAction, {
300
+ onMenuPress: onMenuPress,
307
301
  theme: theme
308
- }), (timeLabel || onSave) && /*#__PURE__*/_jsxs(View, {
309
- style: styles.metaRow,
310
- children: [timeLabel ? /*#__PURE__*/_jsx(Text, {
311
- style: [styles.metaText, {
312
- color: theme.colors.textSecondary
313
- }],
314
- children: timeLabel
315
- }) : /*#__PURE__*/_jsx(View, {}), onSave && /*#__PURE__*/_jsxs(TouchableOpacity, {
316
- onPress: onSave,
317
- style: styles.saveButton,
318
- hitSlop: {
319
- top: 8,
320
- bottom: 8,
321
- left: 8,
322
- right: 8
323
- },
324
- children: [/*#__PURE__*/_jsx(MaterialCommunityIcons, {
325
- name: isSaved ? 'bookmark' : 'bookmark-outline',
326
- size: 18,
327
- color: isSaved ? theme.colors.primary : theme.colors.textSecondary
328
- }), /*#__PURE__*/_jsx(Text, {
329
- style: [styles.saveText, {
330
- color: isSaved ? theme.colors.primary : theme.colors.textSecondary
331
- }],
332
- children: "Save"
333
- })]
334
- })]
335
302
  })]
336
303
  });
337
304
  };
338
305
 
339
- // --- Speaker row sub-component ---
306
+ // --- Badges ---
340
307
 
341
- function SpeakerRow({
342
- speakerIds,
343
- listenerCount,
308
+ // The LIVE badge is the one surface that must NOT theme with the host: it is the
309
+ // brand's "on air" signal, so its red comes from the shared live palette and its
310
+ // content sits on white regardless of light/dark mode.
311
+ function LiveBadge() {
312
+ return /*#__PURE__*/_jsxs(View, {
313
+ className: "flex-row items-center gap-1 rounded-[4px] px-2 py-1",
314
+ style: {
315
+ backgroundColor: LIVE_COLOR
316
+ },
317
+ children: [/*#__PURE__*/_jsx(AnimatedPulse, {
318
+ size: 6,
319
+ color: LIVE_FOREGROUND_COLOR
320
+ }), /*#__PURE__*/_jsx(Text, {
321
+ className: "text-[10px] font-bold text-white",
322
+ children: "LIVE"
323
+ })]
324
+ });
325
+ }
326
+
327
+ // --- Actions (icon-only, like the surrounding feed) ---
328
+
329
+ /**
330
+ * The saved-state toggle. On a SCHEDULED room, saving it is how you ask to be
331
+ * pulled back when it starts, so the icon reads as a reminder bell; anywhere
332
+ * else it is a bookmark.
333
+ */
334
+ function SaveAction({
335
+ onSave,
336
+ isSaved,
337
+ isScheduled,
344
338
  theme
345
339
  }) {
340
+ const icon = isScheduled ? isSaved ? 'bell' : 'bell-outline' : isSaved ? 'bookmark' : 'bookmark-outline';
341
+ return /*#__PURE__*/_jsx(TouchableOpacity, {
342
+ onPress: onSave,
343
+ hitSlop: ACTION_HIT_SLOP,
344
+ accessibilityRole: "button",
345
+ accessibilityLabel: isScheduled ? 'Remind me about this room' : 'Save room',
346
+ accessibilityState: {
347
+ selected: Boolean(isSaved)
348
+ },
349
+ children: /*#__PURE__*/_jsx(MaterialCommunityIcons, {
350
+ name: icon,
351
+ size: 20,
352
+ color: isSaved ? theme.colors.primary : theme.colors.textSecondary
353
+ })
354
+ });
355
+ }
356
+ function MenuAction({
357
+ onMenuPress,
358
+ theme
359
+ }) {
360
+ return /*#__PURE__*/_jsx(TouchableOpacity, {
361
+ onPress: onMenuPress,
362
+ hitSlop: ACTION_HIT_SLOP,
363
+ accessibilityRole: "button",
364
+ accessibilityLabel: "Room options",
365
+ children: /*#__PURE__*/_jsx(MaterialCommunityIcons, {
366
+ name: "dots-horizontal",
367
+ size: 20,
368
+ color: theme.colors.textSecondary
369
+ })
370
+ });
371
+ }
372
+
373
+ // --- Speaker stack (featured card only) ---
374
+
375
+ function SpeakerStack({
376
+ speakerIds,
377
+ hiddenCount
378
+ }) {
379
+ if (speakerIds.length === 0) return null;
346
380
  return /*#__PURE__*/_jsxs(View, {
347
- style: styles.avatarRow,
348
- children: [speakerIds.map(id => /*#__PURE__*/_jsx(SpeakerAvatar, {
349
- userId: id
350
- }, id)), listenerCount > 0 && /*#__PURE__*/_jsx(View, {
351
- style: [styles.countTile, {
352
- backgroundColor: theme.colors.backgroundSecondary,
353
- borderColor: theme.colors.border
354
- }],
381
+ className: "flex-row items-center",
382
+ children: [speakerIds.map((id, index) => /*#__PURE__*/_jsx(View, {
383
+ className: index === 0 ? '' : '-ml-3',
384
+ children: /*#__PURE__*/_jsx(SpeakerAvatar, {
385
+ userId: id
386
+ })
387
+ }, id)), hiddenCount > 0 && /*#__PURE__*/_jsx(View, {
388
+ className: "-ml-3 items-center justify-center rounded-full border-2 border-surface bg-muted",
389
+ style: {
390
+ width: SPEAKER_AVATAR_SIZE,
391
+ height: SPEAKER_AVATAR_SIZE
392
+ },
355
393
  children: /*#__PURE__*/_jsxs(Text, {
356
- style: [styles.countTileText, {
357
- color: theme.colors.textSecondary
358
- }],
359
- children: ["+", formatCompact(listenerCount)]
394
+ className: "text-[13px] font-bold text-muted-foreground",
395
+ children: ["+", formatCompact(hiddenCount)]
360
396
  })
361
397
  })]
362
398
  });
@@ -367,19 +403,17 @@ function SpeakerAvatar({
367
403
  const {
368
404
  useUserById,
369
405
  AvatarComponent,
370
- getCachedFileDownloadUrlSync,
371
- useTheme
406
+ getCachedFileDownloadUrlSync
372
407
  } = useLiveConfig();
373
- const theme = useTheme();
374
408
  const {
375
409
  oxyServices
376
410
  } = useAuth();
377
411
  const profile = useUserById(userId);
378
412
  const avatarUri = getAvatarUrl(profile, oxyServices, getCachedFileDownloadUrlSync);
413
+
414
+ // The ring is the card's own surface color, so overlapping faces stay separable.
379
415
  return /*#__PURE__*/_jsx(View, {
380
- style: [styles.avatarRing, {
381
- borderColor: theme.colors.border
382
- }],
416
+ className: "rounded-full border-2 border-surface",
383
417
  children: /*#__PURE__*/_jsx(AvatarComponent, {
384
418
  size: SPEAKER_AVATAR_SIZE,
385
419
  source: avatarUri,
@@ -387,162 +421,5 @@ function SpeakerAvatar({
387
421
  })
388
422
  });
389
423
  }
390
-
391
- // --- Styles ---
392
-
393
- const styles = StyleSheet.create({
394
- card: {
395
- borderRadius: 12,
396
- borderWidth: 1,
397
- padding: 16,
398
- marginBottom: 12,
399
- gap: 12
400
- },
401
- compactCard: {
402
- width: 200,
403
- minHeight: 140,
404
- borderRadius: 14,
405
- borderWidth: 1,
406
- padding: 12,
407
- justifyContent: 'space-between'
408
- },
409
- // House header
410
- houseRow: {
411
- flexDirection: 'row',
412
- alignItems: 'center',
413
- gap: 6
414
- },
415
- houseName: {
416
- fontSize: 11,
417
- fontWeight: '700',
418
- letterSpacing: 0.5
419
- },
420
- // Title
421
- title: {
422
- fontSize: 32,
423
- fontWeight: '700',
424
- lineHeight: 38
425
- },
426
- // Badges
427
- badgeRow: {
428
- flexDirection: 'row',
429
- alignItems: 'center',
430
- gap: 6,
431
- marginTop: 6
432
- },
433
- liveBadge: {
434
- flexDirection: 'row',
435
- alignItems: 'center',
436
- paddingHorizontal: 8,
437
- paddingVertical: 4,
438
- borderRadius: 4,
439
- gap: 4
440
- },
441
- liveText: {
442
- fontSize: 10,
443
- fontWeight: '700',
444
- color: '#FFFFFF'
445
- },
446
- scheduledBadge: {
447
- flexDirection: 'row',
448
- alignItems: 'center',
449
- paddingHorizontal: 8,
450
- paddingVertical: 4,
451
- borderRadius: 4,
452
- gap: 4
453
- },
454
- scheduledText: {
455
- fontSize: 10,
456
- fontWeight: '700'
457
- },
458
- typeBadge: {
459
- flexDirection: 'row',
460
- alignItems: 'center',
461
- paddingHorizontal: 6,
462
- paddingVertical: 3,
463
- borderRadius: 4,
464
- gap: 3
465
- },
466
- typeText: {
467
- fontSize: 9,
468
- fontWeight: '700'
469
- },
470
- // Speaker avatars
471
- avatarRow: {
472
- flexDirection: 'row',
473
- alignItems: 'center',
474
- gap: 8
475
- },
476
- avatarRing: {
477
- borderRadius: 14,
478
- padding: 2,
479
- borderWidth: 2
480
- },
481
- countTile: {
482
- width: SPEAKER_AVATAR_SIZE + 4,
483
- height: SPEAKER_AVATAR_SIZE + 4,
484
- borderRadius: 14,
485
- borderWidth: 2,
486
- alignItems: 'center',
487
- justifyContent: 'center',
488
- gap: 2
489
- },
490
- countTileText: {
491
- fontSize: 14,
492
- fontWeight: '700'
493
- },
494
- // Metadata
495
- metaRow: {
496
- flexDirection: 'row',
497
- alignItems: 'center',
498
- justifyContent: 'space-between'
499
- },
500
- metaText: {
501
- fontSize: 13
502
- },
503
- saveButton: {
504
- flexDirection: 'row',
505
- alignItems: 'center',
506
- gap: 4
507
- },
508
- saveText: {
509
- fontSize: 13,
510
- fontWeight: '500'
511
- },
512
- // Compact variant
513
- compactHeader: {
514
- flex: 1
515
- },
516
- compactHouseName: {
517
- fontSize: 9,
518
- fontWeight: '700',
519
- letterSpacing: 0.5,
520
- marginBottom: 4
521
- },
522
- compactTitle: {
523
- fontSize: 14,
524
- fontWeight: '600',
525
- lineHeight: 18
526
- },
527
- compactBadgeRow: {
528
- flexDirection: 'row',
529
- marginTop: 4,
530
- gap: 4,
531
- flexWrap: 'wrap'
532
- },
533
- compactFooter: {
534
- flexDirection: 'row',
535
- alignItems: 'center',
536
- gap: 4,
537
- marginTop: 8
538
- },
539
- compactCount: {
540
- fontSize: 11
541
- },
542
- compactHost: {
543
- fontSize: 11,
544
- flex: 1
545
- }
546
- });
547
424
  export default RoomCard;
548
425
  //# sourceMappingURL=RoomCard.js.map