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